机械转码日记【14】C++运算符重载的应用——实现一个日期类计算器

目录

前言

1.运算符重载

2.赋值重载函数:operator=

2.1不写赋值重载函数,编译器会默认生成

3.实现日期计算器

3.1日期类的构造函数

3.2函数复用定义">",">=","<","<=","==","!="

3.2.1复用"<","=="去实现"<="

3.2.2复用"<=",去实现">"

3.2.3复用"<",去实现">="

 3.2.4复用"==",去实现"!="

3.3"+"和"+="

3.3.1"+"

3.3.2分清楚拷贝构造函数和赋值重载函数

3.3.3为什么赋值重载和拷贝构造函数里面的参数都建议用const修饰

3.3.4"+="

3.3.5+和+=互相复用的优劣

3.4"-"和"-="

3.5前置++,--和后置++,--

3.6"-"的另外一种重载形式,日期对象-日期对象

3.7const修饰成员


前言

这篇博客主要是讲了C++的运算符重载,在一个类中,我们不显式写出赋值重载函数,编译器会自动生成一个浅拷贝的赋值重载函数;同时写出一个日期计算器能够加深我们前面所学知识的印象。新人创作者,欢迎大佬们提出你们宝贵的意见和建议!本篇博客的代码已经上传到我的码云了,欢迎有需要的朋友们自取!日期类计算器代码

1.运算符重载

我们前面写的日期类,再某种情况下可能要进行比较,比如比较一个日期谁大谁小,但是可以直接用>和<去比较大小吗?显然是不行的,对于内置类型(int,double,float......)我们完全可以直接用>和<去比较大小;对于自定义类型,C++引入了运算符重载的功能,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。以<为例,其定义方式为:

bool operator<(const Date& d)
	{
		if (_year < d._year
			|| (_year == d._year && _month < d._month)
			|| (_year == d._year && _month == d._month && _day < d._day))
		{
			return true;
		}
		else
		{
			return false;
		}
	}
可以看到我们写的<运算符重载,它是一个函数的形式,它的返回值是bool类型的,函数的参数其实有两个,一个是this指针,一个是常引用日期类型d;运算符的重载函数一般写在类里面的公有成员函数内。
我们要调用这个运算符是如何调用呢:

可以看到有两种方式:

  • 以我们调用对象的成员函数的形式调用:d1.operator<(d2)
  • 直接写成d1<d2,在这里编译器会自动给我们处理

.*::sizeof?:.    注意以上5个运算符不能重载!

2.赋值重载函数:operator=

如果我们要把一个日期类的值赋值给另一个日期类,比如Date d1(2022,5,23),Date d2,d2 = d1;就需要赋值重载,你可能会说,这还不容易吗?你可能会写成这样:

class Date
{
public:
	//普通构造
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
    //赋值重载
    void operator=(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day - d._day;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};

但是这种情况是不能使用连等的:

原因就在于我们的返回类型是void,void是不能赋给别的值的,因此我们应该把返回类型改成Date:

 //赋值重载
    Date operator=(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day - d._day;
        return *this;
	}

更优化的写法是把Date返回改成引用返回,因为如果是值返回,会调用一次拷贝构造函数,会有内存的消耗,而引用返回不是,引用返回直接返回这个变量的别名,且这里出了函数的作用域,*this的内容还在,用引用返回是最优解!

 //赋值重载
    Date& operator=(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day - d._day;
        return *this;
	}

别以为这就完了,我们还有最优化的写法,假如我们写错了,写成了自己赋值给自己,比如Date d1(2022,5,23);d1 = d1;这种情况其实如果调用我们上面写的是会浪费内存时间的,那我们就再做一层改进:

//赋值重载
	Date& operator=(const Date& d)
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day - d._day;
		}
		return *this;
	}

2.1不写赋值重载函数,编译器会默认生成

其实我们不写赋值重载函数,编译器会默认生成一个:

这个时候其实默认生成的赋值重载函数是浅拷贝,对于日期类这样的我们可以不写,但是像我们上篇博客中所提到的栈类,我们不能不写。

3.实现日期计算器

其实我们在项目中,类的声明和定义经常是分离的,所以我们写日期类计算器也进行声明变量分离,如下图定义一个Date.h用来声明日期类的成员变量和成员函数,在Date.cpp中用来定义日期类。

接着我们在Date.h中声明我们的日期类:

#pragma once

#include<iostream>
#include<assert.h>

//项目里面尽量不要全展开,防止命名冲突
using std::cout;
using std::cin;
using std::endl;


class Date
{
public:
	//四年一闰,百年不闰,四百年一闰
	bool isLeapYear(int year)
	{
		return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
	}

	int GetMonthDay(int year, int month);

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

	//拷贝构造和赋值不需要写,因为浅拷贝足够了
	//Date(const Date& d);
	//Date& operator=(const Date& d);
	

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

	Date operator+(int day);
	Date& operator+=(int day);

	Date operator-(int day);
	Date& operator-=(int day);

	// ++d1
	Date& operator++();// 前置
	
	// d1++
	Date operator++(int);// 后置
	
	Date& operator--();// 前置
	
	Date operator--(int);// 后置
	
	// d1 - d2
	int 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);
	// d1 <= d2
	bool operator<=(const Date& d);
	

private:
	int _year;
	int _month;
	int _day;
};

在这里我们不全展开命名空间(实际中在项目里也是一样,防止我们定义的变量与库里面的发生命名冲突);我们不自己写析构函数,因为我们并不需要特殊的功能,变量除了作用域直接销毁就行;拷贝构造函数和赋值拷贝函数我们也不需要写,因为对于日期类来说,浅拷贝已经足够了,我们直接使用编译器默认生成的就行。

3.1日期类的构造函数

我们先来实现日期类的构造函数,因为一年不同的月有不同的天数,年也有平年和闰年之分,所以我们在初始化日期类的对象时,要判断他合不合法,因此日期类的构造函数需要调用两个函数,一个是获取当前月的天数判断合法不合法,另一个是判断当前的年是否是闰年(闰年的2月为29天)。

//判断闰年的函数:四年一闰,百年不闰,四百年一闰
bool isLeapYear(int year)
{
	return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
//获取当前月的天数的函数:
int Date::GetMonthDay(int year, int month)
{
	assert(year >= 0 && month > 0 && month < 13);

	//static,因为它频繁调用,所以加上static就可以节约内存
	//多线程读取数据是没问题的
	const static int monthDayArray[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
	if (month == 2 && isLeapYear(year))
	{
		return 29;
	}
	else
	{
		return monthDayArray[month];
	}
}

以上两个函数,实现方法我相信大家都已经很明白了,但是这里有个细节需要大家注意一下,可以看到我在monthDayArray数组前加了const和static修饰;因为GetMonthDay函数我们需要频繁调用,那么如果我们不加static,每次调用都会生成一个数组,出了函数作用域又被销毁了,这样其实会很影响程序运行的效率,因此加上static我们只会在第一次调用GetMonthDay函数时会创建这个数组,此时数组被存放在静态区,当main函数销毁时才会销毁数组,这样提高了程序的运行效率;加const是为了不让这个数组被修改,也为了线程安全,因为多线程读取数据是不会影响线程安全的,而写数据会。

//构造函数,声明定义分离
//声明给了缺省,定义就不用给了
Date::Date(int year, int month, int day)
{
	if (year>0 && month <= 12 && day <= GetMonthDay(year,month) && month > 0 && day > 0)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	else
	{
		cout << "构造失败" << endl;
	}
}

上述是我们的构造函数,定义构造函数,因为我们是声明和定义分离,从上面的代码我们已经知道构造函数的声明是带了缺省值的,那么我们定义构造函数时,就不能再带缺省值了(原因请看机械转码日记【7】缺省参数部分)。

3.2函数复用定义">",">=","<","<=","==","!="

其实我们在实际项目过程中,要尽可能的去复用我们已经定义的函数,这样不仅可以缩短代码的篇幅长度,也可以减小出错的概率,在这里我们就定义"<"和"==",然后复用这两个运算符重载函数去定义其他的函数。

//能复用的情况尽可能复用
bool Date:: operator<(const Date& d)  
{
	if( (_year == d._year && _month == d._month && _day < d._day)
		|| (_year == d._year && _month < d._month)
		|| (_year < d._year))
	{
		return true;
	}
	else
	{
		return false;
	}
}

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

3.2.1复用"<","=="去实现"<="

//复用"<","=="去实现"<="
bool operator<=(const Date& d) 
{
	return *this < d || *this == d;
}

3.2.2复用"<=",去实现">"

//复用"<=",去实现">"
bool operator>(const Date& d)
{
	return !(*this <= d);
}

3.2.3复用"<",去实现">="

//复用"<",去实现">="
bool operator>=(const Date& d) 
{
	return !(*this < d);
}

 3.2.4复用"==",去实现"!="

//复用"==",去实现"!="
bool operator!=(const Date& d)  
{
	return !(*this == d);
}

3.3"+"和"+="

3.3.1"+"

Date Date::operator+(int day)
{
	Date ret(*this);//需要返回临时变量,防止原来的值被修改

	ret._day += day;
	while (ret._day > GetMonthDay(ret._year, ret._month))
	{
		ret._day -= GetMonthDay(ret._year, ret._month);
		ret._month++;
		if (ret._month == 13)
		{
			++ret._year;
			ret._month = 1;
		}
	}

	return ret;
}

上述是我们实现"+"的写法,有两个地方需要注意,一个是我们需要返回一个临时变量,防止原来的值被修改,如图:

另一个需要注意的地方是我们在这里不能使用引用返回,因为在这里我们是返回一个临时变量,这个临时变量出了作用域就被销毁了,引用返回会造成内存的非法访问!

3.3.2分清楚拷贝构造函数和赋值重载函数

请看下面这段代码:

void test2()
{
	Date d1(2022, 5, 28);
	cout << endl;
	Date d2 = d1+100;//这里是拷贝还是赋值呢?
	cout << endl;
	Date d3;
	cout << endl;
	d3 = d1;//这里是拷贝还是赋值呢?
}

请问Date d2 = d1+100和d3 = d1这两句语句是调用了拷贝构造函数还是赋值重载函数呢?因为我们前面没手动写出赋值重载函数和拷贝构造函数,所以我们验证不了,所以现在我们为了验证,手动把这两个函数写出来:

接下来我们开始验证: 

可以看到Date d2 = d1+100是调用了拷贝构造(+调用了一次,拷贝给d2调用了一次),而d3 = d1是调用的赋值重载函数,我们总结一下:拷贝构造是指用一个对象去初始化另一个同类型的对象,而赋值是两个已经存在的对象去进行操作。

3.3.3为什么赋值重载和拷贝构造函数里面的参数都建议用const修饰

我们把我们刚刚自己写的赋值重载和拷贝构造里的参数里的const去掉,再次运行一下,看看会发生什么:

 程序报错了,为什么呢?我们来分析一下原因:

 因为在实现d1+100时,会调用operator+函数,返回ret时,由于他是一个类对象,返回时会生成一个临时对象,而临时对象具有常性,当d1+100作为右值赋值给d2时,调用拷贝构造函数,但是此时的拷贝构造函数的参数时Date&类型,不是const Date&,这相当于权限的放大,自然就会报错!

3.3.4"+="

+=和+的逻辑是一样的,但是+=之后原来的值会变,所以不需要返回临时变量,出了作用域this指向的内容也还在,这样我们用引用返回就可以了:

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

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

	return *this;
}

在这里还有另一个需要注意的地方,就是我们的day如果是负值,是会报错的,比如:

可以看到我们的日期时非法的,一个月是没有-72日的,因此我们必须加上如果day是负数的的处理程序。

3.3.5+和+=互相复用的优劣

其实我们实现+,可以复用+=;同样的,我们实现+=,也可以复用+;代码如下:

/*   +复用+=   */
Date Date::operator+(int day) 
{
	Date ret(*this);
	ret += day;

	return ret;
}

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

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

	return *this;
}


/*   +=复用+   */
Date Date::operator+(int day)  
{
	Date ret(*this);

	ret._day += day;
	while (ret._day > GetMonthDay(ret._year, ret._month))
	{
		ret._day -= GetMonthDay(ret._year, ret._month);
		ret._month++;
		if (ret._month == 13)
		{
			++ret._year;
			ret._month = 1;
		}
	}

	return ret;
}

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

那么这两种方式哪一种效率更高呢?

答案是+复用+=效率高一下,因为+会调用两次拷贝构造,如果+=复用+,单独写+=是不用调用拷贝构造的,但是复用了+之后,又增加了两次拷贝构造,很划不来。 

3.4"-"和"-="

 写完了+和+=,想必-和-=也很好些吧!代码如下:

/*  -复用-=  */
Date Date::operator-(int day) const
{
	Date ret(*this);
	ret -= day;

	return ret;
}

Date& Date::operator-=(int day)
{
	if (day < 0)
		day = -day;

	_day -= day;
	while (_day <= 0)
	{
		--_month;
		if (_month == 0)
		{
			_month = 12;
			--_year;
		}

		_day += GetMonthDay(_year, _month);
	}
	return *this;
}

3.5前置++,--和后置++,--

如何区分前置++和后置++或者前置--和后置--呢?因为它们的符号都一样,其实C++规定了一种方式,就是用参数区分,如果是后置++或者--,运算符重载的参数里会带一个int值:

 那么我们如何实现前置++和后置++呢?首先我们要搞清楚,前置++是返回++后的值,后置++是返回++前的值。其代码如下:

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

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

同理前置--和后置--的值也如下:

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

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

3.6"-"的另外一种重载形式,日期对象-日期对象

我们再来看看-的另外一种重载形式,日期对象-日期对象,这种情况是算出两个日期相差多少天。我们来实现一下:

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

我们来算一下今天距离武汉第一例新冠肺炎(2019,12,8)已经多少天了(期望疫情早日结束),再用网页上的日期计算器来验证一下我们写的结果!

 结果是对的上的,我们写的代码没有错误。

3.7const修饰成员

我们先来看一下下面这段代码和他的运行结果:

是不是感觉非常奇怪,为什么d1.print不会报错,而d.print就报错了,我们来分析一下: 

首先我们print()函数的参数是Date*类型的,而d1.print传的参数也是Date*类型的,因此不会报错,但是Func函数里面的d.print函数所传的参数是const Date*类型,const Date*类型的参数传给Date*类型是属于权限的放大,是会报错的。那么应该如何修改呢?C++发明了const成员这样一个方法,以print成员函数为例,其使用方法如下:

我们在定义成员函数时,在他的后面加上const,它实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。即修改成const Date*的类型。实际上在我们的日期类计算器中,如果我们不需要对this所指向的内容进行修改,就都可以加上const修饰更加安全。我们的">","<","<=",">=","==","!=","+","-"都可以用const来修饰。

  • 20
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 22
    评论
评论 22
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

逗你笑出马甲线

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值