头文件:
首先,在头文件Date.h中声明日期类
先上代码,然后一步一步解析每个函数
#include<iostream>
#include<assert.h>
using namespace std;
class Date
{
public:
void Print() const;
// 获取某年某月的天数
// 这个函数会被频繁的调用,所以inline(类的里面)
int GetMonthDay(int year, int month)
{
// static,后面进来就不会一直初始化的,第一次就初始化,所以可以提升效率
static int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
int day = days[month];
if (month == 2
&& ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
{
day += 1;
}
return day;
}
// 检查日期是否合法
bool CheckDate()
{
if (_year >= 1
&& _month > 0 && _month < 13
&& _day>0 && _day <= GetMonthDay(_year, _month))
{
return true;
}
else
{
return false;
}
}
// 构造函数会频繁的调用,所以直接放在类里面定义,作为inline
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
assert(CheckDate());
}
private:
int _year;
int _month;
int _day;
};
Print函数
void Print() const;
Print函数就是用来打印特定格式日期的函数。
在C++中,将成员函数声明为const
表示该函数不会修改对象的状态,也就是说,它不会修改对象的成员变量。这对于确保对象的不可变性和代码的健壮性非常重要。
为什么要用const?
- 保护成员变量:声明为
const
可以确保Print
函数在执行过程中不会修改类的成员变量_year
,_month
和_day
。这是因为Print
函数的职责只是打印信息,不应该对对象的状态进行任何修改。 - 提高代码可读性和可维护性:通过在函数签名中显式地添加
const
,可以提高代码的可读性,表明这个函数不会修改对象状态。 - 允许
const
对象调用:如果一个对象是const
类型,例如: -
const Date myDate(2023, 5, 27);
只有当成员函数是const
的情况下,才能调用这个函数。因此,如果Print
函数没有声明为const
,那么就不能在const
对象上调用它。
GetMonthDay函数
int GetMonthDay(int year, int month)
首先静态数组days中包含了每个月份的天数。数组的索引对应月份,其中days[1]
表示1月的天数31天,days[2]
表示2月的天数28天,依此类推。使用静态数组是因为它只需要初始化一次,之后的函数调用中不会重复初始化,从而提高了效率。
然后通过days[month]
获取指定月份的天数,并将其赋值给day
变量。
后面的if判断是否为闰年,并且月份是否为2月。
闰年的判断条件是:年份能被4整除且不能被100整除,或者年份能被400整除。如果满足条件,则说明是闰年且当前月份是2月,因此2月有29天,将day
增加1。
这里有个小细节,month == 2在前面,这可以先判断月份是否为2,可以提高效率。
CheckDate函数
很好理解,就是判断月日是否超出界限。比如月份不能超过12。
接下来,需要重载Date的运算符,比如+-
// 声明
// 不改变的都应该加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;
bool operator<=(const Date& d) const;
Date operator+(int day) const;
Date& operator+=(int day);
// ++d1;
// d1++;
// 直接按特性重载,无法区分
// 特殊处理,使用重载区分,后置++重载增加一个int参数根前置构成函数重载进行区分
Date& operator++(); // 前置
Date operator++(int); // 后置
// d1 - 100
Date operator-(int day);
Date& operator-=(int day);
// 直接按特性重载,无法区分
// 特殊处理,使用重载区分,后置++重载增加一个int参数根前置构成函数重载进行区分
Date& operator--(); // 前置
Date operator--(int); // 后置
// d1 - d2
int operator-(const Date& d) const;
全局函数
全局
//ostream& operator<<(ostream& out, const Date& d); // 运算符重载
// 内联不要声明和定义分离
// 因为这个函数要被频繁调用,所以内联
// 流插入重载
inline ostream& operator<<(ostream& out, const Date& d) // 运算符重载
{
// out就是cout的别名
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
// 流提取重载
inline istream& operator>>(istream& in, Date& d) // 运算符重载
{
in >> d._year >> d._month >> d._day;
assert(d.CheckDate());
return in;
}
对于插入运算符<<重载:
- 输出
Date
对象:这个函数重载了插入运算符<<
,使得可以通过ostream
对象(例如cout
)来输出Date
对象的内容。 ostream& out
:输出流对象的引用,通常是cout
。因为传递的是引用,所以可以直接操作流对象。const Date& d
:常量Date
对象的引用,需要输出的Date
对象。- 支持链式操作:返回
ostream&
类型使得可以将多个输出操作链接在一起,例如cout << date1 << " " << date2;
。
对于提取运算符>>重载:
- 输入
Date
对象:这个运算符重载使得可以通过istream
对象(如cin
)来输入Date
对象的内容。 istream& in
:输入流对象的引用,通常是cin
。因为传递的是引用,所以可以直接操作流对象。Date& d
:非const
的Date
对象的引用,需要输入的Date
对象。- 支持链式操作:返回
istream&
类型使得可以将多个输入操作链接在一起,例如cin >> date1 >> date2;
,连续输入多个Date
对象。
通过重载运算符,可以直接使用 cout << date
和 cin >> date
进行日期的输入输出,简化了代码编写。
由于这两个函数是在类的外面声明的,所以需要在Date里面定义为友元函数
// 友元函数 -- 这个函数内部可以使用Date对象访问私有保护成员
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
Date.cpp
在这个文件中就可以实现在头文件中声明的那些函数了
对于Print函数
// 任何一个类,只需要写一个>= 或者 <= 重载,剩下的比较运算符重载复用即可
// const修饰的是this指针指向的内容,也就是保证了成员函数内部不会修改成员变量
// const对象和非const对象都可以调用这个函数
// void Date::Print(const Date* const this)
void Date::Print() const
{
cout << _year << "/" << _month << "/" << _day << endl;
}
任何一个类,只需要写一个>= 或者 <= 重载,剩下的比较运算符重载复用即可
==运算符
// d1 = d2
bool Date::operator==(const Date& d) const
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
!=运算符
// d1 != d2
bool Date::operator!=(const Date& d) const
{
//return _year != d._year
// && _month != d._month
// && _day != d._day;
return !(*this == d);
}
在C++中,this
指针是指向当前对象的指针。每个成员函数都隐式地包含一个this
指针,用于访问调用该函数的对象。利用this
指针,我们可以在成员函数中引用当前对象。
*this
表示当前对象的引用。通过解引用this
指针,可以得到当前对象的引用,进而可以将当前对象与另一个对象进行比较。
在定义!=
运算符重载时,直接调用==
运算符重载,避免了重复代码。
return !(*this == d);
这行代码首先通过*this
获取当前对象的引用,然后调用==
运算符重载比较当前对象与传入对象d
是否相等,并取反得到不相等的结果
>运算符
// d1 > d2
bool Date::operator>(const Date& d) const
{
if (_year > d._year
|| (_year == d._year && _month > d._month)
|| (_year == d._year && _month == d._month && _day > d._day)) // 年大
{
return true;
}
else
{
return false;
}
}
>=运算符
// d1 >= d2
bool Date::operator>=(const Date& d) const
{
return (*this > d || *this == d);
}
-
调用
>
运算符:*this > d
:使用已经定义好的>
运算符重载函数来判断当前对象是否大于传入的对象d
。 -
调用
==
运算符:*this == d
:使用已经定义好的==
运算符重载函数来判断当前对象是否等于传入的对象d
。 -
逻辑运算:
(*this > d || *this == d)
:如果当前对象大于传入的对象,或者当前对象等于传入的对象,则返回true
,否则返回false
。
<运算符
// d1 < d2
bool Date::operator<(const Date& d) const
{
return !(*this >= d);
}
<反过来就是>=,所以直接调用>=运算符然后取反即可
<=运算符
// d1 <= d2
bool Date::operator<=(const Date& d) const
{
return !(*this > d);
}
<=反过来就是>,所以直接调用>运算符然后取反即可
+=运算符
// d2 += d1 += 100
Date& Date::operator+=(int day)
{
if (day < 0)
{
return *this -= -day;
}
_day += day;
while (_day > GetMonthDay(_year, _month)) // 如果day超过了month或year的范围
{
_day -= GetMonthDay(_year, _month);
++_month;
if (_month == 13)
{
_year++;
_month = 1;
}
}
return *this; // 出了作用域用引用返回,因为返回的是自己this
}
首先
- 如果
day
为负数,则调用-=
运算符重载函数,将负天数转换为正天数并减去。 *this -= -day
:通过this
指针调用-=
运算符重载函数,返回当前对象的引用
然后增加天数
- 将
day
加到当前日期的_day
成员变量中。
然后处理日期溢出
- 使用
while
循环处理日期溢出问题,即如果天数超过了当前月份的天数,则进行调整。 GetMonthDay(_year, _month)
:获取当前年份和月份的天数。_day -= GetMonthDay(_year, _month)
:如果_day
超过当前月份的天数,则减去该月份的天数,并将月份加一。if (_month == 13)
:如果月份超过12月,则将年份加一,并将月份重置为1月。
最后返回当前对象引用 *this
- 返回当前对象的引用以支持链式调用。例如,
d1 += 100
后,可以继续进行其他操作。
+运算符
// d1 + 100
Date Date::operator+(int day) const
{
//Date ret = *this; // 也是拷贝构造
Date ret(*this); // 拷贝构造
ret += day;
return ret;
}
首先
- 使用拷贝构造函数创建一个新的
Date
对象ret
,并将当前对象*this
的值赋给它。这样ret
就是当前对象的副本。 *this
表示当前对象的引用,通过Date ret(*this);
将当前对象复制给ret
,以便在不修改当前对象的情况下进行操作。
然后
- 使用
+=
运算符重载函数将day
天数加到ret
对象中。这个操作会修改ret
对象,但不会影响当前对象*this
。
++运算符(前置和后置)
Date& Date::operator++() // 前置
{
//*this += 1;
//return *this;
return *this += 1;
}
Date Date::operator++(int) // 后置
{
Date tmp(*this);
*this += 1;
return tmp;
}
前置的很好理解
对于后置++
首先
- 使用拷贝构造函数创建一个新的
Date
对象tmp
,并将当前对象*this
的值赋给它。这样tmp
就是当前对象的副本,记录了递增前的状态。 *this
表示当前对象的引用,通过Date tmp(*this);
将当前对象复制给tmp
。
然后递增当前对象的日期
- 使用已经定义的
+=
运算符重载函数将当前对象的日期增加一天。 *this
表示当前对象,通过*this += 1;
调用+=
运算符,将日期增加一天。
然后返回原始对象的副本
- 返回先前创建的副本
tmp
,它包含了递增前的日期。 - 这种设计符合后置递增运算符的语义:先返回原值,然后再递增。
-=运算符
Date& Date::operator-=(int day)
{
if (day < 0)
{
return *this += -day;
}
_day -= day;
while (_day <= 0)
{
--_month;
if (_month == 0)
{
--_year;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
首先
- 如果
day
为负数,则将其转换为正数,并调用+=
运算符重载函数,将负天数转换为加法操作。 *this += -day
:通过this
指针调用+=
运算符重载函数,返回当前对象的引用。
然后
- 将
day
从当前日期的_day
成员变量中减去。
然后处理日期溢出
- 使用
while
循环处理日期溢出问题,即如果天数小于等于0,则进行调整。 --_month
:将月份减一。if (_month == 0)
:如果月份减到0,则年份减一,并将月份重置为12月。_day += GetMonthDay(_year, _month)
:将_day
加上前一个月的天数。
最后
- 返回当前对象的引用,以支持链式调用。例如,
d1 -= 30
后,可以继续进行其他操作。
- 运算符
Date Date::operator-(int day)
{
// 借位
Date ret = *this; // 拷贝构造
ret -= day;
return ret;
}
首先
- 使用拷贝构造函数创建一个新的
Date
对象ret
,并将当前对象*this
的值赋给它。这样ret
就是当前对象的副本。 *this
表示当前对象的引用,通过Date ret = *this;
将当前对象复制给ret
。
然后
- 使用已经定义的
-=
运算符重载函数将day
天数从ret
对象的日期中减去。 *this
表示当前对象,通过ret -= day;
调用-=
运算符,将day
减去。
为什么要拷贝构造一个对象
- 确保不修改当前对象:通过拷贝构造函数创建一个当前对象的副本,可以确保对日期的操作不会修改当前对象,从而满足纯函数的性质,即不修改输入对象,而是返回一个新的结果对象。
- 实现纯函数:重载的减法运算符需要返回一个新的对象而不是修改当前对象。为了实现这一点,必须创建一个当前对象的副本,对副本进行操作,然后返回副本。
- 避免副作用:确保对当前对象的任何修改都不会影响调用者,从而避免副作用,提高代码的健壮性和可维护性。
--操作符(前置和后置)
Date& Date::operator--() // 前置
{
return *this -= 1;
}
Date Date::operator--(int) // 后置,返回之前的值
{
Date tmp(*this);
*this -= 1;
return tmp;
}
前置很容易理解,后置--则和后置++类似,这里不再赘述
-操作符(日期相减)
// d1 - d2
int Date::operator-(const Date& d) const// 日期相减
{
int flag = 1;
// 假设第一个大,第二个小
Date max = *this;
Date min = d;
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (min != max)
{
++min;
++n;
}
return n * flag;
}
首先,初始化一个flag
- 标志变量
flag
用于确定结果的正负。当当前对象大于传入对象时,结果为正;否则为负。
然后
- 假设当前对象
*this
大于传入对象d
。将当前对象赋给max
,将传入对象赋给min
。
判断
- 如果当前对象*this小于传入对象d,则交换
max
和min
,并将flag
设置为 -1,表示结果为负。
然后计算天数差
- 使用
while
循环,直到min
等于max
。在每次循环中,递增min
,并增加计数器n
。这样就计算出了min
到max
的天数差。
最后返回结果
- 返回
n
乘以flag
,得到正确的天数差。flag
确保结果的正负正确。如果当前对象*this大于传入对象d,那么flag为1,天数差为正数,反之亦然。
Test.cpp
至此所有日期类的函数和运算符重载都写出来了,下一步可以开始测试。
测试结果就不放出来了,有兴趣可以自己测试一下并且与真实日期比较一下是否正确,下面列举几个测试的选项:
void TestDate1()
{
Date d1(2023, 8, 24);
Date d2(2024, 7, 25);
Date d3(2021, 1, 18);
cout << (d1 < d2) << endl;
cout << (d1 < d3) << endl;
cout << (d1 == d3) << endl;
cout << (d1 > d3) << endl;
}
void TestDate2()
{
Date d1(2022, 7, 24);
d1 += 5;
d1.Print();
d1 += 50; // 跨月
d1.Print();
d1 += 500; // 跨年
d1.Print();
d1 += 5000; // 跨闰年
d1.Print();
}
void TestDate3()
{
Date d1(2022, 7, 25);
(d1 - 4).Print();
(d1 - 40).Print();
(d1 - 400).Print();
(d1 - 4000).Print();
// d1 - d2
Date d2(2022, 7, 25);
Date d3(2023, 2, 15);
cout << d2 - d3 << endl;
cout << d3 - d2 << endl;
}
void Test4()
{
Date d1(2022, 7, 25);
Date d2(2022, 7, 26);
cout << d1 << d2;
cin >> d1 >> d2;
cout << d1 << d2;
}