✨个人主页: Yohifo
🎉所属专栏: C++修行之路
🎊每篇一句: 图片来源
The pessimist complains about the wind; the optimist expects it to change; the realist adjusts the sails.
悲观主义者抱怨风;乐观主义者期望它改变;现实主义者调整风帆。
🗓️前言
在学完类和对象相关知识后,需要一个程序来供我们练习、巩固知识点,日期类就是我们练习的首选程序,日期类实现简单且功能丰富,相信在完整地将日期类实现后,能对类和对象有更好的掌握及更深的理解
🗓️正文
为了更符合工程标准,这里采用三个文件的方式实现程序
用于声明类和方法的 .h 头文件
Date.h
用于实现类和方法的 .cpp 源文件
Date.cpp
用于测试功能的 .cpp 源文件
test.cpp
📆类的定义
先简单定义一下每个类中都有的默认成员函数
//当前位于文件 Date.h 中usingstd::cout; //采用部分展开的方式usingstd::cin; //采用命名空间namespaceYohifo{ classDate { public: //构造函数,频繁使用且短小的代码直接在类的声明中实现,成为内联函数Date(intyear=2023, intmonth=2, intday=11) :_year(year) , _month(month) , _day(day) {} //拷贝构造函数Date(constDate&d) { _year=d._year; _month=d._month; _day=d._day; } //赋值重载函数Date&operator=(constDate&d) { if (this==&d) return*this; _year=d._year; _month=d._month; _day=d._day; return*this; } //析构函数~Date() { _year=1970; _month=2; _day=11; } private: int_year; //年、月、日int_month; int_day; }; }
📅合法性检验
首先编写第一个函数:合法性检验
检验标准
年不能为0
月在区间 [1, 12] 内,超过为非法
根据年月推算出天数,天数不能操作规定天数,也不能 <= 0
注意:
当前包括后续函数都是采取先在头文件 Date.h 的类中声明,再到 Date.cpp 实现的路径
因历史原因导致的闰年变动这里不考虑,该程序实现的是理想情况下的闰年状态
程序计算范围覆盖至公元前,限度为 [INT_MIN, INT_MAX]
usingnamespaceYohifo; //全局展开命名空间//合法性检验boolDate::check() const{ //年不能为0年if (_year==0) returnfalse; //月份区间 [1, 12]if (_month<1||_month>12) returnfalse; //天数要合理// getMonthDay 函数后续实现if (_day<1||_day>getMonthDay()) returnfalse; returntrue; }
📅判断闰年
闰年二月多一天,因此需要特殊处理
闰年判断技巧: 四年一闰且百年不闰 或者 四百年一闰
//闰年判断boolDate::checkLeapYear() const{ //按照技巧判断if (((_year%4==0) && (_year%100!=0)) || (_year%400==0)) returntrue; elsereturnfalse; }
📅获取年份天数
闰年多一天,为 366 ,非闰年为 365,判断返回即可
//获取年份天数intDate::getYearDay() const{ //复用代码return (checkLeapYear() ?366 : 365); }
📅获取月份天数
根据当前年份和月份,判断当月有多少天
注意: 闰年的二月需要特殊处理
//获取月份天数intDate::getMonthDay() const{ //非闰年情况下每个月天数,其中下标代表月份intarr[] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; //如果为2月,且为闰年,直接返回 2月+1天if (_month==2&&checkLeapYear()) returnarr[_month] +1; elsereturnarr[_month]; }
📆运算符重载
前面学习了 operator 运算符重载,现在正好可以拿来练练手
📅判断等于
两个日期相等的前提是 年、月、日都相等
//运算符重载//判断等于boolDate::operator==(constDate&d) const{ return ((_year==d._year) && (_month==d._month) && (_day==d._day)); }
📅判断小于
注意: 我们的运算顺序都是 左操作数、右操作数,其中隐含的 this 指针默认为 左操作数
*this 小于 d 的逻辑
首选判断年是否小于
年相等,判断月是否小于
年相等,月相等,判断天是否小于
//判断小于boolDate::operator<(constDate&d) const{ if (_year<d._year) //判断年returntrue; elseif (_year==d._year&&_month<d._month) //判断月returntrue; elseif (_year==d._year&&_month==d._month&&_day<d._day) //判断天returntrue; elsereturnfalse;
📅复用至所有判断
善用代码复用,有了等于和小于,我们可以直接写出所有判断
//判断不等于boolDate::operator!=(constDate&d) const{ return!(*this==d); //等于,取反为不等于} //判断小于等于boolDate::operator<=(constDate&d) const{ //小于、等于成立一个即可return ((*this<d) || (*this==d)); } //判断大于boolDate::operator>(constDate&d) const{ //即不小于,也不等于return (!(*this<d) &&!(*this==d)); } //判断大于等于boolDate::operator>=(constDate&d) const{ //大于或等于return ((*this>d) || (*this==d)); }
📅重载流插入、提取
cout、cin 只能输出、输出内置类型,但如果我们对它进行改造一下,就能直接输出我们的自定义类型
注意:
cout 类型为 ostream,cin 类型为 istream
要使得 cout、cin 变为重载后的左操作数,此时的运算符重载就不能写在类内,因为在类中的函数默认 this 为第一个参数,即左操作数
因此这两个函数比较特殊,需要写在外面,但同时又得访问类中的成员,此时就需要 友元函数
两个函数都有返回值,返回的就是cout、cin本身,避免出现 cout << d1 << d2 这种情况
此时可以利用合法性检验了
实现 operator>> 时,右操作数不能用 const 修饰
//在 Date.h 内//新增几个局部展开usingstd::ostream; usingstd::istream; usingstd::endl; namespaceYohifo{ classDate { //声明为类的友元函数friendstd::ostream&operator<<(std::ostream&out, constDate&d2); friendstd::istream&operator>>(std::istream&in, Date&d2); //注意//…… }; //直接定义在头文件中,成为内联函数inlineostream&operator<<(ostream&out, constDate&d) { //此时需要检验日期合法性if (Date(d).check() ==false) { out<<"警告,当前日期非法!"<<endl; out<<"后续操作将会受到限制"<<endl; } out<<d._year<<"年"<<d._month<<"月"<<d._day<<"日"<<endl; returnout; } inlineistream&operator>>(istream&in, Date&d) { Datetmp; flag: cout<<"请入日期,格式为:年 月 日"<<endl; in>>tmp._year>>tmp._month>>tmp._day; //如果输入日期非法,就重新输入if (Date(tmp).check() ==false) { cout<<"警告,当前日期非法!"<<endl; cout<<"日期输入失败,请尝试重新输入!"<<endl; gotoflag; } cout<<"输入成功!"<<endl; returnin; } }
有了这两个运算符重载后,我们就可以直接对自定义类型(日期类对象)直接进行输入输出操作了
Dated1; cin>>d1; //对自定义类型的输入cout<<d1; //对自定义类型的输出
📆日期+=天数
下面涉及两个重要算法
日期 += 天数
日期 -= 天数
这里把 日期 += 天数 介绍清楚了,日期 -= 天数 就很好写了,就是倒着走
有了 日期 += 天数 后,可以直接实现 日期 + 天数
同理也可以实现 日期 - 天数
📅核心思想
注:此时实现的是 日期+=天数
进位思想:天数满了后进位到月份上,月份满后进位至年份上
注意:
每个月对应天数都需要计算,因为每个月都不同
月份为12月时,再+就变成了下一年的一月
假设为公元前,加至0年时,需要特殊处理为公元1年
+= 操作返回的是左操作数本身,应对 (d1 += 10) = 20 这种情况
📅代码实现
//日期+=天数Date&Date::operator+=(constintval) { if (check() ==false) { cout<<"警告,当前日期非法,无法进行操作"<<endl; return*this; } //判断 val,避免恶意操作,如 d1 += -100if (val<0) { //此时需要调用 -=*this-= (-val); return*this; } //因为是 += 不需要创建临时对象//首先把天数全部加至 _day 上_day+=val; //获取当前月份天数intmonthDay=getMonthDay(); //判断进位,直至 _day <= monthDaywhile (_day>monthDay) { //此时大于,先把多余的天数减掉_day-=monthDay; //此时进位一个月++_month; //判断月份是否大于 12if (_month>12) { //此时需要进年++_year; //月份变为1月_month=1; //判断是否为0年if (_year==0) _year=1; //调整 } //重新获取月份天数monthDay=getMonthDay(); } //返回 *this 本身return*this; }
有了这个函数后,我们就可以根据当前日期推算 N 天后的日期
日期+天数 可以直接复用上面的代码,而 日期-=天数 将逻辑反过来就行了,这里不展示代码了,完整代码在文末的 gitee 仓库中
📆日期-日期
日期+日期无意义,但日期-日期有,可以计算两日期差值
日期相减有两种情况:
左操作数小于右操作数,此时返回大于0的值
左操作数大于右操作数,此时返回小于0的值
具体实现时也很好处理,直接用一个 flag 就行了
📅核心思想
先不管左右操作数大小,我们先找出较大操作数与较小操作数
通过较小操作数逐渐逼近较大操作数,其中经过的天数就是差值
步骤:
先把日期对齐,即小操作数日期与大操作数日期平齐
再把月份对齐
最后再把年份对齐就行了
随着步骤的深入,天数计算会越来越快的
除了这种方法外,我们还可以直接一天一天的加,直到相等,当然这种效率较低
📅代码实现
//日期 - 日期constintDate::operator-(constDate&d) const{ if (check() ==false||d.check() ==false) { cout<<"警告,当前日期非法,无法进行操作!默认返回 0"<<endl; return0; } //假设右操作数为较大值Datemax(d); Datemin(*this); intflag=1; //判断if (min>max) { max=*this; min=d; flag=-1; } //小的向大的靠近intdaySum=0; //考虑天while (min._day!=max._day) { min+=1; daySum++; } //考虑月while (min._month!=max._month) { daySum+=min.getMonthDay(); min+=min.getMonthDay(); } //考虑年while (min._year!=max._year) { daySum+=min.getYearDay(); min._year++; } returndaySum*flag; }
这种方法(同轴转动)将会带来一定的性能提升(相对逐天相加来说)
方法 | 相差 1k 年 | 相差 1w 年 | 相差 10w 年 |
同轴转动 | 耗时 0 ms | 耗时 0 ms | 耗时 2 ms |
逐天相加 | 耗时 28 ms | 耗时 297 ms | 耗时 3142 ms |
注:实际差异与电脑性能有关
📆自加、自减操作
自加操作实现很简单,不过需要注意编译器是如何区分两者的
占位参数
因为前置与后置的运算符重载函数名一致,此时需要给运算符多加一个参数以区分,这是由编译器规定的合法行为,占位参数加在后置运算符重载中
📅前置
前置直接复用前面 += 的代码
前置操作是先进行自加或自减,再返回
//前置++Date&Date::operator++() { //直接复用*this+=1; return*this; } //前置--Date&Date::operator--() { *this-=1; return*this; }
📅后置
此时需要借助 占位参数,当启用时,编译器会自动传参,并自动区分,占位参数 类型为 int
后置操作是先记录值,再进行自加或自减,返回之前记录的值
//后置++constDateDate::operator++(int) { //借助临时变量Datetmp(*this); *this+=1; returntmp; } //后置--constDateDate::operator--(int) { Datetmp(*this); *this-=1; returntmp; }
特别注意: 对于自定义类型来说,在进行自加、自减操作时,最好采用前置,因为后置会发生拷贝构造行为,造成资源浪费
📆程序源码
完整的代码在这里 Gitee
🗓️总结
以上就是关于日期类实现的全部内容了,涉及到了前面学的大部分知识,希望大家在看完后能把它独立敲一遍,加深理解
如果你觉得本文写的还不错的话,可以留下一个小小的赞👍,你的支持是我分享的最大动力!
如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正