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

适合人群: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::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

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

相关推荐
callJJ2 小时前
SpringBoot 自动配置原理详解——从“约定优于配置“到源码全程追踪
java·spring boot·后端·spring
曹牧2 小时前
Spring MVC配置文件
java·spring·mvc
I_belong_to_jesus2 小时前
信号处理新书推荐-MATLAB信号处理从入门到精通
开发语言·matlab·信号处理
小江的记录本2 小时前
【分布式】分布式一致性协议:2PC/3PC、Paxos、Raft、ZAB 核心原理、区别(2026必考Raft)
java·前端·分布式·后端·安全·面试·系统架构
做cv的小昊2 小时前
【TJU】应用统计学——第六周作业(3.3 两个正态总体参数的假设检验、3.4 非正态总体参数的假设检验、4.1 一元线性回归分析)
笔记·算法·数学建模·矩阵·回归·线性回归·学习方法
北风toto2 小时前
RestTemplate 的入门使用,直接给上作者的项目Demo
java
小熊Coding2 小时前
Python二手房数据可视化分析+预测+推荐
开发语言·python·信息可视化·django·计算机毕业设计·二手房数据分析·二手房数据可视化分析
疯狂打码的少年2 小时前
JDK 7、8、13 和 20区别深度了解
java·开发语言
艾莉丝努力练剑2 小时前
【Linux线程】Linux系统多线程(九):线程池实现(附代码示例)
linux·运维·服务器·c++·学习·架构