《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码

火速猛戳订阅《C++要笑着学》趣味教学博客
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片

《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
[ 本篇博客热榜最高排名:4 ]

写在前面:
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
啊,朋友们好啊。我是柠檬叶子C,上一章我们讲解了运算符重载,本篇将手把手从零开始一步步实现一个Date类,将会对每个步骤进行详细的思考和解读。

Ⅰ.实现日期类 0x00 引入
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片

为了能够更好地讲解运算符重载的知识,我们将手把手地、一步一步地实现 "日期类" ,
因为通过日期类去讲解运算符重载是比较合适的。
日期类的拷贝构造、赋值、析构我们都可以不用写,让编译器自己生成就行了。
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片


0x00 设计构造函数
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
规范一点,我们声明与定义分离开来。
Date.h

#include using namespace std; class Date { public: Date(int year = 1, int month = 1, int day = 1); // 全缺省构造 void Print() const; // 打印函数private: int _year; int _month; int _day; };

Date.cpp
include "Date.h"Date::Date(int year, int month, int day) { this->_year = year; this->_month = month; this->_day = day; }void Date::Print() const { printf("%d-%d-%d\n", this->_year, this->_month, this->_day); }int main(void) { Date d1; d1.Print(); Date d2(2022, 3, 14); d2.Print(); return 0; }

运行结果如下:
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片

《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
构造函数我们用了全缺省,这样我们不给值的时候也可以打印默认的值。
通过打印函数我们有可以把日期打印出来,这里可以加 const 修饰,
上一篇我们说过,加 const 是很好的,只要不改变都建议加上 const 。
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
比如这里的 Print 是可以加 const 的,
而构造函数这里修改了成员变量,是不能加 const 的。

《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
现在我们现在来思考一个特殊的问题……
如果,我是说如果!我们输入的日期是一个非法的日期呢?
比如:
Date d3(2022, 13, 15); // 作为地球人,怎么会有13月呢? d3.Print();

运行结果如下:
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片

《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
如果有人输入了这种日期,还是能给他打印出来,
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
这合理吗?这不合理!
有人又觉得,谁会拿这种日期初始化啊,这种日期不是一眼就能看出来有问题嘛……

说得好,那这些呢?
int main(void) { Date d4(2022, 2, 29); d4.Print(); Date d5(2009, 2, 29); d5.Print(); Date d6(2000, 2, 29); d6.Print(); return 0; }

《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
是不是没有那么容易一眼看出来了?这都涉及到闰年的问题了。
所以我们需要设计一个函数去判断,用户输入的日期到底合不合法。

Date.h
#include using namespace std; class Date { public: Date(int year = 1, int month = 1, int day = 1); // 全缺省构造 int GetMonthDay(int year, int month) const ; // 获取某年某月对应的天数 void Print() const; // 打印函数private: int _year; int _month; int _day; };

《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
因为每个月天数不统一,所以在写判断日期合法的算法前,
我们需要先设计一个可以获取一年中每个月对应天数的 GetMonthDay 函数。
如何设计呢?写12个 if else?
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
可以是可以,但是好像有点搓啊。
用 switch case 可能还好一点。我们这里可以写个简单的哈希来解决,
我们把每个月的天数放到一个数组里,为了方便还可以把下标为 0 的位置空出来,
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
这样我们就可以直接按照 "月份" 读出对应的 "日期" 了。
根据我们小时候背的口诀,依次把日期填到数组中就行了。
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片

这里还要考虑到我们上面提到的闰年问题,闰年二月会多一天。
如果月份为 2,我们就进行判断,如果是闰年就让获取到的 day + 1 即可。
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片


Date.c
int Date::GetMonthDay(int year, int month) { static int monthDatArray[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; int day = monthDatArray[month]; // 获取天数 if (month == 2// 先判断是否为二月 && ((year % 4 == 0 && year % 100 != 0)// 是二月再判断是否是闰年 || (year % 400 == 0))) { day += 1; // 是闰年,天数+1 } return day; // 返回计算的天数 }

解读:
创建一个 day 去获取月份对应的天数,然后进行判断。
如果传入的月份是2月,就判断是否是闰年。
这里的一个小细节就是我们是先判断传入的月份2月的,
如果不是2月我们是压根没有必要进行闰年判断的,
根据 && 的特性,碰到假后面后不会判断了,所以我们先判断传入的月份是否是2月,
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
是2月 —— 为真,再继续判断是否是闰年。
如果是闰年,让天数+1,就完事了。
有了GetMonthDay 函数,就解决了每个月天数不统一的问题了。

我们可以写判断部分了:
Date::Date(int year, int month, int day) { this->_year = year; this->_month = month; this->_day = day; // 判断日期是否合法 if ( ! (_year >= 0 && (month > 0 && month < 13) && (day > 0 && _day <= GetMonthDay(year, month))) ) { cout << "非法日期: "; this->Print(); // 在类里面不仅仅可以访问成员变量,还可以访问成员函数(this可省略) } }

运行结果如下:
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片

判断用户输入的日期是否合法的功能就写出来了,如果用户输入的日期不合法,
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
就把他铐起来!我们就提示用户这是一个非法日期。


0x01判断大于 operator>
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
比较两个日期的大小。

Date.h
#include using namespace std; class Date { public: Date(int year = 1, int month = 1, int day = 1); // 全缺省构造 int GetMonthDay(int year, int month) const ; // 获取某年某月对应的天数 void Print() const; // 打印函数 bool operator>(const Date& d) const; // d1 > d2private: int _year; int _month; int _day; };


Date.cpp
/* d1 > d2 */ bool Date::operator>(const Date& d) const { if (this->_year > d._year) { return true; } else if (this->_year == d._year && this->_month > d._month) { return true; } else if (this->_year == d._year && this->_month == d._month && this->_day > d._day) { return true; } else { return false; } }

解读:
日期的判断很简单,我们只需要挨个对比就可以了,
结果返回布尔值,没什么好说的。

《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
为了方便测试,我们再开一个 test.cpp 来放测试用例,方便后续测试我们的代码。
test.cpp
void DateTest1() { Date d4(2022, 2, 29); d4.Print(); Date d5(2009, 2, 29); d5.Print(); Date d6(1998, 2, 29); d6.Print(); }void DateTest2() { Date d1(2022, 2, 1); Date d2(2012, 5, 1); cout << (d1 > d2) << endl; Date d3(2022, 3, 15); cout << (d1 > d3) << endl; Date d4(d1); cout << (d1 > d4) << endl; }int main(void) { // DateTest1(); DateTest2(); return 0; }

运行结果如下:
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片



0x02日期加等天数 operator+=
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片

合并一个日期似乎没什么意义,但是加天数的场景就很多了。
比如我们想让当前日期加100天:
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片

加完后,d1 的日期就是加了100天之后的日期了,我们这里要实现的就是这个功能。

Date.h
class Date { public: // ... Date& operator+=(int day); // d1 += 100private: int _year; int _month; int _day; };

《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
日期加天数没有难的,就是一个 "进位" 而已。
把进位搞定就可以了,在动手前我们可以先画个图来分析分析怎么个进法:
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片

很明显,只需要判断加完日期后天数合不合法,
看它加完后的天数有没有超出这个月的天数,如果超过了就不合法。
这个我们刚才已经实现过 GetMonthDay 了,这里就直接拿来用就行了。
如果不合法,我们就进位。天满了往月进,月再满就往年进。
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片


Date.cpp
Date& Date::operator+=(int day) { this->_day += day; // 把要加的天数倒进d1里 while (this->_day > GetMonthDay(this->_year, this->_month)) {// 判断天数是否需要进位 this->_day -= GetMonthDay(this->_year, this->_month); // 减去当前月的天数 this->_month++; // 月份+1if (this->_month == 13) {// 判断月份是否需要进位 this->_month = 1; // 重置月份 this->_year++; // 年份+1 } } return *this; }

《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
首先把天数都倒进 d1 里,之后检查一下天数是否溢出了。
如果溢出就进位,这里的逻辑部分通过我们刚才画的图可以很轻松地实现出来,
天满了就往月进位,月满了就往年进位。
最后返回 *this ,把我们加好的 d1 再递交回去就ok了。
因为出了作用域对象还在,我们可以使用引用返回减少拷贝,岂不美哉?
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片


test.c
void DateTest3() { Date d1(2022, 1, 16); d1 += 100; // 让当前天数+100天 d1.Print(); }int main(void) { // DateTest1(); // DateTest2(); DateTest3(); return 0; }

运行结果如下:
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片



0x03日期加天数 operator+
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片

还是和 += 一样,日期加日期没有什么意义,但是 "日期 + 天数" 还是用得到的。
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片

所以我们重载 operator+ 为一个日期加天数的。

Date.h
class Date { public: // ... Date operator+(int day) const; // d1 + 100private: int _year; int _month; int _day; };

+= 是改变 "本体",但是 + 并不会,所以这里可以加个 const 修饰一下。
+ 和 += 很类似,也是通过 "判断" 就可以实现的,
因为我们刚才已经实现过 += 了,所以我们可以做一个巧妙地复用。
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
复用我们刚才实现的 += ,我们来看看是怎么操作的。

Date.c
/* d1 + 100 */ Date Date::operator+(int day) const { Date ret(*this); // 拷贝构造一个d1 // ret.operator+=(day); ret += day; // 巧妙复用+= return ret; // 出了作用域ret对象就不在了,所以不能用引用返回 }

《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
我们只需要做 "加" 的工作,把算好的日期返回回去就行了,
我们将 "本体" 复制一个 "替身" 来把结果返回去,利用拷贝构造复制出一个 ret 出来,
我们对这个 "替身" 进行加的操作,这样就不会改 "本体" 了
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
顺便提一下,因为它出了作用域会死翘翘,
所以我们 —— 不能用引用返回!不能用引用返回!不能用引用返回!
重点来了,这里巧妙地复用我们刚才已经实现好的 += ,就可以轻松搞定了:
ret.operator+=(day);

如果觉得看起来不爽,我们甚至可以直接这么写:
ret += day;

就是这么浅显易懂,可读性真的是强到炸。
+= 之后 ret 的值就是加过 day 的值了,并且是赋到 ret 身上的,所以 ——
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
直接重仓return 回去,就大功告成了,我们来测试下代码。

test.cpp
void DateTest4() { Date d1(2022, 1, 16); Date ret = d1 + 100; ret.Print(); }int main(void) { // DateTest1(); // DateTest2(); // DateTest3(); DateTest4(); return 0; }

运行结果如下:
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片

复用真的是一件很爽的事情,我们下面的讲解还会疯狂地复用的。

0x04日期减等天数 operator -=
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
我们刚才实现了 operator+= ,现在我们来实现一下 -= 。
+= 进位,那 -= 自然就是借位。
日期如果不合法,往月去借,月不够了,就往年去借。

Date.h
class Date { public: // ... Date& operator-=(int day); // d1 -= 100private: int _year; int _month; int _day; };

我们先把日期减一下,此时如果天数被减成负数了,那我们就需要进行借位操作。

Date.cpp
/* d1 -= 100 */ Date& Date::operator-=(int day) { this->_day -= day; while (this->_day <= 0) {// 天数为0或小于0了,就得借位,直到>0为止。 this->_month--; // 向月借 if (this->_month == 0) {// 判断月是否有得借 this->_year--; // 月没得借了,向年借 this->_month = 12; // 年-1了,月份置为12月 }this->_day += GetMonthDay(this->_year, this->_month); // 把借来的天数加到_day上 } return *this; }

《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
如果减完后的天数小于等于 0,就进入循环,向月 "借位" ,
因为已经借出去了,所以把 月份 - 1 。还要考虑月份会不会借完的情况,
月份为 0 的时候就是没得借了,这种情况就向年借,
之后加上通过 GetMonthDay 获取当月对应的天数,就是所谓的 "借",
循环继续判断,直到天数大于0的时候停止,返回 *this。
出了作用域 *this 还在,所以我们可以使用引用返回 Date& 。
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片


test.cpp
void DateTest5() { Date d1(2022, 3, 20); d1 -= 100; // 2021, 12, 10 d1.Print(); }int main(void) { // DateTest1(); // DateTest2(); // DateTest3(); // DateTest4(); DateTest5(); return 0; }

运行结果如下:
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片


0x05 日期减天数 operator -
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
一样的,没什么好说的,我们复用一下 -= 就可以把 - 实现出来了。

Date.h
class Date { public: // ... Date operator-(int day) const; // d1 - 100 private: int _year; int _month; int _day; };

直接复用就完事了,和 operator+ 思路一样。

Date.cpp
/* d1 - 100 */ Date Date::operator-(int day) const { Date ret(*this); // 拷贝构造一个d1 ret -= day; // ret.operator-=(day); return ret; }

《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
为了顺便带大家体验测试代码的重要性,这里的测试部分我们单独拿出来举例。

0x06体会测试代码的重要性
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
我们来好好测试一下刚才写的 operator- ,这里我们进行一个详细的测试,
测试减去的 day 跨月,跨年甚至跨闰年的情况,这样哪里出问题我们可以一目了然。

test.cpp
void DateTest6() { Date d1(2022, 1, 17); Date ret1 = d1 - 10; ret1.Print(); Date ret2 = d1 - 17; ret2.Print(); Date ret3 = d1 - 30; ret3.Print(); Date ret4 = d1 - 400; ret4.Print(); }int main(void) { // DateTest1(); // DateTest2(); // DateTest3(); // DateTest4(); // DateTest5(); DateTest6(); return 0; }

运行结果如下:
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片

Tips:我们在验证的时候可以再网上找这种在线的日期推算器,来验证一下我们写的对不对。
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片


刚才我们正常测试,确实没什么问题,我们来测试个极端的情况,
? 如果我们给的是 d1 - -100 呢?
void DateTest6() { Date d1(2022, 1, 17); Date ret = d1 - -100; ret.Print(); }

运行结果如下:
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片

我们这里代码是复用 operator-= 的,所以我们得去看看 operator-=
我们发现,在设计 operator-= 的时候是 <= 0 才算非法的,所以这种情况就没考虑到。
我们可以这么设计,在减天数之前对 day 进行一个特判,
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
因为你减负的100就相当于加正的100,就变成加了,

? Date.cpp
/* d1 -= 100 */ Date& Date::operator-=(int day) { if (day < 0) { return *this += -day; } this->_day -= day; while (this->_day <= 0) {// 天数为0或小于0了,就得借位,直到>0为止。 this->_month--; // 向月借 if (this->_month == 0) {// 判断月是否有得借 this->_year--; // 月没得借了,向年借 this->_month = 12; // 年-1了,月份置为12月 }this->_day += GetMonthDay(this->_year, this->_month); // 把借来的天数加到_day上 } return *this; }

《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片

这样就正常了。

我们再把 operator+= 处理一下:
? Date.cpp
/* d1 += 100 */ Date& Date::operator+=(int day) { if (day < 0) { return *this -= -day; } this->_day += day; // 把要加的天数倒进d1里 while (this->_day > GetMonthDay(this->_year, this->_month)) {// 判断天数是否需要进位 this->_day -= GetMonthDay(this->_year, this->_month); // 减去当前月的天数 this->_month++; // 月份+1if (this->_month == 13) {// 判断月份是否需要进位 this->_month = 1; // 重置月份 this->_year++; // 年份+1 } } return *this; }

《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
所以多写几个测试用例,来测一测各种情况,是非常有必要的。


0x07日期后置加加 operator++()
d1++; ++d1;

因为都是 operator++,后置++ 为了跟前置++ 进行区分,增加了 "参数占位" ——《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片


前置++ 就带这个 "占位标记" , 构成函数重载。
operator++(int); // 表示前置++d1++ operator++(); // 表示后置++++d1

我们这里先来实现以下前置++ :

Date.h
Date& operator++(); // ++d1;

因为前置++返回的是加加之后的值,所以我们使用引用返回。
加不加引用就取决于它出了作用域在不在。

Date.cpp
/* ++d1 */ Date& Date::operator++() { *this += 1; return *this; }

《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
这里我们直接复用 +=,加加以后的值就是 *this ,我们返回一下 *this 就行。



0x08日期前置加加 operator++(int)
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
"参数占位" ——《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
来充当后置++。

Date.h
Date operator++(int); // d1++;

因为后置++返回的是加加之前的值,所以我们不用引用返回。

Date.cpp
/* d1++ */ Date Date::operator++(int) { Date ret(*this); // 拷贝构造一个d1 *this += 1; return ret; }

《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
我们在加加之前先拷贝构造一个 "替身" 出来,"本体" 加加后,
把替身 ret 返回回去,就实现了返回加加之前的值。
这里要拷贝构造两次,所以我们推荐以后自定义类型++,使用前置++ 。

test.cpp
void DateTest7() { Date d1(2022, 3, 20); Date ret1 = d1++; // d1.operator++(&d1, 0); Date ret2 = ++d1; // d1.operator++(&d1); }int main(void) { DateTest7(); return 0; }

监测结果如下:
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片


0x09判断日期是否相同 operator==
Date.h
bool operator==(const Date& d) const; // d1 == d2


Date.cpp
/* d1 == d2 */ bool Date::operator==(const Date& d) const { return this->_year == d._year && this->_month == d._month && this->_day == d._day; }

《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
只有年月日都相等才是 true,否则就是 false。


0x0A体会复用的威力
引入:
比如我们要实现 operator<,因为大于我们已经实现过了,
我们现在来写 <,可以直接把大于改成小于:
bool Date::operator<(const Date& d) const { if (this->_year < d._year) { return true; } else if (this->_year == d._year && this->_month < d._month) { return true; } else if (this->_year == d._year && this->_month == d._month && this->_day < d._day) { return true; } else { return false; } }

《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
…… 既然都能这样了,那为什么不用用神奇的复用呢?

我们已经把 operator> 和 == 实现了,剩下的这些我们都可以复用解决。
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
技巧:对于类的比较,实现一个 > 和 == 其它直接复用就完事了。
(当然,实现一个 < 和 == 也行,随你便)
这个技巧不仅仅针对于日期类的比较,所有类要实现比较,都可以用这种方式,
除非它们不互斥(>= 取反是 < ,这就是互斥 )。
bool operator>(const Date& d) const; // d1 > d2 bool operator==(const Date& d) const; // d1 == d2


0x0B判断大于等于 operator>=
Date.h
bool operator<(const Date& d) const; // d1 < d2


Date.cpp
/* d1 >= d2 */ bool Date::operator>=(const Date& d) const { return *this > d || *this == d; }

《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
直接复用解决就完事了,这波真是秦始皇被雷劈,嬴麻了。

0x0C判断小于 operator<
Date.h
bool operator<(const Date& d) const; // d1 < d2

小于,不就是既不大于也不等于嘛。
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
所以我们直接复用 operator>= 就完事了。

Date.cpp
/* d1 < d2 */ bool Date::operator<(const Date& d) const { return !(*this >= d); }

这里取反就可以了,如果 d1 >= d2 为真,取反后 ! 则返回假。反之返回真。

0x0D判断小于等于 operator<=
Date.h
bool operator<=(const Date& d) const; // d1 <= d2


Date.cpp
/* d1 <= d2 */ bool Date::operator<=(const Date& d) const { return !(*this > d); }

《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
小于等于,就是 > 取反,没什么好说的。
这里你还可以用<|| == ,也可以的。

0x0E判断日期是否不相等 operator!=
Date.h
bool operator!=(const Date& d) const; // d1 != d2

《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
不等于和等于逻辑是相反的,直接反 ! 就完事了。

Date.cpp
/* d1 != d2*/ bool Date::operator!=(const Date& d) const { return !(*this == d); }


0x0F日期减日期 operator-
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
我们刚才实现的 operator- 是日期减天数的:
/* d1 - 100 */ Date Date::operator-(int day) const { Date ret(*this); // 拷贝构造一个d1 ret -= day; // ret.operator-=(day); return ret; }


? 如果我们想要计算一下距离暑假还有多少天呢?
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片

《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
那我们就需要写一个日期减日期版本的 operater-

Date.h
Date operator-(int day) const; // holiday - today

我们现在想的是日期减日期,我们可以让小的天数去加大的天数。

Date.cpp
/* holiday - today */ Date Date::operator-(const Date&d) const { // 我默认认为第一个大,第二个小 Date max = *this; Date min = d; int flag = 1; // 立个flag // 如果第一个小,第二个大 if (*this < d) { max = d; min = *this; flag = -1; // 说明我们立的flag立错了,改成-1 } int count = 0; while (min != max) { min++; count++; } return count * flag; }

《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
我们可以用立 flag 法。
我们先立个flag 假设 —— 第一个日期大,第二个日期小。
flag 为 1 表示第一个大第二个小,flag 为 -1 表示第一个小第二个大。
分别创建 max 和 min 表示第一个日期(d1)和第二个日期(d2)。
然后走一遍判断,核实一下立的 flag 对不对,不对的话就纠正一下。
之后我们用计数器的方式来实现就可以了。

test.cpp
void DateTest8() { Date today(2022, 1, 17); Date holiday(2022, 7, 1); cout << (holiday - today) << endl; cout << (today - holiday) << endl; }

《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片



0x10打印今天是周几 PrintWeekDay
? 打印今天是周几什么什么意思?
" 打印今天是周几,就是打印今天是星期几。" —— 节选自《 节选自 <节选> 》
《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片


Date.h
void PrintWeekDay() const;


Date.cpp
void Date::PrintWeekDay() const { // 1900.1.1 日开始算 const char* week[] = { "周一", "周二", "周三", "周四", "周五", "周六", "周日" }; Date start(1900, 1, 1); int count = *this - start; cout << week[count % 7] << endl; }

我们拿 1900年1月1号 作为起始点,两个日期相减得到的结果,
%7 作为下标去访问 week 数组里我们已经准备好的周几,打印出来就可以了。
? 这个 start 反正我们就在这用一下而已,后面也用不着了,这里就可以使用匿名对象:
void Date::PrintWeekDay() const { // 1900.1.1 日开始算 const char* week[] = { "周一", "周二", "周三", "周四", "周五", "周六", "周日" }; int count = *this - Date(1900, 1, 1); // 可以使用匿名对象 cout << week[count % 7] << endl; }


test.cpp
void DateTest9() { Date d1(2021, 3, 19); d1.PrintWeekDay(); }int main(void) { DateTest9(); return 0; }

《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片

《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码
文章图片
大功告成!!!


Ⅱ.完整代码 0x00Date.h
#include using namespace std; class Date { public: Date(int year = 1, int month = 1, int day = 1); // 全缺省构造 int GetMonthDay(int year, int month) const ; // 获取某年某月对应的天数 void Print() const; // 打印函数 bool operator>(const Date& d) const; // d1 > d2 bool operator==(const Date& d) const; // d1 == d2 bool operator>=(const Date& d) const; // d1 >= d2 bool operator<(const Date& d) const; // d1 < d2 bool operator<=(const Date& d) const; // d1 <= d2 bool operator!=(const Date& d) const; // d1 != d2 Date& operator+=(int day); // d1 += 100 Date& operator-=(int day); // d1 -= 100 Date operator+(int day) const; // d1 + 100 Date operator-(int day) const; // d1 - 100 Date& operator++(); // ++d1; Date operator++(int); // d1++; int operator-(const Date& d) const; // 日期减日期 void PrintWeekDay() const; // 返回星期几private: int _year; int _month; int _day; };


0x01Date.cpp
#include "Date.h"/* 获取月对应的天数 */ int Date::GetMonthDay(int year, int month) const { static int monthDatArray[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; // 简陋哈希 int day = monthDatArray[month]; // 获取天数 if (month == 2// 先判断是否为二月 && ((year % 4 == 0 && year % 100 != 0)// 是二月,再判断是否是闰年 || (year % 400 == 0))) { day += 1; // 是闰年,天数+1 } return day; }/* 全缺省构造函数 */ Date::Date(int year, int month, int day) { this->_year = year; this->_month = month; this->_day = day; // 判断日期是否合法 if (!(_year >= 0 && (month > 0 && month < 13) && (day > 0 && _day <= GetMonthDay(year, month)))) { cout << "非法日期: "; this->Print(); // 访问成员函数(this可省略) } }/* 打印函数 */ void Date::Print() const { printf("%d-%d-%d\n", this->_year, this->_month, this->_day); }/* d1 > d2 */ bool Date::operator>(const Date& d) const { if (this->_year > d._year) { return true; } else if (this->_year == d._year && this->_month > d._month) { return true; } else if (this->_year == d._year && this->_month == d._month && this->_day > d._day) { return true; } else { return false; } }/* d1 == d2 */ bool Date::operator==(const Date& d) const { return this->_year == d._year && this->_month == d._month && this->_day == d._day; }/* d1 >= d2 */ bool Date::operator>=(const Date& d) const { return *this > d || *this == d; }/* d1 < d2 */ bool Date::operator<(const Date& d) const { return !(*this >= d); }/* d1 <= d2 */ bool Date::operator<=(const Date& d) const { return !(*this > d); }/* d1 != d2*/ bool Date::operator!=(const Date& d) const { return !(*this == d); }/* d1 += 100 */ Date& Date::operator+=(int day) { if (day < 0) { return *this -= -day; } this->_day += day; // 把要加的天数倒进d1里 while (this->_day > GetMonthDay(this->_year, this->_month)) {// 判断天数是否需要进位 this->_day -= GetMonthDay(this->_year, this->_month); // 减去当前月的天数 this->_month++; // 月份+1if (this->_month == 13) {// 判断月份是否需要进位 this->_month = 1; // 重置月份 this->_year++; // 年份+1 } } return *this; }/* d1 -= 100 */ Date& Date::operator-=(int day) { if (day < 0) { return *this += -day; } this->_day -= day; while (this->_day <= 0) {// 天数为0或小于0了,就得借位,直到>0为止。 this->_month--; // 向月借 if (this->_month == 0) {// 判断月是否有得借 this->_year--; // 月没得借了,向年借 this->_month = 12; // 年-1了,月份置为12月 }this->_day += GetMonthDay(this->_year, this->_month); // 把借来的天数加到_day上 } return *this; }/* d1 + 100 */ Date Date::operator+(int day) const { Date ret(*this); // 拷贝构造一个d1 ret += day; // ret.operator+=(day); return ret; // 出了作用域ret对象就不在了,所以不能用引用返回 }/* d1 - 100 */ Date Date::operator-(int day) const { Date ret(*this); // 拷贝构造一个d1 ret -= day; // ret.operator-=(day); return ret; }/* ++d1 */ Date& Date::operator++() { *this += 1; return *this; }/* d1++ */ Date Date::operator++(int) { Date ret(*this); // 拷贝构造一个d1 *this += 1; return ret; }/* 日期减日期 */ int Date::operator-(const Date& d) const { // 我默认认为第一个大,第二个小 Date max = *this; Date min = d; int flag = 1; // 立个flag // 如果第一个小,第二个大 if (*this < d) { max = d; min = *this; flag = -1; // 说明我们立的flag立错了,改成-1 } int count = 0; while (min != max) { min++; count++; } return count * flag; }/* 返回星期几 */ void Date::PrintWeekDay() const { // 1900.1.1 日开始算 const char* week[] = { "周一", "周二", "周三", "周四", "周五", "周六", "周日" }; int count = *this - Date(1900, 1, 1); // 可以使用匿名对象 cout << week[count % 7] << endl; }


0x02test.cpp
#define _CRT_SECURE_NO_WARNINGS 1 #include "Date.h"void DateTest1() { Date d4(2022, 2, 29); d4.Print(); Date d5(2009, 2, 29); d5.Print(); Date d6(1998, 2, 29); d6.Print(); }void DateTest2() { Date d1(2022, 2, 1); Date d2(2012, 5, 1); cout << (d1 > d2) << endl; Date d3(2022, 3, 15); cout << (d1 > d3) << endl; Date d4(d1); cout << (d1 > d4) << endl; }void DateTest3() { Date d1(2022, 1, 16); d1 += 100; // 让当前天数+100天 d1.Print(); }void DateTest4() { Date d1(2022, 1, 16); Date ret = d1 + 100; d1.Print(); ret.Print(); }void DateTest5() { Date d1(2022, 3, 20); d1 -= 100; // 2021, 12, 10 d1.Print(); }void DateTest6() { Date d1(2022, 1, 17); Date ret = d1 - -100; ret.Print(); }void DateTest7() { Date d1(2022, 3, 20); Date ret1 = d1++; // d1.operator++(&d1, 0); Date ret2 = ++d1; // d1.operator++(&d1); }void DateTest8() { Date today(2022, 1, 17); Date holiday(2022, 7, 1); int ret1 = holiday - today; int ret2 = today - holiday; cout << ret1 << endl; cout << ret2 << endl; }void DateTest9() { Date d1(2022, 3, 19); d1.PrintWeekDay(); }int main(void) { // DateTest1(); // DateTest2(); // DateTest3(); // DateTest4(); // DateTest5(); // DateTest6(); // DateTest7(); // DateTest8(); // DateTest9(); return 0; }



参考资料:
Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .
百度百科[EB/OL]. []. https://baike.baidu.com/.
比特科技. C++[EB/OL]. 2021[2021.8.31]. .
笔者:王亦优
更新: 2022.3.20
? 勘误:暂无
声明: 由于作者水平有限,本文有错误和不准确之处在所难免,本人也很想知道这些错误,恳望读者批评指正!
【《C++要笑着学》|【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码】本章完。

    推荐阅读