一句话核心结论:C++ 操作符重载的”深水区”——用户定义转换会”静默”发生、前置 vs 后置的语义差异、&& || , 绝不要重载、new delete 的 3 个意义。这 4 个条款决定了你写的”重载”是”地道”还是”踩坑”。
系列导航
前言:操作符重载的”深水区”
C++ 操作符重载是 C 的”超能力”——但用得不对 = 灾难。
graph TB
A["C++ 操作符重载"] --> B["用户定义转换\n(条款 5)"]
A --> C["前置 vs 后置\n(条款 6)"]
A --> D["绝不要重载\n&& || , (条款 7)"]
A --> E["new/delete 的 3 个意义\n(条款 8)"]
B -.->|静默陷阱| B1["隐式转换\n可能错误"]
C -.->|语义差异| C1["++i vs i++"]
D -.->|破坏语义| D1["短路求值"]
E -.->|3 个版本| E1["operator new\nnew operator\nplacement new"]
style A fill:#C7CEEA,stroke:#9FA8DA,color:#333
style B fill:#E8D5F5,stroke:#CE93D8,color:#333
style C fill:#FFDAB9,stroke:#FFAB76,color:#333
style D fill:#FFB3C6,stroke:#F48FB1,color:#333
style E fill:#B5EAD7,stroke:#80CBC4,color:#333
style B1 fill:#FFF9C4,stroke:#F9A825,color:#333
style C1 fill:#FFF9C4,stroke:#F9A825,color:#333
style D1 fill:#FFF9C4,stroke:#F9A825,color:#333
style E1 fill:#FFF9C4,stroke:#F9A825,color:#333
一、条款 5:对定制的”类型转换函数”保持警觉
1.1 两种用户定义转换
1 2 3 4 5 6 7 8 9
| class String { public: String(const char* s); };
void process(const String& s);
process("hello");
|
1 2 3 4 5 6 7 8
| class Rational { public: operator double() const; };
Rational r(1, 2); double d = r;
|
1.2 反例 1:静默转换的灾难
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| class Rational { public: Rational(int numerator = 0, int denominator = 1) : n_(numerator), d_(denominator) {} operator double() const { return static_cast<double>(n_) / d_; } private: int n_, d_; };
Rational r(1, 2); double d = r;
Rational r1(100, 1); Rational r2(8, 1); double d = r1 / r2;
|
1.3 反例 2:模板中的”静默”转换
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| template<typename T> void process(const T& x) { }
Rational r(1, 2); process(r);
double d = 3.14; process(d);
void f(Rational r); f(3.14);
|
1.4 解决方案:用 explicit 阻止隐式
1 2 3 4 5 6 7 8 9 10
| class Rational { public: explicit operator double() const { return static_cast<double>(n_) / d_; } };
Rational r(1, 2);
double d = static_cast<double>(r);
|
1 2 3 4 5 6 7 8 9 10
| class String { public: explicit String(const char* s); };
void process(const String& s);
process("hello"); process(String("hello"));
|
1.5 替代方案:命名函数
1 2 3 4 5 6 7 8 9
| class Rational { public: double asDouble() const { return static_cast<double>(n_) / d_; } };
Rational r(1, 2); double d = r.asDouble();
|
1.6 关键启示
- 用户定义转换会”静默”——编译器默默转换
- 优先用
explicit——避免隐式转换 - 优先用命名函数——比转换函数更明确
- C++11 起的
explicit——单参数构造 + 转换函数
二、条款 6:区别 increment/decrement 操作符的前置和后置形式
2.1 前置 vs 后置的语义
C++ 中:
1 2 3
| int i = 0; int a = i++; int b = ++i;
|
2.2 重载前置和后置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| class Counter { int value_; public: Counter(int v = 0) : value_(v) {}
Counter& operator++() { ++value_; return *this; }
Counter operator++(int) { Counter temp(*this); ++value_; return temp; } };
|
关键差异:
| 维度 | 前置 | 后置 |
|---|
| 返回类型 | T& | T(值) |
| 实现 | 直接修改 | 保存+修改+返回临时 |
| 性能 | 优 | 劣(拷贝) |
| 语义 | “先加后用” | “先用后加” |
2.3 性能对比
1 2 3 4 5 6 7
| ++counter; counter++;
|
2.4 实战:迭代器的 ++
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| std::vector<int> v = {1, 2, 3, 4, 5}; auto it = v.begin();
while (it != v.end()) { std::cout << *it++ << "\n"; }
while (it != v.end()) { std::cout << *it << "\n"; ++it; }
|
2.5 重载 - -=
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class Counter { int value_; public: Counter& operator--() { --value_; return *this; } Counter operator--(int) { Counter temp(*this); --value_; return temp; }
Counter& operator+=(int n) { value_ += n; return *this; } Counter& operator-=(int n) { value_ -= n; return *this; } };
|
2.6 关键启示
- 前置返回引用——效率高
- 后置返回值——有拷贝成本
- 优先前置
++i/--i——除非真的需要”原值” - 重载后置的
int 是占位参数——编译器用签名区分
三、条款 7:千万不要重载 &&, || 和 ,
3.1 为什么这 3 个不能重载?
| 操作符 | 原本语义 | 重载后问题 |
|---|
&& | 短路求值 | 重载后变成”函数调用”——所有参数必先求值 |
|| | 短路求值 | 同上 |
, | 从左到右求值 | 重载后变成”函数调用”——求值顺序改变 |
3.2 反例 1:&& 的短路求值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| int* p = ...; if (p && p->isValid()) { }
class Widget { public: operator bool() const; };
Widget w1, w2; if (w1 && w2) { }
|
3.3 反例 2:, 的求值顺序
1 2 3 4 5 6 7 8 9 10 11 12
| int a, b; (a = 1, b = 2);
class Widget { public: Widget& operator,(const Widget& rhs); };
Widget w1, w2; (w1, w2);
|
3.4 解决方案
1 2 3 4 5 6 7 8 9 10 11 12
| class Widget { bool operator&&(const Widget& rhs); };
class Widget { public: bool bothValid(const Widget& rhs) const; };
if (w1.bothValid(w2)) { }
|
3.5 关键启示
&& / || 重载 = 失去短路——静默 bug, 重载 = 失去求值顺序——不可预期- 用命名函数——明确意图
& 和 | 可以重载——但要小心
四、条款 8:了解 new/delete 的不同意义
4.1 3 个 new 的含义
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Widget* p = new Widget(42);
void* mem = operator new(sizeof(Widget));
void* mem2 = operator new(sizeof(Widget), somePtr);
|
4.2 对应的 3 个 delete
1 2 3 4 5 6 7 8
| delete p;
operator delete(mem);
operator delete(mem, somePtr);
|
4.3 实战 1:自定义 operator new
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class Widget { public: static void* operator new(std::size_t size) { std::cout << "Widget::new(size=" << size << ")\n"; return ::operator new(size); } static void operator delete(void* p) { std::cout << "Widget::delete\n"; ::operator delete(p); } };
Widget* w = new Widget(); delete w;
|
4.4 实战 2:placement new
1 2 3 4 5 6 7 8 9 10 11 12
| class BigObject { };
alignas(BigObject) char buffer[sizeof(BigObject)];
BigObject* obj = new (buffer) BigObject();
obj->~BigObject();
|
4.5 实战 3:内存池
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class MemoryPool { static constexpr std::size_t CHUNK = 4096; public: void* allocate(std::size_t n); void deallocate(void* p); };
class PooledObject { public: static void* operator new(std::size_t size) { return pool_.allocate(size); } static void operator delete(void* p, std::size_t size) { pool_.deallocate(p, size); } private: static MemoryPool pool_; };
|
4.6 关键启示
new operator = 分配 + 构造operator new = 只分配placement new = 在指定位置构造- 配套的 delete——placement new 必配套 placement delete
五、4 个条款的”操作符”全景
graph TB
A["C++ 操作符"] --> B["用户定义转换\n(条款 5)"]
A --> C["前置 vs 后置\n(条款 6)"]
A --> D["绝不要重载\n&& || , (条款 7)"]
A --> E["new/delete\n(条款 8)"]
B --> B1["explicit 阻止\n或用命名函数"]
C --> C1["优先前置\n返回引用"]
D --> D1["保留短路求值\n用命名函数"]
E --> E1["new operator\noperator new\nplacement new"]
style A fill:#C7CEEA,stroke:#9FA8DA,color:#333
style B fill:#E8D5F5,stroke:#CE93D8,color:#333
style C fill:#FFDAB9,stroke:#FFAB76,color:#333
style D fill:#FFB3C6,stroke:#F48FB1,color:#333
style E fill:#B5EAD7,stroke:#80CBC4,color:#333
style B1 fill:#FFF9C4,stroke:#F9A825,color:#333
style C1 fill:#FFF9C4,stroke:#F9A825,color:#333
style D1 fill:#FFF9C4,stroke:#F9A825,color:#333
style E1 fill:#FFF9C4,stroke:#F9A825,color:#333
六、常见误区与陷阱
6.1 误区 1:用户定义转换不阻止隐式
1 2 3 4 5 6 7 8 9 10 11
| class String { String(const char* s); }; void process(const String&); process("hello");
class String { explicit String(const char* s); };
|
6.2 误区 2:用后置 ++ 在循环中
1 2 3 4 5
| for (auto it = v.begin(); it != v.end(); it++) { }
for (auto it = v.begin(); it != v.end(); ++it) { }
|
6.3 误区 3:重载 && ||
1 2 3
| class Widget { bool operator&&(const Widget& rhs); };
|
6.4 误区 4:placement new 漏 placement delete
1 2 3 4 5 6 7 8 9 10 11
| class Widget { void* operator new(std::size_t size, void* ptr); };
class Widget { void* operator new(std::size_t size, void* ptr); void operator delete(void* ptr, void*); };
|
七、C++11/14/17 的演进
| 主题 | C++98 时代 | C++11/14/17 时代 |
|---|
| 转换函数 | 默认允许隐式 | explicit 阻止 |
| 单参数构造 | 默认隐式 | explicit 阻止 |
| ++ 后置 | 返回值 | 同 |
重载 && || | 不推荐 | 同 |
| new | 3 个意义 | 同 |
| placement new | 用于栈构造 | 同 + 内存池 |
| 自定义 new | 类特定 | 同 |
| 智能指针 | auto_ptr | unique_ptr + placement 配合 |
C++11 的 explicit 转换函数:
1 2 3 4 5
| class Widget { public: explicit operator bool() const; };
|
八、面试高频考点
8.1 必背题
| 题目 | 答案要点 |
|---|
| 什么是用户定义转换? | 单参数构造 + operator T() |
| 为什么用 explicit? | 阻止隐式转换,避免”静默” |
| 前置 vs 后置 ++? | 前置返回引用,效率高;后置返回值 |
| 为什么不能重载 && || , ? | 失去短路求值 / 求值顺序 |
| new 的 3 个意义? | new operator / operator new / placement new |
| 什么是 placement new? | 在指定位置构造对象 |
| placement new 配套什么? | placement delete |
8.2 高频追问
| 追问 | 关键点 |
|---|
++i 比 i++ 快在哪? | 后置多一次构造+析构+拷贝 |
重载 && 的具体问题? | 失去短路——所有参数都求值 |
为什么 , 不能重载? | 改变求值顺序 |
| new operator 做了什么? | 分配 + 构造 + 返回 |
| operator new 可以被重载吗? | 可以——类特定 / 全局 |
九、配套实验
9.1 实验 1:explicit 阻止隐式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #include <iostream> #include <string>
class String { std::string s_; public: explicit String(const char* s) : s_(s) {} const std::string& data() const { return s_; } };
void process(const String& s) { std::cout << s.data() << "\n"; }
int main() { process(String("hello")); return 0; }
|
9.2 实验 2:前置 vs 后置 ++
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| #include <iostream>
class Counter { int value_ = 0; public: Counter& operator++() { ++value_; return *this; } Counter operator++(int) { Counter temp(*this); ++value_; return temp; } int value() const { return value_; } };
int main() { Counter c; ++c; std::cout << "after ++c: " << c.value() << "\n";
c++; std::cout << "after c++: " << c.value() << "\n";
Counter c2 = ++c; std::cout << "c=" << c.value() << " c2=" << c2.value() << "\n";
Counter c3 = c++; std::cout << "c=" << c.value() << " c3=" << c3.value() << "\n";
return 0; }
|
9.3 实验 3:placement new
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| #include <iostream> #include <new>
class BigObject { int data_ = 42; public: BigObject() { std::cout << "BigObject ctor\n"; } ~BigObject() { std::cout << "BigObject dtor\n"; } int data() const { return data_; } };
int main() { alignas(BigObject) char buffer[sizeof(BigObject)];
BigObject* obj = new (buffer) BigObject(); std::cout << "data = " << obj->data() << "\n";
obj->~BigObject(); return 0; }
|
9.4 实验 4:3 种 new 的区别
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| #include <iostream> #include <new>
class Widget { int x_; public: Widget(int x) : x_(x) { std::cout << "Widget(" << x << ") ctor\n"; } ~Widget() { std::cout << "Widget(" << x_ << ") dtor\n"; } int x() const { return x_; } };
int main() { std::cout << "=== new operator ===\n"; Widget* w1 = new Widget(1); delete w1;
std::cout << "\n=== operator new ===\n"; void* mem = operator new(sizeof(Widget)); Widget* w2 = new (mem) Widget(2); w2->~Widget(); operator delete(mem);
std::cout << "\n=== placement new ===\n"; alignas(Widget) char buf[sizeof(Widget)]; Widget* w3 = new (buf) Widget(3); w3->~Widget();
return 0; }
|
十、回到 4 条黄金法则
| 条款 | 黄金法则 |
|---|
| 05 | 转换函数要 explicit——避免”静默”转换 |
| 06 | 优先前置 ++i/--i——返回引用,效率高 |
| 07 | 绝不重载 && || ,——保留短路与求值顺序 |
| 08 | 区分 new 的 3 个意义:new operator / operator new / placement new |
十一、结尾思考题
思考题 1:为什么 operator double() 在 Rational 类里是”危险”的?用 explicit + 命名函数改进。
思考题 2:i++ 比 ++i 慢在哪里?给出反汇编对比。
思考题 3:实现一个简单的 Counter 类,前置/后置都重载。++c 和 c++ 在汇编层的差异是什么?
思考题 4:解释 new operator 做了哪 3 件事。如果自定义 operator new 但不重载 new operator,结果会怎样?
思考题 5:placement new + placement delete 的”构造失败时”具体指什么?
十二、本篇速查表
| 主题 | 关键 API / 模式 | 适用场景 |
|---|
| 用户定义转换 | explicit / 命名函数 | 避免隐式 |
| 前置 ++ | 返回引用 | 性能 |
| 后置 ++ | 返回值(”原值”) | “先用后加” |
重载 && || | 绝不做 | 保留短路 |
重载 , | 绝不做 | 保留求值顺序 |
| new operator | 分配 + 构造 | 日常 |
| operator new | 只分配 | 自定义内存管理 |
| placement new | 指定位置 | 栈构造 / 内存池 |
十三、系列导航
| # | 文章 | 状态 |
|---|
| 0 | 系列总览 | ✅ 已发布 |
| 1 | 基础议题 | ✅ 已发布 |
| 2 | 本文:操作符 | ✅ 已发布 |
| 3 | 异常(上):异常安全 + RAII | 🔜 计划中 |
| 4 | 异常(下):异常规格、throw 列表、构造异常 | 🔜 计划中 |
| 5 | 效率(上):lazy evaluation、临时对象、RVO | 🔜 计划中 |
| 6 | 效率(下):重载、operator new、内存池、inline | 🔜 计划中 |
| 7 | 技术:虚拟构造、智能指针、引用计数、双重分派 | 🔜 计划中 |
| 8 | 杂项 + 总结:未来时态、标准库、命名空间 | 🔜 计划中 |
下一篇:第 3 篇《异常(上):异常安全的 3 大保证 + RAII》——条款 9-12 一起讲透 C++ 异常的”完整图景”:destructor 避免泄漏、constructor 内阻止泄漏、禁止 exception 流出 destructor、抛 exception 与传参的差异。
行动建议:
- 今天:用
explicit 替换你单参数构造 - 今天:把所有
i++ 改为 ++i(如果不需要”原值”) - 本周:检查你的项目——是否误重载了
&& || - 本周:用 placement new 实现”栈上构造大对象”
- 思考:你的项目里有哪些”用户定义转换在静默发生”?