【C++】:日期类的实现 -- 日期计算器

简介: 【C++】:日期类的实现 -- 日期计算器

前言

1.日期类是一种十分经典的类型。对于C++的初学者,它能够帮助我们融会贯通许多C++的基础知识,它涉及许多的基础语法,比如引用,函数重载,传值/传参返回,构造函数,运算符重载,const成员等等。

如果有不了解的,可以前往我的主页浏览相关文章。

日期计算器可以实现两个日期的比较,两个日期的相减,日期的加减天数等有意义的运算。

2.本文依然采用多文件方式。其中:

Date.h //定义类,存放各函数的声明;

Date.cpp //实现各重载函数;

Test.cpp //测试各函数的功能。

在C++中,由于函数的声明与定义分离,如果要定义成员函数,就要指定类域,这是基本语法。

一,各个函数功能的实现

1. 检查输入的日期是否合法

不管是日期的比较还是日期的运算,第一步都要检查日期的合法性。特别是月份和每个月的天数

代码实现如下:

bool Date::CheakDate()
{
  if (_month < 1 || _month>12
    || _day<1 || _day>GetMonthDay(_year, _month))
  {
    return false;
  }
  else
  {
    return true;
  }
}

2. 构造函数 (初始化函数)

为了方便,在使用默认构造函数时,一般是自己显式的实现一个全缺省构造函数

注意:

在函数的声明和定义分离时,如果要给缺省值,必须在函数声明的时候给。

代码实现如下:

Date::Date(int year, int month, int day)
{
  _year = year;
  _month = month;
  _day = day;
     //日期的源头,是从构造函数里出来的,所以要在这里判断
  if (!CheakDate())
  {
    cout << "日期非法!" << endl;
  }
}

二,比较类的运算符重载

3. <运算符重载

判断两个日期谁更小。思路:先比年,年小就小,年相等比月,月小就小,年月相等比日,日小就小。

代码实现如下:

d1 < d2 隐含的this指针是d1,d是d2的别名

bool Date::operator< (const Date& d) const
{
  if (_year < d._year)
  {
    return true;
  }
  else if (_year == d._year)
  {
    if (_month < d._month)
    {
      return true;
    }
    else if (_month == d._month)
    {
      return _day < d._day;
    }
  }
  return false;
}

4. ==运算符重载

判断两个日期是否相等 。这个比较简单,如果两者的年月日都相等,即相等。

代码实现如下:

bool  Date::operator==(const Date& d) const
{
  return _year == d._year
    && _month == d._month
    && _day == d._day;
}

5. >=运算符重载

有人可能会仿照<运算符重载的方法,使用复杂的逻辑,写各种晦涩的代码实现。其实只要实现了<运算符重载和==运算符重载,下面的日期比较类都是可以复用的。 比如这里的>=,< 取反就是>=

代码实现如下:

bool Date::operator>= (const Date& d) const
{
  return !(*this < d);
}
• 1
• 2
• 3
• 4

6. >运算符重载

<= 取反,就是>。

bool Date::operator> (const Date& d) const
{
  return !(*this <= d);
}

7. <=运算符重载

只要满足<或者=,就是<=。

bool Date::operator<= (const Date& d) const
{
  return *this < d || *this == d;
}

8. !=运算符重载

==去取反,就是!=

bool  Date::operator!=(const Date& d) const
{
  return !(*this == d);
}

9. 获取某月的天数

这个函数是整个日期类的关键,也是最频繁调用的一个函数。由于这个原因,最好把它定义成内联函数,避免每次调用都要开辟空间,可以提升效率。根据C++的语法,定义在类里默认是内联,inline可加可不加

代码实现如下:

这里还有两个优化的细节:

1. month == 2 和后面的取模运算的位置。首先满足是2月,再判断是否是闰年,效率会更高。

2. static的使用,由于该函数频繁调用,把数组放在静态区,避免每次调用函数时每次都要开辟数组空间。

int GetMonthDay(int year, int month)
{
    //断言,确保输入月份的有效性
    assert(month > 0 && month < 13);
    //枚举出月份的天数
   static int monthDayArray[13] = { -1, 31,28,31,30,31,30,31,31,30,31,30,31 };
    //判断2月的平年和闰年
    if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
    {
        return 29;
    }
    else
    {
        return monthDayArray[month];
    }
}

三,运算类的重载

10. 日期+=天数

比如d1 + 50,这里的d1已经改变了

计算过程如下:

注意,每次超过月份天数后,是减当前月的天数。

代码实现如下:

Date& Date::operator+=(int day)
{
  //这里是处理有人传负的天数,转化成调用-=函数
  if (day < 0)
  {
    return *this -= -day;
  }
  //先加上天数
  _day += day;
  //加上天数后超出了月的范围
  while (_day > GetMonthDay(_year, _month))
  {
    //减去当前月的天数,此时月份+1
    _day -= GetMonthDay(_year, _month);
    ++_month;
    //超过12个月时
    if (_month == 13)
    {
      ++_year;//年份+1
      _month = 1;//别忘了月份还要从1月开始
    }
  }
  return *this;
}

11. 日期 + 天数

比如 d1 + 50,d1没有改变。

代码实现如下:

Date Date::operator+(int day) const
{
    //实例化一个临时的局部变量,用拷贝构造
    //把d1的日期拷贝给tmp,这样d1就不会改变
  Date tmp = *this;
  tmp += day;//直接复用+=
  //注意:出了这个函数,tmp会被销毁,所以这里不能用引用返回。
  //      这里是传值返回,所以会形成一个拷贝
  return tmp;
}

12. 日期-=天数

比如 d1- 50,这里的 d1也改变了。

计算过程如下:

注意,这里加(借)的是下一个月的天数。

代码实现如下:

Date& Date::operator-=(int day)
{
    //这里是处理有人传负的天数,转化成调用+=函数
  if (day < 0)
  {
    return *this += -day;
  }
  _day -= day;
  while (_day <= 0)
  {
    --_month;
    if (_month == 0)
    {
      _month = 12;
      _year--;
    }
    //借上一个月的天数
    _day += GetMonthDay(_year, _month);
  }
  return *this;

13. 日期 - 天数

思路同 日期 + 天数。

代码实现如下:

Date Date::operator-(int day) const
{
  Date tmp = *this;
  tmp -= day;
  return tmp;
}

四,前置,后置类的重载

首先要知道前置和后置运算的区别:

前置:返回运算后的值

后置:返回运算前的值

其次,还要理解函数重载和运算符的重载:

函数重载:可以让函数名相同,参数不同的函数存在;

运算符重载:让自定义类型可以用运算符,并且控制运算符的行为,增强可读性

这两者各论各的,没有关系。但是,多个同一运算符重载可以构成函数重载。

我们知道,前置和后置是同一运算的不同形式 ,但是他们的函数名相同,参数都是隐含的this参数,无法构成重载同时存在。所以为了区分,并且构成重载,C++规定:强行给后置(后置++和后置- -)函数的参数增加了一个 int 形参,不需要写形参名。并且这个形参没有任何意义。

14. 前置++

前置++先加,后用,返回的是加之后的值,可以直接复用+=运算符重载,返回的是改变后的 *this。

Date& Date::operator++()
{
  *this += 1;
  return *this;
}

15. 后置++

注意:后置函数多一个形参 int,以便与前置构成重载。

后置++是先用,后加,返回的是加之前的值。所以需要创建一个临时的局部对象 tmp,用拷贝构造把原来的 *this 拷贝给 tmp 。最后返回 tmp。

Date Date::operator++(int)
{
  Date tmp = *this;
  *this += 1;
  return tmp;
}

16. 前置 - -

前置- -和前置++的原理类似。

Date& Date::operator--()
{
  *this -= 1;
  return *this;
}

17. 后置 - -

注意:后置函数多一个形参 int,以便与前置构成重载。

原理与后置++类似。

Date Date::operator--(int) 
{
  Date tmp(*this);
  *this -= 1;
  return tmp;
}

注意:

  • 前置和后置运算,一般建议用前置,因为后置类需要拷贝构造,传值返回,这就会产生两次拷贝和一次析构,而前置却没有这样的消耗,相比之下前置类有优势

18. 日期-日期 返回天数

两个日期相减,返回的是相差的天数,是一个整形。

思路:找出大的年份和小的年份,再定义一个计数器和小的年份一起++,直到和大的年份相等。

比如 d1 - d2

代码实现如下:

//隐含的this指针是d1,d是d2的别名
int Date::operator-(const Date& d) const
{
   //先假设大日期和小日期
  Date max = *this;
  Date min = d;
  
  //默认假设正确
  int flag = 1;
    
    //如果假设错误,就进行改正
  if (*this < d)
  {
    max = d;
    min = *this;
    flag = -1;
  }
  int n = 0;
  //让计数n和小的日期一起加,加到和大的日期相等
  while (min != max)
  {
    ++min;
    ++n;
  }
    
    //flag的正负有妙用
  return n * flag;
}

五,完整代码

Date.h

#pragma once
#include <iostream>
using namespace std;
#include <assert.h>
#include <stdbool.h>
    
class Date
{
     //构造函数
    Date(int year, int month, int day);
    void Print() const;
     
     //定义为内联函数
    int GetMonthDay(int year, int month)
    {
        //断言,确保输入月份的有效性
       assert(month > 0 && month < 13);
       static int monthDayArray[13] = { -1, 31,28,31,30,31,30,31,31,30,31,30,31 };
       
        if (month == 2 && (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
        {
            return 29;
        }
        else
        {
            return monthDayArray[month];
        }
    }
    //检查日期的合法性
    bool CheakDate();
    //两个日期之间的比较
    bool operator< (const Date& d) const;
    bool operator<= (const Date& d) const;
    bool operator> (const Date& d) const;
    bool operator>= (const Date& d) const;
    bool operator==(const Date& d) const;
    bool operator!=(const Date& d) const;
    //d1 += 100,d1已经改变
    Date& operator+=(int day);
    Date& operator-=(int day);
    //d1 + 50,d1不变
    Date operator+(int day) const;
    Date operator-(int day) const;
    // d1 - d2
    int operator-(const Date& d) const;
    // ++d1 -> d1.operator++()
    Date& operator++();
    // d1++ -> d1.operator++(1) 整数任意给
    Date operator++(int);
    //前置,后置--
    Date& operator--();
    Date operator--(int);
private:
    int _year;
    int _month;
    int _day;
};

Date.cpp

#define _CRT_SECURE_NO_WARNINGS 
#include "Date.h"
bool Date::CheakDate()
{
  if (_month < 1 || _month>12
    || _day<1 || _day>GetMonthDay(_year, _month))
  {
    return false;
  }
  else
  {
    return true;
  }
}
//1.缺省参数只能在声明的时候给
//2.成员函数声明与定义分离时,要指定类域
Date::Date(int year, int month, int day)
{
  _year = year;
  _month = month;
  _day = day;
  if (!CheakDate())
  {
    cout << "日期非法!" << endl;
  }
}
void Date::Print() const
{
  cout << _year << "-" << _month << "-" << _day << endl;
}
//思路:先比年,年小就小,年相等比月,月小就小,年月相等比日,日小就小
//d1 <d2 隐含的this是d1, d是d2的别名
bool Date::operator< (const Date& d) const
{
  if (_year < d._year)
  {
    return true;
  }
  else if (_year == d._year)
  {
    if (_month < d._month)
    {
      return true;
    }
    else if (_month == d._month)
    {
      return _day < d._day;
    }
  }
  return false;
}
//先写好大于和等于 或者 小于和等于的函数,其余的进行复用
//d1 <=d2
bool Date::operator<= (const Date& d) const
{
  return *this < d || *this == d;
}
bool Date::operator> (const Date& d) const
{
  return !(*this <= d);
}
bool Date::operator>= (const Date& d) const
{
  return !(*this < d);
}
bool  Date::operator==(const Date& d) const
{
  return _year == d._year
    && _month == d._month
    && _day == d._day;
}
bool  Date::operator!=(const Date& d) const
{
  return !(*this == d);
}
//日期 += 天数 :d1 + 100
//这里的d1 已经改了
Date& Date::operator+=(int day)
{
  //这里是处理有人传负的天数
  if (day < 0)
  {
    return *this -= -day;
  }
  //先加上天数
  _day += day;
  //加上天数后超出了月的范围
  while (_day > GetMonthDay(_year, _month))
  {
    //减去当前月的天数,此时月份+1
    _day -= GetMonthDay(_year, _month);
    ++_month;
    //超过12个月时
    if (_month == 13)
    {
      ++_year;//年份+1
      _month = 1;//别忘了月份还要从1月开始
    }
  }
  return *this;
}
// d1 + 50,d1没有改变
Date Date::operator+(int day) const
{
    //实例化一个临时的局部变量,用拷贝构造,把d1的日期拷贝给tmp,这样d1就不会改变
  Date tmp = *this;
  tmp += day;//直接复用+=
  //注意:出了这个函数,tmp会被销毁,所以这里不能用引用返回。
  //      这里是传值返回,所以会形成一个拷贝
  return tmp;
}
//日期 -= 天数 :d1 - 100
//这里的d1 已经改了
Date& Date::operator-=(int day)
{
  if (day < 0)
  {
    return *this += -day;
  }
  _day -= day;
  while (_day <= 0)
  {
    --_month;
    if (_month == 0)
    {
      _month = 12;
      _year--;
    }
    //借上一个月的天数
    _day += GetMonthDay(_year, _month);
  }
  return *this;
}
Date Date::operator-(int day) const
{
  Date tmp = *this;
  tmp -= day;
  return tmp;
}
// ++d
Date& Date::operator++()
{
  *this += 1;
  return *this;
}
//这两种++ 建议用前置++,因为后置++会产生两次拷贝和一次析构,相比之下前置++有优势。
//d++
Date Date::operator++(int)
{
  Date tmp = *this;
  *this += 1;
  return tmp;
}
//--d
Date& Date::operator--()
{
  *this -= 1;
  return *this;
}
//d--
Date Date::operator--(int) 
{
  Date tmp(*this);
  *this -= 1;
  return tmp;
}
//思路:找出大的年份和小的年份,再定义一个计数器和小的年份一起++,直到和大的年份相等。
//d1 - d2
int Date::operator-(const Date& d) const
{
  Date max = *this;
  Date min = d;
  int flag = 1;
  if (*this < d)
  {
    max = d;
    min = *this;
    flag = -1;
  }
  int n = 0;
  while (min != max)
  {
    ++min;
    ++n;
  }
  return n * flag;
}

Test.cpp

#define _CRT_SECURE_NO_WARNINGS 
#include "Date.h"
void TestDate1()
{
  
  Date d1(2024, 4, 14);
  Date d2 = d1 + 5000;
  d1.Print();
  d2.Print();
  Date d3(2024, 4, 14);
  Date d4 = d3 - 5000;
  d3.Print();
  d4.Print();
  Date d5 (2024, 4, 14);
  d5 += -5000;//转化成-=运算,计算5000天之前的时间
  d5.Print();
}
void TestDate2()
{
  Date d1(2024, 4, 14);
  Date d2 = ++d1;
  d1.Print();
  d2.Print();
  Date d3 = d1++;
  d1.Print();
  d3.Print();
}
int main()
{
  TestDate1();
  return 0;
}

日期的比较类比较简单,不在这里示范,读者自行验证。

比如,调用TestDate1()计算未来的日期和以前的日期:

再比如,调用TestDate2()观察前置与后置运算的区别:

目录
相关文章
|
4天前
|
设计模式 安全 编译器
【C++11】特殊类设计
【C++11】特殊类设计
24 10
|
9天前
|
C++
C++友元函数和友元类的使用
C++中的友元(friend)是一种机制,允许类或函数访问其他类的私有成员,以实现数据共享或特殊功能。友元分为两类:类友元和函数友元。类友元允许一个类访问另一个类的私有数据,而函数友元是非成员函数,可以直接访问类的私有成员。虽然提供了便利,但友元破坏了封装性,应谨慎使用。
40 9
|
4天前
|
存储 编译器 C语言
【C++基础 】类和对象(上)
【C++基础 】类和对象(上)
|
12天前
|
编译器 C++
【C++】string类的使用④(字符串操作String operations )
这篇博客探讨了C++ STL中`std::string`的几个关键操作,如`c_str()`和`data()`,它们分别返回指向字符串的const char*指针,前者保证以&#39;\0&#39;结尾,后者不保证。`get_allocator()`返回内存分配器,通常不直接使用。`copy()`函数用于将字符串部分复制到字符数组,不添加&#39;\0&#39;。`find()`和`rfind()`用于向前和向后搜索子串或字符。`npos`是string类中的一个常量,表示找不到匹配项时的返回值。博客通过实例展示了这些函数的用法。
|
12天前
|
存储 C++
【C++】string类的使用③(非成员函数重载Non-member function overloads)
这篇文章探讨了C++中`std::string`的`replace`和`swap`函数以及非成员函数重载。`replace`提供了多种方式替换字符串中的部分内容,包括使用字符串、子串、字符、字符数组和填充字符。`swap`函数用于交换两个`string`对象的内容,成员函数版本效率更高。非成员函数重载包括`operator+`实现字符串连接,关系运算符(如`==`, `&lt;`等)用于比较字符串,以及`swap`非成员函数。此外,还介绍了`getline`函数,用于按指定分隔符从输入流中读取字符串。文章强调了非成员函数在特定情况下的作用,并给出了多个示例代码。
|
14天前
|
编译器 C++
【C++】如何用C++写一个日期计算器
【C++】如何用C++写一个日期计算器
|
12天前
|
C++
【C++】string类的使用④(常量成员Member constants)
C++ `std::string` 的 `find_first_of`, `find_last_of`, `find_first_not_of`, `find_last_not_of` 函数分别用于从不同方向查找目标字符或子串。它们都返回匹配位置,未找到则返回 `npos`。`substr` 用于提取子字符串,`compare` 则提供更灵活的字符串比较。`npos` 是一个表示最大值的常量,用于标记未找到匹配的情况。示例代码展示了这些函数的实际应用,如替换元音、分割路径、查找非字母字符等。
|
12天前
|
C++
C++】string类的使用③(修改器Modifiers)
这篇博客探讨了C++ STL中`string`类的修改器和非成员函数重载。文章介绍了`operator+=`用于在字符串末尾追加内容,并展示了不同重载形式。`append`函数提供了更多追加选项,包括子串、字符数组、单个字符等。`push_back`和`pop_back`分别用于在末尾添加和移除一个字符。`assign`用于替换字符串内容,而`insert`允许在任意位置插入字符串或字符。最后,`erase`函数用于删除字符串中的部分内容。每个函数都配以代码示例和说明。
|
12天前
|
安全 编译器 C++
【C++】string类的使用②(元素获取Element access)
```markdown 探索C++ `string`方法:`clear()`保持容量不变使字符串变空;`empty()`检查长度是否为0;C++11的`shrink_to_fit()`尝试减少容量。`operator[]`和`at()`安全访问元素,越界时`at()`抛异常。`back()`和`front()`分别访问首尾元素。了解这些,轻松操作字符串!💡 ```
|
12天前
|
存储 编译器 Linux
【C++】string类的使用②(容量接口Capacity )
这篇博客探讨了C++ STL中string的容量接口和元素访问方法。`size()`和`length()`函数等价,返回字符串的长度;`capacity()`提供已分配的字节数,可能大于长度;`max_size()`给出理论最大长度;`reserve()`预分配空间,不改变内容;`resize()`改变字符串长度,可指定填充字符。这些接口用于优化内存管理和适应字符串操作需求。