前言
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()观察前置与后置运算的区别: