日期类的实现

97 阅读15分钟

在声明和定义分开中,作用域限定符的使用

构造函数如何初始化对象

析构函数要不要自定义呢?

不用,因为内置类型自动处理,自定义类型需要

在标准实现功能时,==::==的意义,不要忘记

要注意运算符重载时触发的方式,是类的对象之间直接计算,而不是按正常的类型计算

搞清楚诸如+ ,+=符号在运算的意义,以及对操作对象的影响,不要搞混了

+= 和+在逻辑上的区别

+在实行结束后,是不改变本身的值,而是把+之后的结果作为返回值,故要用要临时变量,只能传值返回。+=最后会改变左操作数的结果,

1.前言

在我们学习完类和对象后,针对类和对象繁杂的知识,我们可以尝试写一个完整的类,来加深了解,今天笔者就介绍一个简单的日期类。在这个日期类里,实现日期比较,日期加减等一些基本功能。

对于我们常用的日期,一般我们只会查看多少天以后是什么日子,或者几个月几年后是什么日子。对于一般的几个月几年后的计算,我们自己简单就能计算出来,而平时生活中我们基本不会去直接算两个日期相加减的情况,因此,在日期类的实现中我们以天数为主。

2.初始化

对于类的初始化,构造函数是必不可少的,但是像我们以往简单的写一个构造函数是不行的。对于日期类,在对象初始化时有可能值是非法的,如下所示:

Date d1(2022,2,29);
Date d2(2022,3,32);
Date d3(2022,13,1);

以上几种情况,是在初始化时有可能出现的,即天数非法,月份非法,而在天数中,又有闰年的特殊情况。以下我们简单说说:

日期非法,由于每个月天数都大不相同,因此判断日期非法的第一步就是获取每月的天数。我们可以写几个if语句,或者用一个switch语句,这里笔者介绍一个比较好用的方法。我们可以创建一个数组,把每个月的天数放进去,对应月份就可以简单的获取天数了。

//整个日期类的实现都是将函数声明和定义分开的,严格点地实现可以帮助我们更好掌握类和对象的知识。要注意在类外实现时不要忘了作用域限定符

int Date:: GetMonthDay(int year, int month)
{
	int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
	int tmp = days[month];
	if (month == 2 &&(year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
	{
		tmp++;
	}
	return tmp;
}

Date::Date(int year, int month, int day)
{
	if (year > 0 && month > 0 && month < 13 && day > 0 && day <= GetMonthDay(year, month))
	{
		this->_year = year;
		this->_month = month;
		this->_day = day;
	}
	else
	{
		cout << "日期非法" << endl;
	}
}

//简单说说判断闰年的原理:因为地球公转,每年的时间是365天多一点点,每4年刚好多出一天,每100年刚好不多,而每400年又刚好多出一天,因此被4整除但不能被100整除。

实现完构造函数,我们接着实现拷贝构造函数,以及打印函数,方便我们查看日期,这就我们好说的,大家可以参考以下代码:

//拷贝构造
Date::Date(const Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}

//打印天数
void Date::show()
{
	cout << _year << "-" << _month << "-" << _day << endl;
}

针对后面我们要实现的几个赋值运算符重载,我们想想在已经实现了拷贝构造后opeartor=要不要实现呢?

答案是要的。拷贝构造仅仅是一个拷贝,它在我们诸多函数运行中都会调用,比如传值返回的时候,产生某些临时变量的时候。而opeartot=,我们想想,不仅仅是d1 = d2,也有可能是d1=d2=d3=d4这样的连续赋值,因此在实现opeartor=时,赋值的结果还要作为返回值传递,这是二者的区别。当然,我们可以实现拷贝构造,在operato=中调用拷贝构造,最后将值返回。

//=实现
Date& Date::operator=(const Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;

	return *this;
}

最后我们再思考一个问题——析构函数要不要实现呢?

在过往的博客中,笔者说过,对于内置类型,编译器会自动回收,自定义类型才会调用对应的析构函数,而日期类的实现又没有涉及到自定义类型,因此不必实现。

3.赋值运算符重载

3.1 +与 +=

我们之前学习运算符重载时说过,重载不能改变运算符本身的含义,这在我们实现+和+=中很好地体现了。我们暂不论实现思路,我们想想+和+=的区别是什么?

d1 = d2 + d3;
d1 += 1;
//在第一个表达式中,我们观察可以发现,对于+,它不会改变两个操作数的值,而是将d2+d3整个表达式的值作为返回值。
//在第二个表达式中,+=直接将表达式计算的值赋给了左操作数d1,因此我们可以得+与+=的区别:+不直接赋值,是把整个计算的结果作为返回值传递,而+=会直接赋值。

在实现之前,我们想想+和+=尽管意义不同,但都要实现加这个过程,那我们还有没有必要去两次都去完整实现呢?答案是不用的,我们完全可以实现一个,另一个再去复用,这样就可以减少代码量了。比如我们实现+=,实现+的时候再调用+=就好了。当然具体实现+也行,只不过由于+要使用临时变量,因此笔者个人认为具体实现+=会方便点,接下来我们就可以尝试实现了,具体如下:

//+=实现
Date& Date:: operator+=(int day)
{
    //如果+=的有右操作数是负数,我们可以直接把它换成-=
	if (day < 0)
	{
		*this -= day;
		return *this;
	}
    //在实现加法时,我们可以把天数全部加上去,然后使用进位的方法,满月进1,满年进1.
	_day = _day + day;
    while(_day > GetMonthDay(_year, _month))
	{
        //注意_month进位的时机
		_day = _day - GetMonthDay(_year, _month);
		_month++;
		if (_month > 12)
		{
			_month = 1;
			_year++;
		}
	}
	return *this;
}

//+实现
//由于+是将计算的结果作为返回值传递,所以我们要利用临时变量计算返回值,再把结果返回,由于是临时变量,故只能传值返回,不能传引用。
Date Date::operator+(int day)
{
	Date tmp(*this);
	tmp += day;
	return tmp;
}


在+=的实现中,细心的朋友可能发现一个循环中我们连续调用了两个GetMonthDay,既然调用的都是同一个月份的,那能不能优化呢?

如果我们在循环外定义临时变量x,假设当前月是3月,此时x = 31,下次再进去循环,x的值还是不变,因此不能使用这种方法。for循环呢?也不太合适,尽管for循环的初始化可以定义临时变量,但是每个月的天数需要我们动态获取,但for循环只初始化一次,这样看来调用两次也是一个无奈的选择了,各位有什么好的办法也可以尝试优化一下。

3.2 思维误区

针对刚开始实现的重载,我们要明白一件事:我们重载操作符,为的是后续可以像内置类型一样正常使用各种操作符,而我们在实现时,要实现的是对象中内置类型数据的各种赋值操作,两者是有区别的,千万不要搞混了。

int x = 1;
int y = 0;
Date d1(2023,2,2);

x = x + y;//正常使用
d1 = d1 + 5//我们要是实现的就是对象可以这样正常使用操作符,这样才是正常的调用
 d1._day = _day + d1._day;//这是我们在内部实现时要做的

3.3-与-=重载

实现-与-=,思路上其实和+与+=是差不多的,只是在具体细节上有点差别,便不再多说了,具体如下:

//-=实现
Date& Date:: operator-=(int day)
{
    //和+=一样,减负数就调用加法
	if (day < 0)
	{
		*this += day;
		return *this;
	}

	_day = _day - day;
	while (_day <= 0)
	{
        //这里_month退位的时机和+=不一样,大家自己感受下
		_month--;
		int x = GetMonthDay(_year, _month);
		_day =_day + x;		
		if (_month == 0)
		{
			_year--;
			_month = 12;
		}
	}
	return *this;
}

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

3.4日期-

这里单独把日期-拿出来,因为前面的-与-=实现的是对象直接和天数加减,日期-要实现的两个日期相互加减,尽管我们说两个日期相减不太常用,但是可以的话我们还是来实现一下,同时学习一下,像日期类的数据,怎么实现相减,思路如下:

(1)日期相减,按常规思路,我们可以直接相减,得出还剩几年几个月几天,但是我们思考一下,实现日期类的意义是什么?

我们如果想知道两个日期之间有几年,有几个月,我们自己就可以计算出,这样我们就可以大致推算出,日期-的用途,即知道两个日期间到底有几天。

这样看来,单纯两个日期-,如果直接相减,那么为了获得天数,我们还要计算天数,由于我们此时得到的日期并不是固定的,而天数进位也是不固定的,因此在后续获得天数上就非常麻烦了,这种方法我们就先不考虑了。

(2)有朋友可能想到,既然两个日期直接相减不方便,那我借一个日期来做比较是不是可以。这确实是个好思路,拿1971.1.1来说(这一天刚好是星期一,由此计算更加方便),我们把减法转换成加法,分别计算出和1971.1.1的天数差距,进而得出两个日期之间的天数差距。

这不失为一个好办法,那我们再想想这个过程可不可以优化?可不可以不借助中间变量呢?实际上,当我们思想扭转过来,把减法装换成加法,就很简单了。我们完全可以让二者计数,选出较小值,让较小值计数逼进较大值,从而获得相差的天数。代码如下:

//日期-
int Date::operator-(const Date& d)
{
	int flag = 1;
	Date max = *this;
	Date min = d;
    //注意这种获得大小值的思路,默认最大值最小值,比较后根据结果再调整最值。其实这种思路和比较后再决定最值差不多,但是方便得是在逻辑上一开始我们定下了最值,把参数换成了最值,方便我们思考
	if (*this < d)
	{
		max = d;
		min = *this;
		flag = -1;
	}

	int n = 0;
	while(min != max)
	{
		n++;
		min++;
	}
	return n * flag;
}

(3)尽管我们淘汰了临时值直接计数计算差值,获取了天数,但是我们只会单纯计算天数吗,更多的我们会想知道到底是星期几,农历几号,公历几号。由此,结合1971.1.1这个特殊的日期,我们可以通过与它的减法来计算出星期几,以满足我们实际的需求,代码如下:

//借助我们实现-和-=的思路来实现,对求余不太清楚的朋友,可以自己画个图感受一下
void Date::GetWeekDay()
{
	Date tmp(1971, 1, 1);
	int x = *this - tmp;
	const char* arr[] = { "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期天" };
	cout << arr[x % 7] << endl;

}

3.5 前置++ 和后置++

在类和对象赋值运算符重载中我们介绍了前置++和后置++ 的区别,这里就不在多说了,不太明白的朋友可以看看之前的博客:

源码如下,大家可以借鉴一下:

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

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

3.6opeartor<< 和operator>>

既然我们实现了日期类,针对C++的流输入和流提取,我们能不能对==<<==和==>>==进行重载,让它们支持直接打印日期呢?

首先我们看看cout的资料:

image-20230222235557960

我们可以发现,cout是在ostream这个类内的,而C++cout支持自动识别类型,也是靠函数重载完成的,因此我们只需要根据上面的函数声明,照猫画虎写一个就行(同样的,流输入cin也是这样的,只不过它属于istream)。但是由于我们在使用cout时需要遵守一定的格式,故我们最好在类外声明和定义,这样可以保证参数与输出格式一致。代码如下,大家可以参考一下:

ostream& operator<<(ostream& _cout, Date& d)
{
	cout << d._year << "-" <<  d._month << "-" <<  d._day;
	return _cout;
}

void Test2()
{
	Date d1(2023, 2, 22);
	cin >> d1;
	cout << d1 << endl;//endl表示一行输入结束,然后输出下一行,cin才是流输入
}
int main()
{
	Test2();
	return 0;
}
//调用看一看

image-20230223132023722

4比较运算符重载

其实完成运算符重载,日期类就基本完备了,但是我们完整一点,同时让大家更了解函数复用,我们再实现一下==,>, < , >=, <=, !=这几个运算符。相信大家再完成+,+=等赋值运算符重载后,这几个运算符对大家都不算太难,我们主要想想我们需不需要再去实现每一个呢?

其实不用,我们可以只实现两个,比如>和==,剩下的运算符都可以用这两个函数表示,我们看看:

//>实现
bool Date::operator>(const Date& d)
{
	if ((_year > d._year) 
		|| (_year == d._year && _month > d._month)
		|| (_month == d._month && _day > d._day))
	{
		return true;
	}
	else
	{
		return false;
	}
}

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

// >=运算符重载
	bool operator >= (const Date& d)
	{
		return *this > d || *this == d;
	}


	// <运算符重载
	bool operator < (const Date& d)
	{
		return !(*this >= d);
	}

	// <=运算符重载
	bool operator <= (const Date& d)
	{
		return !(*this > d);
	}

	// !=运算符重载
	bool operator != (const Date& d)
	{
		return !(*this == d);
	}


以上几个运算符重载实现大家具体感受一下,最后把源码展示出来给大家参考一下:

//Date.h

#pragma once
#include<iostream>

using namespace std;

//流输入,流提取
//ostream& operator<<(ostream& _cout, Date& d);
//获取某年某月的天数
class Date
{
public:
	friend ostream& operator<<(ostream& _cout, Date& d);
	friend istream& operator>>(istream& _cin, Date& d);

	//获取天数
	int GetMonthDay(int year, int month);

	//全缺省的构造函数
	/*Date(int year = 1, int month = 1, int day = 1)
	{
		if (year > 0 && month > 0 && month < 13 && day > 0 && day <= GetMonthDay(year, month))
		{
			this->_year = year;
			this->_month = month;
			this->_day = day;
		}
		else
		{
			cout << "日期非法" << endl;
		}
	}*/
	Date(int year = 1, int month = 1, int day = 1);

	//拷贝构造
	Date(const Date& d);

	//析构函数
	/*~Date();*/
	/*~Date();*/


	//赋值运算符重载
	Date& operator=(const Date& d);
    
	//展示函数
	void show();

	//日期+=天数
	Date& operator+=(int day);

	//日期+天数
	Date operator+(int day);

	// 日期-天数
	Date operator-(int day);

	// 日期-=天数
	Date& operator-=(int day);

	//前置++
	Date& operator++();

	//后置++
	Date operator++(int);

	// >运算符重载
	bool operator>(const Date& d);

	// ==运算符重载
	bool operator==(const Date& d);

	// >=运算符重载
	bool operator >= (const Date& d)
	{
		return *this > d || *this == d;
	}

	// <运算符重载
	bool operator < (const Date& d)
	{
		return !(*this >= d);
	}

	// <=运算符重载
	bool operator <= (const Date& d)
	{
		return !(*this > d);
	}

	// !=运算符重载
	bool operator != (const Date& d)
	{
		return !(*this == d);
	}

	// 日期-日期 返回天数
	int operator-(const Date& d);

	void GetWeekDay();
private:
	int _year;
	int _month;
	int _day;
};
//Date.cpp
#include"Date.h"

//构造函数,这样合适吗,对于我们的初始化,万一非法了怎么办?应该要判断,对于闰年怎么判断
Date::Date(int year, int month, int day)
{
	if (year > 0 && month > 0 && month < 13 && day > 0 && day <= GetMonthDay(year, month))
	{
		this->_year = year;
		this->_month = month;
		this->_day = day;
	}
	else
	{
		cout << "日期非法" << endl;
	}
}

//获取天数,判断闰年,讲讲闰年的大概原理,原理是每年多出一点点世界,4年刚好多出一天,而100又抵消了,400年又多出一天,因此4年闰,百年不闰,400年闰
int Date:: GetMonthDay(int year, int month)
{
	int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
	int tmp = days[month];
	if (month == 2 &&(year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
	{
		tmp++;
	}
	return tmp;
}

//拷贝构造
Date::Date(const Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}

//打印天数
void Date::show()
{
	cout << _year << "-" << _month << "-" << _day << endl;
}

//=实现
Date& Date::operator=(const Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;

	return *this;
}

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

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

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

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

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

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

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

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

//>实现
bool Date::operator>(const Date& d)
{
	if ((_year > d._year) 
		|| (_year == d._year && _month > d._month)
		|| (_month == d._month && _day > d._day))
	{
		return true;
	}
	else
	{
		return false;
	}
}

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

//日期-
int Date::operator-(const Date& d)
{
	int flag = 1;
	Date max = *this;
	Date min = d;
	if (*this < d)
	{
		max = d;
		min = *this;
		flag = -1;
	}

	int n = 0;
	while(min != max)
	{
		n++;
		min++;
	}
	return n * flag;
}

//计算星期几
void Date::GetWeekDay()
{
	Date tmp(1971, 1, 1);
	int x = *this - tmp;
	const char* arr[] = { "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期天" };
	cout << arr[x % 7] << endl;

}

//流输入流提取
ostream& operator<<(ostream& _cout, Date& d)
{
	cout << d._year << "-" <<  d._month << "-" <<  d._day;
	return _cout;
}

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