当前位置: 首页 > news >正文

【C++】手撕日期类——运算符重载完全指南(含易错点+底层逻辑分析)

适合人群:C++ 初学者 / 大一新生 本文会把每一个函数的算法逻辑讲清楚,不只是贴代码。


目录

一、前言:日期类能学到什么

二、类的整体定义

三、辅助函数:获取指定月份的天数

实现代码

实现细节说明

为什么下标从 0 开始存 -1?

static 关键字的作用?

inline 关键字的作用?

四、构造函数实现

全缺省构造函数

拷贝构造函数

注意事项

五、基本功能:打印函数

六、算术运算符重载

1. 日期 += 天数(核心实现)

2. 日期 + 天数(复用 +=)

3. 日期 -= 天数(核心实现)

4. 日期 - 天数(复用 -=)

七、自增自减运算符

1. 前置++

2. 后置++

3. 前置--

4. 后置--

八、比较运算符

1. 基础比较运算符(完整实现)

2. 派生比较运算符(复用 == 和 >)

九、日期差值计算

日期 - 日期

算法说明

优化思路

十、测试用例(main 函数)

十一、深入探讨:+ 复用 += 还是 += 复用 +?

1. 代码复用角度分析

2. 优劣对比(重要)

3. 关键差异详解

4. 行业实践

5. 具体到日期类的建议

6. 最终结论

总结


一、前言:日期类能学到什么

日期类是学习 C++ 面向对象编程、运算符重载的经典练习。通过它你可以掌握:

  • 构造函数 / 拷贝构造函数的写法
  • operator+=operator+等运算符重载的规范写法
  • 前置++和后置++的本质区别
  • const成员函数的使用时机
  • 代码复用的设计思路(核心)

二、类的整体定义

先把整个类的声明写出来,后面逐个实现每个函数:

#include <iostream> using namespace std; class Date { public: // 构造 / 拷贝构造 Date(int year = 2026, int month = 1, int day = 1); Date(const Date& d); // 辅助工具 inline int GetMonthDay(int year, int month); void print() const; // 算术运算符 Date& operator+=(int day); Date operator+(int day) const; Date& operator-=(int day); Date operator-(int day) const; // 自增自减 Date& operator++(); // 前置++ Date operator++(int); // 后置++ Date& operator--(); // 前置-- Date operator--(int); // 后置-- // 比较运算符 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; // 日期差 int operator-(const Date& d) const; private: int _year; int _month; int _day; };

三、辅助函数:获取指定月份的天数

实现代码

inline int Date::GetMonthDay(int year, int month) { // static:整个程序只初始化一次,不会每次调用都重建数组 static const int dayArray[13] = { -1, 31,28,31,30,31,30,31,31,30,31,30,31 }; int day = dayArray[month]; // 单独处理2月闰年情况 if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))) { day = 29; } return day; }

实现细节说明

为什么下标从 0 开始存 -1?

月份是 1~12,如果dayArray[1]对应 1 月,dayArray[2]对应 2 月……那直接用dayArray[month]就行,不需要写dayArray[month - 1],代码更直观。下标 0 没有对应月份,填 -1 只是占位,防止下标越界时拿到奇怪的值。

static关键字的作用?

static修饰局部变量时,变量存放在静态区,整个程序生命周期内只初始化一次。

这个函数会在+=的 while 循环里被反复调用,

加了static不会每次进函数都重建这个数组,有一定性能收益。

inline关键字的作用?

建议编译器把函数体展开到调用处,减少函数调用的跳转开销。

GetMonthDay在循环里被频繁调用,加inline是合理的优化。

注意这只是"建议",编译器可以忽略。

闰年判断公式:

能被 4 整除 且 不能被 100 整除→ 普通闰年(如 2024)
能被 400 整除→ 世纪闰年(如 2000)
两者满足其一即为闰年


四、构造函数实现

全缺省构造函数

Date::Date(int year=2026 int month=1, int day=1) { // 校验日期合法性 if (year >= 1 && month >= 1 && month <= 12 && day >= 1 && day <= GetMonthDay(year, month)) { _year = year; _month = month; _day = day; } else { cout << "日期非法:year=" << year << " month=" << month << " day=" << day << endl; _year = -1; _month = 0; _day = 0; } }

拷贝构造函数

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

注意事项

为什么参数要带缺省值?

带缺省值的构造函数可以实现"全缺省"调用:Date d;不传参数也合法,相当于Date d(2026, 1, 1)。这比写两个重载版本(一个无参,一个有参)更简洁。

拷贝构造的参数为什么是const Date&

  • &:引用传参,避免拷贝自身(如果值传参,传的过程中又需要调用拷贝构造,死循环)
  • const:保证被拷贝的对象在函数内不会被修改

五、基本功能:打印函数

void Date::print() const { cout << _year << " / " << _month << " / " << _day << endl; }

const是因为print只读取成员变量,不修改它们。加了const之后,const Date对象也能调用这个函数;不加的话,const对象调用会报错。


六、算术运算符重载

1. 日期 += 天数(核心实现)

算法逻辑:

_day先直接加上天数,然后进入 while 循环不断"借位":

如果当前_day超过了本月的天数,就把_day减去本月天数,然后月份进一位;

如果月份超过 12,就进位到下一年,月份重置为 1。

关键:月份进位之后,当前月份就变了,所以GetMonthDay要用新月份来算。

Date& Date::operator+=(int day) { // 如果加的是负数,转换为减法 if (day < 0) return *this -= -day; _day += day; while (_day > GetMonthDay(_year, _month)) { // 减去本月天数,月份进一位 _day -= GetMonthDay(_year, _month); if (++_month == 13) // 月份超过12,年份进位 { _month = 1; ++_year; } } return *this; // 返回修改后的自身(引用) }

逐步追踪示例:

d1 = 2024/1/30,执行d1 += 3

初始:_year=2024, _month=1, _day=30
执行:_day += 3 → _day = 33

循环第1次:GetMonthDay(2024, 1) = 31,33 > 31
_day -= 31 → _day = 2
_month++ = 2,未超过12

循环第2次:GetMonthDay(2024, 2) = 29(2024是闰年),2 <= 29,退出循环

结果:2024/2/2 ✓

返回值为什么是Date&

*this是当前对象本身,函数结束后它依然存在(不是局部变量),所以可以返回引用,避免一次拷贝。


2. 日期 + 天数(复用 +=)

Date Date::operator+(int day) const { Date tmp(*this); // 拷贝一份,不动原对象 tmp += day; // 复用 +=,核心逻辑只写一次 return tmp; // 返回新对象(值返回,不能是引用!) }

关键点:为什么这里不能返回引用?

tmp是函数内的局部变量,函数执行结束后tmp就被销毁了

如果返回Date&,调用方拿到的是一个已经销毁的对象的引用——这是悬空引用(dangling reference),行为未定义,是非常严重的错误。

注意:

operator+不修改原对象,所以函数签名后面要加const。这样const Date对象也能执行+运算。

static和const正确的理解是,

1,static表示这个函数属于这个类,而不是单独的一个对象,并且编译器不会自动传this指针

只能调用非静态成员函数

2,const表示当前调用的this指针的内容不可以修改


3. 日期 -= 天数(核心实现)

算法逻辑:

_day先直接减去天数,然后进入 while 循环"借位"

:如果_day减到 ≤ 0,说明跨月了,就把月份退一位,再把退回来的那个月的天数加给_day

注意:要先退月份,再加天数,因为要加的是退回去那个月的天数。

Date& Date::operator-=(int day) { if (day < 0) return *this += -day; _day -= day; while (_day <= 0) { // 先退月份 if (--_month < 1) { _year--; _month = 12; } // 再加上退回去那个月的天数 _day += GetMonthDay(_year, _month); } return *this; }

逐步追踪示例:

d1 = 2024/3/1,执行d1 -= 1

初始:_year=2024, _month=3, _day=1
执行:_day -= 1 → _day = 0

循环第1次:_day=0,满足 <= 0
_month-- = 2,未低于1
_day += GetMonthDay(2024, 2) = 29 → _day = 29

循环第2次:_day=29,不满足 <= 0,退出循环

结果:2024/2/29 ✓

易错陷阱:顺序不能反。

如果先加天数、再退月份,那加的是当前月(3月)的天数而不是上个月(2月)的天数,结果就会出错。


4. 日期 - 天数(复用 -=)

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

operator+,局部对象,值返回,不加引用。

注意:

这里的operator-接受的是int参数,表示"日期减去天数",返回新日期。

后面还有另一个operator-接受const Date&参数,表示"两个日期相减求天数差",

两者是不同的重载,不要搞混。


七、自增自减运算符

1. 前置++

先加 1,再返回加后的自身。

Date& Date::operator++() { *this += 1; return *this; // 返回加后的自身,可以返回引用 }

2. 后置++

先保存旧值,再加 1,返回旧值。

Date Date::operator++(int) // int 是哑元参数,只用来区分前/后置,不代表任何实际含义 { Date tmp(*this); // 保存当前状态 *this += 1; // 自增 return tmp; // 返回旧值(局部对象,必须值返回) }

注意:

后置++int参数是 C++ 规定的语法约定,编译器靠它区分前置和后置,调用时不需要传这个参数:

d++; // 编译器自动填 int=0,调用 operator++(int) ++d; // 调用 operator++()

3. 前置--

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

4. 后置--

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

重要区别总结:

版本写法返回值效率
前置++++d返回加后自身的引用较高(无拷贝)
后置++d++返回加前的旧值副本较低(多一次拷贝)

在不需要旧值的情况下,优先用前置++,比如for循环的迭代器建议写++it而不是it++


八、比较运算符

1. 基础比较运算符(完整实现)

> 运算符的重载:

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

逻辑:先比年,年大直接返回 true;年相等再比月;月也相等再比日。

== 运算符的重载:

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

2. 派生比较运算符(复用 == 和 >)

有了==>之后,其他四个比较运算符都不需要重新写逻辑,直接组合:

>= 运算符的重载:

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 !(*this == d); }

设计模式:

这是经典的"最小化实现"原则:

只完整实现最底层的==>,其余全部通过逻辑组合推导出来。

好处是:

如果比较逻辑有 bug,只需要改一处(>==),

不会出现同一个 bug 在六个函数里各出现一次的尴尬情况。


九、日期差值计算

日期 - 日期

int Date::operator-(const Date& d) const { Date max = *this; Date min = d; int sign = 1; // 确保 max >= min,sign 记录最终结果的符号 if (max < min) { max = d; min = *this; sign = -1; } int count = 0; while (min != max) { ++min; count++; } return sign * count; }

算法说明

用"小的日期一天天++,直到等于大的日期"来计数。

这是最直观的暴力方法:相差多少天就循环多少次。

sign用来处理负数情况:

d1 - d2如果 d1 比 d2 小,结果应该是负数,所以提前记录符号,最后乘回去。

优化思路

现在这个实现是 O(n),n 是两个日期相差的天数。如果两个日期相差 10 年,就要循环 3650 多次。

更高效的做法:把日期转成"距某个基准日的总天数",然后直接相减,是 O(1) 的。但对于学习运算符重载来说,现在这个写法逻辑清晰,已经足够。


十、测试用例(main 函数)

int main() { Date d1(2024, 1, 1); Date d2(d1); // 拷贝构造 // 测试比较 if (d1 == d2) cout << "d1 == d2" << endl; // 测试 += d1 += 60; d1.print(); // 2024/3/1 // 测试 +(不改变原对象) Date d3 = d1 + 10; d1.print(); // 2024/3/1(d1 不变) d3.print(); // 2024/3/11 // 测试前置/后置++ Date d4 = ++d1; // d1先加,d4=加后的d1 Date d5 = d1++; // d5=加前的d1,d1再加 d1.print(); // 2024/3/3 d4.print(); // 2024/3/2 d5.print(); // 2024/3/2 // 测试日期差 Date start(2024, 1, 1); Date end(2024, 12, 31); cout << end - start << endl; // 365 return 0; }

十一、深入探讨:+复用+=还是+=复用+

这是整篇文章的核心设计问题,同样适用于--=

1. 代码复用角度分析

方案 A:+复用+=(推荐方案)

// += 包含核心进位逻辑 Date& Date::operator+=(int day) { _day += day; while (_day > GetMonthDay(_year, _month)) { _day -= GetMonthDay(_year, _month); if (++_month == 13) { _month = 1; ++_year; } } return *this; } // + 拷贝后调用 += Date Date::operator+(int day) const { Date tmp(*this); tmp += day; return tmp; }

执行d1 + 10时的具体步骤:

第1步:拷贝构造 tmp(复制 d1 的状态,d1 完全不变)
第2步:tmp += 10(进入 operator+=,执行进位逻辑)
第3步:return tmp(值返回,触发一次拷贝/移动构造,tmp 销毁)

核心进位逻辑只在 operator+= 里出现了一次。


方案 B:+=复用+(不推荐方案)

// + 包含核心逻辑 Date Date::operator+(int day) const { Date tmp(*this); tmp._day += day; while (tmp._day > tmp.GetMonthDay(tmp._year, tmp._month)) { tmp._day -= tmp.GetMonthDay(tmp._year, tmp._month); if (++tmp._month == 13) { tmp._month = 1; ++tmp._year; } } return tmp; } // += 调用 + 然后赋值给自身 Date& Date::operator+=(int day) { *this = *this + day; // 先生成新对象,再赋值覆盖自身 return *this; }

执行d1 += 10时的具体步骤:

第1步:调用 operator+,内部拷贝构造 tmp,执行进位逻辑,生成结果对象
第2步:operator+ 返回时,值返回触发一次拷贝构造(生成临时对象)
第3步:operator= 把临时对象赋值给 *this(又一次拷贝)
第4步:临时对象销毁(析构)

共额外产生 2~3 次拷贝/赋值。


2. 优劣对比(重要)

对比维度方案 A(推荐)方案 B(不推荐)
核心逻辑位置+=里,写一次+里,写一次,但+=要绕一圈
d1 += 10的额外拷贝次数0 次(直接修改自身)2~3 次
d1 + 10的额外拷贝次数1 次(拷贝出 tmp)1 次(拷贝出 tmp)
语义是否直观是。+=就是"改自己",+是"生成新的"否。+=靠"生成新的再覆盖"实现,绕弯子
行业惯例STL、Boost 均采用此方式罕见于生产代码

为什么方案 A 更优?

核心原因有两点:

第一,语义更自然。

+=的本意是"就地修改自身",它理应是最直接的操作,不需要绕到+里再赋值回来。

+的本意是"生成新对象不动原来的",它调用+=是合理的——先复制一份,在复制上就地改,然后返回复制。

第二,性能更好。

方案 A 里d1 += 10是零拷贝的(直接在*this上操作),方案 B 里d1 += 10要经历"生成新对象 → 赋值覆盖 → 销毁旧的"这一套流程。

Date这种小对象影响不大,但如果换成std::stringstd::vector,或者你自己写的大容器,方案 B 会明显更慢。


3. 关键差异详解

两个方案的差异本质上来自两个运算符的语义不同:

+= → 就地修改,修改完返回自身引用(Date&),零拷贝 + → 不动原对象,生成并返回新对象(Date 值),有拷贝

方案 A 让"有拷贝"的+去调用"无拷贝"的+=,额外开销只有那一次必要的拷贝(复制原对象到 tmp)。

方案 B 让"无拷贝"的+=去调用"有拷贝"的+,然后再用赋值覆盖自身,相当于把本来不必要的拷贝强行引入了进来。


4. 行业实践

C++ STL 中的std::stringstd::vectorstd::chrono::duration等,无一例外都采用方案 A:+=是核心实现,+是基于+=的封装。

cppreference 上的建议也明确指出:

应该把复合赋值运算符(如+=)实现为成员函数,然后让二元运算符(如+)调用它。


5. 具体到日期类的建议

对日期类而言,方案 A 还有一个实际好处:GetMonthDay里有月份进位的稍复杂逻辑,如果在++=里各写一遍,将来发现闰年判断有 bug,要改两个地方。采用方案 A,这段逻辑只在+=里出现,改一处就够了。


6. 最终结论

运算符重载中,凡是有"就地版"(+=、-=、*=)和"生成新对象版"(+、-、*)成对出现的情况, 统一遵循: 就地版(+=)包含核心逻辑,直接操作 *this,返回 *this 引用 新对象版(+)拷贝 *this 到临时对象,调用就地版,返回临时对象

记住这一条,以后不管写什么类,运算符重载的组织方式都不会出错。


总结

知识点核心结论
+=vs+的设计+=写核心逻辑,+拷贝后调用+=
返回引用 vs 返回值返回自身成员用引用,返回局部变量用值
前置++ vs 后置++前置返回引用更高效,后置多一次拷贝
比较运算符只实现==>,其余组合出来
const成员函数不修改成员变量的函数都要加const
static局部数组只初始化一次,适合频繁调用的辅助函数
闰年判断%4==0 && %100!=0%400==0

如有错误,欢迎评论区指正。 转载请注明出处。

http://www.jsqmd.com/news/674524/

相关文章:

  • 《每个女孩都是生活家》
  • 如何利用智能照明控制器实现城市照明的“零扰民”运维?
  • ML:数据集、训练集与测试集
  • Ubuntu服务器Docker安装后必做的三件事:换源、装Portainer、设自启(避坑实录)
  • Meta烧Token成KPI,OpenClaw引发AI成本结构重塑:不拼算力拼效率
  • LeetCode热题100-单词拆分
  • 1.7k stars!Mozilla 出手了!开源 AI 客户端 Thunderbolt,让企业真正掌控自己的 AI!
  • 质子成像诊断随机磁场技术
  • 了解新能源电爪产线适配性,专业新能源汽车制造电爪厂家挑选 - 品牌2026
  • 别再用`yum install gcc`了!手把手教你源码编译安装GCC 11.2.0,打造专属开发环境
  • 2026年专业伺服电爪厂商甄选指南:伺服电爪精准控制解析 - 品牌2026
  • 利用层次聚类来提升知识检索的性能
  • SQL练习题及答案与详细分析
  • 告别网页版卡顿!手把手教你用BLAST+在Ubuntu上搭建本地序列比对环境(附批量建库脚本)
  • Dify工业知识库冷启动难题破解:仅需3人·2天·1台国产服务器,完成某汽车零部件集团全厂知识纳管
  • Go语言的文件处理操作
  • 可学习上采样方法改进YOLOv5特征图恢复:从原理到实战全解析
  • Display Driver Uninstaller终极指南:5步彻底解决显卡驱动安装难题
  • 头歌操作系统课后作业2.1
  • MySQL 索引命中机制详解
  • 追忆李商隐加密此情到惘然
  • 2026年质量好的草坪砖/四川透水砖公司哪家好 - 行业平台推荐
  • 用 BAPI 打通 SAP Gateway OData 服务,经典 SEGW 路线一次讲透
  • 每天 700 次开合跳,2 个月暴瘦一圈!在家就能练的燃脂神器
  • 2026年伺服电爪供应商选择,伺服电爪性能保障体系 - 品牌2026
  • 手把手教你用WAN2.2生成视频:SDXL风格节点详解,小白也能出片
  • SeanLib系列函数库-MyFlash
  • 30岁测试工程师的焦虑!
  • 扫频正弦啁啾信号在音频测量中的优势与应用
  • 因果AI:用户增长领域的“决策透视镜”