由于种种原因已经很久没有写博客了,最近刚学完C++类和对象,所以决定将印象比较深刻的
日期类计算器作为回归之作:
一.引言
本文的核心内容:旨在基于C++实现一个功能完整的日期类,涵盖运算符重载、日期计算等核心功能。依旧同之前一样采用函数封装的形式,对类的结构设计、核心功能实现以及测试用例设计与验证这三部分分别实现。
二.日期类核心需求分析
-
基础功能:日期合法性校验(闰年 / 平年、年、月、日范围、 2 月天数 的处理)
-
比较运算符:<、==、<=、>、>=、!= 重载,支持日期大小判断
-
算术运算符:
-
日期 + 天数、日期 += 天数(前置 / 后置 ++)
-
日期 – 天数、日期 -= 天数(前置 / 后置 —)
-
两个日期相减,计算间隔天数
-
-
输入输出:重载 <<、>>,支持日期的便捷打印与输入
三.日期类设计与实现(代码部分)
为了增强文章的可读性,我将按照核心功能实现、类的结构设计、测试用例设计与验证这样的顺序依次介绍,实际撰写顺序根据个人习惯而定。
(一)核心功能实现(Date.cpp)
1.日期合法性校验:
构造函数+GetMonthDay函数(静态数组存储每月天数、闰年判断)
//构造函数:
Date::Date(int year, int month, int day)
{
if (month > 0 && month < 13
&& day>0 && day <= GetMonthDay(year, month))
{
_year = year;
_month = month;
_day = day;
}
else
{
cout << "非法日期" << endl;
assert(false);
}
}
核心逻辑1:
- 1.月份必须在1-12之间;
- 2.日期必须在“当月天数”范围之内,通过GetMonthDay函数动态获取;
- 3.非法处理:快速定位非法日期(如2023-12-32等),打印提示并终止程序;
//GetMonthDay函数:
int Date::GetMonthDay(int year, int month)
{
//优化:局部静态,会被频繁调用,局部静态可以防止频繁创建;
static int daysArr[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
{
return 29;
}
else
{
return daysArr[month];
}
}
核心逻辑2:
- 1.静态数组:下标0弃用,下标1-12对应1-12月的默认天数;
- 2.妙用:static修饰,数组仅初始化一次,可以防止频繁创建,提高性能;
2.比较运算符重载:基于基础运算符复用
假设本次整个程序旨在实现以下功能:
| < | == | <= |
| > | >= | != |
| += | + | ++(前置) |
| ++(后置) | -= | – |
| –(前置) | –(后置) | -(日期间减法) |
以上功能已经几乎囊括了所有的日期类计算功能,当然也可自行补充。当我们一个一个去实现以上函数时,会略显繁琐,那么是否有更加简便的方法呢,这就涉及到函数复用实现,我们可以先基础实现其中几个函数,假设我先实现<、==这两个函数,那么剩余的<=、>、>=、!=都可以用前两个函数复用实现。
(1)基础实现:<、==
bool Date::operator<(const Date& x) const
{
if (_year < x._year)
{
return true;
}
else if (_year == x._year && _month < x._month)
{
return true;
}
else if (_year == x._year && _month == x._month && _day < x._day)
{
return true;
}
return false;
}
bool Date::operator==(const Date& x) const
{
return _year == x._year
&& _month == x._month
&& _day == x._day;
}
(2)复用实现:<=、>、>=、!=
bool Date::operator<=(const Date& x) const
{
return *this < x || *this == x;
}
bool Date::operator>(const Date& x) const
{
return !(*this <= x);
}
bool Date::operator>=(const Date& x) const
{
return !(*this < x);
}
bool Date::operator!=(const Date& x) const
{
return !(*this == x);
}
核心逻辑3:
- 1.通过“取反/组合”复用,减少冗余代码,降低维护成本;
- 2.*this本质是对隐式this指针的解引用,代表调用成员函数的对象本体;
3.日期加法:+=、+、++(前置、后置)
(1)operator+= & operator+
Date& Date::operator+=(int day)
{
if (day < 0)
{
return *this -= -day;
}
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
++_month;
if (_month == 13)
{
++_year;
_month = 1;
}
}
return *this;
}
Date Date::operator+(int day) const
{
Date tmp(*this);
tmp += day; //优化:复用+=
return tmp;
//tmp._day += day;
//while (tmp._day > GetMonthDay(tmp._year, tmp._month))
//{
//tmp._day -= GetMonthDay(tmp._year, tmp._month);
//++tmp._month;
//if (tmp._month == 13)
//{
//++tmp._year;
//tmp._month = 1;
//}
//}
//return tmp;
}
核心对比表1:
| 核心功能 | 原地增加天数,修改原对象 | 计算新日期,原对象保持不变 |
| 返回值类型 | Date&(自身引用) | Date(新对象值) |
| 对原对象影响 | 直接修改 | 无任何修改 |
| 链式调用 | 支持(如 d1 += 5 += 3) | 仅可作为右值链式计算(如 d1+5+3) |
| 性能 | 无拷贝,性能高 | 至少 1 次拷贝,性能稍低 |
| 实现逻辑 | 核心加法逻辑(跨月 / 负数处理) | 拷贝原对象后复用 += 实现 |
| 典型使用场景 | 需修改原日期(如日程顺延) | 需保留原日期,获取计算结果 |
| 代码联系 | 作为加法运算的核心实现,被 operator+ 复用 |
基于 operator+= 实现,无独立的加法逻辑 (注释部分为独立实现) |
| 代码差别 | 直接操作当前对象的 _year/_month/_day,无对象拷贝 | 先拷贝原对象到临时变量,调用 += 后返回新对象 |
| 关键代码解析 |
1. 负数天数转 -= 处理; 2. _day += day 后通过 while 循环处理跨月 / 跨年,逐月扣减天数直至日期合法; 3. 返回 *this(自身引用) |
1. Date tmp(*this):拷贝构造临时对象,完全复制原对象的 _year/_month/_day; 2. tmp += day:复用 += 的核心逻辑完成临时对象的日期计算; 3. return tmp:返回计算后的临时对象,原对象不受影响; 4. 函数后 const:强制保证原对象(*this)不被修改 |
(2)operator++前置 & operator++后置
//涉及到前置++、后置++,默认前置++
Date& Date::operator++()
{
*this += 1;
return *this; //前置++返回++之后的值
}
//后置++:
//增加int这个参数不是为了具体的值,只是为了避免函数名相同,仅仅是占位
//跟前置++构成重载
Date Date::operator++(int)
{
Date tmp = *this;
*this += 1;
return tmp;
}
核心对比表2:
| 核心功能 | 先将日期 + 1,再返回修改后的对象 | 先返回原日期,再将对象 + 1 |
| 参数特征 | 无参数 | 带 int 占位参数(仅用于重载区分,无实际意义) |
| 返回值类型 | Date&(自身引用) | Date(原状态的临时对象) |
| 对原对象影响 | 直接修改(日期 + 1) | 直接修改(日期 + 1) |
| 链式调用 | 支持(如 ++d1 += 5) | 不支持链式修改(如 d1++ = 5 编译报错) |
| 性能 | 无拷贝,性能高 | 需拷贝原对象,性能稍低 |
| 实现逻辑 | 复用 *this += 1,直接返回自身引用 | 1. 拷贝原对象到临时变量;2. *this += 1;3. 返回临时变量 |
| 典型使用场景 | 需优先使用修改后的值(如 ++d < 日期) | 需先使用原值,再修改(如 d++ < 日期) |
| 代码联系 | 作为自增核心实现,后置 ++ 复用其日期 + 1 逻辑 | 基于前置 ++ 的核心逻辑(+=1),增加拷贝和返回原状态的步骤 |
| 代码差别 | 无对象拷贝,直接修改后返回自身 | 先拷贝保留原状态,修改后返回原状态的临时对象 |
| 关键代码解析 | *this += 1; return *this:直接修改当前对象,返回引用保证链式调用 |
1. Date tmp = *this:拷贝原对象保留初始状态; 2. *this += 1:复用 += 逻辑完成自增; 3. return tmp:返回修改前的原对象 |
4.日期减法:-=、-、–(前置、后置)
(1)operator-= & operator-
Date& Date::operator-=(int day)
{
if (day < 0)
{
return *this += -day;
}
_day -= day;
while (_day <= 0)
{
–_month;
if (_month == 0)
{
_month = 12;
–_year;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
Date Date::operator-(int day) const
{
Date tmp(*this);
tmp -= day;
return tmp;
}
(2)operator–前置 & operator–后置
//前置–:
Date& Date::operator–()
{
*this -= 1;
return *this;
}
//后置–:
Date Date::operator–(int)
{
Date tmp = *this;
*this -= 1;
return tmp;
}
对比同上;
5.日期相减:operator-(两个日期)
int Date::operator-(const Date& d) const
{
//判断两个日期的大小;
Date max = *this;
Date min = d;
int flag = 1;
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (min != max)
{
++min;
++n;
}
return n * flag;
}
核心逻辑4:
-
1.统一计算方向:先确定大小日期(max 存大日期,min 存小日期),避免处理 “负数天数” 的循环;
-
2.计数规则:小日期每次 ++,计数器 n++,直到 min == max,最终 n * flag 得到带符号的间隔天数(如 d1(2023-04-26) – d2(2024-04-26) 返回 -366,d2 – d1 返回 366);
6.全局输入输出重载:
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
istream& operator>>(istream& in, Date& d)
{
int year, month, day;
in >> year >> month >> day;
if (month > 0 && month < 13
&& day>0 && day <= d.GetMonthDay(year, month))
{
d._year = year;
d._month = month;
d._day = day;
}
else
{
cout << "非法日期" << endl;
assert(false);
}
return in;
}
核心对比表3:
| 核心功能 | 格式化输出日期对象的年 / 月 / 日 | 读取用户输入的年 / 月 / 日,赋值并校验日期合法性 |
| 函数类型 | 全局函数(配合友元访问私有成员) | 全局函数(配合友元访问私有成员) |
| 参数特征 | ostream& out + const Date& d(d 不可改) | istream& in + Date& d(d 需修改) |
| 返回值类型 | ostream&(流对象引用) | istream&(流对象引用) |
| 对原对象影响 | 仅读取,无任何修改 | 校验合法后修改,非法则断言终止 |
| 链式调用 | 支持(如 cout << d1 << d2) | 支持(如 cin >> d1 >> d2) |
| 实现逻辑 | 直接输出 d._year/_month/_day,无校验 |
1. 读取年 / 月 / 日到临时变量; 2. 校验日期合法性; 3. 合法则赋值给 d,非法则终止 |
| 典型使用场景 | 打印 / 展示日期(如日志、结果输出) | 接收用户输入日期(如业务录入、交互场景) |
| 代码联系 | 均为全局函数,依赖友元访问 Date 私有成员 | 均为全局函数,依赖友元访问 Date 私有成员 |
| 代码差别 | 仅读操作,无需校验,逻辑简单 | 读写操作,必须校验,非法日期需处理(断言) |
| 关键代码解析 | out << d._year << "年" << d._month << "月" << d._day << "日":格式化输出,返回 out 支持链式调用 |
1. in >> year >> month >> day:读取输入值; 2. 调用 d.GetMonthDay 校验天数; 3. 合法则赋值给 d,否则断言; 4. 返回 in 支持链式输入 |
(二)类的结构设计(Date.h)
写完Date.cpp内函数之后,就可以把相应的函数放进Date.h中进行声明:
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
class Date
{
//友元函数声明:
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
Date(int year = 1, int month = 1, int day = 1);
void Print() const //加上const之后,普通对象和const对象都可以调用;
{
cout << _year << "-" << _month << "-" << _day << endl;
}
bool operator<(const Date& x) const;
bool operator==(const Date& x) const;
bool operator<=(const Date& x) const;
bool operator>(const Date& x) const;
bool operator>=(const Date& x) const;
bool operator!=(const Date& x) const;
int GetMonthDay(int year, int month);
Date& operator+=(int day);
Date operator+(int day) const;
Date& operator++();
Date operator++(int);
Date& operator-=(int day);
Date operator-(int day) const;
Date& operator–();
Date operator–(int);
int operator-(const Date& d) const;
private:
int _year;
int _month;
int _day;
};
//流插入:
ostream& operator<<(ostream& out, const Date& d);
//流输出:
istream& operator>>(istream& in, Date& d);
要点:
-
1.友元函数声明:
//友元函数声明:
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
- 因为operator<< / operator>> 是全局函数(非成员函数),但需要访问 Date 类的私有成员(_year/_month/_day),所以要用友元。其在类内部进行声明,通常放在开头,仅声明不实现。
-
2.构造函数:
Date(int year = 1, int month = 1, int day = 1);
-
带默认参数:默认构造出 1年1月1日,支持三种调用方式:
-
Date d;(全用默认值);
-
Date d(2023);(year=2023,month/day 用默认);
-
Date d(2023, 4, 26);(全自定义)
-
-
3.函数后置const:
void Print() const //加上const之后,普通对象和const对象都可以调用;
{
cout << _year << "-" << _month << "-" << _day << endl;
}
bool operator<(const Date& x) const;
bool operator==(const Date& x) const;
bool operator<=(const Date& x) const;
bool operator>(const Date& x) const;
bool operator>=(const Date& x) const;
bool operator!=(const Date& x) const;
Date operator+(int day) const;
Date operator-(int day) const;
int operator-(const Date& d) const;
- 后置 const 是函数的 “只读” 标签:承诺不修改对象,编译器帮你兜底校验;
- 只读操作的函数(如比较、取值、operator+)必须加 const,保证 const 对象能调用;
- 修改操作的函数(如 +=、++、–)不能加 const,因为核心逻辑就是修改对象。
-
4.输入输出重载:
//流插入:
ostream& operator<<(ostream& out, const Date& d);
//流输出:
istream& operator>>(istream& in, Date& d);
- operator<< / operator>> 的第一个参数是 ostream& / istream&(流对象),若作为成员函数,调用形式会变成 d1 << cout,不符合直觉,因此设计为全局函数,配合友元声明实现对私有成员的访问。
(三)测试用例设计与验证(Test.cpp)
#define _CRT_SECURE_NO_WARNINGS 1
#include"Date.h"
void TestDate1()
{
Date d1(2023, 4, 26);
d1 += 100;
d1.Print();
Date d2(2023, 4, 26);
Date d3(d2 + 100); //Date d3 = d2 + 100;
d2.Print();
d3.Print();
}
void TestDate2()
{
Date d1(2023, 4, 26);
++d1;
d1.Print();
d1++;
d1.Print();
}
void TestDate3()
{
Date d1(2023, 4, 26);
d1 -= -25;
d1.Print();
d1 += -50;
d1.Print();
}
void TestDate4()
{
Date d1(2023, 4, 26);
d1–;
d1.Print();
–d1;
d1.Print();
}
void TestDate5()
{
Date d1(2023, 4, 26);
Date d2(2024, 4, 26);
cout << d1 – d2 << endl;
cout << d2 – d1 << endl;
}
void TestDate6()
{
Date d1(2023, 4, 26);
cout << d1;
operator<<(cout, d1);
Date d2(2024, 4, 26);
Date d3(2025, 4, 26);
cout << d2 << d3 << d1;
cin >> d2 >> d1;
cout << d2 << d1;
}
void TestDate7()
{
Date d2(2024, 4, 26);
d2.Print();
const Date d3(2025, 4, 26); //const主要涉及到权限的问题;
d3.Print();
d2 < d3;
cout << (d2 < d3) << endl;
d3 < d2;
cout << (d3 < d2) << endl;
}
int main()
{
//TestDate1();
//TestDate2();
//TestDate3();
//TestDate4();
//TestDate5();
//TestDate6();
TestDate7();
return 0;
}

这一部分主要是我自己写的几个测试函数,这个根据自己想测试的东西自行定义就行,我写的作为参考,就不加以赘述了……





