四、赋值运算符重载函数
定义:内置类型,语言层面本就支持运算符,但是自定义类型,默认不支持运算符。C++运算符重载的目的是为了能够让自定义类型可以像内置类型一样使用运算符,需要哪个运算符,就重载哪个运算符。
运算符重载和函数重载,虽然都使用了重载,但是两者之间没有关联:
(1)函数重载时支持定义同名函数
(2)运算符重载是为了让自定义类型可以像内置类型一样去使用运算符。
函数原型:返回值类型 operator操作符(参数列表)
特性:
(1)不能通过连接其他符号来创建新的操作符:比如operator@
(2)重载操作符必须有一个类类型或者枚举类型的操作数
(3)用于内置类型的操作符,其含义不能改变
(4)作为类成员的重载函数时,其形参看起来比操作数数目少1个的成员函数操作符有一个默认的形参this,默认为第一个形参
(5)".*" 、"::" 、"sizeof" 、"?:" 、"." 这5个运算符不能重载
(6)不能把运算符定义成全局,如果参数是私有成员变量就不能访问该参数了,要写成成员函数。
运算符重载实例:
(1)==运算符重载
1. #include<iostream> 2. using namespace std; 3. 4. class Date 5. { 6. public: 7. //构造函数 8. Date(int year = 2022, int month = 4, int day = 8) 9. { 10. _year = year; 11. _month = month; 12. _day = day; 13. } 14. 15. //成员变量公有 16. public: 17. int _year; 18. int _month; 19. int _day; 20. }; 21. 22. //operator==运算符重载 23. bool operator==(Date x1, Date x2) 24. { 25. return x1._year == x2._year 26. && x1._month == x2._month 27. && x1._day == x2._day; 28. } 29. 30. int main() 31. { 32. Date d1(2022, 4, 12); 33. Date d2(2022, 4, 13); 34. 35. //两种调用方式: 36. //1.可读性不强 37. operator==(d1, d2); 38. 39. //2.当编译器看到==自定义类型,会去检查日期类有没有==的重载运算符,如果有重载会转换成operator==(d1, d2)去调用operator==函数 40. d1 == d2; 41. 42. return 0; 43. }
但以上写法只能针对成员变量属性是公有的情况,如果成员变量属性是私有,全局的operator==定义就会因无法访问成员变量而报错。但把operator==定义成成员函数就会只有一个显式的参数(d2),并且调用方式也会发生变化:
1. #include<iostream> 2. using namespace std; 3. 4. class Date 5. { 6. public: 7. //构造函数 8. Date(int year = 2022, int month = 4, int day = 8) 9. { 10. _year = year; 11. _month = month; 12. _day = day; 13. } 14. 15. bool operator==(Date d) 16. { 17. return _year == d._year 18. && _month == d._month 19. && _day == d._day; 20. } 21. 22. //成员变量私有 23. private: 24. int _year; 25. int _month; 26. int _day; 27. }; 28. 29. int main() 30. { 31. Date d1(2022, 4, 12); 32. Date d2(2022, 4, 13); 33. 34. //两种调用方式: 35. //1.调用方式相对于全局操作符重载函数发生了变化,编译器会把调用转换成d1.operate==(&d1,d2) 36. d1.operator==(d2); 37. 38. //2.先在全局看有没有operator==函数,如果没有就会去类里面重载成d1.operate==(&d1,d2) 39. d1 == d2; 40. 41. return 0; 42. }
(2)其他运算符重载请见下一篇文章【C++】-- 实现Date类的各种运算符重载
赋值运算符重载和拷贝构造的区别:
(1)拷贝构造创建了一个之前不存在的对象,是用同类对象初始化的拷贝。
(2)定义:赋值运算符重载虽然也是拷贝行为,但是拷贝时该对象已经存在且被初始化,现在把一个对象赋值拷贝给另一个对象。
如下是拷贝构造还是赋值运算符重载呢?
Date d6 = d1;
现在定义d6,说明之前d6不存在,因此是拷贝构造。
特性:
(1)参数类型:返回引用
(2)返回值:返回*this
(3)检测:需要判断是否自己给自己赋值
(4)一个类如果没有显式定义赋值运算符重载,编译器也会默认生成,完成浅拷贝
现在想实现将d2的值赋值给d1:
1. int main() 2. { 3. Date d1(2022, 9, 6);//调用构造函数 4. Date d2; 5. 6. d1.Print(); 7. d2.Print(); 8. 9. d1 = d2; 10. d1.Print(); 11. d2.Print(); 12. 13. return 0; 14. }
赋值运算符重载函数应该怎么写呢?如果这么写的话,传值需要调拷贝构造,调完拷贝构造再执行operator=这个函数,很麻烦:
1. //d1 = d2 2. Date& operator=(const Date d) 3. { 4. 5. }
因此要用传引用
1. //赋值运算符重载函数 2. //d1 = d2; //d1.operator=(&d1,d2) 3. Date& operator=(const Date& d)//void operator=(Date* this,const Date& d) 4. { 5. _year = d._year; 6. _month = d._month; 7. _day = d._day; 8. 9. return *this; 10. }
假如有这样的连续赋值场景,会从右向左结合,两次调用赋值运算符重载:
d1 = d2 = d3;
对于以下表达式,将k先赋值给j,j = k的返回值是j,再将j赋值给i,最后表达式的返回值是i:
i = j = k;
同理,执行d2 = d3时,d2作为this,d3作为d,返回值是d2,要返回d2的值,只需要返回*this即可:
1. //赋值运算符重载函数 2. //d1 = d2; //d1.operator=(&d1,d2) 3. Date operator=(const Date& d)//void operator=(Date* this,const Date& d) 4. { 5. _year = d._year; 6. _month = d._month; 7. _day = d._day; 8. 9. return *this; 10. } 11. 12. void Print() 13. { 14. cout << _year << "-" << _month << "-" << _day << endl; 15. } 16. 17. //析构函数:清理资源 18. ~Date() 19. { 20. cout << "~Date()" << endl;//在析构函数内打印 21. }
不过对于上面的传值返回Date operator=(const Date& d),会生成一个临时对象,会把*this拷贝给临时对象,再拿临时对象作为表达式的返回值,但是用*this构造初始化另外一个对象,会调用拷贝构造函数,为了验证会调用拷贝构造函数,将拷贝构造函数放开:
1. #include <iostream> 2. using namespace std; 3. 4. class A 5. { 6. public: 7. //构造函数 8. A(int a = 1) 9. { 10. _a = a; 11. } 12. 13. //拷贝构造函数 14. A(const A& a) 15. { 16. cout << "A(const A& a)" << endl; 17. _a = a._a; 18. } 19. 20. //析构函数 21. ~A() 22. { 23. cout << "~A()" << endl; 24. } 25. 26. private: 27. int _a; 28. }; 29. 30. class Date 31. { 32. public: 33. //构造函数 34. Date(int year = 2022, int month = 4, int day = 8) 35. { 36. _year = year; 37. _month = month; 38. _day = day; 39. 40. cout << "Date()" << endl; 41. } 42. 43. //拷贝构造函数 44. Date(Date& d)//Date(Date* this, Date& d) 45. //d4是this, 形参d是d1的引用,是d1内存的别名 46. { 47. _year = d._year; 48. _month = d._month; 49. _day = d._day; 50. 51. cout << "call copy constructors" << endl; 52. } 53. 54. //赋值运算符重载函数 55. //d1 = d2; //d1.operator=(&d1,d2) 56. Date operator=(const Date& d)//void operator=(Date* this,const Date& d) 57. { 58. _year = d._year; 59. _month = d._month; 60. _day = d._day; 61. 62. return *this; 63. } 64. 65. void Print() 66. { 67. cout << _year << "-" << _month << "-" << _day << endl; 68. } 69. 70. //析构函数:清理资源 71. ~Date() 72. { 73. cout << "~Date()" << endl;//在析构函数内打印 74. } 75. 76. private: 77. int _year; 78. int _month; 79. int _day; 80. 81. A _a; 82. }; 83. 84. int main() 85. { 86. Date d1(2022, 9, 6);//调用构造函数 87. Date d2; 88. Date d3; 89. 90. d1.Print(); 91. d2.Print(); 92. 93. d1 = d2 = d3; 94. 95. d1.Print(); 96. d2.Print(); 97. 98. return 0; 99. }
发现调用了拷贝构造函数:
为了减少拷贝构造,传引用返回有一个前提,如果出了函数作用域,引用还在,就可以使用传引用返回,现在出了赋值运算符重载函数,*this还在,因为*this是d2,d2是在main函数中创建的,生命周期在赋值运算符重载函数外面,出了赋值运算符重载函数,d2还在,因此将Date operator=(const Date& d)改为使用引用返回就不会调拷贝构造
1. //赋值运算符重载函数 2. //d1 = d2; //d1.operator=(&d1,d2) 3. Date& operator=(const Date& d)//void operator=(Date* this,const Date& d) 4. { 5. _year = d._year; 6. _month = d._month; 7. _day = d._day; 8. 9. return *this; 10. }
没有调用拷贝构造函数:
现在还需要检查可能的误操作,比如对象是否自己给自己赋值,如下面代码:
d1 = d1;
赋值运算符重载函数可以加一个判断:
1. //d1 = d2; //d1.operator=(&d1,d2) 2. Date& operator=(const Date& d)//void operator=(Date* this,const Date& d) 3. { 4. 5. if (this != &d)//对d取地址,判断this的值和d的地址是否相同,如果不是自己给自己赋值,才需要拷贝 6. { 7. _year = d._year; 8. _month = d._month; 9. _day = d._day; 10. } 11. 12. return *this; 13. }
就算我们不写赋值运算符,编译器也会默认生成,同拷贝构造一样:
(1)对于内置类型,会完成浅拷贝,比如Date类不需要我们写赋值运算符重载,Stack类需要自己写。
(2)对于自定义类型,会调用自定义类型的赋值运算符重载完成拷贝。
定义:将const修饰的类成员函数称为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
比如有如下场景:假如把Date类的operator==运算符重载函数写错了
1. bool operator==(const Date& d) 2. { 3. return (_year == d._year) 4. && (_month == d._month) 5. && (_day == d._day); 6. }
将其中的一个"=="错写成"=" :
1. bool operator==(const Date& d) //bool operator==(Date* this,const Date& d) 2. { 3. return (_year == d._year) 4. && (_month = d._month) 5. && (_day == d._day); 6. }
虽然编译没有问题,但是这会导致this的值被修改了,并且执行结果也错误:
1. int main() 2. { 3. Date d1(2022, 9, 6); 4. Date d2(2022, 3, 6); 5. 6. cout << (d1 == d2) << endl; 7. d1.Print(); 8. d2.Print(); 9. 10. return 0; 11. }
这不符合要求,仅仅是比较而已,但是被比较对象的值却被修改了。const最大的作用是保护对象和变量,d2传给了d,d是d2的别名,const已经保护了d,那d1如何保护呢?由于this是隐含的,那么const为了保护this,应该如何加?把const加在成员函数的后面,叫做const修饰成员函数
1. bool operator==(const Date& d) const 2. { 3. return (_year == d._year) 4. && (_month = d._month) 5. && (_day == d._day); 6. }
现在编译就会报错了:
这里const修饰的是*this,函数中不小心改变的成员变量,编译时就会被检查出来,在成员函数中,如果不需要修改成员变量的成员函数,建议都加上const。
思考:
(1) const对象可以调用非const成员函数吗?
不可以,因为this指针是const的,传到非const形参中,是权限放大,不允许
(2)非const对象可以调用const成员函数吗?
可以,因为this指针是非const的,传到const形参中,是权限缩小,允许
(3)const成员函数内可以调用其它的非const成员函数吗?
不可以,因为this指针是const的,传到非const形参中,是权限放大,不允许
(4)非const成员函数内可以调用其它的const成员函数吗?
可以,因为this指针是非const的,传到const形参中,是权限缩小,允许
五、取地址操作符重载和const取地址操作符重载
这两个操作符一般不需要重载,编译器默认生成的已经够用,重载没有价值。
1. #include <iostream> 2. using namespace std; 3. 4. class Date 5. { 6. public: 7. //构造函数 8. Date(int year = 2022, int month = 4, int day = 8) 9. { 10. _year = year; 11. _month = month; 12. _day = day; 13. } 14. 15. //取地址操作符重载--取对象地址 16. Date* operator&() 17. { 18. return this; 19. } 20. 21. //const取地址操作符重载--取const对象地址 22. const Date* operator&() const 23. { 24. return this; 25. } 26. 27. private: 28. int _year; 29. int _month; 30. int _day; 31. }; 32. 33. int main() 34. { 35. Date d1(2022, 9, 6); 36. Date d2(2022, 3, 6); 37. 38. cout << &d1 << endl; 39. cout << &d2 << endl; 40. 41. return 0; 42. }
只是取地址而已:
六、总结
如果我们不写,编译器对内置类型不做处理,自定义类型会调用它的构造函数和析构函数进行处理
如果我们不写,内置类型会完成浅拷贝,自定义类型会调用它的拷贝构造函数和赋值运算符重载函数。
一般不需要重载,编译器默认生成的已经够用,重载没有价值。