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

重载运算符有两种写法:成员函数 和 非成员函数(通常是友元)。语法是 返回类型 operator符号(参数表)。选哪种看一件事:左右操作数是否对称——c1 + c2 两种都行,但 2 + c1 只有非成员写得到(因为 2 不是 Complex 对象,没法去调它的成员函数)。另外有一批硬规则:= [] () -> 必须是成员函数;. .* :: ?: sizeof 五个不能重载。

📝 本节和前面几节的关系

11.2.1 说明"为什么值得重载",本节回答"具体怎么写、写在哪"。这里要用到两笔旧账:10.5.3 的友元函数("不是成员、没有 this、能同时进多个类"——当时说过"运算符重载是它最典型的用途",本节兑现)、第六章的重载决议(同名多版本、按实参选)。另外,本节会出现一个"对称性"的直觉——它和数学里的写法有关。

一、语法:operator 关键字

运算符重载函数的"名字"由两部分拼成:

operator  +   ←  符号本身
   ↑
固定前缀
Complex operator+(const Complex &a, const Complex &b) { ... }   // 非成员
Complex Complex::operator+(const Complex &o) const { ... }      // 成员
bool    operator==(const Complex &a, const Complex &b) { ... }  // 比较
ostream &operator<<(ostream &os, const Complex &c) { ... }      // 输出

注意几个怪地方:

现象 解释
返回值类型写在最前面 和普通函数一样
成员版本要带回来 类名:: 10.1.2 的规矩,类外实现必须标归属
const 写在参数表后 10.4.1 的 const 成员函数(只读、能被 const 对象调用)
参数个数的"潜规则" 成员版本的参数比操作数少一个(因为左操作数是 this)

那条"潜规则"值得展开:

// 成员版:写成 1 个参数,用时是 a + b
bool operator==(const Complex &o) const { ... }

// 非成员版:写成 2 个参数,用时也是 a == b
bool operator==(const Complex &a, const Complex &b) { ... }

因为成员函数的左操作数就是"那个对象自己"(10.4.4 的 this)——a == b 展开成 a.operator==(b)。

二、两种写法的选择标准

// 写法一:成员函数
class Complex {
public:
    Complex operator+(const Complex &o) const { ... }
};
// 用法:c1 + c2  ✅        2 + c1  ❌(2 不是 Complex 对象,没有成员函数可调)

// 写法二:非成员函数(常配 friend)
Complex operator+(const Complex &a, const Complex &b) { ... }
// 用法:c1 + c2  ✅        2 + c1  ✅(2 会隐式转成 Complex)

判断表:

运算符 建议写法 理由
= [] () -> 必须是成员 语言规定(见下方"五个必须"表)
复合赋值 += -= *=/= 成员 左操作数一定是本类对象,天然对称
单目 ++ -- ! - 成员 只作用在自己身上
双目算术 + - * / 非成员(友元) 为了让 2 + c 也成立,左右对称
比较 == != < > 非成员(友元) 同上,0 == c 也应该成立
输入输出 << >> 必须非成员 左操作数是 ostream/istream,不是你的类

核心原则一句话:

"左操作数可能是别的东西(内置类型、cout)" → 写成非成员;"左操作数一定是我自己" → 写成成员。

写非成员时,如果要用类的 private 成员,就把这个函数声明为 friend(10.5.3)。如果成员本来就是 public,连 friend 都不用写。

三、五个"必须写成成员"的运算符

运算符 为什么必须成员
= 赋值 语言规定。它也是"四个不继承的特殊函数"之一(11.1.1)
[] 下标 语言规定;而且要能当左值用(v[i] = 5;)
() 函数调用 语言规定(这就是"仿函数"/函数对象)
-> 成员访问 语言规定(智能指针靠它)
类型转换运算符(operator double()) 语言规定(11.2.4)

四、五个"不能重载"的运算符

符号 是什么
. 成员访问(. 本身)
.* 通过成员指针访问(和 ->* 相对)
:: 作用域解析(10.1.2)
?: 三目运算符(3.1.8)
sizeof 求大小(3.1.7)

为什么不能:它们管的是"语言本身的机制"(作用域、类型大小、成员怎么找)——改这个就等于改语言规则。另外,只能重载"操作数里至少有一个是自定义类型"的运算符:你不能给 1 + 2 改含义。

五、动手 Demo(含实例04:通过重载运算符实现求和)

实例04 的现场:给复数类加上"求和"的能力。这里做成"成员版 + 友元版并存"的对照实验,把本节所有规则一次验证。

#include <iostream>
using namespace std;

class Complex {
private:
    double re, im;

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

    // ① 成员版本:能处理 c1 + c2
    Complex operator+(const Complex &o) const {
        return Complex(re + o.re, im + o.im);
    }

    // ② 友元版本(非成员):左右对称,2 - c1 也成立
    friend Complex operator-(const Complex &a, const Complex &b);

    // ③ 输出流:必须是非成员(左操作数是 ostream)
    friend ostream &operator<<(ostream &os, const Complex &c);
};

Complex operator-(const Complex &a, const Complex &b) {
    return Complex(a.re - b.re, a.im - b.im);   // 友元:能直接碰 private
}

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

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

    cout << "a = " << a << ",b = " << b << endl;   // 连续输出,靠的就是返回 os
    cout << "a + b = " << a + b << endl;            // ① 成员版
    cout << "a - b = " << a - b << endl;            // ② 友元版

    // cout << 2 + a << endl;                       // ❌ 编译错:+ 是成员版,2 调不到
    // cout << 2 - a << endl;                       // ✅ 这行反而能编过(因为 - 是友元版)

    return 0;
}

看到什么算成功:

a = 5+3i,b = 2+1i
a + b = 7+4i
a - b = 3+2i

读结果:

行 走的是哪种写法 为什么
a + b 成员版本 左操作数 a 是 Complex,可以 a.operator+(b)
a - b 友元版本 全局 operator-,两个参数
cout << a 友元版本 左操作数是 ostream,成员写法根本做不到

最后那句"编译错"是本节的核心证据:2 + a 之所以不行,不是因为 + 写错了,而是因为"成员函数的左操作数必须是自己类的对象"——2 不是。

30 秒实验(把这段做完,本节就通了): 1. 第一步:把那行 // cout << 2 + a << endl; 的注释去掉 → 编译错:no match for 'operator+' (operand types are 'int' and 'Complex')。 2. 第二步:把 + 改成友元写法——在类里删掉成员 operator+,改成: cpp friend Complex operator+(const Complex &a, const Complex &b); 类外定义: cpp Complex operator+(const Complex &a, const Complex &b) { return Complex(a.re + b.re, a.im + b.im); } 3. 第三步:把 2 + a 那行的注释去掉,重新编译 → 现在编过了,输出 7+3i。 4. 为什么:2 隐式转换成 Complex(2, 0)(构造函数有默认参数),于是 operator+(Complex(2,0), a) 成立。 5. 这一步做完,你应该能自己回答:"为什么对称的运算符建议写成非成员"——因为这样左操作数才不受"必须是本类对象"的限制。

六、常见误区

  1. "重载运算符都要写成友元"——不是。= [] () -> 必须成员;单目和复合赋值也推荐成员;只有"左右可能不对称"的和"左操作数是 cout/cin"的才需要非成员。而且一旦参数都是 public 成员,连 friend 都不用写。
  2. "友元函数是成员函数的一种"——不是(10.5.3)。友元函数没有 this,定义时不能写 类名::,调用起来就像普通函数(add(a,b) 而不是 a.add(b))。
  3. "成员版的参数和操作数一样多"——少一个。因为左操作数是 this:a == b → a.operator==(b),所以成员版只写 1 个参数。
  4. "<< 可以写成成员函数"——不可以。它的左操作数是 ostream,不是你的类的对象,所以必须是非成员函数(一般声明为 friend)。这条几乎人人踩过。
  5. "重载运算符不需要返回值"——要看情况:<< 必须返回 ostream&(否则没法 cout << a << b;)、+= 必须返回 T&(才能 a += b += c)、+ 返回 T(新对象)。返回值写错,报错常常出现在"下一行",很难查(11.2.3 有完整对照表)。
  6. "可以重载 . 或 ::"——不行。. .* :: ?: sizeof 五个一律不能重载——它们管的是语言机制本身。
自测三问(点开对答案) 1. 成员版和友元版重载运算符,参数个数差几个?原因是什么?——**成员版少一个**。因为**成员函数的左操作数就是 `this`**(`a + b` ≡ `a.operator+(b)`)。 2. 为什么 `2 + a` 用成员版编不过,用非成员版就能编过?——**成员函数必须由"本类对象"发起调用**,`2` 不是 `Complex` 对象,所以找不到 `2.operator+(a)`。**非成员版的两个参数都能做隐式转换**,`2` 会转成 `Complex(2, 0)`,于是调用成立。 3. 哪五个运算符必须写成成员函数?哪五个完全不能重载?——**必须是成员**:`=`、`[]`、`()`、`->`、**类型转换运算符**。**不能重载**:`.`、`.*`、`::`、`?:`、`sizeof`。

相关笔记