前言:
C++为了增强代码的可读性引入了运算符重载,运算符重载具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型为:返回值类型 operator操作符(参数列表)。
注意:
1、常用的符号有+、-、*、/、++、--、==、=、>、<、>=、<=等符号,不能通过连接其他符号来创建新的操作符比如operator@、operator$等。
2、重载操作符必须有一个类类型参数。
3、用于内置类型的运算符,其含义不能改变,例如:内置的类型+,不能改变其含义。
4、作为类成员函数重载时,其形参看起来比操作数数目少一,因为成员函数的第一个参数为隐藏的this.
5、注意:.* :: sizeof ?: . 这5个运算符不能重载,这是经常要考察的内容。
🏆一、赋值运算符重载
1、赋值运算符重载格式
·参数类型:const T&,传递引用可以提高传参效率。
·返回值类型: T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值。
·检查是否自己给自己赋值。
·返回*this:要符合连续赋值的含义。
赋值运算符重载的声明格式是这样的:
类名 & operator=(const 类名& 类类型参数)
我们以日期类为例:
class Date { public: Date& operator=(const Date& d) { if(this != &d) { _year = d._year; _month = d._month; _day = d._day; } return *this; } private: int _year ; int _month ; int _day ; };
👓1.1参数设计细节
1、传引用并且用const限制的原因是,传值会调用拷贝构造,const限制可以防止被修改。
2、返回值返回引用主要是为了支持连续赋值。
假如我返回void类型:
void operator=(const Date& d) { if(this!= &d) { _year = d._year; _month = d._month; _day = d._day; } return; }
可以发现,我们的确完成了拷贝的任务,但是返回值是void类型的话会出现什么问题呢,无法连续赋值!
因为没有返回值是无法连续赋值的,可能这样说还不够清楚,我就举个简单的例子:
int main() { int i, j; i = j = 10; (i = j = 10)++;//可以修改 cout << i << " " << j << endl; return 0; }
观察编译器默认的连续赋值是怎样的呢?我们可以总结出如下规律:
1、赋值从右往左:10赋给j,j=10有一个返回值,然后赋给i。
2、返回的值是可以修改的,可以基本判定返回的值是左值(赋值运算符=的左边),因为右值是作为const常量传参的,返回右值就不可被修改且缩小了权限。
所以我们就可以写出:
Date& operator=(const Date& d) { if(this != &d) { _year = d._year; _month = d._month; _day = d._day; } return *this; }
1、可能有的老铁困惑:出了函数,this就要被销毁,这里为什么不返回类类型,而是返回类引用呢?因为这里this虽然被销毁,但是作为别名,我们要赋值的类对象是存在的。所以使用引用是可以的,而且可以少一次拷贝(传值返回是需要拷贝构造的)。
2、至于返回*this而不返回d的原因也很简单,d是被const限制的,如果返回d意味着返回值不可被改变,权限缩小,这是不合适的。
👓1.2 赋值运算符只能重载成类的成员函数不能重载成全局函数
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数 Date& operator=(Date& left, const Date& right) { if (&left != &right) { left._year = right._year; left._month = right._month; left._day = right._day; } return left; } // 编译失败: // error C2801: “operator =”必须是非静态成员
如果我们写在类外面编译器是不支持的,因为赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
而且我们在类外是访问不了private内的类型的。
👓1.3需要写赋值重载函数的场景
我们知道赋值运算符重载是编译器默认生成的函数,那么如果我们不写这个函数,编译器自动生成的能否完成赋值拷贝的功能呢?
🖊①用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝
class Date { public: int GetMonthDay(int year, int month) { static int monthDayArray[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 monthDayArray[month]; } } Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; //检查日期的合法性 if (!(year >= 1 && (month >= 1 && month <= 12) && (day >= 1 && day <= GetMonthDay(year, month)))) { cout << "非法日期" << endl; } } Date(const Date& d1) { _year = d1._year; _month = d1._month; _day = d1._day; } /*Date& operator=(const Date& d) { _year = d._year; _month = d._month; _day = d._day; return *this; }*/ void Print() { cout << _year << "/" << _month << "/" << _day << endl; } private: int _year; int _month; int _day; }; void TestDate1() { Date d1; Date d2(2022, 10, 8); Date d3; d1.Print(); d1 = d2; d1.Print(); } int main() { TestDate1(); return 0; }
我们可以看到,它是可以完成赋值重载的功能的,那么是不是我们就不用赋值重载或者,哪些情况需要我们去写呢?
🖊②自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值
这里和拷贝构造函数是十分相似的,不写析构函数的不用写赋值重载函数。
什么意思?就是说不涉及动态开辟内存或者文件管理的不需要写赋值重载。
比如说一个自定义类型栈,如果我不去写它的赋值重载,会发生什么?
我们可以看到直接崩掉,崩掉的原因是什么呢?
我们可以看到,没有写赋值重载函数st1的_a和st2的_a指向了同一块空间。那会有什么危害呢?
指向同一块空间不仅会导致在进程结束的时候,对栈st2的_a空间析构两次(free两次),也会发生内存泄露。即st1的_a开辟的空间没有被free掉,这是十分危险的!!
有的老铁可能要说,那不简单,我直接realloc一下不就行了?真的是这样吗?我们再来分析一下。
这里的情况有三种:
1、栈st1的_a开辟的空间小于st2的_a。
2、栈st1的_a开辟的空间等于st2的_a。
3、栈st1的_a开辟的空间大于st2的_a。
那可能我们能这样分析:如果小于,就realloc后拷贝复制。如果等于直接覆盖赋值,如果大于就直接赋值。这样判断是否麻烦了?而且如果我st1的_a开辟的空间是10000个字节,而栈st2的_a开辟的空间只有1000个字节,只将st2的_a赋值给st1的_a是否太过于浪费空间?
所以这里的操作是直接free掉,然后拷贝复制。
我们看到这样好像赋值重载成功了。但是这里还是有老六的情况出现。
🖊③自己赋值自己
如果把st1赋值给st1,就会出现这样的状况,因为我们先把st1的_a给free掉了,然后再去赋值st1的_a,这样显然会出错,所以为了防止这样的状况,我们就加一层判断。
👓 1.4赋值重载和拷贝构造函数的对比
通过对赋值重载的介绍,大家是否发现它和拷贝构造很是相似,那么我们就来对比一下。
Date d1; Date d2(2022, 10, 8); Date d3(d2);//拷贝构造(初始化) 一个初始化另一个,还没初始化 d1 = d2;
我们发现赋值重载的特点是这两个对象都已经存在,而且初始化构建好了,是已经存在的两个对象之间的拷贝。而拷贝构造函数是初始化,一个初始化另一个且这个类对象还没有创建。
Date d2(2022, 10, 8); Date d4=d2;
这种情形算作是拷贝构造还是赋值重载呢?单看符号似是赋值重载,但我们要看本质,它的意义是初始化且拷贝(因为d4原来不存在),所以严格上讲它是拷贝构造。
拷贝构造和赋值重载的区别:
1、拷贝构造是初始化,一个初始化另一个,这个类对象还没创建;
2、赋值重载的特点是这两个对象都已经存在,并且初始化构建好了,是已经存在的两个对象之间的拷贝。
3、赋值重载函数和拷贝构造函数还有一个共同特点是当需要写析构函数时,它们都需要写。
🏆二、运算符重载
上面只是介绍了赋值运算符重载,当然还有很多运算符重载,它们是如何实现的呢?博主以日期类为例,介绍各类运算符重载。
日期类的创建和声明:
class Date { public: int GetMonthDay(int year, int month) { static int monthDayArray[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 monthDayArray[month]; } } Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; //检查日期的合法性 if (!(year >= 1 && (month >= 1 && month <= 12) && (day >= 1 && day <= GetMonthDay(year, month)))) { cout << "非法日期" << endl; } } //fopen fclose Date(const Date& d1) { _year = d1._year; _month = d1._month; _day = d1._day; } //运算符重载的意义是可读性 Date& operator=(const Date& d)//赋值可以传值,不会出现无穷递归,但是尽量使用传引用 { _year = d._year; _month = d._month; _day = d._day; return *this;//这样写不好,返回Date,传值返回也是拷贝构造, } //赋值重载 void Print() { cout << _year <<"/"<<_month<<"/"<<_day<< endl; } Date& operator+=(int day); //日期+天数 Date& operator-=(int day); Date operator-(int day); Date operator+(int 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) { 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& operator++(); Date operator++(int); //后置多两次拷贝,所以避免使用后置 private: int _year; int _month; int _day; };
👓2.1、基本运算符重载
🖊①==和!=
bool 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); }
🖊②>和<
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); }
这些都比较简单,唯一需要注意的就是博主十分推荐复用,可以方便很多。
🖊③+=和+
这个有点意思,如果给我们一个日期,让我们计算比如100天后是什么日期,我们如何计算呢?
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; return ret; }
1、对于+=,返回的还是自身,所以我们选择返回引用,对于函数体较大且不经常调用的我们一般建议放在类外实现。对于+,复用即可。
2、我们需要注意的一个细节是如果+=的天数是一个负数,我们需要注意,虽然不太可能出现,但还是应该考虑到。