运算符重载

可重载的运算符

大部分运算符都可重载:

双目算术运算符+ (加),-(减),*(乘),/(除),% (取模)
关系运算符==(等于),!= (不等于),< (小于),> (大于),<=(小于等于),>=(大于等于)
逻辑运算符||(逻辑或),&&(逻辑与),!(逻辑非)
单目运算符+ (正),-(负),*(指针),&(取地址)
自增自减运算符++(自增),–(自减)
位运算符| (按位或),& (按位与),~(按位取反),^(按位异或),,« (左移),»(右移)
赋值运算符=, +=, -=, *=, /= , % = , &=, |=, ^=, «=, »=
空间申请与释放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博客