这一节是"各种常见运算符怎么写"的清单,核心是一张返回值对照表:改自己的(+= ++)返回引用 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 参数纯粹是"记号",用来区分两个版本。调用的时候你不用管:
- 写
++a→ 编译器调operator++() - 写
a++→ 编译器调operator++(0)(参数值固定是 0)
语义上的差别(本质是"返回哪一刻的自己"):
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,就好了。 这一改一改回,你就明白为什么"产生新对象的运算符必须按值返回"。
六、常见误区
- "
+也应该返回引用,省一次复制"——绝不能。+的结果是新造的对象(局部变量),函数一返回它就销毁了,返回它的引用=悬空引用。只有当返回的是*this(本对象)时才返回引用。 - "前置
++和后置++可以只写一个"——两个都要写才完整。只写前置,a++会去调那个带int的版本,没有就编译错。 - "后置
++里那个int参数要自己传值"——不用。它只是个"记号",编译器自动补 0。你写a++就行。 - "返回值类型随便写,反正能编过"——后果会出现在"下一行"。比如
<<不返回os,cout << a << b;会在第二处<<报错(报错离现场很远,很难查)。返回值对照表里的每一行,都有它的理由。 - "赋值运算符不用管自赋值"——类里有指针/资源时必须管。
a = a;时先delete后读,读到的是已经释放的内存。那一行if (this == &o) return *this;是保命符。 - "
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&`**。- ☐ 11.2.4 转换运算符(下一节)
相关笔记
- 重载运算符的形式与规则(含实例04) —— 上一节:成员还是友元
- 转换运算符 —— 下一节:让自定义类型变成内置类型
- 隐藏的this指针 ——
return *this;与链式调用 - 复制构造函数 ——
Complex tmp(*this);靠的就是它 - 判断左值与右值 ——
operator[]为什么要返回引用 - 🔷 C++基础 · 索引 —— 全课路线图