目录
3.2函数复用定义">",">=","<","<=","==","!="
3.3.3为什么赋值重载和拷贝构造函数里面的参数都建议用const修饰
前言
这篇博客主要是讲了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来修饰。