运算符重载
可重载的运算符
大部分运算符都可重载:
双目算术运算符 | + (加),-(减),*(乘),/(除),% (取模) |
---|---|
关系运算符 | ==(等于),!= (不等于),< (小于),> (大于),<=(小于等于),>=(大于等于) |
逻辑运算符 | ||(逻辑或),&&(逻辑与),!(逻辑非) |
单目运算符 | + (正),-(负),*(指针),&(取地址) |
自增自减运算符 | ++(自增),–(自减) |
位运算符 | | (按位或),& (按位与),~(按位取反),^(按位异或),,« (左移),»(右移) |
赋值运算符 | =, +=, -=, *=, /= , % = , &=, |=, ^=, «=, »= |
空间申请与释放 | new, delete, new[ ] , delete[] |
其他运算符 | ()(函数调用),->(成员访问),,(逗号),[](下标) |
所以,只需要记住不可重载的:
.
:成员访问.*
:成员指针的成员访问::
: 域运算符?:
: 三目运算符
还有,不建议重载的:
&&
,||
: 重载会影响短路机制- 改变原运算符含义的。比如
+
重载成减法 - 改变运算符的优先级、结合方向或操作数
只能用成员函数重载的:
=
,()
,[]
,->
类内重载
类内重载,重载函数作为类的成员函数。运算符的操作数(中的第一个)隐含了 this 指针。
比如对 Test 类的运算符进行重载,可以实现如下合法运算:
class Test{...};
Test t, s;
t + s;
t + 1;
t + "test";
!t;
*t;
注意:
=
[]
()
->
只能类内重载
这四个运算符情况比较特殊。
- 对于
=
赋值运算符:
我们知道,编译器会隐式地定义赋值操作符重载函数。如果我们在类外又进行重载,就会发生二义性。因为类外的所谓重载函数,比如通过友元实现,并不是这个类的成员函数。
所以编译器提供的函数,和我们类外声明的函数会同时存在。
在发生赋值运算的时候,就会产生二义性。虽然这种代码在此之前就会编译不过。
- 对于
[]
()
->
:
类外重载能为我们提供什么?可以让第一个参数不是 this 指针。
而这三个操作符,第一个参数一定必须要是 this,否则会出现下面这堆畸形儿:
MyArr arr;
5[arr];
MyFunc func;
char c = '0';
c(func, 6);
MyObject obj;
"wtf"->obj;
当然,=
操作符也是一样的。
- 可以总结为:这四个操作符,必须保证参数的顺序。特别地,对于
=
,还要避免二义性。
类外重载
对于一目运算符,类内重载完全够用了。但是对于二目运算符存在这种情况:1 + t
以及 cout<<t
。这是类内重载无法匹配的,因为类内重载中 this 默认为第一个参数。所以此时需要类外重载。
class Test{
friend Test operator+ (const int a, const Test b);
};
Test operator+ (const int a, const Test b);
也可以不用友元函数实现,但要提供对应成员的 get / set 方法。
注意:
- 流运算符、第一个参数不为 this 指针的二目运算符,只能在类外重载
- 叫成“类外重载”其实有误导性。因为无论是通过友元函数,还是 Getter/Setter 方法,我们声明的新的运算符重载函数不是类的成员函数
写法细节
这里只提供比较特殊的写法细节。
区分单目和双目
相同字符的运算符可能有两种含义,比如 -
是减法或者负号, *
是乘法或者解引用。重载时怎么区分呢?
既然是重载,同名函数要区分开来自然是靠参数的对应关系。单目运算符对应的函数接受一个参数,双目两个。
T operator-(const T x);
T operator-(const T x, const T y);
流运算符
流运算符可以用来 cin 和 cout 输入输出。当然也可以对其他流进行重载。
要引入 iostream 头文件,并且只能在类外重载。
istream& operator>>(istream &stream, T &t) {
stream >> t.x;
return stream;
}
ostream& operator<<(ostream &stream, T &t) {
stream << t.x;
return stream;
}
赋值运算符
默认重载
编译器默认会提供一个赋值运算符重载函数。
- 逐个成员赋值,即逐个调用它们的赋值运算符函数
- 对含有对象成员的类,该定义是递归的
warning
有问题,还待研究
赋值运算符是唯一一个不能被派生类继承的运算符函数。
原因:
- 基类的赋值操作不一定适用于派生类。比如派生类新增了成员。
- 编译器还是会生成一个默认的赋值号函数。二者可能会冲突。
class Base{
Base& operator= (int other) {}
};
class Derived: public Base{
// Base::operator= (int other) {}
}
复制赋值
- 处理自赋值
- 返回引用,以便
a = b = c = d
这种语句
// 复制赋值
T& operator=(const T& other)
{
// 防止自赋值
if (this == &other)
return *this;
// 假设 *this 保有可重用资源,例如一个在堆的缓冲区分配的 mArray
if (size != other.size) // *this 中的存储不可复用
{
temp = new int[other.size]; // 分配存储,如果抛出异常则等同于什么也不做
delete[] mArray; // 销毁 *this 中的存储
mArray = temp;
size = other.size;
}
std::copy(other.mArray, other.mArray + other.size, mArray);
return *this;
}
移动赋值
移动赋值和上面的注意点是一样的,但要额外注意:
- 使 other 遗留在合法状态
// 移动赋值
T& operator=(T&& other) noexcept
{
// 防止自赋值
if (this == &other)
return *this; // delete[]/size=0 也可以
delete[] mArray; // 释放 *this 中的资源
mArray = std::exchange(other.mArray, nullptr); // 令 other 遗留在合法状态
size = std::exchange(other.size, 0);
return *this;
}
自增自减
- 用一个 dummy 参数
int
来标识后缀 - 后缀自增会返回旧的对象
// 前缀自增
X& operator++()
{
// 实际上的自增在此进行
return *this; // 以引用返回新值
}
// 后缀自增
X operator++(int)
{
X old = *this; // 复制旧值
operator++(); // 前缀自增
return old; // 返回旧值
}
参考
CppReference
为什么有的操作符重载函数只能是成员函数?_为什么赋值运算符只能作为成员函数进行重载-CSDN博客