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