Bootstrap

【C++】类与对象初级应用篇:打造自定义日期类与日期计算器(2w5k字长文附源码)

在这里插入图片描述

一、日期类的实现

    在前面的内容中,我们讲解了六大默认成员函数,基本上对类和对象有了简单的认识,今天这篇文章我们就来实现一下之前一直拿来举例用的日期类,顺便基于日期类实现一下日期计算机,做点实用的东西让大家感觉到自己的进步

    在开始正式学习之前,我们先在这里做一下强调,就是我们在实现日期类的时候,采用声明和定义分离的方式来写,这样使得我们的代码的可读性更高,声明写在头文件中,定义写在.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)
	{
		//是润年那么二月份天数+1
		day[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说明今年结束了,年进位,月份改为1
		if (_month == 13)
		{
			_year++;
			_month = 1;
		}
	}
	//返回当前日期类对象,因为我们是对当前对象直接进行修改的
	//并且由于当前对象出了作用域也不会销毁,所以可以返回引用,减少拷贝
	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;
}

    在上面的代码和注释中,我详细解答了关于+和+=的问题,其中+不能影响到当前对象,所以拷贝一个副本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说明今年结束了,年进位,月份改为1
		if (_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说明今年结束了,年进位,月份改为1
		if (_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;
	//由于是前置++,使用修改后的结果
	//由于当前对象出作用域不销毁,所以直接返回*this
	return *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;
	//返回--前的结果,也就是tmp
	return 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;
	//符号位标志默认为1
	int flag = 1;
	//如果第二个日期更大,那么就交换,同时让符号位flag = -1
	if (*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;
	else
		return 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)
	{
		//是润年那么二月份天数+1
		day[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说明今年结束了,年进位,月份改为1
		if (_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;
	//由于是前置++,使用修改后的结果
	//由于当前对象出作用域不销毁,所以直接返回*this
	return *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;
	//返回--前的结果,也就是tmp
	return 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;
	//符号位标志默认为1
	int flag = 1;
	//如果第二个日期更大,那么就交换,同时让符号位flag = -1
	if (*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;
	else
		return 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~

;