C++ 类和对象入门(四):日期类 Date 的运算符重载实现详解 C 类和对象入门四日期类 Date 的运算符重载实现详解 星恒随风个人主页❄️ 个人专栏《指针合集》《C语言基础》《数据结构》《机器学习导论》《前端基础》《python基础》✨ 数据即知识压缩即智能目录C 类和对象入门四日期类 Date 的运算符重载实现详解前言一、日期类 Date 需要哪些基础成员1.1 成员变量1.2 构造函数1.3 日期合法性检查1.4 获取某年某月的天数二、比较运算符怎么重载2.1 先实现 operator2.2 再实现 operator2.3 其他比较运算符复用已有逻辑三、日期 天数怎么实现3.1 operator 的语义3.2 加天数的核心思路3.3 负数天数怎么处理四、日期 天数怎么实现4.1 operator 不应该修改原对象4.2 复用 operator五、日期 - 天数怎么实现5.1 operator- 的语义5.2 减天数的核心思路5.3 负数天数怎么处理六、日期 - 天数怎么实现6.1 operator- 不修改原对象七、日期 - 日期怎么实现7.1 operator-(const Date d) 的语义7.2 基本思路八、前置 和后置 怎么区分8.1 前置 8.2 后置 8.3 前置 为什么返回引用后置 为什么返回值九、-- 运算符怎么实现9.1 前置 --9.2 后置 --十、流插入和流提取为什么通常写成全局函数10.1 operator 的问题10.2 为什么返回 ostream10.3 operator 同理10.4 为什么需要友元函数十一、const 成员函数为什么重要11.1 const 修饰成员函数11.2 const 本质修饰 this 指针十二、取地址运算符重载简单了解十三、日期类运算符重载的设计主线十四、本文总结前言前三篇我们已经讲了构造函数析构函数拷贝构造函数赋值运算符重载深浅拷贝运算符重载的基本规则这一篇直接从一个完整案例入手日期类 Date 的实现。带大家一起练习一下运算符重载。比如Dated1(2024,4,14);Dated2(2024,4,25);d1d2;d1d2;d1100;d2-10;d2-d1;d1;d1;coutd1;cind1;这些写法看起来很自然。但对编译器来说Date是我们自己定义的类型它不知道两个日期怎么比较也不知道日期加 100 天该怎么算。所以我们需要通过运算符重载把这些规则写出来。ps本文不会在正文中贴完整项目代码而是重点解释每个运算符为什么这么设计、怎么实现。完整.cpp文件作为附件资源供大家单独下载使用。一、日期类 Date 需要哪些基础成员1.1 成员变量日期类最基础的三个成员变量是int_year;int_month;int_day;分别表示年月日一个日期对象的状态基本就由这三个值决定。1.2 构造函数日期类可以提供一个全缺省构造函数Date(intyear1900,intmonth1,intday1);这样既可以写Date d1;也可以写Dated2(2024,4,14);这里选择1900-1-1作为默认日期是为了让默认对象处于一个明确、合法的状态。1.3 日期合法性检查日期类不能随便接受数据。比如Dated(2024,13,40);这明显不是合法日期。所以我们通常会写一个检查函数boolCheckDate()const;它需要判断月份是否在 1 到 12 之间天数是否大于等于 1天数是否没有超过当前月份的最大天数。1.4 获取某年某月的天数日期计算离不开每个月有多少天。可以写一个函数staticintGetMonthDay(intyear,intmonth);普通月份比较简单1月31天2月28天或29天3月31天4月30天...最关键的是二月。如果是闰年二月有 29 天。闰年规则是(year%40year%100!0)||(year%4000)这部分是日期类后续加减天数的基础。二、比较运算符怎么重载2.1 先实现 operator日期比较最基础的是小于booloperator(constDated)const;判断思路很直接第一先比年份。if(_year!d._year)return_yeard._year;第二年份相同再比月份。if(_month!d._month)return_monthd._month;第三年月都相同再比天。return_dayd._day;这个顺序和我们日常比较日期完全一致。2.2 再实现 operator相等也很简单。两个日期相等必须满足年相等月相等日相等booloperator(constDated)const{return_yeard._year_monthd._month_dayd._day;}2.3 其他比较运算符复用已有逻辑有了和其他比较运算符就不必重复写复杂逻辑。比如booloperator(constDated)const{return*thisd||*thisd;}大于可以写成booloperator(constDated)const{return!(*thisd);}大于等于booloperator(constDated)const{return!(*thisd);}不等于booloperator!(constDated)const{return!(*thisd);}这种写法的好处是只把核心比较逻辑写一遍其他运算符尽量复用。这样代码更短也更能偷懒毕竟谁不想急头白脸的当一个CV工程也更不容易写错。三、日期 天数怎么实现3.1 operator 的语义对于d1100;语义是在 d1 自己身上增加 100 天。所以operator应该修改当前对象本身。函数返回值通常写成Dateoperator(intday);为什么返回引用因为这样可以支持连续操作也可以减少拷贝。3.2 加天数的核心思路假设当前日期是2024-4-14执行d100;可以先简单地把天数加到_day上_dayday;然后不断判断_day 是否超过当前月份的最大天数如果超过就减去当前月天数然后月份加一。while(_dayGetMonthDay(_year,_month)){_day-GetMonthDay(_year,_month);_month;if(_month13){_year;_month1;}}这就像日历翻页日子超过本月最大天数进入下个月如果月份超过 12进入下一年。3.3 负数天数怎么处理如果用户写d-100;本质上就是d-100;所以可以直接复用if(day0){return*this--day;}这样和-可以互相配合减少重复代码。四、日期 天数怎么实现4.1 operator 不应该修改原对象对于Date d2d1100;语义是得到一个新日期但 d1 本身不变。所以operator通常写成Dateoperator(intday)const;这里加const表示这个函数不会修改当前对象。4.2 复用 operator实现时不要重新写一遍加天数逻辑。可以这样做Date Date::operator(intday)const{Date tmp*this;tmpday;returntmp;}意思是先拷贝一个临时对象tmp对tmp执行 day返回tmp。这样operator的核心逻辑复用了operator。五、日期 - 天数怎么实现5.1 operator- 的语义d1-100;表示在 d1 自己身上减去 100 天。所以它会修改当前对象返回值写成Dateoperator-(intday);5.2 减天数的核心思路先直接减_day-day;如果_day 0说明当前月份不够减需要向上一个月借天数。while(_day0){--_month;if(_month0){--_year;_month12;}_dayGetMonthDay(_year,_month);}这里可以理解成当前日不够回到上一个月把上一个月的天数补给_day如果月份从 1 月退到 0 月就变成上一年的 12 月。5.3 负数天数怎么处理同样如果写d--100;就等价于d100;所以可以复用if(day0){return*this-day;}六、日期 - 天数怎么实现6.1 operator- 不修改原对象对于Date d2d1-100;应该得到一个新日期。d1本身不变。所以函数可以写成Dateoperator-(intday)const;实现方式类似operatorDate tmp*this;tmp-day;returntmp;也就是复用operator-。七、日期 - 日期怎么实现7.1 operator-(const Date d) 的语义对于intnd2-d1;我们希望得到两个日期之间相差多少天。所以函数声明可以写成intoperator-(constDated)const;7.2 基本思路先判断哪个日期大哪个日期小。Date max*this;Date mind;intflag1;if(*thisd){maxd;min*this;flag-1;}然后让小日期不断直到追上大日期。每加一天计数器加一。intn0;while(min!max){min;n;}最后返回returnn*flag;如果左边日期更大返回正数。如果左边日期更小返回负数。这种写法简单直观适合入门理解。不过要注意如果两个日期相差非常大这种写法效率不是最高。后面学得更深入后可以用“日期转总天数”的方式优化。八、前置 和后置 怎么区分8.1 前置 前置d1;语义是先加一天再返回加完后的自己。函数写法Dateoperator(){*this1;return*this;}返回引用因为返回的是当前对象本身。8.2 后置 后置d1;语义是先保存旧值再加一天最后返回旧值。为了和前置区分C 规定后置要多一个int形参。Dateoperator(int){Datetmp(*this);*this1;returntmp;}这里的int只是为了区分前置和后置。它的值没有实际意义通常不写参数名。8.3 前置 为什么返回引用后置 为什么返回值前置返回的是加完后的当前对象return*this;当前对象还存在所以可以返回引用。后置返回的是加之前的旧值Datetmp(*this);这个旧值保存在局部对象tmp中。函数结束后tmp会销毁所以不能返回引用只能返回值。这也是前置通常比后置更高效的原因之一。九、-- 运算符怎么实现9.1 前置 –前置--和前置类似Dateoperator--(){*this-1;return*this;}语义是先减一天再返回当前对象。9.2 后置 –后置--和后置类似Dateoperator--(int){Datetmp(*this);*this-1;returntmp;}语义是先保存旧值再减一天返回旧值。十、流插入和流提取为什么通常写成全局函数10.1 operator 的问题我们希望这样输出日期coutd1;如果把operator写成成员函数成员函数的第一个参数默认是this也就是左操作数会变成Date对象。那调用形式可能会变成d1cout;这明显不符合使用习惯。所以operator通常写成全局函数ostreamoperator(ostreamout,constDated);这样第一个参数是cout第二个参数是日期对象。10.2 为什么返回 ostream为了支持连续输出coutd1d2;所以要返回输出流本身returnout;10.3 operator 同理输入日期时希望这样写cind1;所以operator也通常写成全局函数istreamoperator(istreamin,Dated);注意这里Date d不能加const。因为输入操作要修改日期对象。10.4 为什么需要友元函数如果_year、_month、_day是private全局函数不能直接访问它们。解决办法有几种把成员变量改成 public不推荐提供 Get 函数把operator和operator声明为友元函数改成成员函数但流插入不适合这么做。常见做法是声明友元friendostreamoperator(ostreamout,constDated);friendistreamoperator(istreamin,Dated);友元函数不是成员函数但可以访问类的私有成员。十一、const 成员函数为什么重要11.1 const 修饰成员函数日期类里很多函数应该声明成const。例如voidPrint()const;booloperator(constDated)const;Dateoperator(intday)const;这里函数参数列表后面的const表示这个成员函数不会修改当前对象。11.2 const 本质修饰 this 指针普通成员函数中this可以粗略理解成Date*constthis而const成员函数中this可以理解成constDate*constthis也就是说在 const 成员函数中不能修改当前对象的成员变量。这样做的好处是代码语义更清楚const 对象也能调用这些成员函数编译器可以帮我们检查是否误修改对象。十二、取地址运算符重载简单了解日期类还可以重载取地址运算符Date*operator(){returnthis;}constDate*operator()const{returnthis;}这两个函数一般了解即可。实际开发中很少需要自己重载取地址运算符。如果没有特殊需求直接使用编译器默认生成的即可。十三、日期类运算符重载的设计主线日期类中运算符很多但主线其实不乱。可以这样理解第一组比较运算符!核心先实现和其他复用。第二组日期和天数运算--核心先实现和-和-复用它们。第三组自增自减--本质就是日期加一天或减一天。第四组日期减日期d1-d2返回两个日期相差的天数。第五组输入输出因为左操作数是流对象所以通常写成全局函数并配合友元访问私有成员。如果按照这条线理解就不会觉得日期类代码是一堆零散函数。十四、本文总结这一篇从日期类实现出发讲了常见运算符重载的设计思路。比较运算符先实现和其他比较尽量复用日期加减天数和-修改当前对象和-返回新对象不修改当前对象的函数要加const自增自减前置版本返回修改后的当前对象后置版本返回修改前的旧值后置版本用一个int形参和前置版本区分日期减日期返回两个日期之间相差的天数入门版本可以用逐日递增方式实现输入输出和通常重载为全局函数返回流对象引用以支持连续输入输出如果要访问私有成员可以声明为友元函数