前言
上一期我们讲到类的6个默认构造函数中的拷贝构造函数,这一期我们继续往下讲,当然难点肯定是运算符重载了。
一、运算符重载
运算符重载是c++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数相似。
函数名字为:关键字operator后面接需要重载的运算符符号
函数原型:返回值类型operator操作符(参数列表)
注意:
1.不能通过连接其他符号来创建新的操作符。比如:operator@
2.重载操作符必须有一个类类型参数
3.用于内置类型的运算符,其含义不能改变。例:内置的整形 + ,不能改变+的含义,比如把加法弄成乘法之类的。
4.作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this。
5.有五个运算符是不能重载的。1 .*(点星) 2 ::(域名限定符) 3 sizeof 4 ?:(三目操作符) 5 . (成员访问操作符)
class Date { public: Date(int year = 10, int month = 10, int day = 10) { _year = year; _month = month; _day = day; } void print() { cout << _year << "年" << _month << "月" << _day << "日" << endl; } private: int _year; int _month; int _day; }; int main() { Date d1(2023,2,10); Date d2(2023,2,5); d2.print(); return 0; }
像上面的d1和d2两个参数该如何比较大小呢?以前在C语言我们通常写一个函数是传两个日期的地址过去然后挨个用指针访问去比较最后返回布尔值,这样会非常的麻烦,而c++的运算符重载正好解决了这个问题。:
class Date { public: Date(int year = 10, int month = 10, int day = 10) { _year = year; _month = month; _day = day; } void print() { cout << _year << "年" << _month << "月" << _day << "日" << endl; } bool operator==(const Date& d) { return _year == d._year && _month == d._month && _day == d._day; } private: int _year; int _month; int _day; }; int main() { Date d1(2023,2,10); Date d2(2023,2,10); cout << (d1 == d2) << endl; return 0; }
通过上面的代码和图片大家应该可以看到运算符重载对于自定义类型有多方便,而运算符重载的规则我们也讲过,那么写到类外什么样子呢?
(由于类外不可访问类内私有成员可以先将私有成员改为共用或者知道友元函数的用友元)
类外的区别就是多一个参数,因为在类内有this指针。 cout打印d1==d2加括号的原因是流插入操作符的优先级高于==。
符号>的重载:
bool operator>(const Date& d) { if (_year > d._year) { return true; } else if (_year == d._year && _month > d._month) { return true; } else if (_year == d._year && _month == d._month && _day > d._day) { return true; } else { return false; } }
符号<=的重载:
bool operator<=(const Date& d) { return !(*this > d); }
我们可以发现运算符很多都是实现一两个其他的就可以复用了。小于等于不就是大于的取反吗,只需要知道类内函数有隐藏的参数this默认指向第一个操作数即可。
符号<:
1. bool operator<(const Date& d) 2. { 3. return !(*this > d)&&!(*this==d); 4. }
小于就是大于等于的取反。
符号>=:
1. bool operator>=(const Date& d) 2. { 3. return !(*this < d); 4. }
大于等于就是小于的取反。
符号!=:
1. bool operator!=(const Date& d) 2. { 3. return !(*this == d); 4. }
赋值运算符重载
赋值运算符的重载格式:
1.参数类型 const T&,传递引用可以提高传参效率。
2.返回值类型T& ,返回引用可以提高返回的效率,有返回值的目的是为了实现连续赋值
3.检测自己给自己赋值(因为两个一样的变量再去赋值会消耗空间)
4.返回*this 要复合连续赋值的含义
注意:赋值运算符的重载并不是强制要求参数为&,这里要和拷贝构造区分,赋值运算符即使用传值调用也可以使用不会发生无穷调用因为在传自定义类型参数的时候先拷贝构造一个临时变量然后将这个变量赋值给变量即可。
Date& operator=(const Date& d) { _year = d._year; _month = d._month; _day = d._day; return *this; }
一般来说赋值给另一个对象是不需要返回值的,但是为了实现连续赋值那么就必须返回被赋值的那个变量,由于被赋值的对象不会被销毁,所以为了不调用拷贝构造函数浪费空间直接使用引用返回即可。如果有人写成d1= d1这样的代码,那么不就白白的浪费了空间吗,所以我们直接判断一下
Date& operator=(const Date& d) { if (this != &d) { _year = d._year; _month = d._month; _day = d._day; } return *this; }
this是左操作数的地址,&d是右操作数的地址,这样的好处就是即使赋值时出现操作数一样的情况也不会白白浪费空间。
注意:运算符的顺序不是从右往左也不是从左往右,这是要看操作符的结合性的。
比如: += 是从右往左开始的 + 是从左往右开始的
我们之前说过,类的6个默认函数即使我们不写编译器也会写一个默认的,那么默认的能完成赋值重载的任务吗?这个问题和拷贝赋值是一样的,对于内置类型编译器可以完成赋值,但是对于像栈那样需要开不同空间的必须我们手动去写一个赋值重载。
前面讲拷贝构造的时候忘记了一个细节,那就是向上图中红色框起来的也是拷贝构造,有些人会有疑问,这里不是用赋值重载了吗?其实并不是,赋值重载的调用是针对两个已经实例化好的或定义的对象,而像上图中d5还没有定义出来是在实例化的过程中,是用d1初始化d5,用一个对象初始化一个对象用的是拷贝构造。
下面我们利用运算符重载实现一个计算日期的小程序,小程序的功能包括:日期+天数,日期+=天数,日期-天数,日期-=天数,日期的前置++后置++,日期的前置--后置--,日期-日期相差多少天,下面先展示源代码然后我们一个函数一个函数讲解:
class Date { public: Date(int year = 2023, int month = 2, int day = 10) { if (year > 0 && (month >= 1 && month <= 12) && (day > 0 && day <= GetMonthDay(year, month))) { _year = year; _month = month; _day = day; } else { cout << "日期不合法:" << endl; exit(-1); } } void Print() { cout << _year << "年" << _month << "月" << _day << "日" << endl; } bool operator==(const Date& d) { return _year == d._year && _month == d._month && _day == d._day; } bool operator<(const Date& d) { if (_year < d._year) { return true; } else if (_year == d._year && _month < d._month) { return true; } else if (_year == d._year && _month == d._month && _day < d._day) { return true; } else { return false; } } bool operator>=(const Date& d) { return !(*this < 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); } Date& operator=(const Date& d) { if (this != &d) { _year = d._year; _month = d._month; _day = d._day; } return *this; } int GetMonthDay(int year, int month) { int MonthArray[13] = { 0,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 MonthArray[month]; } } //日期 += 天数 Date& operator+=(int day) { if (day < 0) { *this -= -day; return *this; } _day += day; while (_day > GetMonthDay(_year, _month)) { _day -= GetMonthDay(_year, _month); _month++; if (_month == 13) { _year++; _month = 1; } } return *this; } //日期 + 天数 Date operator+(int day) { Date tmp(*this); tmp += day; return tmp; } //前置++ Date& operator++() { *this += 1; return *this; } //后置++ Date operator++(int) { Date tmp(*this); *this += 1; return tmp; } //日期 -= 天数 Date& operator-=(int day) { if (day < 0) { *this += -day; return *this; } _day -= day; while (_day <= 0) { _month--; if (_month == 0) { _year--; _month = 12; } _day += GetMonthDay(_year, _month); } return *this; } //日期 - 天数 Date operator-(int day) { Date tmp(*this); tmp -= day; return tmp; } //前置-- Date& operator--() { *this -= 1; return *this; } //后置-- //int参数 仅仅是为了占位,根前置重载区分 Date operator--(int) { Date tmp(*this); *this -= 1; return tmp; } //日期相减(得到的是天数) int operator-(const Date& d) { Date Max = *this; Date Min = d; int flag = -1; if (*this < d) { Max = d; Min = *this; flag = 1; } int n = 0; while (Max != Min) { n++; Min++; } return n * flag; } private: int _year; int _month; int _day; }; int main() { Date d1(2023, 12, 3); Date d2(2023, 6, 4); //cout << (d1 > d2) << endl; /*d1 = --d2; d1.Print(); d2.Print();*/ //d2.Print(); //cout << (d1 - d2) << endl; d2 =d1 - -100; d2.Print(); return 0; }
首先,我们在构造函数中初始化的时候要确保日期是合法的,不能出现月数小于0或者大于12的,并且天数要大于0小于当月最大天数,所以我们在构造函数中加了一个判断,当日期不合法时就输出"日期不合法"并且退出程序。
int GetMonthDay(int year, int month) { int MonthArray[13] = { 0,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 MonthArray[month]; } }
因为我们计算日期的时候必须知道每个月是多少天而且还有闰年二月是29天的情况,所以我们写了一个函数得到每个月的天数,数组有13个是因为数组是从0开始我们为了方便直接在第一个位置加一个0即可。
接下来我们讲解日期+=天数:
Date& operator+=(int day) { if (day < 0) { *this -= -day; return *this; } _day += day; while (_day > GetMonthDay(_year, _month)) { _day -= GetMonthDay(_year, _month); _month++; if (_month == 13) { _year++; _month = 1; } } return *this; }
在这里由于不确定要加多少天如果加1000天那么就要重复上图的步骤所以这是一个循环,当天数大于当月最大天数的时候就进入循环,需要注意的是当月数加到13就说明越界了要及时改为合法月数。因为+=就是会改变本身的值,并且在函数结束后日期也没有被销毁,所以我们采用传引用的方式减少拷贝构造的消耗。判断天数是否小于0是因为我们不知道有人会不会写成-数,如果是负数那就是-=一个正数。
Date operator+(int day) { Date tmp(*this); tmp += day; return tmp; }
日期+天数那么是不会改变本身的,所以我们需要拷贝构造一个变量,这时候直接复用+=操作符即可,由于tmp是函数中的临时变量,函数结束就会销毁,所以不能采用传引用的方式。
//前置++ Date& operator++() { *this += 1; return *this; } 前置++是先++在使用,所以直接+1返回即可。 //后置++ Date operator++(int) { Date tmp(*this); *this += 1; return tmp; }
后置++是先使用再++,也就是说我们必须用一个变量接收开始的日期,然后自己+1返回开始没有+1的那个值即可,由于这个值是临时变量所以只能用传值返回。需要注意的是,编译器区分前置++和后置++的点是后置++的参数有一个int,这个int是占位符没有实际作用,也不用写参数,写一个int即可。
//日期 -= 天数 Date& operator-=(int day) { if (day < 0) { *this += -day; return *this; } _day -= day; while (_day <= 0) { _month--; if (_month == 0) { _year--; _month = 12; } _day += GetMonthDay(_year, _month); } return *this; }
需要注意的是减去天数后如果大于0就说明本月的天数够用不需要向上个月借,等于0也需要借因为没有2月0日,为了让借到的天数是上个月的所以月份--后再加上借的天数,与+=同理都要判断day是否为负数,为负数就变成了+上一个正数,那为什么+和-我们没有判断呢?因为+和-我们是用+=和-=复用的。
//日期 - 天数 Date operator-(int day) { Date tmp(*this); tmp -= day; return tmp; }
这里与+一样复用就可以。前置--和后置--也与++一样。
//日期相减(得到的是天数) int operator-(const Date& d) { Date Max = *this; Date Min = d; int flag = -1; if (*this < d) { Max = d; Min = *this; flag = 1; } int n = 0; while (Max != Min) { n++; Min++; } return n * flag; }
日期相减实现起来也很简单,我们以之前的为负,以后的为正,先定义两个变量Max和Min来存放两个日期,我们默认是第一个日期大于第二个日期,当第一个大于第二个日期的时候就说明是之前的那么让flag为负,如果第一个日期小于第二个日期,就让flag为正。然后我们用n来记录天数,当两个日期不相等就进入循环,让n和小的那个日期自加直到相等我们就能计算出有多少天了。
那么我们每次调用函数去打印日期是不是不方便呢?能不能直接用cout打印日期呢?答案是可以的,我们通过重载<<运算符即可完成。我们现在类中声明然后再类外实现。
那么我们写了一个为什么不能调用呢?
我们只能通过调用函数的方式去调用,和我们想的并不一样,这怎么办呢?我们用cout不能直接调用的原因是操作符的左边是左操作数,右边是右操作数,而我们常用的打印习惯是右操作数,那么我们先来看一下左操作数是否能正确调用:
我们发现是可以正确调用的,但是很奇怪我们喜欢写到右边。解决这个问题之前我们要先知道运算符重载在类中第一个操作符是*this,而像我们那样的写法很明显out是右操作数了,我们要的是out去作为左操作数,想要让out成为左操作数将运算符重载写到外面了不就解决了吗,因为外面是没有*this的。
但是当我们写到定义在类外发现不能访问类内的成员了,这里的解决方式有多个,我们讲两个简单的即可,第一个将类内私有改为公有如下图:
第二个是用友元函数,我们将这个函数设为类的友元就可以访问类的所有成员了。
为什么编译器的cout支持多个打印我们的不可以呢?这是因为我们没有返回值,我们应该要将out返回这样就能连续打印了,因为out出了作用域没有被销毁所以我们可以返回其引用。
这样就解决了打印自定义类型的问题,接下来我们再重载一下cin,与cout一样只需要改一下参数即可。
为什么输入的参数d我们不加const了呢?这是因为我们输入会改变const的值如果加了const就不能改变了。
在这里需要注意一下,类里面的短小函数,适合做内联的函数,直接是在类里面定义的。
const成员
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
class A { public: void Print() { cout << _a << endl; } private: int _a; }; int main() { /*A aa; aa.Print();*/ const A aa; //权限的放大 aa.Print(); return 0; }
在这里为什么会报错呢?因为aa的类型是const A* ,而传递给this指针后变成了A* ,所以这里是权限的放大,想要解决只需要给this指针也用const修饰,如下图:
这里函数后面的const修饰的是this指针,让this指针变成了const A*。
而上图是权限的缩小,权限的缩小是没有问题的,调用Func函数从A*变成了const A*.
总结:内部不改变成员变量的成员函数,最好加上const,const对象和普通对象都可以调用,比如下面的代码:
#include <assert.h> class Array { public: int& operator[](int i) { assert(i < 10); return _a[i]; } const int& operator[](int i) const { assert(i < 10); return _a[i]; } private: int _a[10]; int _size; }; void Func(const Array& d) { for (int i = 0; i < 10; i++) { cout << d[i] << " "; } } int main() { Array ay; for (int i = 0; i < 10; i++) { ay[i] = i * 10; } for (int i = 0; i < 10; i++) { cout << ay[i] << " "; } Func(ay); return 0; }
取地址重载
取地址重载和赋值运算符重载一样都会由编译器自己生成,当然有需求也可以自己去写。
本来自定义类型用运算符必须自己重载,但是赋值运算符和取地址重载编译器生成的就够用。
当然如果我们不想让别人获取我们的地址我可可以返回一个假地址如上图所示
以上就是取地址重载的用法,总之不是非常必要是不用去自己写取地址重载和const取地址重载的。
总结
学会运算符重载是学习c++必备的技能,c++独特的就是自定义类型,而运算符重载可以解决自定义类型使用运算符的问题。