【C++】类与对象初级应用篇:打造自定义日期类与日期计算器(2w5k字长文附源码)
文章目录
- 一、日期类的实现
- 1. 日期类的默认成员函数的分析与实现
- 构造函数
- 其它默认成员函数
- 2. 各种逻辑比较运算符重载
- 3. 日期加与减天数
- 日期加天数系列
- 日期减天数系列
- 日期加减天数的最后修定
- ++和- -系列
- 4. 日期减日期
- 方法一
- 方法二
- 5. 流插入与流提取重载
- 流插入重载
- 流提取重载(含修正默认构造)
- 6. 源码
- 二、基于日期类实现日期计算器
一、日期类的实现
在前面的内容中,我们讲解了六大默认成员函数,基本上对类和对象有了简单的认识,今天这篇文章我们就来实现一下之前一直拿来举例用的日期类,顺便基于日期类实现一下日期计算机,做点实用的东西让大家感觉到自己的进步
在开始正式学习之前,我们先在这里做一下强调,就是我们在实现日期类的时候,采用声明和定义分离的方式来写,这样使得我们的代码的可读性更高,声明写在头文件中,定义写在.cpp文件中,如下:
1. 日期类的默认成员函数的分析与实现
我们之前讲过六大默认成员函数,如果我们没有手动实现,它们就会被编译器默认生成,在一些情况下有的默认成员函数不需要我们手动实现,需要我们具体分析,我们这样,按照我们学习的顺序来分析对应默认成员函数是否需要我们自己实现
构造函数
首先就是构造函数,日期类的构造函数需要我们自己实现吗?结论是需要我们实现,并且大多数类都需要手动写构造函数,除非这个类的成员变量都是自定义类型,这样默认生成的构造函数就可以去调用这个自定义类型的默认构造
但是只要一个类有内置类型的成员变量我们基本上就需要自己手动写构造函数,因为默认生成的构造函数对内置类型的成员变量一般不做处理,而日期类的成员变量就全部都是内置类型,所以我们最好给日期类写一个自己的默认构造,我们选择最为全面的全缺省默认构造,将一个日期类对象默认初始化为2025年1月1日,如下:
//Date.h
#include <iostream>
using namespace std;class Date
{
public://默认构造的声明Date(int year = 2025, int month = 1, int day = 1);private:int _year;int _month;int _day;
};
//Date.cpp
#include "Date.h"//默认构造的定义,要注意这里要指定类域,否则编译器找不到
Date::Date(int year, int month, int day)
{_year = year;_month = month;_day = day;
}
在上面我们完整地写出了日期类的一个默认构造,如果我们不传参数,实例化出来的日期类对象默认就是25年1月1日,如果我们传参了就按照我们传的参数来
并且在上面的示例中,我们同时给出了声明和定义的代码,这是为了给大家看看日期类的基本结构,以及声明和定义是如何分离的,分离时定义要指定类域,以后我们直接分析思路,给出函数定义的代码,不会给出声明的代码,但是在源码部分会一次性全部给出
接下来我们为了方便观察和调试,我们先暂时写一个打印函数用用,等我们后面写了流插入运算符的重载就可以用cout,现在我们先写一个打印函数用着,如下:
//打印函数
void Date::Print()
{cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
接下来我们就开始写段代码来测试我们写的默认构造,如下:
int main()
{Date d1;Date d2(2025, 5, 1);d1.Print();d2.Print();return 0;
}
我们来看看代码运行结果:
可以看到代码没有问题,我们接下来看看其它默认成员函数的情况
其它默认成员函数
我们将其它默认成员函数放在一起很明显就是因为其它默认成员函数不用我们自己写,那么我们就来分析一下为什么其它的默认成员函数我们不需要写
首先是析构函数,由于日期类的成员变量都是内置类型,并且没有指向堆上的空间,所以不需要我们自己写析构函数,而拷贝构造和赋值重载则更好分析,因为我们之前教过一个技巧:写了析构才写拷贝构造和赋值重载,没有写析构就不用写,而我们的日期类就是不需要写析构,所以拷贝构造和赋值重载也不用写
还有最后两个默认成员函数分别是普通对象取地址重载和const对象取地址重载,我们之前就说过,基本上两个取地址重载都不需要我们自己写,默认生成的就够我们用了,除非我们不想让别人轻易拿到对象的地址才自己写,否则基本上都不管
所以总结下来就是,剩下的5个默认成员函数不需要我们自己去实现,我们只需要写一个默认构造即可,所以日期类在默认成员函数上还是很简单的,接下来我们来看各种逻辑比较运算符的重载
2. 各种逻辑比较运算符重载
逻辑比较运算符包括了等于、不等于、大于、大于等于、小于、小于等于,完成的就是两个对象之间的大小关系的比较,我们现在要比较的就是两个日期的大小关系,基本上所有类实现逻辑比较运算符重载的思路都是实现其中两个,然后其它重载函数通过复用即可解决
假设我们有等于和大于的运算符重载,那么不等于就是等于取反,大于等于就是大于或者等于,小于就是既不大于也不等于,小于等于就是大于取反,可以看到,我们只需要重载两个运算符,其它运算符就都可以通过复用的方式实现,接下来我们就按照上面的思路先实现等于以及大于的重载
其中等于的重载很简单,就是判断两个日期的年月日是否全部相同,大于重载就稍微复杂点了,首先看当前对象的年是否大,年大就大,年相等就看当前对象是否月大,月大就大,如果月相等就看当前对象是否天大,天大就大,否则就不大,看起来好像也并不复杂,只需要几个if就可以搞定,我们来实现一下,如下:
//等于重载(不是赋值)
bool Date::operator==(const Date& d)
{return _year == d._year&& _month == d._month&& _day == d._day;
}//大于重载
bool Date::operator>(const Date& d)
{if (_year > d._year)return true;else if (_year == d._year && _month > d._month)return true;else if (_year == d._year && _month == d._month)return _day > d._day;//如果不满足上面的条件就小return false;
}
接下来我们来测试一下这两个函数有没有问题,如果这两个函数有问题可能会影响后面其它函数的复用,如下:
int main()
{Date d1;Date d2(2025, 5, 1);d1.Print();d2.Print();if (d1 == d2)cout << "相等" << endl;if (d1 > d2)cout << "d1大" << endl;if(d2 > d1)cout << "d2大" << endl;return 0;
}
根据我们的预想,首先d1不等于d2,第一个cout不会被执行,d1小于d2,第二个cout不会被执行,而第三个cout则会执行,因为d2大于d1,我们来看看代码的运行结果是否跟预期一致,如下:
可以看到第3个cout确实被执行了,跟我们的预期一致,代码没有问题,所以我们写的等于和大于重载没有问题,接下来剩下的逻辑运算符重载我们就通过复用这两个重载即可,如下:
//不等于重载
bool Date::operator!=(const Date& d)
{return !(*this == d);
}//大于等于重载
bool Date::operator>=(const Date& d)
{return *this > d || *this == d;
}//小于重载
bool Date::operator<(const Date& d)
{return !(*this > d || *this == d);
}//小于等于重载
bool Date::operator<=(const Date& d)
{return !(*this > d);
}
是不是看起来一点都不难呢?接下来我们就来一一测试一下这些函数,如下:
int main()
{Date d1;Date d2(2025, 5, 1);d1.Print();d2.Print();if (d1 != d2)cout << "d1不等于d2" << endl;if(d1 >= d2)cout << "d1大于或等于d2" << endl;if(d1 < d2)cout << "d1小于d2" << endl;if(d1 <= d2)cout << "d1小于或等于d2" << endl;return 0;
}
根据我们的预期,其中第一个cout会被执行,因为d1不等于d2,第二个cout则不会执行,因为d2更大,第三个cout会执行,d1小于d2,那么自然第四个cout就会被执行了,满足d1小于d2的条件,那么我们来看看代码运行结果,看看是否如我们所料,如下:
可以看到确实按照如我们所料,只有第二个cout没有被执行,其它三个cout都被执行了,代码没有问题,接下来我们就进入今天的重点了,也就是日期的运算部分,一起来看看吧!
3. 日期加与减天数
日期加天数系列
日期加一个天数,它的含义就是看当前日期过这么多天是哪个日期,所以日期加一个天数返回的就是一个日期,那么该怎么加呢?其中最难的地方就是如何处理进位,也就是天满了要给月进位,月满了要给年进位,月还好只有12个月,但是每个月的天数都不同,并且润年和平年也会影响一个月的天数,情况比较复杂
所以我们采取的方法就是,不管如何,要加多少天就先通通加到天数上去,然后判断,如果超出当前月的天数,就让对象中的天减去这个月的天数,让月进位,也就是让月+1,进位后要判断是否月份变成13,如果变成了13说明月满了,要给年进位,年就+1,然后让月重新变成1,循环往复就可以得到结果,可能有点不好理解,我们画画图就好了,如下:
现在大致思路我们知道了,接下来就差一个我们如何知道一个月是多少天,所以我们可以设计一个函数,传过去年份和月份我们就可以得到那一年的那一月多少天,具体实现思路就是用一个数组将平年每个月的天数记录上,然后判断出来是润年就再让二月加一天即可,如下:
//获取某年某月有多少天
int GetMonthDay(int year, int month)
{//平年每个月多少天,月份作为下标可以找到对应月份的天数int day[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };//判断是否是润年if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0){//是润年那么二月份天数+1day[2]++;}//最后返回对应月份的天数return day[month];
}
那么现在我们就可以通过调用这个函数得到某年某月的天数了,接下来我们就该实现日期加天数了,但是这里我们还有一个关键点需要区分,就是+和+=的区分,+不会影响对象本身,而+=会改变对象的值,我们可以举例说明,如下:
int a = 10;
//这里的a不会发生变化
int n = a + 2;
//这里的a会发生变化
a += 2;
在上面的示例中我们可以看到,单纯的+和+=的返回值是相同的,都是12,但是在int n = a + 2这条语句中不会改变a的大小,而语句a += 2则会改变a的大小,简单的说+不会影响变量的值,而+=会,这就是它们的区别,记住这一点就好,接下来我也不多解释,直接放出+和+=的代码,我们来看看区别在哪里,如下:
//日期+=一个天数
Date& Date::operator+=(int day)
{//在+=中直接对当前对象进行修改_day += day;//如果当前日期类对象的天数大于当前月份的天数就进入循环while (_day > GetMonthDay(_year, _month)){//直接减去当前日期类对象本月的天数_day -= GetMonthDay(_year, _month);//当前日期类对象月进位_month++;//如果月份变成13说明今年结束了,年进位,月份改为1if (_month == 13){_year++;_month = 1;}}//返回当前日期类对象,因为我们是对当前对象直接进行修改的//并且由于当前对象出了作用域也不会销毁,所以可以返回引用,减少拷贝return *this;
}//日期+一个天数
Date Date::operator+(int day)
{//由于+不会修改当前对象的值,所以我们要创建临时变量来使用//用当前对象拷贝构造出来一个相等的tmp日期对象//后面我们的操作就是对tmp的操作,这样就不会影响到当前对象Date tmp(*this);//让tmp的天数加上day,这样就不会影响*thistmp._day += day;//循环的逻辑与+=相似,只是+=是对*this操作,而+是对tmp的操作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;}}//由于我们没有对*this做操作,所以我们不能返回*this//我们要返回tmp,我们都是对tmp进行操作//但是tmp是临时对象,出了作用域要自动销毁,不能返回引用return tmp;
}
在上面的代码和注释中,我详细解答了关于+和+=的问题,其中+不能影响到当前对象,所以拷贝一个副本tmp使用,+=要影响当前对象,那么就可以直接对当前对象进行修改,接下来我们来测试一下上面的+和+=有没有问题,如下:
int main()
{cout << "+的测试" << endl;Date d1(2025, 11, 10);Date d2 = d1 + 75;d1.Print();d2.Print();cout << "+=的测试" << endl;d1 += 75;d1.Print();return 0;
}
按照我们的预期,在+的测试中,d1不会有任何变化,而d2就是d1这个日期75天后的日期,而在+=的测试中,d1则是会被直接更改,我们来看看代码运行结果:
可以看到代码的结果符合我们的预期,在+的测试中d1没有发生变化,在+=测试中又成功修改了d1,接下来我们再讲一点扩展内容,我们先来对比一下+和+=的代码,如下:
我们发现+和+=的代码高度相似,我们似乎可以对其中一个进行复用,我们分别写出+复用+=,以及+=复用+的代码,我们看看,哪一种更好,但是我们要注意,它们不能同时复用对方,否则肯定会出错,如下:
//日期+=一个天数
Date& Date::operator+=(int day)
{//复用+*this = *this + day;return *this;
}----------------------不能同时复用-----------------------------
//日期+一个天数
Date Date::operator+(int day)
{//复用+=Date tmp(*this);tmp += day;return tmp;
}
通过上面的代码我们可以看到,无论我们选择哪一个进行复用代码都很简洁,所以我们接下来要考虑的就是效率的问题了,在之前+的代码中我们会发现它不仅需要自己拷贝出来一个tmp,还要进行传值返回,会有拷贝导致效率降低,而+=的代码中全程都是对*this做修改,没有tmp的拷贝,并且最后还是以引用返回,提高了效率
所以总结下来就是,我们直接只写+=,+直接复用+=即可,因为+=节省了拷贝,但是如果我们写一个+,然后让+=复用+的话,那么我们执行+=的时候就会凭空多出两次拷贝,影响我们的效率,所以我们最终+=和+的代码如下:
//日期+=一个天数
Date& Date::operator+=(int day)
{//在+=中直接对当前对象进行修改_day += day;//如果当前日期类对象的天数大于当前月份的天数就进入循环while (_day > GetMonthDay(_year, _month)){//直接减去当前日期类对象本月的天数_day -= GetMonthDay(_year, _month);//当前日期类对象月进位_month++;//如果月份变成13说明今年结束了,年进位,月份改为1if (_month == 13){_year++;_month = 1;}}//返回当前日期类对象,因为我们是对当前对象直接进行修改的//并且由于当前对象出了作用域也不会销毁,所以可以返回引用,减少拷贝return *this;
}//日期+一个天数
Date Date::operator+(int day)
{//复用+=,减少拷贝Date tmp(*this);tmp += day;return tmp;
}
上面就是我们+和+=的最终代码了,我们通过效率的分析,选择了让+复用+=,既可以让我们的代码更加简洁,又可以提高效率,库库好用
日期减天数系列
我们学习了日期加天数系列,日期减天数系列就要简单多了,首先我们知道日期减天数系列肯定也会包括–和–=这两种运算符重载,并且它们的代码肯定也是类似的,可以进行复用,并且由于–不能对当前对象做更改,所以会拷贝,我们最好写一个–=,然后让–复用–=即可,接下来我们就一起来分析日期–=天数怎么实现
我们采用的方法和日期加天数的方法差不多,也是先不管其他的,先把我们要减去的天数减了再说,如果减了之后,当前天数小于或者等于0了,说明我们减多了,当前月份的天数都减完了,要从前面借,就让月份减1,然后再让当前的_day + 本月的天数
如果还小于0就继续上面的过程去借,如果我们借着借着发现月份变成0了,说明今年已经被借完了,所以我们要让年份减1,让月份重新回到12继续执行上面的操作,我们还是来画画图来讲解更好懂,如下:
在上面我们演示了一个日期减去天数的几乎所有情况,接下来我们就按照上面的思路来将–=的代码写出来,然后让–来复用–=,如下:
//日期-=一个天数
Date& Date::operator-=(int day)
{//不管三七二十一,先让天数相减_day -= day;//如果天数小于等于0说明当前月份的天数已经减完了//当前日期不合法,我们要循环地继续往前面借while (_day <= 0){//当前月份的天数已经减完了,直接走到上一个月份_month--;//此时月份可能变成0,说明今年都已经减完了//所以我们要做一下判断,如果月份等于0就让年份减1,月份改为12月if (_month == 0){_year--;_month = 12;}//做完判断之后此时的月份就是有效的//我们让当前的天数加上当年当月的天数_day += GetMonthDay(_year, _month);//如果_day <= 0,那么还会进入循环继续上面的过程}//由于我们是对*this直接做更改,所以可以直接返回//并且*this在出了作用域之后不被销毁,所以我们可以通过传引用返回减少拷贝return *this;
}//日期-一个天数
Date Date::operator-(int day)
{//-的逻辑中不能修改*this,所以拷贝构造一个tmp出来做修改Date tmp(*this);//直接复用-=,让tmp自身减去这个天数tmp -= day;//最后tmp就是我们的结果,直接返回即可//要注意tmp是临时对象,出作用域要销毁,不能传引用返回return tmp;
}
在上面的代码中,我们直接使用了上面图中的思路,并且给了详细的注释,希望大家能看懂,最后我们就来测试一下这两个函数有没有问题,如下:
int main()
{cout << "-的测试" << endl;Date d1(2025, 2, 9);//这里d1不会发生变化Date d2 = d1 - 75;d1.Print();d2.Print();cout << "-=的测试" << endl;//这里d1会直接发生变化d1 -= 75;d1.Print();return 0;
}
按照我们的预期,在–的测试中,我们不会对d1进行修改,而在–=的测试中则会对d1直接进行修改,我们来看看代码运行结果,如下:
可以看到代码没有问题,跟我们上面图中算的,以及刚刚预想的一致,没有问题,最后我们再对日期加减天数作最后修定即可,解决一些用户输入可能带来的bug
日期加减天数的最后修定
其实上面我们写的日期加减天数的代码逻辑还有一点问题,因为凡事不一定都是按照我们的预想来的,用户有可能会出现一些我们没有考虑到的情况,在之前的代码中我们都下意识的认为用户会输入一个正数,但是有没有可能用户会输入一个负数呢?
也就是说,我们日期加一个负的天数,可以看作想算当前日期n天之前的日期,所以也不能说是无意义的,同理,日期减一个负数逻辑上也说的通,所以我们在这部分主要就是来判断一下,如果用户传给我们的天数是负数怎么办
其实也不难,如果是加一个负的天数,我们就可以看作是减这个天数,调用operator-=即可,如果是减一个负的天数,我们就可以看作是加这个天数,调用operator+=即可,注意我们只需要更改+=和-=的逻辑,因为+和–都是复用+=和–=的,不需要修改,那么我们日期加减天数函数经过修定后的代码为:
//日期+=一个天数
Date& Date::operator+=(int day)
{//在进行加等前,先判断day是否为负if (day < 0){//如果为负直接调用operator-=*this -= -day;return *this;}//在+=中直接对当前对象进行修改_day += day;//如果当前日期类对象的天数大于当前月份的天数就进入循环while (_day > GetMonthDay(_year, _month)){//直接减去当前日期类对象本月的天数_day -= GetMonthDay(_year, _month);//当前日期类对象月进位_month++;//如果月份变成13说明今年结束了,年进位,月份改为1if (_month == 13){_year++;_month = 1;}}//返回当前日期类对象,因为我们是对当前对象直接进行修改的//并且由于当前对象出了作用域也不会销毁,所以可以返回引用,减少拷贝return *this;
}//日期-=一个天数
Date& Date::operator-=(int day)
{//在进行减等前,先判断day是否为负if (day < 0){//如果为负直接调用operator+=*this += -day;return *this;}//不管三七二十一,先让天数相减_day -= day;//如果天数小于等于0说明当前月份的天数已经减完了//当前日期不合法,我们要循环地继续往前面借while (_day <= 0){//当前月份的天数已经减完了,直接走到上一个月份_month--;//此时月份可能变成0,说明今年都已经减完了//所以我们要做一下判断,如果月份等于0就让年份减1,月份改为12月if (_month == 0){_year--;_month = 12;}//做完判断之后此时的月份就是有效的//我们让当前的天数加上当年当月的天数_day += GetMonthDay(_year, _month);//如果_day <= 0,那么还会进入循环继续上面的过程}//由于我们是对*this直接做更改,所以可以直接返回//并且*this在出了作用域之后不被销毁,所以我们可以通过传引用返回减少拷贝return *this;
}
++和- -系列
++和- -系列就比较简单了,相当于就是给当前日期加或减一天,我们可以直接复用上面我们写好的日期加减的函数,关键在于怎么区分前置和后置,接下来我们先来写++,以++为例来讲解怎么写前置和后置
在上一篇文章中我们讲解了运算符重载,其中第9条规则就说明了怎么区分前置后后置,文章链接:【C++】揭秘类与对象的内在机制(核心卷之运算符重载、赋值重载与取址重载的奥秘)
第9条规则告诉我们,在重载前置++时,我们不需要写任何的参数,而重载后置++时,需要带一个int的形参,这个形参可以没有变量名,只有一个int类型,因为它没有其它作用,它只是一个帮我们区分前置和后置的标识
当然,除了语法上的区分,我们也要能够区分它们之间的逻辑关系,其中前置++是先对当前对象自增1,然后再使用,使用++后的结果,而后置++则是先使用,然后再对当前对象自增1,使用的是修改前的结果,那么我们如何实现这一点呢?
其实不难,因为总的来说无论前置还是后置,我们都要对当前对象作出修改,只是如何让它们使用的结果不同呢?很简单,只要对返回值做修改即可,前置++由于要使用++后的结果,所以我们将当前对象自增1后直接返回,后置++由于要使用++前的结果,所以可以在最开始的时候拷贝一个副本,然后对当前对象自增1,最后返回副本即可,代码如下:
//日期前置++(没有形参int)
Date& Date::operator++()
{//直接复用+=*this += 1;//由于是前置++,使用修改后的结果//由于当前对象出作用域不销毁,所以直接返回*thisreturn *this;
}//日期后置++(有形参int与前置作区分)
Date Date::operator++(int)
{//提前拷贝一份tmp用作返回Date tmp(*this);//复用+=对当前对象自增1*this += 1;//返回修改前的结果,也就是tmp//并且由于tmp出了作用域要销毁,所以传值返回return tmp;
}
代码是不是很简单呢?只要我们区分好了前置和后置,实现它们就并不难,那么接下来我们就继续来实现前置和后置- - ,中间过程基本上和++一致,只是从加1天变成减1天,如下:
//日期前置--(没有形参int)
Date& Date::operator--()
{//前置--可以直接操作*this -= 1;return *this;
}//日期后置--(有形参int与前置作区分)
Date Date::operator--(int)
{//后置--要返回修改前的结果,所以先拷贝Date tmp(*this);//拷贝后对当前对象进行修改*this -= 1;//返回--前的结果,也就是tmpreturn tmp;
}
那么++和- -的重载我们也就讲到这里,大家可以自行测试一下,这里我就不再测试了,我们继续进入下一个函数的解析
4. 日期减日期
日期和日期我们只写相减,因为相加没有任何意义,而两个日期相减才会有意义,就是这两个日期相隔的天数,那么日期之间相减该怎么办呢?不能直接让年份月份和天直接相减,因为年份有润年和平年,同时月份不同,对应的天数也不同,直接相减是肯定不行的
这里我给大家提供两个思路,一个较为复杂,但是效率高,一个较为简单,但是效率不如另一个方法高,但是我们主要还是使用方法二,因为它实在太简单了,并且当今计算机已经很快了,所以方法二效率上其实也不低,但是我们这里还是都分别讲一下,可以拓宽我们的思维
方法一
我们可以找到两个日期分别到它们年份的1月1日相隔多少天,让它们相减得到一个差值,这就是月和日的差距,最后算出两个年份之间有多少天,方法就是先直接算出相隔年份,然后再乘以365,并且这些年中间有多少润年就再加几天,这就是年和年之间的差距,最后拿年之间的差距加上之前算的日和月的差距即可,比较复杂,但是高效,我们画图演示演示,如下:
在上面我们完整演示了一个日期减一个日期的过程,其实现在我们都已经大致知道代码怎么写了,但是最后还是要再强调一个细节,就是两个日期不一定被减日期更大,所以我们要注意先找出较大的那个日期,然后用较大的日期减去较小的日期
同时我们要设置一个符号标志,返回的时候返回算出的天数 * flag,如果前面那个日期较大,那么就不修改它,最后我们算出来的天数就是正的,如果后面那个日期较大,就把这个符号标志设置为-1,这样我们最后得到的结果就是负的,这样才符合我们的预期,接下来我们一起来编写代码,如下:
//计算当前日期到当年1月1日的天数
int GetMonthDayGap(int year, int month, int day)
{//天数直接相减,因为1日已经是最小的天了,获得天的差距int gap = day - 1;//接下来从1月枚举month的前一月,将中间月的天数通通加起来for (int i = 1; i < month; i++){//gap加上当前年份i月的日期gap += GetMonthDay(year, i);}//最后gap中存放的就是当前日期到当年1月1日的天数,返回gap即可return gap;
}//计算两个日期年之间差距多少天
int GetYearGap(int greateryear, int lessyear)
{//计算年之间的差距,先假设每一年都是365天算出一个结果int yeargap = (greateryear - lessyear) * 365;//接下来统计两个年份中润年有多少年,润年多一天,把少加的一天加回来int leapyearnum = 0;for (int i = lessyear; i < greateryear; i++){if ((i % 4 == 0 && i % 100 != 0) || i % 400 == 0)leapyearnum++;}//最后有多少个润年,就加回来多少天得到最终结果yeargap += leapyearnum;return yeargap;
}//日期-日期
int Date::operator-(const Date& d)
{//默认认为第一个日期更大Date greater = *this;Date less = d;//符号位标志默认为1int flag = 1;//如果第二个日期更大,那么就交换,同时让符号位flag = -1if (*this < d){greater = d;less = *this;flag = -1;}//计算两个日期到它们那年1月1日的天数,也就是抛开年来算,两个日期月和天之间的差距int monthdaygap1 = GetMonthDayGap(greater._year, greater._month, greater._day);int monthdaygap2 = GetMonthDayGap(less._year, less._month, less._day);//定义一个用于最终返回的变量ret来接收它们相减的结果//现在ret存放的就是抛开年来算,两个日期月和天之间的差距int ret = monthdaygap1 - monthdaygap2;//最后让ret加上抛开月和天时,年的差距就能得到最终结果ret += GetYearGap(greater._year, less._year);return ret * flag;
}
在上面代码过程中,我们完美复刻了画图时的思路,接下来我们写一段代码来测试一下我们这个日期减日期有没有问题,如下:
int main()
{/*Date d1;Date d2 = ++d1;*/Date d1(2025, 3, 9);Date d2(2021, 4, 3);int gap = d1 - d2;cout << gap << endl;return 0;
}
按照我们的预期,最终的结果应该是1436,我们来看看代码运行结果吧,如下:
可以看到我们写的日期减日期没有问题,接下来我们试试d2 - d1,看看如果是小的日期减大的日期能不能得到我们预期的负天数,如下:
可以看到代码没有问题,小日期减去大日期也没有问题,可以得到负天数,符合逻辑,接下来我们就来介绍方法二,方法二非常简单,一学就懂,贼爽,我们一起来看看吧!
方法二
方法二的简单之处在于思路,我们不是要计算两个日期之间的差值吗?并且两个日期中大部分情况下都有大日期和小日期之分,日期相等差距为0就不说了,那么我们可不可以直接找出小的那个日期,让小的日期一直++,在++期间记录天数,那么当小日期追上大日期时,就得到了日期之间的差距
同时我们也还是可以设置一个符号标志变量flag,如果前面的那个日期大flag就默认为1不变,如果后面日期更大的话flag就要变成-1,这样大日期减小日期就能得到正数,小日期减大日期就可以得到负数,非常完美,接下来我们就来写代码,如下:
//日期-日期方法二(效率比方法一低,但是非常简单)
int Date::operator-(const Date& d)
{//下面是找较大日期和较小日期的过程Date greater(*this);Date less(d);int flag = 1;if (*this < d){greater = d;less = *this;flag = -1;}int gap = 0;while (less != greater){less++;gap++;}return flag * gap;
}
大家看到这段代码惊讶不,简直不知道比之前那个方法简单多少倍了,我们主要关心的是它能不能解决两个日期相等的情况,其实是可以解决的,因为如果刚开始less == greater,就不会进入循环,会直接返回gap的默认值0,间接解决了这个问题,最后我们来测试一下这个代码,测试代码还是用之前的,如下:
可以看到代码没有问题,那么如果让我们选择一个方法来写,其实我们直接选择简单的方法二就行了,因为现在最多才2000多年,现在计算机的速度已经非常快了,几乎耗费不了多少时间,方法二还简单,所以我们下次要写就可以写方法二,但是如果有效率要求我们就必须使用方法一了
5. 流插入与流提取重载
流插入重载
日期类的流插入与流提取重载也是一个重点,因为这是我们第一次接触它们,并且它们和之前的运算符重载都不同,因为它们不能重载为成员函数,这个点我们后面会讲到,我们现在先把它重载为成员函数,看看究竟会发生什么
但是在写之前我们简单认识一个小结论,就是我们平常用的cout其实是ostream类类型的对象,而cin则是istream类类型的对象,知道这个我们才方便运算符重载的时候传参,至于这两个类到底是什么,后面我会出专门出一期C++IO流相关的博客,现在我们还是继续回到的流插入和流提取的重载上
那么我们现在知道了cout的类型,写出流插入运算符的重载就不难了,因为流插入运算符的两个操作数我们都有了,应该是Date类的对象,一个是ostream类型的cout,我们现在将它重载为成员函数看看会发生什么,如下:
//流插入运算符<<的重载(这里的形参out就是cout的别名)
//注意要返回cout的引用,这样才支持一次输出多个日期
//这类似于赋值重载的返回,赋值重载也是必须返回才能支持连续赋值
ostream& Date::operator<<(ostream& out)
{//out就是cout,直接按照之前的格式写就行out << _year << "年" << _month << "月" << _day << "日";//返回cout的引用,这样才支持一次输出多个日期return out;
}
在上面的代码中,我们将流插入运算符重载为了成员函数,看起来是不是好像没什么毛病,那么为什么我们之前要说它不能重载为成员函数呢?我们来一起测试一下就好了,测试代码如下:
int main()
{Date d1(2025, 1, 23);cout << d1;return 0;
}
我们运行一下测试代码看看有没有问题,如下:
可以看到代码出现了报错,这是为什么呢?这其实涉及到我们之前讲过的运算符重载的知识,如果将一个运算符重载为成员函数,那么这个运算符的第一个参数默认就是当前对象,而当前情况下的第二个参数才是我们的out,如图:
根据上图的箭头我们就知道了,如果将流插入重载为成员函数,就会导致我们正常操作的参数传反了,上面的那种调用写法就不对,按照上图给我们的感觉,应该把cout和d1倒着写才行,我们来画图演示一下:
如上图,当我们将d1和cout调过来写传参才是正确的,可以看着都怪怪的,它真的可以运行成功吗,我们来看看代码运行结果:
我们可以看到居然真的运行成功了,并且和我们重载时的格式一样,但是这样肯定是不对的,看着就怪怪的,不是我们正常的写法,导致这个问题的原因就是我们将流插入重载为了成员函数,让第一个参数默认成了当前对象,所以我们的流插入不能重载为成员函数,只能重载为普通函数
那么接下来我们就按照普通函数的标准来写一下流提取重载,方法跟上面一致,如下:
//流插入运算符<<的重载(这里的形参out就是cout的别名)
//注意要返回cout的引用,这样才支持一次输出多个日期
//这类似于赋值重载的返回,赋值重载也是必须返回才能支持连续赋值
ostream& operator<<(ostream& out, const Date& d)
{//out就是cout,直接按照之前的格式写就行out << d._year << "年" << d._month << "月" << d._day << "日";//返回cout的引用,这样才支持一次输出多个日期return out;
}
但是其实写到这里还是有一点问题,就是我们现在的流插入重载不是成员函数,不能直接访问内部的三个私有成员变量,如果直接运行会报错,解决的办法有多种,常用的方法有两个,一个是在类内部提供获取成员变量值的函数,另一个则是将这个函数声明为当前类的友元函数
但是友元函数我们还没有讲到,在下一篇文章才会讲,所以我们现在就用笨一点的方法,在类内部提供获取成员变量值的成员函数,如下:
//获取当前对象的年
int GetYear() const
{return _year;
}//获取当前对象的月
int GetMonth() const
{return _month;
}//获取当前对象的日
int GetDay() const
{return _day;
}
上面这三个函数由于过于简单,所以我们直接在.h中写了就好,不用声明和定义分离,接下来我们再改造一下刚刚写的流插入重载,如下:
//流插入运算符<<的重载(这里的形参out就是cout的别名)
//注意要返回cout的引用,这样才支持一次输出多个日期
//这类似于赋值重载的返回,赋值重载也是必须返回才能支持连续赋值
ostream& operator<<(ostream& out, const Date& d)
{//out就是cout,直接按照之前的格式写就行out << d.GetYear() << "年" << d.GetMonth() << "月" << d.GetDay() << "日";//返回cout的引用,这样才支持一次输出多个日期return out;
}
接下来我们再来测试一下上面的函数,看看将流插入重载为普通函数后能否达到我们预期的效果,如下:
可以看到现在的流插入就正常了,可以像平常一样使用,连续输出多个对象也没有问题,那么流插入重载完成之后我们就来写流提取重载,方法和流插入类似,也是只能重载为普通函数,我们一起来具体学习一下
流提取重载(含修正默认构造)
流提取重载和流插入重载不同的一点在于,它要对当前对象的成员变量作修改,如果我们还是用刚刚那种办法就要麻烦一点,因为我们不仅需要创建变量来获取键盘上的数据,我们还要写成员函数来对成员变量作修改,所以这里为了避免麻烦我们就直接将流提取重载声明为Date类的友元函数
虽然我们没有讲过这个知识点,但是由于它不难,所以这里直接给大家简单介绍一下,一个类的友元函数就是当前类信任的外部函数,这个函数可以直接访问和修改我的成员变量,因为我们是朋友,我相信你,所以友元的声明也特别有意思,就是在类中找个位置,前面写上friend,后面跟上函数的声明即可,如下:
friend istream& operator>>(istream& in, Date& d);
这样我们就把流提取的重载声明为了Date类的友元函数,可以直接对成员变量作修改了,上面的流插入重载也可以这样搞,可以更快了,不需要写获取成员变量的函数了,这里我就不带大家修改了,大家可以自己用这个方法重新写写上面的流插入重载,接下来我们直接来写流提取重载,如下:
//流提取运算符>>的重载(这里的形参in就是cin的别名)
istream& operator>>(istream& in, Date& d)
{//打印提示信息cout << "请输入年、月、日" << endl;//从键盘上读取用户的输入cin >> d._year >> d._month >> d._day;//做一下格式控制更加美观cout << endl;//返回in的引用,方便连续的输入return in;
}
我们来测试一下上面的流提取重载有没有问题,如下:
可以看到流提取重载函数的运行确实没毛病,但是你看出来有没有怪怪的地方,就是1月怎么会有32天呢?这可能就是用户不小心输错了,所以为了避免这种情况的发生,我们写一个检查函数,检查用户输入的日期是否合法,不合法就重新输入,如下:
//检查日期是否合法
bool CheckDate(int year, int month, int day)
{//年不能为负,月要大于0小于13//日要大于0,小于等于当前年那个月的天数if (year > 0&& month > 0 && month < 13&& day > 0 && day <= GetMonthDay(year, month))return true;elsereturn false;
}
接下来我们再将这个函数用到流提取重载中,如下:
//流提取运算符>>的重载(这里的形参in就是cin的别名)
istream& operator>>(istream& in, Date& d)
{bool ret = true;do{//如果ret为false,说明刚刚输入错误//重新进入了循环,这里我们就给予提示if (!ret)cout << "日期非法,请重新输入!" << endl;//打印提示信息cout << "请输入年、月、日" << endl;//从键盘上读取用户的输入cin >> d._year >> d._month >> d._day;//判断日期是否合法ret = CheckDate(d._year, d._month, d._day);} while (!ret);//做一下格式控制更加美观cout << endl;//返回in的引用,方便连续的输入return in;
}
接下来我们再来测试一下这个流提取重载有没有问题,如下:
可以看到我们代码的逻辑已经比较好了,流提取重载也就写到这里了,但是既然我们都已经写了日期检查函数,那么我们最好再优化一下我们的默认构造,因为默认构造也可能出现不小心写错日期的情况
我们就这样设计,如果传来的日期非法,那么我们就提示一下用户,同时将日期就更改为默认日期2025年1月1日,如下:
//由于这个函数写在下面,上面要使用就声明一下
bool CheckDate(int year, int month, int day);//默认构造
Date::Date(int year, int month, int day)
{bool ret = CheckDate(year, month, day);if (!ret){cout << "当前对象初始化日期不合法,已修改为默认日期2025年1月1日" << endl;year = 2025, month = 1, day = 1;}_year = year;_month = month;_day = day;
}
接下来我们继续进行测试,故意实例化对象时传错误的日期,看看会发生什么,如下:
可以看到程序做出了提示,并帮我们把日期调整为了默认日期,那么写到这里我们的日期类终于搞完了,是不是感觉成就感满满呢?接下来我们给出源码之后就来根据我们写的日期类来实现一个日期计算器,让我们的程序变得有价值
6. 源码
我们按照先.h文件后.cpp文件的顺序给出源代码,首先是.h文件,如下:
//Date.h
#pragma once#include <iostream>
using namespace std;class Date
{friend istream& operator>>(istream& in, Date& d);
public://默认构造Date(int year = 2025, int month = 1, int day = 1);//打印函数void Print();//获取当前对象的年int GetYear() const{return _year;}//获取当前对象的月 int GetMonth() const{return _month;}//获取当前对象的日int GetDay() const{return _day;}//等于重载(不是赋值)bool operator==(const Date& d);//大于重载bool operator>(const Date& d);//不等于重载bool operator!=(const Date& d);//大于等于重载bool operator>=(const Date& d);//小于重载bool operator<(const Date& d);//小于等于重载bool operator<=(const Date& d);//日期+=一个天数Date& operator+=(int day);//日期+一个天数Date operator+(int day);//日期-=一个天数Date& operator-=(int day);//日期-一个天数Date operator-(int day);//日期前置++(没有形参int)Date& operator++();//日期后置++(有形参int与前置作区分)Date operator++(int);//日期前置--(没有形参int)Date& operator--();//日期后置--(有形参int与前置作区分)Date operator--(int);//日期-日期int operator-(const Date& d);//流插入与流提取不能重载为成员函数//ostream& operator<<(ostream& out);private:int _year;int _month;int _day;
};//流插入运算符<<的重载(这里的形参out就是cout的别名)
//注意要返回cout的引用,这样才支持一次输出多个日期
//这类似于赋值重载的返回,赋值重载也是必须返回才能支持连续赋值
ostream& operator<<(ostream& out, const Date& d);//流提取运算符>>的重载(这里的形参in就是cin的别名)
istream& operator>>(istream& in, Date& d);
接下来是.cpp文件,如下:
#define _CRT_SECURE_NO_WARNINGS#include "Date.h"//由于这个函数写在下面,上面要使用就声明一下
bool CheckDate(int year, int month, int day);//默认构造
Date::Date(int year, int month, int day)
{bool ret = CheckDate(year, month, day);if (!ret){cout << "当前对象初始化日期不合法,已修改为默认日期2025年1月1日" << endl;year = 2025, month = 1, day = 1;}_year = year;_month = month;_day = day;
}//打印函数
void Date::Print()
{cout << _year << "年" << _month << "月" << _day << "日" << endl;
}//等于重载(不是赋值)
bool Date::operator==(const Date& d)
{return _year == d._year&& _month == d._month&& _day == d._day;
}//大于重载
bool Date::operator>(const Date& d)
{if (_year > d._year)return true;else if (_year == d._year && _month > d._month)return true;else if (_year == d._year && _month == d._month)return _day > d._day;//如果不满足上面的条件就小return false;
}//不等于重载
bool Date::operator!=(const Date& d)
{return !(*this == d);
}//大于等于重载
bool Date::operator>=(const Date& d)
{return *this > d || *this == d;
}//小于重载
bool Date::operator<(const Date& d)
{return !(*this > d || *this == d);
}//小于等于重载
bool Date::operator<=(const Date& d)
{return !(*this > d);
}//获取某年某月有多少天
int GetMonthDay(int year, int month)
{//平年每个月多少天,月份作为下标可以找到对应月份的天数int day[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };//判断是否是润年if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0){//是润年那么二月份天数+1day[2]++;}//最后返回对应月份的天数return day[month];
}//日期+=一个天数
Date& Date::operator+=(int day)
{//在进行加等前,先判断day是否为负if (day < 0){//如果为负直接调用operator-=*this -= -day;return *this;}//在+=中直接对当前对象进行修改_day += day;//如果当前日期类对象的天数大于当前月份的天数就进入循环while (_day > GetMonthDay(_year, _month)){//直接减去当前日期类对象本月的天数_day -= GetMonthDay(_year, _month);//当前日期类对象月进位_month++;//如果月份变成13说明今年结束了,年进位,月份改为1if (_month == 13){_year++;_month = 1;}}//返回当前日期类对象,因为我们是对当前对象直接进行修改的//并且由于当前对象出了作用域也不会销毁,所以可以返回引用,减少拷贝return *this;复用+//*this = *this + day;//return *this;
}//日期+一个天数
Date Date::operator+(int day)
{由于+不会修改当前对象的值,所以我们要创建临时变量来使用用当前对象拷贝构造出来一个相等的tmp日期对象后面我们的操作就是对tmp的操作,这样就不会影响到当前对象//Date tmp(*this);让tmp的天数加上day,这样就不会影响*this//tmp._day += day;循环的逻辑与+=相似,只是+=是对*this操作,而+是对tmp的操作//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;// }//}由于我们没有对*this做操作,所以我们不能返回*this我们要返回tmp,我们都是对tmp进行操作但是tmp是临时对象,出了作用域要自动销毁,不能返回引用//return tmp;//复用+=Date tmp(*this);tmp += day;return tmp;
}//日期-=一个天数
Date& Date::operator-=(int day)
{//在进行减等前,先判断day是否为负if (day < 0){//如果为负直接调用operator+=*this += -day;return *this;}//不管三七二十一,先让天数相减_day -= day;//如果天数小于等于0说明当前月份的天数已经减完了//当前日期不合法,我们要循环地继续往前面借while (_day <= 0){//当前月份的天数已经减完了,直接走到上一个月份_month--;//此时月份可能变成0,说明今年都已经减完了//所以我们要做一下判断,如果月份等于0就让年份减1,月份改为12月if (_month == 0){_year--;_month = 12;}//做完判断之后此时的月份就是有效的//我们让当前的天数加上当年当月的天数_day += GetMonthDay(_year, _month);//如果_day <= 0,那么还会进入循环继续上面的过程}//由于我们是对*this直接做更改,所以可以直接返回//并且*this在出了作用域之后不被销毁,所以我们可以通过传引用返回减少拷贝return *this;
}//日期-一个天数
Date Date::operator-(int day)
{//-的逻辑中不能修改*this,所以拷贝构造一个tmp出来做修改Date tmp(*this);//直接复用-=,让tmp自身减去这个天数tmp -= day;//最后tmp就是我们的结果,直接返回即可//要注意tmp是临时对象,出作用域要销毁,不能传引用返回return tmp;
}//日期前置++(没有形参int)
Date& Date::operator++()
{//直接复用+=*this += 1;//由于是前置++,使用修改后的结果//由于当前对象出作用域不销毁,所以直接返回*thisreturn *this;
}//日期后置++(有形参int与前置作区分)
Date Date::operator++(int)
{//提前拷贝一份tmp用作返回Date tmp(*this);//复用+=对当前对象自增1*this += 1;//返回修改前的结果,也就是tmp//并且由于tmp出了作用域要销毁,所以传值返回return tmp;
}//日期前置--(没有形参int)
Date& Date::operator--()
{//前置--可以直接操作*this -= 1;return *this;
}//日期后置--(有形参int与前置作区分)
Date Date::operator--(int)
{//后置--要返回修改前的结果,所以先拷贝Date tmp(*this);//拷贝后对当前对象进行修改*this -= 1;//返回--前的结果,也就是tmpreturn tmp;
}//计算当前日期到当年1月1日的天数
int GetMonthDayGap(int year, int month, int day)
{//天数直接相减,因为1日已经是最小的天了,获得天的差距int gap = day - 1;//接下来从1月枚举month的前一月,将i月的天数通通加起来for (int i = 1; i < month; i++){//gap加上当前年份i月的日期gap += GetMonthDay(year, i);}///最后gap中存放的就是当前日期到当年1月1日的天数,返回gap即可return gap;
}//计算两个日期年之间差距多少天
int GetYearGap(int greateryear, int lessyear)
{//计算年之间的差距,先假设每一年都是365天算出一个结果int yeargap = (greateryear - lessyear) * 365;//接下来统计两个年份中润年有多少年,润年多一天,把少加的一天加回来int leapyearnum = 0;for (int i = lessyear; i < greateryear; i++){if ((i % 4 == 0 && i % 100 != 0) || i % 400 == 0)leapyearnum++;}//最后有多少个润年,就加回来多少天得到最终结果yeargap += leapyearnum;return yeargap;
}//日期-日期方法一(高效但复杂一点)
int Date::operator-(const Date& d)
{//默认认为第一个日期更大Date greater = *this;Date less = d;//符号位标志默认为1int flag = 1;//如果第二个日期更大,那么就交换,同时让符号位flag = -1if (*this < d){greater = d;less = *this;flag = -1;}//计算两个日期到它们那年1月1日的天数,也就是抛开年来算,两个日期月和天之间的差距int monthdaygap1 = GetMonthDayGap(greater._year, greater._month, greater._day);int monthdaygap2 = GetMonthDayGap(less._year, less._month, less._day);//定义一个用于最终返回的变量ret来接收它们相减的结果//现在ret存放的就是抛开年来算,两个日期月和天之间的差距int ret = monthdaygap1 - monthdaygap2;//最后让ret加上抛开月和天时,年的差距就能得到最终结果ret += GetYearGap(greater._year, less._year);return ret * flag;
}//日期-日期方法二(效率比方法一低,但是非常简单)
//int Date::operator-(const Date& d)
//{
// //下面是找较大日期和较小日期的过程
// Date greater(*this);
// Date less(d);
// int flag = 1;
// if (*this < d)
// {
// greater = d;
// less = *this;
// flag = -1;
// }
// int gap = 0;
// while (less != greater)
// {
// less++;
// gap++;
// }
// return flag * gap;
//}//流插入和流提取不能重载为成员函数
//ostream& Date::operator<<(ostream& out)
//{
// //out就是cout,直接按照之前的格式写就行
// out << _year << "年" << _month << "月" << _day << "日";
// //返回cout的引用,这样才支持一次输出多个日期
// return out;
//}//流插入运算符<<的重载(这里的形参out就是cout的别名)
//注意要返回cout的引用,这样才支持一次输出多个日期
//这类似于赋值重载的返回,赋值重载也是必须返回才能支持连续赋值
ostream& operator<<(ostream& out, const Date& d)
{//out就是cout,直接按照之前的格式写就行out << d.GetYear() << "年" << d.GetMonth() << "月" << d.GetDay() << "日";//返回cout的引用,这样才支持一次输出多个日期return out;
}//流提取运算符>>的重载(这里的形参in就是cin的别名)
//istream& operator>>(istream& in, Date& d)
//{
// int year, month, day;
// cout << "请输入年、月、日" << endl;
// cin >> year >> month >> day;
// d = { year, month, day };
// return in;
//}//检查日期是否合法
bool CheckDate(int year, int month, int day)
{//年不能为负,月要大于0小于13//日要大于0,小于等于当前年那个月的天数if (year > 0&& month > 0 && month < 13&& day > 0 && day <= GetMonthDay(year, month))return true;elsereturn false;
}//流提取运算符>>的重载(这里的形参in就是cin的别名)
istream& operator>>(istream& in, Date& d)
{bool ret = true;do{//如果ret为false,说明刚刚输入错误//重新进入了循环,这里我们就给予提示if (!ret)cout << "日期非法,请重新输入!" << endl;//打印提示信息cout << "请输入年、月、日" << endl;//从键盘上读取用户的输入cin >> d._year >> d._month >> d._day;//判断日期是否合法ret = CheckDate(d._year, d._month, d._day);} while (!ret);//做一下格式控制更加美观cout << endl;//返回in的引用,方便连续的输入return in;
}
二、基于日期类实现日期计算器
在上面我们实现了一个日期类,接下来我们就基于日期类实现一个日期计算器,只要有了我们的日期类,完成这个计算器就只需要调用刚刚写的日期类的接口,甚至大部分接口都用不到,我们就只实现一下日期加减天数和日期减日期就可以了
整个计算器的核心还是使用do…while循环完成,中间使用switch语句帮我们控制用户的选项,然后在日期类的基础上封装几个函数即可,代码如下:
#include "Date.h"//菜单
void Menu()
{cout << "**************************" << endl;cout << "***** 0. 退出计算器 *****" << endl;cout << "***** 1. 日期 + 天数 *****" << endl;cout << "***** 2. 日期 - 天数 *****" << endl;cout << "***** 3. 日期 - 日期 *****" << endl;cout << "**************************" << endl;
}//日期+天数
void DateAddDay()
{Date d;int day;//这里相当于直接调用了流提取重载cin >> d;cout << "请输入需要为当前日期加上多少天:" << endl;cin >> day;cout << endl;//这里相当于直接调用了日期加天数的运算符重载cout <<"最终结果为:" << d + day << endl << endl;
}//日期-天数
void DateSubDay()
{Date d;int day;//这里相当于直接调用了流提取重载cin >> d;cout << "请输入需要为当前日期减去多少天:" << endl;cin >> day;cout << endl;//这里相当于直接调用了日期减天数的运算符重载cout << "最终结果为:" << d - day << endl << endl;
}//日期-日期
void DateSubDate()
{Date d1, d2;cout << "开始输入第一个日期" <<endl;//这里相当于直接调用了流提取重载cin >> d1;cout << "开始输入第二个日期" << endl;cin >> d2;//这里相当于直接调用了日期减日期的运算符重载cout << "最终结果为:" << d1 - d2 << "天" << endl << endl;
}int main()
{int input = 0;do{Menu();cout << "请选择:";cin >> input;switch (input){case 0:cout << "已退出计算器" << endl;break;case 1:DateAddDay();break;case 2:DateSubDay();break;case 3:DateSubDate();break;default:cout << "请输入有效的数字进行选择!" << endl;break;}} while (input);return 0;
}
最后我们来看看日期计算器运行后的效果,如下:
那么今天我们关于日期类的分析以及对日期计算器的实现就到这里啦,总共2万5千多字,希望大家能够有所收获,有什么不懂欢迎私信问我
bye~
相关文章:
【C++】类与对象初级应用篇:打造自定义日期类与日期计算器(2w5k字长文附源码)
文章目录 一、日期类的实现1. 日期类的默认成员函数的分析与实现构造函数其它默认成员函数 2. 各种逻辑比较运算符重载3. 日期加与减天数日期加天数系列日期减天数系列日期加减天数的最后修定和- -系列 4. 日期减日期方法一方法二 5. 流插入与流提取重载流插入重载流提取重载(含…...
ROS应用之SwarmSim在ROS 中的协同路径规划
SwarmSim 在 ROS 中的协同路径规划 前言 在多机器人系统(Multi-Robot Systems, MRS)中,SwarmSim 是一个常用的模拟工具,可以对多机器人进行仿真以实现复杂任务的协同。除了任务分配逻辑以外,SwarmSim 在协同路径规划方…...
Shell特殊位置变量以及常用内置变量总结
目录 1. 特殊的状态变量 1.1 $?(上一个命令的退出状态) 1.2 $$(当前进程的 PID) 1.3 $!(后台进程的 PID) 1.4 $_(上一条命令的最后一个参数) 2.常用shell内置变量 2.1 echo&…...
【ollama通过命令行启动后如何在网页端查看运行】
ollama通过命令行启动后如何在网页端查看运行 http://localhost:11434/...
【MySQL】初始MySQL、库与表的操作
目录 基本使用 使用案例 SQL分类 存储引擎 库的操作 字符集和校验规则 查看系统默认字符集和校验规则 查看数据库支持的字符集 查看数据库支持的字符集校验规则 指定编码常见数据库 校验规则对数据库的影响 操纵数据库 库的备份与恢复 表的操作 创建表 查看表 …...
信息学奥赛一本通 1342:【例4-1】最短路径问题
【题目描述】 平面上有n个点(n<100),每个点的坐标均在-10000~10000之间。其中的一些点之间有连线。 若有连线,则表示可从一个点到达另一个点,即两点间有通路,通路的距离为两点间的直线距离。现在的任务是…...
芯片AI深度实战:基础篇之langchain
基于ollama, langchain,可以构建一个自己的知识库,比如这个 Build Your Own RAG App: A Step-by-Step Guide to Setup LLM locally using Ollama, Python, and ChromaDB | HackerNoon 这是因为: 以上范例就实现了这样一个流程: 系列文章&…...
Autogen_core 测试代码:test_cache_store.py
目录 原始代码测试代码代码中用到的typing注解 原始代码 from typing import Dict, Generic, Optional, Protocol, TypeVarT TypeVar("T")class CacheStore(Protocol, Generic[T]):"""This protocol defines the basic interface for store/cache o…...
AI大模型开发原理篇-1:语言模型雏形之N-Gram模型
N-Gram模型概念 N-Gram模型是一种基于统计的语言模型,用于预测文本中某个词语的出现概率。它通过分析一个词语序列中前面N-1个词的出现频率来预测下一个词的出现。具体来说,N-Gram模型通过将文本切分为长度为N的词序列来进行建模。 注意:这…...
Haproxy入门学习二
一、Haproxy的算法 1.haproxy通过固定参数balance指明对后端服务器的调度算法,其中balance参数可以配置在listen或backend选项中 2.haproxy的调度算法分为静态和动态调度算法,其中有些算法可以根据参数在静态和动态算法中相互转换 3.静态算法:…...
【C++题解】1014. 编程求1+1/2+1/3+...+1/n
问题:1014. 编程求11/21/3…1/n 类型:简单循环 题目描述: 编程求 11/21/3⋯1/n 。 输入: 输入一行,只有一个整数 n(1≤n≤200) 。 输出: 输出只有一行(这意味着末尾有一个回车符号&#x…...
Java基础知识-第14章-Java注解
1、注解(Annotation)概述 从JDK5.0开始,Java增加了对元数据(MetaData) 的支持,也就是Annotation(注解)Annotation其实就是代码里的特殊标记,这些标记可以在编译,类加载,运行时被读取,并执行相应的处理。通…...
python算法和数据结构刷题[1]:数组、矩阵、字符串
一画图二伪代码三写代码 LeetCode必刷100题:一份来自面试官的算法地图(题解持续更新中)-CSDN博客 算法通关手册(LeetCode) | 算法通关手册(LeetCode) (itcharge.cn) 面试经典 150 题 - 学习计…...
【javaweb项目idea版】蛋糕商城(可复用成其他商城项目)
该项目虽然是蛋糕商城项目,但是可以复用成其他商城项目或者购物车项目 想要源码的uu可点赞后私聊 技术栈 主要为:javawebservletmvcc3p0idea运行 功能模块 主要分为用户模块和后台管理员模块 具有商城购物的完整功能 基础模块 登录注册个人信息编辑…...
人格分裂(交互问答)-小白想懂Elasticsearch
通过交互式追问了解一个中间件 ? 啥是Elasticsearch ! 分布式搜索和分析引擎 ? 为啥是分布式搜索,单体难道用不了吗 ? 实际上是说这个东西可以分布式部署 ! 单机可用但扩展性差,分布式通过分片、副本和负载均衡实现海量数据存储与高并发处理 ? 提…...
独立开发者日刊 | Deepseek 统一多模态 AI Janus 开源 |语音秒变文章 | 免费 AI 视频答案引擎
独立开发者产品日刊,每日汇集 ProductHunt 热榜产品介绍,⚡️ 1句Slogan榨干产品灵魂,⚡️ 3秒 get 全球独立开发者的爆款灵感。关注小前,每日捕获全球产品灵感。 Jotform for Canva 标语:为 Canva 创建强大表单类别&a…...
1999-2020年 全国各地区-财政状况分析-一般预算收入-各项税收-个人所得税
1999-2020年 全国各地区-财政状况分析-一般预算收入-各项税收-个人所得税https://download.csdn.net/download/2401_84585615/89575946 https://download.csdn.net/download/2401_84585615/89575946 一般预算收入是指各级政府按照预算法规定,将预计取得的各项收入纳…...
基础项目实战——3D赛车(c++)
目录 前言一、渲染引擎二、关闭事件三、梯形绘制四、轨道绘制五、边缘绘制六、草坪绘制七、前后移动八、左右移动九、曲线轨道十、课山坡轨道十一、循环轨道十二、背景展示十三、引入速度十四、物品绘制十五、课数字路障十六、分数展示十七、重新生成十八、…...
AboutDialog组件的功能和用法
文章目录 1 概念介绍2 使用方法3 示例代码 我们在上一章回中介绍了AlertDialog Widget相关的内容,本章回中将介绍AboutDialog Widget.闲话休提,让我们一起Talk Flutter吧。 1 概念介绍 我们在这里说的AboutDialog是一种弹出式窗口,和上一章回中介绍的Al…...
hdfs之读写流程
写入流程: 客户端Client想将文件a.txt上传至hdfs,首先向Namenode发送请求进行权限校验,Namenode通过后会计算出来三个节点,并将这三个节点告知客户端,客户端将输入进行切割成块,一个一个的块进行传输&…...
链表的介绍
目录 引言优缺点与链表相似的数据结构注意事项单向链表的实现基础实现创建类创建成员变量创建特殊方法 增加数据push_back方法insert方法 删除数据del_back方法del_index方法 clear方法查询数据at方法与重载的中括号运算符toArray方法indexOf方法 修改数据获取链表大小测试方法…...
背着开发板回家过年~
大家好,我是bug菌~ 小明,过年了, 别再死磕什么STM32底层驱动、linux平台总线驱动框架、嵌入式实时操作系统源码这些了。 你背着电脑和开发板回家,压根没一点实际用处。 发小们潇洒地出入各种娱乐场所,唱着歌࿰…...
Kafka 日志存储 — 磁盘存储
Kafka 依赖与磁盘来存储和缓存消息,采用文件追加的方式来写入消息。顺序写盘的速度快于随机写内存。 1 磁盘存储 除顺序写入外,Kafka中大量使用了页缓存、零拷贝等技术来进一步提升吞吐性能。 1.1 页缓存 页缓存是操作系统实现的一种磁盘缓存&#x…...
【机器学习】自定义数据集 使用tensorflow框架实现逻辑回归并保存模型,然后保存模型后再加载模型进行预测
一、使用tensorflow框架实现逻辑回归 1. 数据部分: 首先自定义了一个简单的数据集,特征 X 是 100 个随机样本,每个样本一个特征,目标值 y 基于线性关系并添加了噪声。tensorflow框架不需要numpy 数组转换为相应的张量࿰…...
RK3568中使用QT opencv(显示基础图像)
文章目录 一、查看对应的开发环境是否有opencv的库二、QT使用opencv 一、查看对应的开发环境是否有opencv的库 在开发板中的/usr/lib目录下查看是否有opencv的库: 这里使用的是正点原子的ubuntu虚拟机,在他的虚拟机里面已经安装好了opencv的库。 二、…...
Brave132 编译指南 Windows 篇:获取源码(六)
1. 引言 在 Brave 浏览器 132 版本的编译过程中,获取源代码是至关重要的第一步。源代码包含了 Brave 浏览器的所有核心功能、特性和组件的实现细节,是深入理解、定制和优化 Brave 的基础。通过获取和管理源代码,开发者能够深入探索 Brave 的…...
解决 pip install 出现 error: subprocess-exited-with-error 错误的方法
解决 pip install 出现 error: subprocess-exited-with-error 错误的方法_pip安装报错 subprocess-CSDN博客文章浏览阅读10w次,点赞62次,收藏86次。通过上述步骤,我们成功解决了 pip install 时出现的 error: subprocess-exited-with-error 错…...
网络安全攻防实战:从基础防护到高级对抗
📝个人主页🌹:一ge科研小菜鸡-CSDN博客 🌹🌹期待您的关注 🌹🌹 引言 在信息化时代,网络安全已经成为企业、政府和个人必须重视的问题。从数据泄露到勒索软件攻击,每一次…...
DeepSeek大模型技术解析:从架构到应用的全面探索
一、引言 在人工智能领域,大模型的发展日新月异,其中DeepSeek大模型凭借其卓越的性能和广泛的应用场景,迅速成为业界的焦点。本文旨在深入剖析DeepSeek大模型的技术细节,从架构到应用进行全面探索,以期为读者提供一个…...
Deepseek的api调用报错乱码问题
最近的deepseek也是很火,但是在调用api的过程中也会出现一些大大小小的问题,所以这里也给出一种问题和他的解决方案,报错的类型如下图所示 API Streaming Failed Command failed with exit code 1: powershell (Get-CimInstance -ClassName W…...
.NET Core 中依赖注入的使用
ASP.NET Core中服务注入的地方 在ASP.NET Core项目中一般不需要自己创建ServiceCollection、IServiceProvider。在Program.cs的builder.Build()之前向builder.Services中注入。在Controller中可以通过构造方法注入服务。 低使用频率的服务 把Action用到的服务通过Action的参…...
Mysql Resultset 解析记录
Mysql Resultset 解析记录 结果集消息头字段定义结果数据完整spicy文件 结果集消息头 消息头由消息体长度消息序列号消息体组成;消息头长度为3字节,消息序列号长度为1字节。 结果集的消息头消息体内容为结果集的列数。 结果集消息头的spicy1格式如下&a…...
ThinkPhp伪静态设置后,访问静态资源也提示找不到Controller
ThinkPhp没有配置伪静态时,除了默认的IndexController能访问,其他路由Controller都访问不到,提示404错误。配置了伪静态后就解决了这个问题。 但是当我的ThinkPhp后台项目中有静态资源放在public目录(或子目录)中需要…...
【回溯+剪枝】找出所有子集的异或总和再求和 全排列Ⅱ
文章目录 1863. 找出所有子集的异或总和再求和解题思路:子集问题解法(回溯 剪枝)47. 全排列 II解题思路:排序 回溯 剪枝 1863. 找出所有子集的异或总和再求和 1863. 找出所有子集的异或总和再求和 一个数组的 异或总和 定义为…...
单细胞-第五节 多样本数据分析,打分R包AUCell
文件在单细胞\5_GC_py\1_single_cell\3.AUCell.Rmd 1.基因 rm(list = ls()) load("g.Rdata")2.AUCell https://www.ncbi.nlm.nih.gov/pmc/articles/PMC9897923 IF: NA NA NA用这个文章里的方法,将单细胞亚群的marker基因与ros相关基因取交集,用作AUCell的基因集…...
锁升级过程与优化操作
前文我们学习了CAS自旋锁知道CAS对应的就是一条指令操作,属于一种轻量级锁,那么有轻必有重,从无锁到轻量级锁到重量级锁是一个升级过程,此文我们对锁升级的过程以及一些优化锁的操作一探究竟。 1. 锁升级 从前文 《程序员不可能不…...
android主题设置为..DarkActionBar.Bridge时自定义DatePicker选中日期颜色
安卓自定义DatePicker选中日期颜色 背景:解决方案:方案一:方案二:实践效果: 背景: 最近在尝试用原生安卓实现仿element-ui表单校验功能,其中的的选择日期涉及到安卓DatePicker组件的使用&#…...
Kafka常见问题之 `javax.management.InstanceAlreadyExistsException`
文章目录 Kafka常见问题之 javax.management.InstanceAlreadyExistsException1. 概述2. 常见原因3. 具体异常示例4. 解决方案4.1 确保单一 Kafka Producer 实例4.2 配置 Kafka Broker 和 Producer 使用唯一的 JMX 名称(对于Producer重点检查 client.id)4…...
数据分析系列--③RapidMiner算子说明及数据预处理
一、算子说明 1.新建过程 2.算子状态灯 状态灯说明: (1)状态指示灯: 红色:指示灯说明有参数未被设置或输入端口未被连接等问题; 黄色:指示灯说明还未执行算子,不管配置是否基本齐全; 绿色:指示灯说明一切正常,已成功执行算子。 (2)三角…...
Gradle配置指南:深入解析settings.gradle.kts(Kotlin DSL版)
文章目录 Gradle配置指南:深入解析settings.gradle.kts(Kotlin DSL版)settings.gradle.kts 基础配置选项单项目配置多项目配置 高级配置选项插件管理(Plugin Management)基础配置模板案例:Android项目标准配…...
专为课堂打造:宏碁推出三款全新耐用型 Chromebook
IT之家 1 月 25 日消息,宏碁(Acer)昨日(1 月 24 日)发布公告,针对教育市场,推出 Chromebook Spin 512 (R857T)、Chromebook Spin 511 (R757T) 和 Chromebook 511 (C737) 三款产品,兼…...
电商系统-用户认证(三)基于公钥解析JWT令牌
一、 基于私钥生成jwt令牌 步骤: 导入认证服务 将shangcheng_user_auth工程导入到项目中去,如下图 启动eureka,再启动认证服务 3) 认证服务中创建测试类 public class CreateJwtTest { /**** 创建令牌测试*/Testpublic voi…...
验证回文串
hello 大家好!今天开写一个新章节,每一天一道算法题。让我们一起来学习算法思维吧! function isPalindrome(s) {// 第一步:将字符串中的所有大写字符转换为小写字符s s.toLowerCase();// 第二步:使用正则表达式移除所…...
Java定时任务实现方案(四)——Spring Task
Spring Task 这篇笔记,我们要来介绍实现Java定时任务的第四个方案,使用Spring Task,以及该方案的优点和缺点。 Spring Task是Spring框架提供的一个轻量级任务调度框架,用于简化任务调度的开放,通过注解或XML配置的…...
Python 数据分析 - Matplotlib 绘图
Python 数据分析 - Matplotlib 绘图 简介绘图折线图单线多线子图 散点图直方图条形图纵置横置多条 饼图 简介 Matplotlib 是 Python 提供的一个绘图库,通过该库我们可以很容易的绘制出折线图、直方图、散点图、饼图等丰富的统计图,安装使用 pip install…...
深入探讨数据库索引类型:B-tree、Hash、GIN与GiST的对比与应用
title: 深入探讨数据库索引类型:B-tree、Hash、GIN与GiST的对比与应用 date: 2025/1/26 updated: 2025/1/26 author: cmdragon excerpt: 在现代数据库管理系统中,索引技术是提高查询性能的重要手段。当数据量不断增长时,如何快速、有效地访问这些数据成为了数据库设计的核…...
【Redis】hash 类型的介绍和常用命令
1. 介绍 Redis 中存储的 key-value 本身就是哈希表的结构,存储的 value 也可以是一个哈希表的结构 这里每一个 key 对应的一个 哈希类型用 field-value 来表示 2. 常用命令 命令 介绍 时间复杂度 hset key field value 用于设置哈希表 key 中字段 field 的值为…...
World Creator地形导入UE
修改导出分辨率1009x1009, 虚幻默认参数的整体分辨率是1009 导出预设选择高度图(heigh map)格式选择PNG 16位,或者RAW 16位,需要反转y轴(与虚幻不同),命名格式会自动带一个 , 将改成_ 或者删掉自己命名 &am…...
mybatis(104/134)
动态sql标签,用于选择查询 if标签 where标签 :自动生成where,取决于后面有没有条件,会自动去除条件前面的and和or,不会去除语句后面的 trim标签:自动生成where,在语句后自动去除后缀and和or for…...
制造企业的成本核算
一、生产成本与制造费用的区别 (1)生产成本,是直接用于产品生产,构成产品实体的材料成本。 包括企业在生产经营过程中实际消耗的原材料、辅助材料、备品备件、外购半成品、燃料、动力包装物以及其它直接材料,和直接参加产品生产的工人工资,以及按生产工人的工资总额和规…...