适合人群:C++ 初学者 / 大一新生 本文会把每一个函数的算法逻辑讲清楚,不只是贴代码。
目录
[为什么下标从 0 开始存 -1?](#为什么下标从 0 开始存 -1?)
[static 关键字的作用?](#static 关键字的作用?)
[inline 关键字的作用?](#inline 关键字的作用?)
[1. 日期 += 天数(核心实现)](#1. 日期 += 天数(核心实现))
[2. 日期 + 天数(复用 +=)](#2. 日期 + 天数(复用 +=))
[3. 日期 -= 天数(核心实现)](#3. 日期 -= 天数(核心实现))
[4. 日期 - 天数(复用 -=)](#4. 日期 - 天数(复用 -=))
[1. 前置++](#1. 前置++)
[2. 后置++](#2. 后置++)
[3. 前置--](#3. 前置--)
[4. 后置--](#4. 后置--)
[1. 基础比较运算符(完整实现)](#1. 基础比较运算符(完整实现))
[2. 派生比较运算符(复用 == 和 >)](#2. 派生比较运算符(复用 == 和 >))
[日期 - 日期](#日期 - 日期)
[十、测试用例(main 函数)](#十、测试用例(main 函数))
[十一、深入探讨:+ 复用 += 还是 += 复用 +?](#十一、深入探讨:+ 复用 += 还是 += 复用 +?)
[1. 代码复用角度分析](#1. 代码复用角度分析)
[2. 优劣对比(重要)](#2. 优劣对比(重要))
[3. 关键差异详解](#3. 关键差异详解)
[4. 行业实践](#4. 行业实践)
[5. 具体到日期类的建议](#5. 具体到日期类的建议)
[6. 最终结论](#6. 最终结论)
一、前言:日期类能学到什么
日期类是学习 C++ 面向对象编程、运算符重载的经典练习。通过它你可以掌握:
- 构造函数 / 拷贝构造函数的写法
operator+=、operator+等运算符重载的规范写法- 前置
++和后置++的本质区别 const成员函数的使用时机- 代码复用的设计思路(核心)
二、类的整体定义
先把整个类的声明写出来,后面逐个实现每个函数:
cpp
#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;
};
三、辅助函数:获取指定月份的天数
实现代码
cpp
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)
两者满足其一即为闰年
四、构造函数实现
全缺省构造函数
cpp
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;
}
}
拷贝构造函数
cpp
Date::Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
注意事项
为什么参数要带缺省值?
带缺省值的构造函数可以实现"全缺省"调用:Date d; 不传参数也合法,相当于 Date d(2026, 1, 1)。这比写两个重载版本(一个无参,一个有参)更简洁。
拷贝构造的参数为什么是 const Date&?
&:引用传参,避免拷贝自身(如果值传参,传的过程中又需要调用拷贝构造,死循环)const:保证被拷贝的对象在函数内不会被修改
五、基本功能:打印函数
cpp
void Date::print() const
{
cout << _year << " / " << _month << " / " << _day << endl;
}
加 const 是因为 print 只读取成员变量,不修改它们。加了 const 之后,const Date 对象也能调用这个函数;不加的话,const 对象调用会报错。
六、算术运算符重载
1. 日期 += 天数(核心实现)
算法逻辑:
_day 先直接加上天数,然后进入 while 循环不断"借位":
如果当前 _day 超过了本月的天数,就把 _day 减去本月天数,然后月份进一位;
如果月份超过 12,就进位到下一年,月份重置为 1。
关键:月份进位之后,当前月份就变了,所以 GetMonthDay 要用新月份来算。
cpp
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. 日期 + 天数(复用 +=)
cpp
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。
注意:要先退月份,再加天数,因为要加的是退回去那个月的天数。
cpp
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. 日期 - 天数(复用 -=)
cpp
Date Date::operator-(int day) const
{
Date tmp(*this);
tmp -= day;
return tmp;
}
同 operator+,局部对象,值返回,不加引用。
注意:
这里的 operator- 接受的是
int参数,表示"日期减去天数",返回新日期。后面还有另一个 operator- 接受
const Date&参数,表示"两个日期相减求天数差",两者是不同的重载,不要搞混。
七、自增自减运算符
1. 前置++
先加 1,再返回加后的自身。
cpp
Date& Date::operator++()
{
*this += 1;
return *this; // 返回加后的自身,可以返回引用
}
2. 后置++
先保存旧值,再加 1,返回旧值。
cpp
Date Date::operator++(int) // int 是哑元参数,只用来区分前/后置,不代表任何实际含义
{
Date tmp(*this); // 保存当前状态
*this += 1; // 自增
return tmp; // 返回旧值(局部对象,必须值返回)
}
注意:
后置 ++ 的 int 参数是 C++ 规定的语法约定,编译器靠它区分前置和后置,调用时不需要传这个参数:
d++; // 编译器自动填 int=0,调用 operator++(int)
++d; // 调用 operator++()
3. 前置--
cpp
Date& Date::operator--()
{
*this -= 1;
return *this;
}
4. 后置--
cpp
Date Date::operator--(int)
{
Date tmp(*this);
*this -= 1;
return tmp;
}
重要区别总结:
| 版本 | 写法 | 返回值 | 效率 |
|---|---|---|---|
| 前置++ | ++d |
返回加后自身的引用 | 较高(无拷贝) |
| 后置++ | d++ |
返回加前的旧值副本 | 较低(多一次拷贝) |
在不需要旧值的情况下,优先用前置 ++,比如 for 循环的迭代器建议写 ++it 而不是 it++。
八、比较运算符
1. 基础比较运算符(完整实现)
> 运算符的重载:
cpp
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;年相等再比月;月也相等再比日。
== 运算符的重载:
cpp
bool Date::operator==(const Date& d) const
{
return (_year == d._year &&
_month == d._month &&
_day == d._day);
}
2. 派生比较运算符(复用 == 和 >)
有了 == 和 > 之后,其他四个比较运算符都不需要重新写逻辑,直接组合:
>= 运算符的重载:
cpp
bool Date::operator>=(const Date& d) const
{
return (*this == d || *this > d);
}
< 运算符的重载:
cpp
bool Date::operator<(const Date& d) const
{
return !(*this >= d); // 不大于等于,就是小于
}
<= 运算符的重载:
cpp
bool Date::operator<=(const Date& d) const
{
return !(*this > d); // 不大于,就是小于等于
}
!= 运算符的重载:
cpp
bool Date::operator!=(const Date& d) const
{
return !(*this == d);
}
设计模式:
这是经典的"最小化实现"原则:
只完整实现最底层的 == 和 >,其余全部通过逻辑组合推导出来。
好处是:
如果比较逻辑有 bug,只需要改一处(> 或 ==),
不会出现同一个 bug 在六个函数里各出现一次的尴尬情况。
九、日期差值计算
日期 - 日期
cpp
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 函数)
cpp
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:+ 复用 +=(推荐方案)
cpp
// += 包含核心进位逻辑
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:+= 复用 +(不推荐方案)
cpp
// + 包含核心逻辑
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::string、std::vector,或者你自己写的大容器,方案 B 会明显更慢。
3. 关键差异详解
两个方案的差异本质上来自两个运算符的语义不同:
+= → 就地修改,修改完返回自身引用(Date&),零拷贝
+ → 不动原对象,生成并返回新对象(Date 值),有拷贝
方案 A 让"有拷贝"的
+去调用"无拷贝"的+=,额外开销只有那一次必要的拷贝(复制原对象到 tmp)。方案 B 让"无拷贝"的
+=去调用"有拷贝"的+,然后再用赋值覆盖自身,相当于把本来不必要的拷贝强行引入了进来。
4. 行业实践
C++ STL 中的 std::string、std::vector、std::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 |
如有错误,欢迎评论区指正。 转载请注明出处。