不用运算符重载,"复数相乘再加一个复数"得写成 addComplex(multiplyComplex(a, b), c);重载之后,它就是 a * b + c。同一个人、同一件数学,后者不用注释也看得懂。所以必要性只有一个字:可读性——让自定义类型的代码,长得像数学。
11.2 立了规矩(不能发明符号、不能改优先级),本节回答"那为什么还要折腾它"。内容很轻,但它是后面三节(11.2.2 ~ 11.2.4)的动机:知道了"为什么值得",再学"怎么写"才不会觉得是在背语法。同时本节会回收 7.6.2 指向函数的指针:既然运算符就是函数,那它也能像函数一样——被调用、被传参。
一、没有重载运算符时,代码长什么样
假设你写了个"复数"类(复数=实部 + 虚部,比如 1+2i)。要算 (a * b) + c:
// 全部靠函数名
Complex t = multiplyComplex(a, b);
Complex r = addComplex(t, c);
痛点一条条数:
| 痛点 | 说明 |
|---|---|
| 名字看不懂数学 | multiplyComplex(a, b) 要"读一遍名字"才知道是在乘 |
| 嵌套时括号成灾 | add(sub(mul(a,b), c), div(d,e))——括号读三遍还是晕 |
| 同一个意思两种写法 | 内置 int 写 a * b,你的类型写 multiplyComplex(a, b),同一个算法换类型就得换写法 |
| 模板/泛型不友好 | 想写"能对 int 和 Complex 通用的代码",一个用 *、一个用函数名,没法通用 |
最后一条在工程里最要命:标准库里的 sort、accumulate、各种算法,内部就是拿 + < * 这些符号去干活的。你的类型"没有 +",它就用不了那些现成的算法。
二、有了重载运算符之后
Complex r = a * b + c; // 一眼就是数学
对比一眼:
| 不用重载 | 用了重载 |
|---|---|
addComplex(multiplyComplex(a, b), c) |
a * b + c |
isEqualComplex(a, b) |
a == b |
printComplex(complexConjugate(a)) |
cout << -a |
| 要对照函数名才能读懂 | 读符号就懂 |
收益清单:
- 代码像数学——
a * b + c不需要任何注释。 - 括号层级自然——
a * b + c的优先级和int一致,读法一致。 - 和内置类型"同款写法"——
int/double/Complex都用同一个表达式形状。 - 能和标准库/算法配合——需要
+的地方你的类型也能进。
三、什么时候"真的需要"重载运算符
判据:你造的这个类型,是不是一个"数学意义上的值"?
| 类型 | 值得重载的符号 | 为什么 |
|---|---|---|
复数 Complex |
+ - * / == != << |
复数有完整的四则运算 |
分数 Fraction |
+ - * / < == |
分数要比较、要通分 |
日期 Date |
+(加天数)、-(差多少天)、< == |
"日期 + 7 天"非常自然 |
金额 Money |
+ - * == << |
加法语义天然 |
矩阵 Matrix |
+ * == |
线性代数 |
向量 Vector |
+ - * (点乘) == [] |
索引访问尤其需要 [] |
图形 Shape |
<<(打印) |
打印比运算更常用 |
反例(不该重载的):
| 情况 | 为什么不该 |
|---|---|
Person + Person |
"两个人相加"是什么?符号不贴切,语义说不清 |
Car * 5 |
"汽车乘 5"没有意义 |
| 用一个类表达"服务器连接" | 连接没有"相加"的语义;有 ==(比较)就够 |
| 想重载逻辑与/逻辑或来改变短路行为 | 做不到(重载后不再短路,所以这两个运算符的重载几乎总是错的) |
一句话判据:如果你得写注释解释"这个
+是在干什么",那就不该重载它。
四、动手 Demo:同一个算法的三种写法
#include <iostream>
using namespace std;
class Complex {
public:
double re, im;
Complex(double r = 0, double i = 0) : re(r), im(i) {}
};
// 写法一:全靠普通函数
Complex addComplex(const Complex &a, const Complex &b) {
return Complex(a.re + b.re, a.im + b.im);
}
// 写法二:靠重载运算符(本节主角)
Complex operator+(const Complex &a, const Complex &b) {
return Complex(a.re + b.re, a.im + b.im);
}
void print(const char *tag, const Complex &c) {
cout << tag << c.re << (c.im >= 0 ? "+" : "") << c.im << "i" << endl;
}
int main() {
Complex a(1, 2), b(3, 4);
print("addComplex(a, b) 得到 ", addComplex(a, b));
print("a + b 得到 ", a + b);
return 0;
}
看到什么算成功:
addComplex(a, b) 得到 4+6i
a + b 得到 4+6i
读结果:两行结果一模一样——同一个算法、同一个结果,唯一的差别是"读起来要多久"。
这里还顺手兑现了一件事:
operator+写成了全局函数(不是成员),而且没有用friend——因为Complex的re、im是public的。"要不要友元"取决于"需不需要碰私有成员",不是所有运算符重载都要友元(11.2.2 细讲)。
30 秒实验:在 main 里再加一句:
print("2 + a 得到 ", 2 + a);
它能编过,输出 2 + a 得到 3+2i。为什么?因为 2 隐式转换成了 Complex(2, 0)(构造函数带默认参数,相当于"单参可调用",10.2),然后走 operator+(Complex, Complex)。
而如果 operator+ 是写成"成员函数"的,这行就编不过——成员函数的左操作数必须是你那个类的对象,2 不行。这就是 11.2.2 要讲的核心取舍:成员 vs 友元。
五、常见误区
- "运算符重载能让语言更强"——能力上一点没多。函数能做到的一模一样(
add(a,b))。它买的是可读性。 - "自定义类型都能重载运算符"——不是"都能",是"该不该"。
Person、Car、Server这类没有数学语义的类型,重载+只会让人困惑。有==(判断相等)和<<(打印)往往就够了。 - "重载运算符是可有可无的花活"——在工程里不是。标准库的算法、容器(如
sort、accumulate)天生依赖<、+、==这些符号。你的类型没这些符号,就用不上整套现成的工具。 - "
+和+=都要写一遍,很麻烦"——可以互相实现:先写+=,再用+=写+(复制一份自己、加上对方、返回)。省事且不容易写错(11.2.3 有示范)。 - "重载了
<<就能cout << obj"——方向要对。<<必须重载成operator<<(ostream &os, const T &obj),左操作数是os不是你的对象。这条决定了它基本必须是非成员函数(11.2.2、11.2.3)。
自测三问(点开对答案)
1. 不用运算符重载时,(a*b)+c 要写成什么样?重载之后呢?——写成 `addComplex(multiplyComplex(a, b), c)`;重载之后就是 **`a * b + c`**。 2. 运算符重载带来的好处里,哪一条对工程影响最大?——**能和标准库/泛型算法配合**。`sort`、`accumulate` 这类工具**内部就是靠 `<`、`+`、`==` 干活的**;你的类型没有这些符号,就用不上它们。(其余好处主要是可读性。) 3. 什么样的类型"不该"重载运算符?——**没有数学/符号直觉的类型**(人、汽车、网络连接……)。判据:**如果你要写注释解释"这个 `+` 在干什么",就不该重载它。**- ☐ 11.2.2 重载运算符的形式与规则(下一节)
相关笔记
- 重载运算符 —— 上一节:运算符本来就是函数
- 重载运算符的形式与规则(含实例04) —— 下一节:成员还是友元
- 重载函数 —— 重载的原始定义
- 指向函数的指针 —— 运算符也是函数
- 友元函数 —— 友元函数不是成员
- 🔷 C++基础 · 索引 —— 全课路线图