C++ | Date 日期类详解

目录

简介

日期类总代码  |  Date

类的定义 & 构造 & Print

类的定义

构造函数 & Print

比较类,如<、>、<=......

值加减类,如+、-、+=、-=......

加减类具体分类

判断某个月有多少天 GetMonthDay

日期类 + / +=(- / -=) 整数

日期类 - 日期类

日期类++ / --(前置后置)

流相关,如<<、>>(cout、cin)

结语


简介

日期类Date实现的本质就是  运算符重载  和  赋值运算符重载

我们要实现自定义类型像内置类型一样相加减,肯定会用到operator关键字+操作符

接下来我们要实现的分为三类:

  1. 比较类,如<、>、<=......
  2. 值加减类,如+、-、+=、-=......
  3. 流相关,如<<、>>(cout、cin)

日期类总代码  |  Date

如果有需要代码的同学,下方的链接是我上传到 gitee 里的代码

2024 - 5 - 3 - gitee - Date

类的定义 & 构造 & Print

我们会建三个文件:

  1. test.cpp
  2. Date.cpp
  3. Date.h

其中我们的Date.h是头文件,我们的函数声明与类的定义,以及头文件都被包含在头文件里面

另外,我们在正式开始开始讲解日期类之前,我们需要先将类给定义出来,然后将其的构造函数、析构函数、Print函数写好

类的定义

代码如下:

class Date
{
public:
	
private:
	int _year;
	int _month;
	int _day;
};

构造函数 & Print

我们现在写函数都需要现在类中声明一份,然后再到Date.cpp中去实现逻辑,最后到test.cpp中进行测试

Date.h 内部代码如下:

class Date
{
public:
    //构造函数声明
	Date(int year = 1, int month = 1, int day = 1);

	void Print();
private:
	int _year;
	int _month;
	int _day;
};

Date.cpp 内部逻辑实现:

Date::Date(int year, int month, int day)
	:_year(year)
	, _month(month)
	, _day(day)
{}

void Date::Print()
{
	cout << _year << " "
		<< _month << " "
		<< _day << endl;
}

由于我们分成了头文件与.cpp文件,所以我们在.cpp文件中实现逻辑之前我们得先Date::一下

不然我们就没法使用我们定义的类

比较类,如<、>、<=......

在开始之前,我得先介绍一个符号防止有同学没听说过:!

这个是逻辑取反:0 取反就是非 0,非 0 取反就是 0

对于这一类,我们的实现逻辑是这样的:

我们先实现<,然后再实现==

当我们将这两个实现了之后,我们就可以复用这两个,然后将其余所有比较类的函数全都表示出来

试想一下:我们实现了<和==,当我们要实现<=怎么办

那是不是同时满足<和==就算是<=

这是我们要实现>怎么办

当一个数不<=时,即<=取反( ! 符号)就是 >

闲话少叙,我们先来实现一下 小于<

bool Date::operator<(const Date& d)const
{
	if (_year < d._year)
		return true;
	else if (_year == d._year)
	{
		if (_month < d._month)
			return true;
		else if (_month == d._month)
		{
			if (_day < d._day)
				return true;
		}
	}
	return false;
}

注意看,我们在上文中实现的逻辑是:如果年小于,那就小于

如果年相等(大于的情况统一出判断之后返回false),那我们就看月,如果月小于,那就小于

如果月相等,那就看天,天小于,那就小于

如果上述条件都不符合,那就是大于或者等于,反正就是不小于

注:我们这里要的只是小于,如果等于或大于,那就不是小于

接着我们来实现一下 等于==

bool Date::operator==(const Date& d)const
{
	return _year == d._year 
		&& _month == d._month 
		&& _day == d._day;
}

等于的实现逻辑相当简单,只有在我的年月日都相等的时候我的两个日期才想等

但凡有一个不相等,那就是不相等,全相等我们就直接返回一个 1,反之则是 0

接着就是很有意思的复用环节了,我们来实现一下 小于等于<=

bool Date::operator<=(const Date& d)const
{
	return *this < d || *this == d;
}

你会看到,我们直接将自定义类型像内置类型一样进行了比较

但是这些比较的符号都是我们刚刚才实现过的:<、==

当两个日期既小于又等于时,那么这两个日期就是小于等于

趁热打铁,我们再一鼓作气将 >、>=、!=给一起实现了

bool Date::operator>(const Date& d)const
{
	return !(*this <= d);
}

bool Date::operator>=(const Date& d)const
{
	return !(*this < d);
}

bool Date::operator!=(const Date& d)const
{
	return !(*this == d);
}

我们能看到,大于就是小于等于取反

大于等于就是小于取反

不等于就是等于取反

值加减类,如+、-、+=、-=......

加减类具体分类

加减类里面其实还分了三类

  1. 日期类 + / +=(- / -=) 整数(某个日期的多少天之后)
  2. 日期类 - 日期类(宝宝,今天离我们刚开始在一起已经过了多少天了呀)
  3. 日期类++ / --(前置后置) 

判断某个月有多少天 GetMonthDay

在正式开始讲之前,我们为了防止后序的逻辑太乱,所以我们就单独将判断某个月有多少天的逻辑拿出来单独实现

而这个函数我们可以在类里面实现,这是为了类的包装,增加安全性、

当然如果你非要将其定义在全局也不是不可以,随你喜欢,出事了与我无瓜

接下来我们来讲一讲判断某个月有多少天的逻辑

由于一年有12个月,如果一个月一个月地返回的话,那就太挫了

所以我们可以创建一个数组,返回之时我们就可以把月当下标返回了

int MonthDayArray[13] =
{ -1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

可以看到,我们创建了一个13个数的数组,因为第一个的下表为0,我们要的效果是1月对应下标1

但是平年和闰年的判断关乎了二月的大小,所以我们不妨假设二月是28天

接着我们在下面判断今年是不是闰年且   要返回的月份是不是2月,如果不是二月,那我们判不判断闰年其实没意义

所以,如果月为2月,如果传过来的年判断为闰年,我们直接返回29即可

如果不是闰年或者根本就不是二月,那我们就返回每个月对应的对应数组下标中的值即可

代码如下:

int GetMonthDay(int year, int month)
{
	assert(month < 13 && month > 0);
	int MonthDayArray[13] =
	{ -1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

	if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0))
		return 29;
	else
		return MonthDayArray[month];
}

日期类 + / +=(- / -=) 整数

首先我们先来想一个问题:+和+=之间有什么不同

+是将值传过去,自身并不会改变,比如 int i = n + m; 其中 n 和 m 并没有改变

而 += 是自身的值会改变

所以我们面对这两类函数时,返回值也不一样,因为面对自身不改变的情况,我们只能创建一个临时变量,但是临时变量出了函数会销毁,所以我们的返回值不能为引用,需要让其调用拷贝构造函数,所以返回值类型为 Date

但是如果是对象自身要改变的情况,那我们就无需创建临时变量,直接在原对象的基础上进行改变即可,最后直接返回*this,所以返回值的类型需为引用,为了防止调用不必要的拷贝构造影响效率,所以参数的返回值类型为 Date&

我们先来实现一下 +=

+= 的逻辑如下:

我们先将 _day 与 day 直接相加,然后我们再对 _day 进行减小,每次减一个月的天数

我们while循环内的逻辑就是,当我的 _day 减小到比 GetMonthDay 函数返回的天数要大时,就证明我们的天数还需要再减

就好比我的月本来是 24,我想知道 100 天后是那一天,我就先 24 + 100 = 124

但是124肯定不止一个月的量,所以我们需要减

假设现在是3月,我就124 - 31 = 93,然后就来到了 4 月,但是还是有多,我就继续减,现在是 4 月就减 30,93 - 30 = 63......

直到 _day 是正常的,循环终止

我们每减一次日期,我们的月就要++一次

当我们的月为13时,我们就将13手动置为1,随后年++

Date& Date::operator+=(int day)
{
	_day += day;

	while (_day > GetMonthDay(_year, _month))
	{
		_day -= GetMonthDay(_year, _month);
		++_month;
		if (_month == 13)
		{
			_month = 1;
			++_year;
		}
	}
	return *this;
}

接着我们来实现一下 +

+其实就可以直接复用+=

我们可以直接创建一个和*this一摸一样的Date类型对象tmp

我们只需要对tmp进行改变(+=)最后返回即可

代码如下:

Date Date::operator+(int day)const
{
	Date tmp = *this;
	tmp += day;
	return tmp;
}

接着是 -= 的实现

-= 的逻辑和 += 很类似,只不过我们现在是直接 _day - day

如此一来,我们的 _day 就为负数,我们就需要不断对其进行加的操作,并在此过程中注意月的--和年的--

但是有一个点需要注意的是,如果调用这个函数的人,要-=却传了一个负数过来,那就单独对其进行判断

如果传过来的是负数,那我们就将其 += 上负的参数即可

代码如下:

Date& Date::operator-=(int day)
{
	if (day < 0)
		return *this += - day;

	_day -= day;

	while (_day <= 0)
	{
		_day += GetMonthDay(_year, _month);
		--_month;
		if (_month == 0)
		{
			_month = 12;
			--_year;
		}
	}
	return *this;
}

最后是 - 的实现

类似的,我们的减就是实例化一个和 *this 一摸一样的临时对象 tmp(拷贝构造),然后对其进行 -= 操作,最后返回 tmp 即可

代码如下:

Date Date::operator-(int day)const
{
	Date tmp = *this;
	tmp -= day;
	return tmp;
}

日期类 - 日期类

可能有人会疑惑,为什么只有日期类的-,而没有+

这是因为日期类的+是没有意义的,试想,2024年1月13号 + 2024年3月31号有什么意义?

而 - 之所以有意义是因为,如果未来有一天你的对象问你:宝,这是我们在一起的第几天啊?

也就是两个日期之间相差几天,这个是有意义的

而我们要实现这个的逻辑也较为简单:

我们只需要将两个日期分为大和小

我们找出小的那一个,不断让其+=1,然后再额外设置一个整形变量,小的那个日期类每+=1一次,整型变量就 ++ 1次

当小的那个日期和大的那个日期相等的时候,我们创建出来的那个整型变量的值就是两个日期之间的差值

当然,为了防止两个日期之间的差值为负数的情况,我们应该先将this指针指向的设置为max,另一个参数指向的为min,如果此时发现设置的max比min要小,那我们就将两者进行交换,随后设置一个变量flag,如果存在max小于min而后两者需要交换的情况,那我们就将flag赋值为-1,否则flag为1

最后返回的结果是两个日期之间的差值*flag

代码如下:

int Date::operator-(const Date& d)
{
	Date max = *this;
	Date min = d;
	int flag = 1;
	int count = 0;
	if (*this < d)
	{
		max = d;
		min = *this;
		flag = -1;
	}
	while (max != min)
	{
		min += 1;
		count++;
	}
	return count * flag;
}

日期类++ / --(前置后置)

其实我们++或--的逻辑在我们将+=和-=实现完了之后都变得非常简单

我们++本质将就是+=1,那我们之间将日期类+=1即可实现++

直接将日期类-=1即可得到--

需要详细讲解的是前置与后置

我们前置与后置唯一的区别就在于,一个是先++后使用,一个是先使用后++

但是我们在写操作符名称的时候,都只能写operator++,所以我们无法分辨哪个是前置哪个是后置

本贾尼博士也发现了这个漏洞,所以就规定说,在传参数的括号中加上一个int的就是后置++

而这个int并不是说要传值的意思,里面传多少都没用,可以理解为就是对编译器的一个提醒,告诉编译器我现在的这个++是后置++

而我们前置与后置的函数返回值类型也是不一样的

前置是直接对*this指向的内容进行修改,修改完后直接返回*this即可,而*this并不是临时变量,出了函数并不会被销毁,所以我们函数的返回类型是引用,这样可以防止多调用拷贝构造影响效率,Date&

后置是需要在函数内部实例化一个临时变量tmp,而我们直接对*this进行修改,最后将tmp返回,因为我们实现的是后置++,*this指向的内容需要更改,但是我们需要返回的是一个更改之前的值,所以我们需要备一份。但是对象tmp是临时的,出了函数的作用域就会被销毁,所以我们只能任其调用拷贝构造,所以返回类型不能为引用Date

综上,++ / --(前置 / 后置)代码如下:

Date& Date::operator--()//前置
{
	*this -= 1;
	return *this;
}

Date Date::operator--(int)//后置
{
	Date tmp = *this;
	*this -= 1;
	return tmp;
}

Date& Date::operator++()//前置
{
	*this += 1;
	return *this;
}

Date Date::operator++(int)//后置
{
	Date tmp = *this;
	*this += 1;
	return tmp;
}

流相关,如<<、>>(cout、cin)

我们在C语言中曾学到了printf和scanf

我们都知道其用法,就是先指定类型,接着我们再将要打印的变量放上去

如果只是打印内置类型还好,但如果是打印自定义类型呢?我们没办法用printf打印自定义类型

本贾尼博士之所以整了一个cout、cin(流插入流提取),就是因为想像打印内置类型一样打印自定义类型的,并且cout可以自动判断要打印变量的类型

我们来看一张图:

如上我们可以看到,cout 和 cin 是两个实例化出来的全局对象,类的类型分别是 ostream、istream

而 ostream、istream 又被包在iostream里面

而我们如果想要实现可以像内置类型一样打印自定义类型的话,我们只需要自己实现一下operator<< 和 operator>> 即可

我们先不考虑连续打印的问题,返回值先设置为void

而我们的参数就传一个ostream&类型的对象即可,剩下还有一个this指针(这里埋一个坑)

代码如下:

void Date::operator<<(ostream& out)
{
	out << _year << " " << _month << " " << _day << endl;
}

但是当我们想要调用的时候,却会发现调用不了

Date s1(2024, 1, 13);
cout<<s1;

你会发现调用不了,这是因为我们在调用的时候,要遵循的顺序是参数的顺序

看我们函数内实现的逻辑,我们的第一个参数是this指针,随后才是ostream

这就意味着如果按照上面写的函数的话,我们就需要将调用的顺序改一下我们才能调用成功

Date s1(2024, 1, 13);
s1<<cout;

如果还不理解的话,我们来显示调用一下我们的函数相信你就能看得懂了

s1.operator<<(cout)

综上,我们如果这么写的话与我们日常的使用逻辑相悖,所以我们需要做出相应的调整

但是this指针已经将第一个参数的位置给牢牢地占住了,我们没有办法做更改

所以我们需要将这个函数变为全局的

当这个函数编程全局的的时候,我们就能够自己定义参数的位置了

void operator<<(ostream& out, const Date& d)

但是如上我们就=还是不能正常调用

这是因为当我们将这个函数变为全局函数的时候,这个函数就无法调用类里面的私有变量

为此,我们需要使用友元的方式进行解决

我们在这里先简单提一下友元的概念,具体的知识我们放到下一篇博客类和对象下中去讲

友元,就是说:我是你的朋友,所以你的东西就是我的东西,但是只是你把我当朋友,我还没有把你当朋友,所以你的东西我能用,但我的东西你不能用

要定义成友元也比较简单,我们只需将函数的声明在类中任意位置放一份,并在最前面加上一个friend即可,如下:

friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);

.h文件全:

class Date
{
	friend ostream& operator<<(ostream& out, const Date& d);
	friend istream& operator>>(istream& in, Date& d);

public:
	Date(int year = 1, int month = 1, int day = 1);

	void Print();

	int GetMonthDay(int year, int month)
	{
		assert(month < 13 && month > 0);
		int MonthDayArray[13] =
		{ -1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

		if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0))
			return 29;
		else
			return MonthDayArray[month];
	}

	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);//*this要变,故不加const
	Date operator+(int day)const;
	Date& operator-=(int day);
	Date operator-(int day)const;

	int operator-(const Date& d);

	Date& operator--();
	Date operator--(int);
	Date& operator++();
	Date operator++(int);

	//日期类无需析构函数,编译器默认生成的就够用
private:
	int _year;
	int _month;
	int _day;
};

ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);

所以我们的<<就变成了:

void operator<<(ostream& out, const Date& d)
{
	out << d._year << " " << d._month << " " << d._day << endl;
}

但是,如果我们遇到需要连续打印的情况,我们写的这个函数就不够用了,因为没有返回值

但是如果我们想要支持连续打印的话,我们需要将out传回去

所以,最终版本如下:

ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << " " << d._month << " " << d._day << endl;
	return out;
}

接着我们来看一下   >>

这个也是一样的,我们的istream是一个类,我们同样将其设置为全局函数,最后通过友元的方式令其能够访问到私有变量,而为了支持连续赋值,所以我们同样需要一个返回值,返回值的类型就是istream&

代码如下:

istream& operator>>(istream& in, Date& d)
{
	in >> d._year >> d._month >> d._day;
	return in;
}

结语

到这里我们的日期类就结束了

如果你觉得这篇博客对你有帮助的话,希望各位能够多多支持!!

(比心)

  • 23
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值