1.运算符重载引入
【1】从函数重载说起
- 函数重载是在一定作用域内,多个函数名相同但参数列表不同的函数重载
- 编译时由编译器根据实际调用时给的实参情况来判定本次实际用哪个函数,这个过程叫重载决策
- 重载函数本质上就是多个独立函数,重载机制在编译时发生,运行时不参与
- 函数重载的意义就是避免我们胡乱起名,方便编写类库覆盖所有可能操作,是一种语法糖
【2】什么是运算符重载
- 什么是运算符?譬如
+ - * / %
等算术运算符和> < == !=
等关系运算符就是典型的可重载运算符(但不是所有的运算符都可以重载,譬如sizeof) - 运算符诞生于C语言中,用来对变量进行某种“预定义”的运算操作,这种预定义是编译器预制好的,编译时会翻译对应到CPU机器码
- 面向对象时代带来新的问题:==两个对象如何运算?==譬如:Person a, b, c; c = a + b; 此处的+让编译器如何解读?见下面示例
【3】运算符重载示例
- class coordinate(坐标), 2个属性x和y,直接对2个对象执行+,编译不通过
- 重载+运算符后,再编译并执行,查看结果,分析结果
#include <iostream>
using namespace std;
class coordinate
{
public:
int x;
int y;
coordinate();
coordinate(int x0,int y0);
void print(void);
//定义类的时候,提供一个运算符重载的对应解析函数即可
//返回值 函数名 参数列表
coordinate operator+ (const coordinate other);
};
//默认构造函数
coordinate::coordinate()
{
x = 0;
y = 0;
}
//带参数构造函数
coordinate::coordinate(int x0,int y0)
{
x = x0;
y = y0;
}
//打印坐标函数
void coordinate::print(void)
{
cout << "(" << this->x <<","<< this->y << ")" <<endl;
}
//运算符重载"+"解析函数
coordinate coordinate::operator+ (const coordinate other)
{
//在该函数内,去实现"+"的真正应该做的操作
coordinate tmp;
tmp.x = this->x + other.x;
tmp.y = this->y + other.y;
return tmp;
}
int main()
{
coordinate a(1,3);
coordinate b(2,5);
coordinate c;
c = a+b; //被编译器翻译成: c = a.operator+(b);
c.print(); // 成功打印 (3,8)
return 0;
}
- 总结
【4】运算符重载的本质
- 表面上,运算符重载是对C++源生运算符的意义,在某个class中做重定义
- 本质上,运算符被映射到执行相应的成员函数,所以运算符重载其实是重定义对象的运算符所对应的函数
【5】运算符重载的意义
- 运算符重载是一种语法特性,C++全面支持,Java不支持,python有限度的支持
- 没有运算符重载照样写代码,所有操作全部通过显式调用相应成员函数来完成即可
- 运算符重载是一种语法糖,可以让代码“看起来更简洁,更优雅”,将复杂实现隐藏在类的内部
- 运算符重载机制加大了类库作者的工作量,减少了调用类库写功能的人的书写量
- C++支持运算符重载机制有其理念和历史原因,是对写法简洁和效率优秀的综合考量
- 赋值运算符=重载和引用结合,可有效提升代码效率,后面会有案例详解
- 赋值运算符=重载时要注意有指针成员时会涉及浅拷贝和深拷贝,后面会有案例详解
- 运算符重载一定程度上体现了C++的多态性,因为同样的运算符在不同的class中表现是不同的
2.理解运算符重载的关键点
【1】理解运算符重载机制的两部曲
- 记住、理解、并且能轻松认出写出运算符重载函数的格式、名称、对应的运算符。
- 理解在运算符重载函数中this表示谁,参数表示谁,返回值对应谁,这个是重中之重。
【2】通过代码实践来深度理解运算符重载函数
- 运算符+的重载中的对应关系回顾
coordinate coordinate::operator+ (const coordinate other)
{
//在该函数内,去实现"+"的真正应该做的操作
coordinate tmp;
tmp.x = this->x + other.x;
tmp.y = this->y + other.y;
return tmp;
}
总结:a + b; 等价于 a.operator+(b)
,a对应this,b对应函数参数other,a+b的表达式的值对应函数返回值
- 运算符=的重载中的对应关系
//运算符重载"="解析函数1_2
void coordinate::operator=(const coordinate other)
{
//在该函数内,去实现"="的真正应该做的操作
//c = a; c是this,a是other,c=a整个表达式的值是返回值
this->x = other.x;
this->y = other.y;
return ; //实现为无返回值的函数时,在对对象进行 = 的时候不能连等,也就是说不支持 d = (c = a) ;
}
coordinate coordinate::operator=(const coordinate other)
{
//在该函数内,去实现"="的真正应该做的操作
//c = a; c是this,a是other,c=a整个表达式的值是返回值
this->x = other.x;
this->y = other.y;
return *this; //实现为有返回值的函数时可以支持 d = (c = a) ;这样连等的形式
}
总结:c = a; 等价于 c.operator=(a);
c对应this,a对应other,c=a整个表达式的值(其实就是c)对应函数返回值
- 运算符+=的重载中的对应关系
//运算符重载"+="解析函数
void coordinate::operator+=(const coordinate other)
{
//a += b; 效果上等价于 a = a + b;
// c = (a += b); 我们一般不会这样写,所以定义成无返回值就行
//a是this ,b是other ,操作完成a 的值改变,b的值不改变
this->x = this->x + other.x;
this->y = this->y + other.y;
return;
}
总结:a += b; 等价于 a.operator+=(b);
a对应this,b对应other,a+=b的整体表达式对应返回值
运算符重载总的规则:运算符左边的是this,右边的是other,运算符加操作数的整个表达式的返回值就是返回值
【3】运算符=的默认提供问题
- =运算符有点特殊,编译器会提供一个默认的=运算符的重载,所以不提供时也能用
- 如果自己显式写了=运算符的重载函数,则会覆盖编译器自动提供的那一个
【4】总结
- 编译器会为每个自定义class提供一个默认的赋值运算符的重载,而我们可以覆盖这个重载
- 盯紧表达式实际对应执行哪个函数,盯紧函数的参数是谁,this是谁,返回值是谁,一切都清楚了
- a+b有可能不等于b+a,这个也是运算符重载衍生出来的不好把控的风险问题
3.运算符重载函数中的细节
【1】赋值运算符重载与拷贝构造函数
- 区分初始化时的赋值(一般就叫初始化),和非初始化时的赋值(一般就叫赋值)
- 实验验证初始化和赋值时各自对应
【2】赋值运算符重载函数参数中的const
- 如果运算符对其右侧的操作数并不改变,则应该在参数列表中使用const引用
- C++的const要求非常严格,在c = a + b;中如果=运算符的重载函数参数列表中没有用const会编译报错。
【3】避免赋值运算符中的自赋值
- 自赋值就是Person a; a = a;
- 自赋值如果不处理,轻则浪费效率,重则导致内存丢失(该深拷贝时做了浅拷贝)
- 避免自赋值很简单,只需要在赋值运算符重载函数所有操作前加上一个判断 if (this != &other)即可
4.赋值运算符重载函数返回引用
【1】返回引用好处1:提升程序效率
- 赋值运算符重载函数返回值可以返回对象类型,也可以返回对象引用类型,都能工作。
- 区别在于:返回引用可以避免一次返回值值传递的对象复制,这需要消耗资源的。
- 虽然C++语法并未强制要求,但是好的写法是应该返回引用
【2】返回引用好处2:允许连续赋值式
- 返回对象而不是引用时,在连续赋值(c = a = b;)时会编译可以,运行也可以,但是效率低同1中所讲。
- 原因是先执行a=b操作,返回值再作为第2次赋值运算的右值(也就是函数参数),对象和引用是类型兼容的
- 连等在返回对象和引用时都可以,但是在返回void时就不可以了
【3】传参为什么要传引用
- 如果传对象,则调用时是值传递,调用时需要复制一次,增加额外开销
- 如果传指针,则重载后,使用时为了符合函数参数格式必须写成 a = &b(这里a和b都是对象);这种,不符合C语言写法习惯,也有歧义
- 实际测试发现真的传指针时,写 a=b; a=&b;都能编译通过,且运行正确。但是确实很别扭。