C++ 类和对象入门(四):日期类 Date 的运算符重载实现详解

🔥 星恒随风: 个人主页 ❄️ 个人专栏: 《指针合集》 | 《C语言基础》 | 《数据结构》 | 《机器学习导论》 | 《前端基础》 | 《python基础》 ✨ 数据即知识,压缩即智能
目录
- [C++ 类和对象入门(四):日期类 Date 的运算符重载实现详解](#C++ 类和对象入门(四):日期类 Date 的运算符重载实现详解)
-
- 前言
- [一、日期类 Date 需要哪些基础成员?](#一、日期类 Date 需要哪些基础成员?)
-
- [1.1 成员变量](#1.1 成员变量)
- [1.2 构造函数](#1.2 构造函数)
- [1.3 日期合法性检查](#1.3 日期合法性检查)
- [1.4 获取某年某月的天数](#1.4 获取某年某月的天数)
- 二、比较运算符怎么重载?
-
- [2.1 先实现 operator<](#2.1 先实现 operator<)
- [2.2 再实现 operator==](#2.2 再实现 operator==)
- [2.3 其他比较运算符复用已有逻辑](#2.3 其他比较运算符复用已有逻辑)
- [三、日期 += 天数怎么实现?](#三、日期 += 天数怎么实现?)
-
- [3.1 operator+= 的语义](#3.1 operator+= 的语义)
- [3.2 加天数的核心思路](#3.2 加天数的核心思路)
- [3.3 负数天数怎么处理?](#3.3 负数天数怎么处理?)
- [四、日期 + 天数怎么实现?](#四、日期 + 天数怎么实现?)
-
- [4.1 operator+ 不应该修改原对象](#4.1 operator+ 不应该修改原对象)
- [4.2 复用 operator+=](#4.2 复用 operator+=)
- [五、日期 -= 天数怎么实现?](#五、日期 -= 天数怎么实现?)
-
- [5.1 operator-= 的语义](#5.1 operator-= 的语义)
- [5.2 减天数的核心思路](#5.2 减天数的核心思路)
- [5.3 负数天数怎么处理?](#5.3 负数天数怎么处理?)
- [六、日期 - 天数怎么实现?](#六、日期 - 天数怎么实现?)
-
- [6.1 operator- 不修改原对象](#6.1 operator- 不修改原对象)
- [七、日期 - 日期怎么实现?](#七、日期 - 日期怎么实现?)
-
- [7.1 operator-(const Date& d) 的语义](#7.1 operator-(const Date& d) 的语义)
- [7.2 基本思路](#7.2 基本思路)
- [八、前置 ++ 和后置 ++ 怎么区分?](#八、前置 ++ 和后置 ++ 怎么区分?)
-
- [8.1 前置 ++](#8.1 前置 ++)
- [8.2 后置 ++](#8.2 后置 ++)
- [8.3 前置 ++ 为什么返回引用,后置 ++ 为什么返回值?](#8.3 前置 ++ 为什么返回引用,后置 ++ 为什么返回值?)
- [九、-- 运算符怎么实现?](#九、-- 运算符怎么实现?)
-
- [9.1 前置 --](#9.1 前置 --)
- [9.2 后置 --](#9.2 后置 --)
- 十、流插入和流提取为什么通常写成全局函数?
-
- [10.1 operator<< 的问题](#10.1 operator<< 的问题)
- [10.2 为什么返回 ostream&?](#10.2 为什么返回 ostream&?)
- [10.3 operator>> 同理](#10.3 operator>> 同理)
- [10.4 为什么需要友元函数?](#10.4 为什么需要友元函数?)
- [十一、const 成员函数为什么重要?](#十一、const 成员函数为什么重要?)
-
- [11.1 const 修饰成员函数](#11.1 const 修饰成员函数)
- [11.2 const 本质修饰 this 指针](#11.2 const 本质修饰 this 指针)
- 十二、取地址运算符重载简单了解
- 十三、日期类运算符重载的设计主线
- 十四、本文总结
前言
前三篇我们已经讲了:
- 构造函数
- 析构函数
- 拷贝构造函数
- 赋值运算符重载
- 深浅拷贝
- 运算符重载的基本规则
这一篇直接从一个完整案例入手:
日期类 Date 的实现。
带大家一起练习一下运算符重载,。
比如:
cpp
Date d1(2024, 4, 14);
Date d2(2024, 4, 25);
d1 < d2;
d1 == d2;
d1 + 100;
d2 - 10;
d2 - d1;
++d1;
d1++;
cout << d1;
cin >> d1;
这些写法看起来很自然。
但对编译器来说,Date 是我们自己定义的类型,它不知道两个日期怎么比较,也不知道日期加 100 天该怎么算。
所以我们需要通过运算符重载,把这些规则写出来。
ps:本文不会在正文中贴完整项目代码,而是重点解释每个运算符为什么这么设计、怎么实现。完整 .cpp 文件作为附件资源供大家单独下载使用。
一、日期类 Date 需要哪些基础成员?
1.1 成员变量
日期类最基础的三个成员变量是:
cpp
int _year;
int _month;
int _day;
分别表示:
- 年
- 月
- 日
一个日期对象的状态,基本就由这三个值决定。
1.2 构造函数
日期类可以提供一个全缺省构造函数:
cpp
Date(int year = 1900, int month = 1, int day = 1);
这样既可以写:
cpp
Date d1;
也可以写:
cpp
Date d2(2024, 4, 14);
这里选择 1900-1-1 作为默认日期,是为了让默认对象处于一个明确、合法的状态。
1.3 日期合法性检查
日期类不能随便接受数据。
比如:
cpp
Date d(2024, 13, 40);
这明显不是合法日期。
所以我们通常会写一个检查函数:
cpp
bool CheckDate() const;
它需要判断:
- 月份是否在 1 到 12 之间;
- 天数是否大于等于 1;
- 天数是否没有超过当前月份的最大天数。
1.4 获取某年某月的天数
日期计算离不开每个月有多少天。
可以写一个函数:
cpp
static int GetMonthDay(int year, int month);
普通月份比较简单:
cpp
1月 31天
2月 28天或29天
3月 31天
4月 30天
...
最关键的是二月。
如果是闰年,二月有 29 天。
闰年规则是:
cpp
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
这部分是日期类后续加减天数的基础。

二、比较运算符怎么重载?
2.1 先实现 operator<
日期比较最基础的是小于:
cpp
bool operator<(const Date& d) const;
判断思路很直接:
第一,先比年份。
cpp
if (_year != d._year)
return _year < d._year;
第二,年份相同,再比月份。
cpp
if (_month != d._month)
return _month < d._month;
第三,年月都相同,再比天。
cpp
return _day < d._day;
这个顺序和我们日常比较日期完全一致。
2.2 再实现 operator==
相等也很简单。
两个日期相等,必须满足:
- 年相等
- 月相等
- 日相等
cpp
bool operator==(const Date& d) const
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
2.3 其他比较运算符复用已有逻辑
有了 < 和 ==,其他比较运算符就不必重复写复杂逻辑。
比如:
cpp
bool operator<=(const Date& d) const
{
return *this < d || *this == d;
}
大于可以写成:
cpp
bool operator>(const Date& d) const
{
return !(*this <= d);
}
大于等于:
cpp
bool operator>=(const Date& d) const
{
return !(*this < d);
}
不等于:
cpp
bool operator!=(const Date& d) const
{
return !(*this == d);
}
这种写法的好处是:
只把核心比较逻辑写一遍,其他运算符尽量复用。
这样代码更短 也更能偷懒,毕竟谁不想急头白脸的当一个CV工程 ,也更不容易写错。

三、日期 += 天数怎么实现?
3.1 operator+= 的语义
对于:
cpp
d1 += 100;
语义是:
在 d1 自己身上增加 100 天。
所以 operator+= 应该修改当前对象本身。
函数返回值通常写成:
cpp
Date& operator+=(int day);
为什么返回引用?
因为这样可以支持连续操作,也可以减少拷贝。
3.2 加天数的核心思路
假设当前日期是:
cpp
2024-4-14
执行:
cpp
d += 100;
可以先简单地把天数加到 _day 上:
cpp
_day += day;
然后不断判断:
cpp
_day 是否超过当前月份的最大天数?
如果超过,就减去当前月天数,然后月份加一。
cpp
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
++_month;
if (_month == 13)
{
++_year;
_month = 1;
}
}
这就像日历翻页:
- 日子超过本月最大天数;
- 进入下个月;
- 如果月份超过 12;
- 进入下一年。

3.3 负数天数怎么处理?
如果用户写:
cpp
d += -100;
本质上就是:
cpp
d -= 100;
所以可以直接复用:
cpp
if (day < 0)
{
return *this -= -day;
}
这样 += 和 -= 可以互相配合,减少重复代码。
四、日期 + 天数怎么实现?
4.1 operator+ 不应该修改原对象
对于:
cpp
Date d2 = d1 + 100;
语义是:
得到一个新日期,但 d1 本身不变。
所以 operator+ 通常写成:
cpp
Date operator+(int day) const;
这里加 const,表示这个函数不会修改当前对象。
4.2 复用 operator+=
实现时不要重新写一遍加天数逻辑。
可以这样做:
cpp
Date Date::operator+(int day) const
{
Date tmp = *this;
tmp += day;
return tmp;
}
意思是:
- 先拷贝一个临时对象
tmp; - 对
tmp执行+= day; - 返回
tmp。
这样 operator+ 的核心逻辑复用了 operator+=。
五、日期 -= 天数怎么实现?
5.1 operator-= 的语义
cpp
d1 -= 100;
表示:
在 d1 自己身上减去 100 天。
所以它会修改当前对象,返回值写成:
cpp
Date& operator-=(int day);
5.2 减天数的核心思路
先直接减:
cpp
_day -= day;
如果 _day <= 0,说明当前月份不够减,需要向上一个月借天数。
cpp
while (_day <= 0)
{
--_month;
if (_month == 0)
{
--_year;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
这里可以理解成:
- 当前日不够;
- 回到上一个月;
- 把上一个月的天数补给
_day; - 如果月份从 1 月退到 0 月,就变成上一年的 12 月。

5.3 负数天数怎么处理?
同样,如果写:
cpp
d -= -100;
就等价于:
cpp
d += 100;
所以可以复用:
cpp
if (day < 0)
{
return *this += -day;
}
六、日期 - 天数怎么实现?
6.1 operator- 不修改原对象
对于:
cpp
Date d2 = d1 - 100;
应该得到一个新日期。
d1 本身不变。
所以函数可以写成:
cpp
Date operator-(int day) const;
实现方式类似 operator+:
cpp
Date tmp = *this;
tmp -= day;
return tmp;
也就是复用 operator-=。
七、日期 - 日期怎么实现?
7.1 operator-(const Date& d) 的语义
对于:
cpp
int n = d2 - d1;
我们希望得到两个日期之间相差多少天。
所以函数声明可以写成:
cpp
int operator-(const Date& d) const;
7.2 基本思路
先判断哪个日期大,哪个日期小。
cpp
Date max = *this;
Date min = d;
int flag = 1;
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
然后让小日期不断 ++,直到追上大日期。
每加一天,计数器加一。
cpp
int n = 0;
while (min != max)
{
++min;
++n;
}
最后返回:
cpp
return n * flag;
如果左边日期更大,返回正数。
如果左边日期更小,返回负数。
这种写法简单直观,适合入门理解。
不过要注意:如果两个日期相差非常大,这种写法效率不是最高。后面学得更深入后,可以用"日期转总天数"的方式优化。
八、前置 ++ 和后置 ++ 怎么区分?
8.1 前置 ++
前置 ++:
cpp
++d1;
语义是:
先加一天,再返回加完后的自己。
函数写法:
cpp
Date& operator++()
{
*this += 1;
return *this;
}
返回引用,因为返回的是当前对象本身。
8.2 后置 ++
后置 ++:
cpp
d1++;
语义是:
先保存旧值,再加一天,最后返回旧值。
为了和前置 ++ 区分,C++ 规定后置 ++ 要多一个 int 形参。
cpp
Date operator++(int)
{
Date tmp(*this);
*this += 1;
return tmp;
}
这里的 int 只是为了区分前置和后置。
它的值没有实际意义,通常不写参数名。

8.3 前置 ++ 为什么返回引用,后置 ++ 为什么返回值?
前置 ++ 返回的是加完后的当前对象:
cpp
return *this;
当前对象还存在,所以可以返回引用。
后置 ++ 返回的是加之前的旧值:
cpp
Date tmp(*this);
这个旧值保存在局部对象 tmp 中。
函数结束后 tmp 会销毁,所以不能返回引用,只能返回值。
这也是前置 ++ 通常比后置 ++ 更高效的原因之一。
九、-- 运算符怎么实现?
9.1 前置 --
前置 -- 和前置 ++ 类似:
cpp
Date& operator--()
{
*this -= 1;
return *this;
}
语义是:
先减一天,再返回当前对象。
9.2 后置 --
后置 -- 和后置 ++ 类似:
cpp
Date operator--(int)
{
Date tmp(*this);
*this -= 1;
return tmp;
}
语义是:
先保存旧值,再减一天,返回旧值。
十、流插入和流提取为什么通常写成全局函数?
10.1 operator<< 的问题
我们希望这样输出日期:
cpp
cout << d1;
如果把 operator<< 写成成员函数,成员函数的第一个参数默认是 this,也就是左操作数会变成 Date 对象。
那调用形式可能会变成:
cpp
d1 << cout;
这明显不符合使用习惯。
所以 operator<< 通常写成全局函数:
cpp
ostream& operator<<(ostream& out, const Date& d);
这样第一个参数是 cout,第二个参数是日期对象。
10.2 为什么返回 ostream&?
为了支持连续输出:
cpp
cout << d1 << d2;
所以要返回输出流本身:
cpp
return out;

10.3 operator>> 同理
输入日期时,希望这样写:
cpp
cin >> d1;
所以 operator>> 也通常写成全局函数:
cpp
istream& operator>>(istream& in, Date& d);
注意这里 Date& d 不能加 const。
因为输入操作要修改日期对象。
10.4 为什么需要友元函数?
如果 _year、_month、_day 是 private,全局函数不能直接访问它们。
解决办法有几种:
- 把成员变量改成 public,不推荐;
- 提供 Get 函数;
- 把
operator<<和operator>>声明为友元函数; - 改成成员函数,但流插入不适合这么做。
常见做法是声明友元:
cpp
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
友元函数不是成员函数,但可以访问类的私有成员。
十一、const 成员函数为什么重要?
11.1 const 修饰成员函数
日期类里很多函数应该声明成 const。
例如:
cpp
void Print() const;
bool operator<(const Date& d) const;
Date operator+(int day) const;
这里函数参数列表后面的 const 表示:
这个成员函数不会修改当前对象。
11.2 const 本质修饰 this 指针
普通成员函数中,this 可以粗略理解成:
cpp
Date* const this
而 const 成员函数中,this 可以理解成:
cpp
const Date* const this
也就是说:
在 const 成员函数中,不能修改当前对象的成员变量。
这样做的好处是:
- 代码语义更清楚;
- const 对象也能调用这些成员函数;
- 编译器可以帮我们检查是否误修改对象。
十二、取地址运算符重载简单了解
日期类还可以重载取地址运算符:
cpp
Date* operator&()
{
return this;
}
const Date* operator&() const
{
return this;
}
这两个函数一般了解即可。
实际开发中很少需要自己重载取地址运算符。
如果没有特殊需求,直接使用编译器默认生成的即可。
十三、日期类运算符重载的设计主线
日期类中运算符很多,但主线其实不乱。
可以这样理解:
第一组,比较运算符:
cpp
< <= > >= == !=
核心先实现 < 和 ==,其他复用。
第二组,日期和天数运算:
cpp
+= + -= -
核心先实现 += 和 -=,+ 和 - 复用它们。
第三组,自增自减:
cpp
++ --
本质就是日期加一天或减一天。
第四组,日期减日期:
cpp
d1 - d2
返回两个日期相差的天数。
第五组,输入输出:
cpp
<< >>
因为左操作数是流对象,所以通常写成全局函数,并配合友元访问私有成员。
如果按照这条线理解,就不会觉得日期类代码是一堆零散函数。
十四、本文总结
这一篇从日期类实现出发,讲了常见运算符重载的设计思路。
比较运算符:
- 先实现
<和== - 其他比较尽量复用
日期加减天数:
+=和-=修改当前对象+和-返回新对象- 不修改当前对象的函数要加
const
自增自减:
- 前置版本返回修改后的当前对象
- 后置版本返回修改前的旧值
- 后置版本用一个
int形参和前置版本区分
日期减日期:
- 返回两个日期之间相差的天数
- 入门版本可以用逐日递增方式实现
输入输出:
<<和>>通常重载为全局函数- 返回流对象引用以支持连续输入输出
- 如果要访问私有成员,可以声明为友元函数