运算符重载(operator overloading)就是给自定义类型(类 / 结构体)定义运算符的行为。默认情况下,int a + int b 这种内置类型能 +,但 MyClass a + MyClass b 编译器不知道怎么办——除非你告诉它「+ 在我的类上应该怎么算」。

一、为什么需要运算符重载

举一个简单的例子:复数类想实现两个复数相加:

1
2
3
struct Complex {
double real, imag;
};

如果直接写 c1 + c2,编译报错:no match for 'operator+' ...。

通过运算符重载,可以写成像内置类型一样自然:

1
Complex c3 = c1 + c2;      // 像 int 那样用

二、运算符重载的语法

两种形式:

形式 1:成员函数

1
2
3
4
5
6
struct Complex {
double real, imag;
Complex operator+(const Complex& other) const {
return {real + other.real, imag + other.imag};
}
};

调用 c1 + c2 时,编译器翻译成 c1.operator+(c2)。

形式 2:全局函数(通常配合友元)

1
2
3
4
5
6
7
8
struct Complex {
double real, imag;
friend Complex operator+(const Complex& a, const Complex& b);
};

Complex operator+(const Complex& a, const Complex& b) {
return {a.real + b.real, a.imag + b.imag};
}

调用 c1 + c2 时,编译器翻译成 operator+(c1, c2)。

三、完整示例:复数类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
using namespace std;

struct Complex {
double real, imag;

Complex(double r = 0, double i = 0) : real(r), imag(i) {}

// 成员函数形式
Complex operator+(const Complex& other) const {
return {real + other.real, imag + other.imag};
}

// 等于比较
bool operator==(const Complex& other) const {
return real == other.real && imag == other.imag;
}

// 输出运算符:通常用友元全局函数
friend ostream& operator<<(ostream& os, const Complex& c) {
os << c.real << " + " << c.imag << "i";
return os;
}
};

int main() {
Complex c1(1, 2), c2(3, 4);
Complex c3 = c1 + c2;
cout << c3 << endl; // 4 + 6i
cout << (c1 == c2) << endl; // 0
return 0;
}

四、可以重载的运算符

类别 运算符
算术 + - * / %
比较 == != < > <= >=
逻辑 `&& \ \ ` (不推荐,会失去短路求值)
位运算 `& \ ^ ~ << >>`
自增减 ++ --(分前置、后置)
赋值 `= += -= *= /= %= &= \ = ^= <<= >>=`
下标 []
调用 ()
成员访问 ->
类型转换 operator T()
流 << >>(必须用友元)

⚠️ 不能重载的运算符:::(作用域)、.(成员访问)、.*、?:(三目)、sizeof、类型相关运算符。重载也不会改变优先级和结合性。

五、关键细节

5.1 重载为成员函数 vs 全局函数

情形 推荐形式
单目运算符、=、()、[]、-> 必须成员函数
<< >> 流运算符 必须友元全局函数(因为左侧是 ostream)
对称二元运算符(+ - * /) 都可以;需要访问私有成员时用友元

5.2 operator= 拷贝赋值与移动赋值

如果类里有指针 / 资源,要重载拷贝赋值和移动赋值(参考 RAII、智能指针那一篇):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Buffer {
char* data;
public:
Buffer& operator=(const Buffer& other) { // 拷贝赋值
if (this != &other) {
delete[] data;
data = new char[strlen(other.data) + 1];
strcpy(data, other.data);
}
return *this;
}
Buffer& operator=(Buffer&& other) noexcept { // 移动赋值
if (this != &other) {
delete[] data;
data = other.data;
other.data = nullptr;
}
return *this;
}
};

Rule of Three/Five:如果需要析构、拷贝构造、拷贝赋值三者之一,通常另外两个也需要;C++11 之后加上移动版本就是「Rule of Five」。

5.3 前置 ++ 和后置 ++ 的区分

1
2
3
4
5
6
7
8
9
10
11
12
13
class Counter {
int n;
public:
Counter& operator++() { // 前置 ++n
++n;
return *this;
}
Counter operator++(int) { // 后置 n++:int 是占位参数,仅用于区分
Counter tmp = *this;
++n;
return tmp;
}
};

前置版本返回引用,后置版本返回值——所以前置 ++ 比后置 ++ 略快(少一次拷贝)。循环里建议写 ++it 而不是 it++。

5.4 函数调用运算符 operator():让对象像函数

1
2
3
4
5
6
7
8
9
10
11
struct Add {
int operator()(int a, int b) const {
return a + b;
}
};

int main() {
Add add;
cout << add(3, 4); // 7 —— 对象 add 像函数一样被调用
return 0;
}

这种重载了 () 的对象叫函数对象(仿函数),STL 算法里常用。

5.5 类型转换运算符 operator T()

1
2
3
4
5
6
7
8
9
10
class Fraction {
int num, den;
public:
operator double() const {
return (double)num / den;
}
};

Fraction f(3, 4);
double x = f; // 隐式转换:x = 0.75

容易被滥用,导致隐式转换意外发生。优先用显式 explicit operator T()(C++11 起)。

六、友元函数与友元类

类内声明 friend,让外部函数 / 类能访问本类的 private / protected 成员。友元关系不传递、不继承。

1
2
3
4
5
6
7
class Matrix;

class Vec {
int x, y;
friend class Matrix; // Matrix 能访问 Vec 的 private
friend Vec operator+(const Vec&, const Vec&); // 这个全局函数能访问
};

友元会破坏封装,慎用。常见合理场景:流运算符 << >>、需要对称访问两对象的运算符。

七、易错点清单

  1. 重载 && || 会失去短路求值:这两个运算符的求值顺序在 C++ 里是被语言规定的,重载后变成普通函数调用,不再短路。除非你完全知道在做什么,否则不要重载。
  2. 重载 , 也会失去顺序保证。同理避免。
  3. operator= 没处理自赋值:a = a; 不检查 this == &other 会把自己 delete 掉再拷。
  4. 重载流运算符忘了返回 ostream&:return os; 漏掉就断了链式调用,cout << a << b; 编译报错。
  5. 忘了 const 修饰成员运算符:成员运算符没加 const,就不能在 const 对象上用。
  6. 重载运算符改变了语义:比如把 + 写成"相减"。会极大降低可读性,重载后语义应当和内置运算符一致。

八、什么时候不要重载

  • 内置类型上重载 —— 不允许;
  • 没有自然语义时(比如让 + 表示"输出")—— 可读性灾难;
  • 团队 / 项目里大家都不知道你重载了什么 —— 沟通成本高。

九、极简速记

场景 推荐写法
一般二元算术 成员函数 operator+
<< / >> 流 友元全局函数
自增减 前置 T& operator++();后置 T operator++(int)
下标 / 调用 成员函数 operator[] / operator()
赋值 必须成员函数,注意自赋值
仿函数 成员函数 operator(),广泛用于 STL

十、小结

  • 运算符重载 = 给自定义类型定义运算符行为,让代码更自然;
  • = () [] -> 必须成员函数,流运算符必须友元;
  • 写资源管理类时遵守 Rule of Three/Five;
  • 不要重载 && || ,,会破坏语言本身的求值规则;
  • 重载运算符的语义要和内置运算符保持一致,不要"挂羊头卖狗肉"。