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

机械转码日记【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.运算符重载

我们前面写的日期类,再某种情况下可能要进行比较,比如比较一个日期谁大谁小,但是可以直接用>和和<去比较大小;对于自定义类型,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#include//项目里面尽量不要全展开,防止命名冲突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);// ++d1Date& operator++();// 前置// d1++Date operator++(int);// 后置Date& operator--();// 前置Date operator--(int);// 后置// d1 - d2int 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 <= d2bool 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  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  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  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值:

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

// ++d1Date& 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来修饰。