在学习了C++的6个默认成员函数后,我们现在动手实现一个完整的日期类,来加强对这6个默认成员函数的认识。
这是日期类中所包含的成员函数和成员变量:
构造函数
// 函数:获取某年某月的天数
inline int GetMonthDay(int year, int month)
{
// 静态数组,存储普通年份每个月的天数。注意:数组索引0未使用,实际月份从1开始
static int dayArray[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
// 默认情况下,直接从数组中获取对应月份的天数
int day = dayArray[month];
// 特殊情况处理:如果是二月(即month == 2),并且year是闰年
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
{
// 闰年的二月有29天
day = 29;
}
// 返回计算得到的该月天数
return day;
}
// 类Date的构造函数,用于初始化一个日期对象
Date::Date(int year, int month, int day)
{
// 检查传入的年、月、日是否构成一个合法的日期
if (year >= 0 // 年份需为非负数
&& month >= 1 && month <= 12 // 月份需在1-12之间
&& day >= 1 && day <= GetMonthDay(year, month)) // 日需在1-该月最大天数之间
{
// 如果合法,则设置日期成员变量的值
_year = year;
_month = month;
_day = day;
}
else
{
// 如果日期不合法,这里简单通过控制台输出错误信息
// 更严谨的做法应该是抛出一个异常,让调用者来决定如何处理这个错误
cout << "非法日期" << endl;
cout << year << "年" << month << "月" << day << "日" << endl;
}
}
GetMonthDay函数中的三个细节:
- 该函数可能被多次调用,所以我们最好将其设置为内联函数。
- 函数中存储每月天数的数组最好是用static修饰,存储在静态区,避免每次调用该函数都需要重新开辟数组。
- 逻辑与应该先判断month == 2是否为真,因为当不是2月的时候我们不必判断是不是闰年。
注意:当函数声明和定义分开时,在声明时注明缺省参数,定义时不标出缺省参数。
打印函数
// 打印函数
void Date::Print() const
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
日期and天数
日期 += 天数
对于+=运算符,我们先将需要加的天数加到日上面,然后判断日期是否合法,若不合法,则通过不断调整,直到日期合法为止。
调整日期的思路:
- 若日已满,则日减去当前月的天数,月加一。
- 若月已满,则将年加一,月置为1。
- 反复执行1和2,直到日期合法为止。
// 重载运算符 '+=',使Date对象可以加上一个整数天数
Date& Date::operator+=(int day)
{
// 如果要添加的天数是负数,转换成减法操作并调用operator-=,以复用已实现的逻辑
if (day < 0)
{
// 通过将负数转正并调用减法操作符来实现减去天数的功能
*this -= -day;
}
else
{
// 直接将天数加到当前日期的天数上
_day += day;
// 确保累加后的日期是合法的,如果不是,则逐步调整至合法日期
while (_day > GetMonthDay(_year, _month))
{
// 若累加后超过当月天数,从下个月的天数中继续累加
_day -= GetMonthDay(_year, _month);
// 进入下一个月
_month++;
// 如果月份超过12,则年份增加,并将月份重置为1
if (_month > 12)
{
_year++;
_month = 1;
}
}
}
// 返回当前对象的引用,支持链式赋值
return *this;
}
注意:当需要加的天数为负数时,转而调用-=运算符重载函数。
日期 + 天数
+运算符的重载,我们可以复用上面已经实现的+=运算符的重载函数。
但要注意:虽然我们返回的是加了之后的值,但是对象本身的值并没有改变。就像a = b + 1,b + 1的返回值是b + 1,但是b的值并没有改变。所以我们还可以用const对该函数进行修饰,防止函数内部改变了this指针指向的对象。
// 重载运算符 '+',允许Date对象与一个表示天数的整数相加,生成一个新的Date对象
Date Date::operator+(int day) const
{
// 创建当前日期对象的一个副本tmp,避免修改原始对象的值
Date tmp(*this); // 使用拷贝构造函数创建副本
// 复用已实现的operator+=方法,将天数加到tmp上
tmp += day;
// 返回累加天数后的新日期对象
return tmp;
}
注意:+=运算符的重载函数采用的是引用返回,因为出了函数作用域,this指针指向的对象没有被销毁。但+运算符的重载函数的返回值只能是传值返回,因为出了函数作用域,对象tmp就被销毁了,不能使用引用返回。
日期 -= 天数
对于-=运算符,我们先用日减去需要减的天数,然后判断日期是否合法,若不合法,则通过不断调整,直到日期合法为止。
调整日期的思路:
- 若日为负数,则月减一。
- 若月为0,则年减一,月置为12。
- 日加上当前月的天数。
- 反复执行1、2和3,直到日期合法为止。
// 重载运算符 '-=', 允许Date对象减去一个表示天数的整数,并直接修改当前对象的日期
Date& Date::operator-=(int day)
{
if (day < 0)
{
// 如果要减去的天数是负数,则转换为加上一个正数天数,复用operator+=方法
*this += -day;
}
else
{
// 直接减去天数
_day -= day;
// 确保日期合法性:如果_day小于等于0,需要向前调整月份乃至年份
while (_day <= 0)
{
// 减少月份,若月份变为0,则同时减少年份并设置月份为12(上年的12月)
_month--;
if (_month == 0)
{
_year--;
_month = 12;
}
// 根据调整后的年月获取该月的实际天数,并累加到_day中,直至_day为正数,确保日期有效
_day += GetMonthDay(_year, _month);
}
}
// 返回当前对象的引用,以便支持链式赋值
return *this;
}
注意:当需要减的天数为负数时,转而调用+=运算符重载函数。
日期 - 天数
和+运算符的重载类似,我们可以复用上面已经实现的-=运算符的重载函数,而且最好用const对该函数进行修饰,防止函数内部改变了this指针指向的对象。
// 重载运算符 '-',允许Date对象减去一个表示天数的整数,并返回一个新的Date对象表示结果
Date Date::operator-(int day) const
{
// 创建当前日期对象的一个副本tmp,以避免修改原始对象
Date tmp(*this); // 利用拷贝构造函数创建副本
// 复用已实现的operator-=方法,从副本tmp中减去指定的天数
tmp -= day;
// 返回减去天数后的新日期对象
return tmp;
}
注意:-=运算符的重载函数采用的是引用返回,但-运算符的重载函数的返回值只能是传值返回,也是由于-运算符重载函数中的tmp对象出了函数作用域被销毁了,所以不能使用引用返回。
前置and后置
前置 ++
前置++,我们可以复用+=运算符的重载函数。
// 重载前置自增运算符 '++',使Date对象自身向前推进一天并返回修改后的对象
Date& Date::operator++()
{
// 直接复用已实现的operator+=方法,将当前日期对象增加一天
*this += 1;
// 返回经过自增操作后的当前对象(即指向自身的引用)
return *this;
}
后置 ++
由于前置++和后置++的运算符均为++,为了区分它们的运算符重载,我们给后置++的运算符重载的参数加上一个int型参数,使用后置++时不需要给这个int参数传入实参,因为这里int参数的作用只是为了跟前置++构成重载。
// 重载后置自增运算符 '++',使Date对象自身向前推进一天,并返回自增前的日期对象
Date Date::operator++(int)
{
// 创建当前日期对象的一个副本tmp,用于保存自增前的日期
Date tmp(*this); // 使用拷贝构造函数创建副本
// 复用已实现的operator+=方法,将当前日期对象增加一天
*this += 1;
// 返回自增操作前的日期对象tmp
return tmp;
}
注意:后置++也是需要返回加了之前的值,只能先用对象tmp保存之前的值,然后再然对象加一,最后返回tmp对象。由于tmp对象出了该函数作用域就被销毁了,所以后置++只能使用传值返回,而前置++可以使用引用返回。
前置 --
复用前面的-=运算符的重载函数。
// 重载前置自减运算符 '--',使Date对象自身向后回退一天并返回修改后的对象
Date& Date::operator--()
{
// 直接复用已实现的operator-=方法,将当前日期对象减去一天(即向前一天)
*this -= 1;
// 返回经过自减操作后的当前对象(即指向自身的引用)
return *this;
}
后置--
// 重载后置自减运算符 '--',使Date对象自身向后回退一天,并返回回退前的日期对象
Date Date::operator--(int)
{
// 创建当前日期对象的一个副本tmp,用来保存自减操作前的日期
Date tmp(*this); // 利用拷贝构造函数创建副本
// 复用已实现的operator-=方法,将当前日期对象减去一天(即向后一天)
*this -= 1;
// 返回自减操作执行前的日期对象tmp
return tmp;
}
日期类的大小关系比较
日期类的大小关系比较需要重载的运算符看起来有6个,实际上我们只用实现两个就可以了,然后其他的通过复用这两个就可以实现。
注意:进行日期的大小比较,我们并不会改变传入对象的值,所以这6个运算符重载函数都应该被const所修饰。
>运算符的重载
>运算符的重载很简单,先判断年是否大于,再判断月是否大于,最后判断日是否大于,这其中有一者为真则函数返回true,否则返回false。
// 重载大于比较运算符 '>', 用于比较两个Date对象的大小
bool Date::operator>(const Date& d) const
{
// 首先比较年份,如果当前对象的年份大于参数对象的年份,则当前对象更大,返回true
if (_year > d._year)
{
return true;
}
// 如果年份相同,则继续比较月份
else if (_year == d._year)
{
// 当前对象的月份大于参数对象的月份,则当前对象更大,返回true
if (_month > d._month)
{
return true;
}
// 如果月份也相同,则继续比较日
else if (_month == d._month)
{
// 当前对象的日大于参数对象的日,则当前对象更大,返回true
if (_day > d._day)
{
return true;
}
}
}
// 如果以上条件都不满足,说明当前对象不大于参数对象,返回false
return false;
}
==运算符的重载
==运算符的重载也是很简单,年月日均相等,则为真。
// 重载等于比较运算符 '==', 用于判断两个Date对象是否表示相同的日期
bool Date::operator==(const Date& d) const
{
// 同时检查年、月、日是否分别相等
return (_year == d._year) // 年份相等
&& (_month == d._month) // 月份相等
&& (_day == d._day); // 日相等
// 所有条件都满足时,认为两个Date对象表示相同的日期,返回true
// 否则,返回false
}
>=运算符的重载
// 重载大于等于比较运算符 '>=', 判断当前Date对象是否大于或等于另一个Date对象
bool Date::operator>=(const Date& d) const
{
// 使用逻辑或运算符检查当前对象是否大于(使用已重载的>运算符)或等于(使用已重载的==运算符)参数对象d
return (*this > d) || (*this == d);
// 如果任何一个条件为真(即当前对象大于d,或者等于d),则返回true,表示当前对象大于等于d
// 否则,返回false
}
<运算符的重载
// 重载小于比较运算符 '<', 判断当前Date对象是否小于另一个Date对象
bool Date::operator<(const Date& d) const
{
// 通过逻辑非操作符'!'来反转大于等于运算的结果,从而判断当前对象是否小于参数对象d
return !(*this >= d);
// 如果当前对象不大于等于d(即不等于d也不大于d),则返回true,表示当前对象小于d
// 否则,返回false
}
<=运算符的重载
// 重载小于等于比较运算符 '<=', 判断当前Date对象是否小于或等于另一个Date对象
bool Date::operator<=(const Date& d) const
{
// 通过逻辑非操作符'!'来反转大于运算的结果,从而判断当前对象是否小于或等于参数对象d
return !(*this > d);
// 如果当前对象不大于d(即小于d或等于d),则返回true,表示当前对象小于等于d
// 否则,返回false
}
!=运算符的重载
// 重载不等于比较运算符 '!=', 判断当前Date对象是否与另一个Date对象表示不同的日期
bool Date::operator!=(const Date& d) const
{
// 通过逻辑非操作符'!'来反转等于运算的结果,从而判断当前对象是否不等于参数对象d
return !(*this == d);
// 如果当前对象不等于d(即年、月、日中有任一不同),则返回true,表示两个日期不同
// 否则,返回false 表示两个日期相同
}
日期 - 日期
日期 - 日期,即计算传入的两个日期相差的天数。我们只需要让较小的日期的天数一直加一,直到最后和较大的日期相等即可,这个过程中较小日期所加的总天数便是这两个日期之间差值的绝对值。若是第一个日期大于第二个日期,则返回这个差值的正值,若第一个日期小于第二个日期,则返回这个差值的负值。
// 重载减法运算符 '-', 用于计算两个Date对象之间的天数差
int Date::operator-(const Date& d) const
{
// 初始化两个临时Date对象,假设*this为较大日期(max),d为较小日期(min)
Date max = *this;
Date min = d;
// 初始化标记变量flag为1,表示差值预期为正
int flag = 1;
// 如果假设错误,即*this实际上小于d,则交换max和min,并将flag设为-1以表示差值应为负
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
// 初始化计数器n用于记录累加的总天数
int n = 0;
// 当较小的日期(min)不等于较大的日期(max)时,持续累加天数
while (min != max)
{
// 将较小的日期增加一天
min++;
// 总天数累加
n++;
}
// 最终返回n乘以flag,以确保正确的正负符号,即两个日期之间的实际天数差
return n * flag;
}
代码中使用flag变量标记返回值的正负,flag为1代表返回的是正值,flag为-1代表返回的是负值,最后返回总天数与flag相乘之后的值即可。