🔷 第十一章 继承与派生
📄 一句话概括

不用运算符重载,"复数相乘再加一个复数"得写成 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
要对照函数名才能读懂 读符号就懂

收益清单:

  1. 代码像数学——a * b + c 不需要任何注释。
  2. 括号层级自然——a * b + c 的优先级和 int 一致,读法一致。
  3. 和内置类型"同款写法"——int/double/Complex 都用同一个表达式形状。
  4. 能和标准库/算法配合——需要 + 的地方你的类型也能进。

三、什么时候"真的需要"重载运算符

判据:你造的这个类型,是不是一个"数学意义上的值"?

类型 值得重载的符号 为什么
复数 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 友元。

五、常见误区

  1. "运算符重载能让语言更强"——能力上一点没多。函数能做到的一模一样(add(a,b))。它买的是可读性。
  2. "自定义类型都能重载运算符"——不是"都能",是"该不该"。Person、Car、Server 这类没有数学语义的类型,重载 + 只会让人困惑。有 ==(判断相等)和 <<(打印)往往就够了。
  3. "重载运算符是可有可无的花活"——在工程里不是。标准库的算法、容器(如 sort、accumulate)天生依赖 <、+、== 这些符号。你的类型没这些符号,就用不上整套现成的工具。
  4. "+ 和 += 都要写一遍,很麻烦"——可以互相实现:先写 +=,再用 += 写 +(复制一份自己、加上对方、返回)。省事且不容易写错(11.2.3 有示范)。
  5. "重载了 << 就能 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. 什么样的类型"不该"重载运算符?——**没有数学/符号直觉的类型**(人、汽车、网络连接……)。判据:**如果你要写注释解释"这个 `+` 在干什么",就不该重载它。**

相关笔记