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

这一节是"各种常见运算符怎么写"的清单,核心是一张返回值对照表:改自己的(+= ++)返回引用 T&;产生新值的(+ - *)返回对象 T。两个反复出现的技巧:① 用 += 实现 +(少写重复代码);② 前置和后置 ++ 靠一个"占位 int 参数"区分——operator++() 是前置,operator++(int) 是后置。

📝 本节和前面几节的关系

11.2.2 讲了"写在哪、参数几个、哪些不让写";本节讲"具体每个符号的惯用写法"。这里回收两笔账:10.4.4 的 this 与 return *this;(链式调用的实现基础)、10.2.2 的复制构造函数(+ 要"复制一份自己"就靠它)。同时本节是整章最"工程"的一节——它讲的东西你在任何 C++ 项目里都会看到。

一、返回值对照表(本节最重要的一张表)

运算符 返回类型 为什么
+= -= *= /= T&(return *this;) 要支持 a += b += c:b += c 必须先改 b、再返回 b 自己
= 赋值 T& 同上(支持 a = b = c)
+ - * / T(新对象) 原对象不该变,结果是新值
== != < > <= >= bool 只回答"真/假"
<< ostream&(return os;) 支持 cout << a << b;
>> istream&(return is;) 支持 cin >> a >> b;
[] T& 要能出现在 = 左边:v[0] = 5;
前置 ++ / -- T& 先改自己,再返回自己
后置 ++ / -- T(旧值的副本) 语义是"返回加之前的值",所以不能返回自己(那时已经变了)
! bool 逻辑非
单目 -(取负) T 产生新值,不改原对象

一条总规律:

"返回值是不是'原来那个对象'?" 是 → T&;不是 → T。 "调用后要不要能继续往后面写(链式)?" 要 → 必须返回引用。

二、技巧一:用 += 实现 +

Complex &operator+=(const Complex &o) {      // 先写"改自己"的版本
    re += o.re;
    im += o.im;
    return *this;
}

Complex operator+(const Complex &o) const {  // 再用它拼出"不改自己"的版本
    Complex tmp(*this);                      // 复制一份自己(10.2.2)
    tmp += o;                                // 改那份副本
    return tmp;                              // 交出副本
}

好处:

好处 说明
逻辑只写一遍 += 的算法就是 + 的算法,"改在哪份对象上"不同而已
不容易写错 万一将来算法要改(比如加进位、加越界判断),只改一处
不会忘 const + 必须是 const 成员函数(它不改自己)

三、技巧二:前置 / 后置 ++ 怎么区分

C++ 的规定很"怪"但很好记:

T &operator++();          // 前置 ++a:无参数
T  operator++(int);       // 后置 a++:多一个 int 占位参数(调用时没人传它)

那个 int 参数纯粹是"记号",用来区分两个版本。调用的时候你不用管:

语义上的差别(本质是"返回哪一刻的自己"):

T &operator++() {         // 前置:先加,再把自己交出去
    ++自己;
    return *this;         // 返回"加之后"的自己
}

T operator++(int) {       // 后置:先把旧的存一份,加完再把旧的交出去
    T old(*this);         // 复制一份旧值
    ++自己;
    return old;           // 返回"加之前"的副本
}

为什么要这么绕:因为 a++ 的含义是"让 a 加 1,然后这个表达式的值是加之前的值"。

四、技巧三:自赋值检查(写赋值运算符时)

T &operator=(const T &o) {
    if (this == &o) return *this;    // ★ 自赋值:a = a,直接返回
    // ... 释放旧资源、复制新资源 ...
    return *this;
}

为什么必须检查:如果类里有指针成员,赋值时要"先 delete 旧的、再 new 新的"。如果是自赋值,你刚 delete 掉的就是自己正要读的内存——改完之后自己已经空了。

this == &o 就是 10.4.4 的 this(自己的地址)和"参数对象的地址"比较。这一行也是"三法则"(10.3)里最容易被忘的一句。

五、动手 Demo:一个"完整够用"的 Complex

#include <iostream>
using namespace std;

class Complex {
private:
    double re, im;

public:
    Complex(double r = 0, double i = 0) : re(r), im(i) {}

    // ① 复合赋值:返回引用,支持 a += b += c
    Complex &operator+=(const Complex &o) {
        re += o.re;
        im += o.im;
        return *this;                       // 10.4.4 的 *this
    }

    // ② 双目算术:返回新对象,不改自己(const 成员函数)
    Complex operator+(const Complex &o) const {
        Complex tmp(*this);                 // 复制一份(10.2.2)
        tmp += o;                           // 用 += 实现 +
        return tmp;
    }

    // ③ 比较
    bool operator==(const Complex &o) const {
        return re == o.re && im == o.im;
    }
    bool operator!=(const Complex &o) const { return !(*this == o); }

    // ④ 前置 ++:先加,返回自己(引用)
    Complex &operator++() {
        re += 1;
        return *this;
    }
    // ⑤ 后置 ++:用 int 占位参数区分,返回加之前的副本
    Complex operator++(int) {
        Complex old(*this);
        re += 1;
        return old;
    }

    // ⑥ 输出:返回 ostream&,才能连续输出
    friend ostream &operator<<(ostream &os, const Complex &c) {
        os << c.re << (c.im >= 0 ? "+" : "") << c.im << "i";
        return os;
    }
};

int main() {
    Complex a(1, 1), b(2, 2), c(3, 3);

    cout << "a = " << a << endl;

    a += b;
    cout << "a += b 之后 a = " << a << endl;

    cout << "a + b = " << a + b << "(a 没变:" << a << ")" << endl;

    Complex d(1, 1);
    cout << "++d = " << ++d << endl;        // 先加
    cout << "d++ = " << d++ << endl;        // 先返回旧值
    cout << "d   = " << d << endl;          // 现在才看到加过两次

    cout << "a == b ? " << (a == b) << endl;
    cout << "c == c ? " << (c == c) << endl;
    return 0;
}

看到什么算成功:

a = 1+1i
a += b 之后 a = 3+3i
a + b = 5+5i(a 没变:3+3i)
++d = 2+1i
d++ = 2+1i
d   = 3+1i
a == b ? 0
c == c ? 1

读结果(把 8 行读成 3 句话):

现象 结论
a += b 之后 a 变成 3+3i 复合赋值改的是自己(返回引用就是为了支持 a += b += c)
a + b = 5+5i 但 a 仍是 3+3i 双目算术不改自己(所以返回 T,所以 + 是 const 成员函数)
++d 打印 2+1i,d++ 也打印 2+1i 妙处在这:++d 是"加完之后的 d"(已是 2+1i);d++ 是"加之前的副本"(那时还是 2+1i)。随后 d 变成 3+1i——两次自增都生效了,只是后置的那个"交出来的是旧账"

30 秒实验: 1. 把 operator+ 的返回类型从 Complex 改成 Complex &(想"省一次复制"),重新编译运行。 2. 你会看到 a + b 那行打出来的内容不对(乱数),或者干脆是上次留下的残值——因为返回的是局部变量 tmp 的引用,而 tmp 在函数返回的那一刻就已经销毁了。这是"悬空引用",典型的未定义行为:有时看着正常,有时是垃圾值,换个编译器就变。 3. 改回 Complex,就好了。 这一改一改回,你就明白为什么"产生新对象的运算符必须按值返回"。

六、常见误区

  1. "+ 也应该返回引用,省一次复制"——绝不能。+ 的结果是新造的对象(局部变量),函数一返回它就销毁了,返回它的引用=悬空引用。只有当返回的是 *this(本对象)时才返回引用。
  2. "前置 ++ 和后置 ++ 可以只写一个"——两个都要写才完整。只写前置,a++ 会去调那个带 int 的版本,没有就编译错。
  3. "后置 ++ 里那个 int 参数要自己传值"——不用。它只是个"记号",编译器自动补 0。你写 a++ 就行。
  4. "返回值类型随便写,反正能编过"——后果会出现在"下一行"。比如 << 不返回 os,cout << a << b; 会在第二处 << 报错(报错离现场很远,很难查)。返回值对照表里的每一行,都有它的理由。
  5. "赋值运算符不用管自赋值"——类里有指针/资源时必须管。a = a; 时先 delete 后读,读到的是已经释放的内存。那一行 if (this == &o) return *this; 是保命符。
  6. "operator[] 返回 T 就行"——不行(要返回 T&)。因为 v[0] = 5; 要求 v[0] 是个能写的左值。返回 T 的话,你改的是一个临时副本,改完就没了(3.4 的左值/右值知识)。
自测三问(点开对答案) 1. `+=` 和 `+` 的返回值分别是什么?为什么?——**`+=` 返回 `T&`**(`return *this;`),因为它改的是自己,**返回引用才能支持 `a += b += c`**;**`+` 返回 `T`**(新对象),因为它**不改原对象**,结果是新造的,**返回引用会悬空**。 2. 前置 `++` 和后置 `++` 在函数签名上怎么区分?语义有什么不同?——**后置版多一个 `int` 占位参数**:`T &operator++()` 是前置、`T operator++(int)` 是后置。**前置返回"加之后的自己";后置返回"加之前的副本"**。 3. `<<` 为什么必须返回 `ostream&`?——为了支持**连续输出**:`cout << a << b;` 会先算 `cout << a`,**它的结果必须还是个 `ostream`**,才能接着 `<< b`。**同理 `>>` 返回 `istream&`**。

相关笔记