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

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;
}

意思是:

  1. 先拷贝一个临时对象 tmp
  2. tmp 执行 += day
  3. 返回 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_dayprivate,全局函数不能直接访问它们。

解决办法有几种:

  • 把成员变量改成 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 形参和前置版本区分

日期减日期:

  • 返回两个日期之间相差的天数
  • 入门版本可以用逐日递增方式实现

输入输出:

  • <<>> 通常重载为全局函数
  • 返回流对象引用以支持连续输入输出
  • 如果要访问私有成员,可以声明为友元函数
相关推荐
wuminyu2 小时前
Java锁机制之park与futex系统级协同机制解析
java·linux·c语言·jvm·c++
疯狂打码的少年2 小时前
编译程序与解释程序的区别
java·开发语言·笔记
caimouse5 小时前
reactos编码规范
c语言·开发语言
xieliyu.9 小时前
Java算法精讲:双指针(三)
java·开发语言·算法
数智工坊10 小时前
机器人运动控制:采样、优化与学习三大流派深度对比与实战
android·学习·机器人
CryptoPP10 小时前
快速对接东京证券交易所API数据:实战指南与代码示例
开发语言·人工智能·windows·python·信息可视化·区块链
ZC跨境爬虫10 小时前
跟着 MDN 学JavaScript day_7:数学运算与逻辑判断实战测试
开发语言·前端·javascript·学习·ecmascript
阳区欠11 小时前
【LangChain】LLM基础介绍
开发语言·python·langchain
Jinkxs11 小时前
Java 跨域14-Java 与区块链(Hyperledger)集成
java·开发语言·区块链