适合人群C 初学者 / 大一新生 本文会把每一个函数的算法逻辑讲清楚不只是贴代码。目录一、前言日期类能学到什么二、类的整体定义三、辅助函数获取指定月份的天数实现代码实现细节说明为什么下标从 0 开始存 -1static 关键字的作用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 year2026 int month1, int day1) { // 校验日期合法性 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初始_year2024, _month1, _day30执行_day 3 → _day 33循环第1次GetMonthDay(2024, 1) 3133 31_day - 31 → _day 2_month 2未超过12循环第2次GetMonthDay(2024, 2) 292024是闰年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正确的理解是1static表示这个函数属于这个类而不是单独的一个对象并且编译器不会自动传this指针只能调用非静态成员函数2const表示当前调用的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初始_year2024, _month3, _day1执行_day - 1 → _day 0循环第1次_day0满足 0_month-- 2未低于1_day GetMonthDay(2024, 2) 29 → _day 29循环第2次_day29不满足 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; // 编译器自动填 int0调用 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 minsign 记录最终结果的符号 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/1d1 不变 d3.print(); // 2024/3/11 // 测试前置/后置 Date d4 d1; // d1先加d4加后的d1 Date d5 d1; // d5加前的d1d1再加 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 次拷贝出 tmp1 次拷贝出 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成员函数不修改成员变量的函数都要加conststatic局部数组只初始化一次适合频繁调用的辅助函数闰年判断%40 %100!0或%4000如有错误欢迎评论区指正。 转载请注明出处。