一句话核心结论 :好的 C++ 接口设计有一条核心原则——让对的用法亮起来,让错的用法编译失败 。本章 8 个条款教你掌握:类的本质(class = type)、参数传递的最优姿势(pass-by-const-ref)、成员函数 vs 非成员函数的取舍、类型转换的克制、namespace 与友元的边界、模板 vs 继承的工程选择。
系列导航 前言:什么是”好的接口”? C++ 是一门”接口密集”的语言——你每天都在写 class、function、template,每一个都是给其他程序员(或未来的自己)的契约 。
一个”好”的接口:
✅ 正确表达意图 ——名字、参数、返回值都”自解释” ✅ 易用 ——vector::push_back(x) 看上去就知道干什么 ✅ 难误用 ——Date(int day, int month, int year) 比 Date(int, int, int) 不易错 ✅ 高内聚、低耦合 ——只暴露必需的”承诺” 一个”差”的接口:
❌ 隐式转换 ——string s = "hello" + 5; 看上去很怪 ❌ 传值陷阱 ——void f(std::vector<int> v); 调用时拷贝整个 vector ❌ 紧耦合 ——友元、继承关系错综复杂 本章 8 个条款会逐一击破这些”坏接口”的设计。
一、条款 18:让接口容易被正确使用,不易被误用 1.1 案例:日期类的接口 1 2 3 4 5 6 7 8 9 class Date {public : Date (int year, int month, int day); };
问题 :参数都是 int——编译器无法验证”月份在 1-12 之间”,”日”和”月”顺序可以颠倒。
1.2 解决方案 1:导入新类型 1 2 3 4 5 6 7 8 9 10 11 enum class Year { ... };enum class Month { Jan = 1 , Feb, ..., Dec };enum class Day { };class Date {public : Date (Year y, Month m, Day d); Date d (Year{2024 }, Month::Mar, Day{30 }) ; };
1.3 解决方案 2:限制类型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 struct Month { int val; static Month Jan () { return {1 }; } static Month Feb () { return {2 }; } }; class Date {public : Date (int year, Month month, int day); };
1.4 解决方案 3:const 正确性 1 2 const Investment* createInvestment () ;
1.5 解决方案 4:与”内建类型”行为一致 1 2 3 4 5 std::vector<int > v; v.size (); v.push_back (42 ); v[0 ] = 10 ;
好的接口应该和”用户已经熟悉的接口”一致 ——这样学习成本最低。
1.6 “trading C API 兼容性”的实战 1 2 3 4 5 6 7 8 9 10 11 12 13 14 enum class InvestmentType { Stock, Bond, RealEstate };Investment* createInvestment (InvestmentType type) { switch (type) { case InvestmentType::Stock: return new Stock (); case InvestmentType::Bond: return new Bond (); case InvestmentType::RealEstate: return new RealEstate (); default : return nullptr ; } }
优势 :
编译器强制 enum——int 不行 防止”魔法数字”——createInvestment(2) 看不懂 增加新类型时编译器会警告”未处理所有 case” 1.7 关键启示 防止”对的代码看起来像错的” ——如 Date d(30, 3, 2024) 顺序颠倒防止”错的代码看起来像对的” ——如 Date d(2024, 13, 30) 编译通过类型系统是”编译期验证”的工具 ——多用 enum class、struct wrapper二、条款 19:设计 class 犹如设计 type 2.1 C++ class 的”超能力” C++ 的 class 不是 C 的 struct + 函数那么”单薄”——它支持:
新类型的创建 :自己的构造函数、析构函数运算符重载 :operator+、operator[]、operator-> 自定义内存管理 :new/delete 重载、placement new继承与多态 :virtual 函数模板参数化 :类模板类型转换 :operator T() 隐式转换、explicit 阻止“设计 class = 设计 type” ——这意味着你必须像设计语言内建类型一样 思考。
2.2 设计 checklist graph TB
A["新 type 怎么创建 + 销毁?"] --> B["对象的初始化 + 赋值\n有何区别?"]
B --> C["值传递时\n发生了什么?"]
C --> D["合法值的限制?"]
D --> E["继承图?\n是否多态?"]
E --> F["需要哪些转换?"]
F --> G["operator 和成员\n函数的可见性?"]
G --> H["未声明的接口?\n拷贝/移动/比较?"]
H --> I["谁来用?\n他们怎么用?"]
I --> J["是否泛型?\n是否模板?"]
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 F fill:#FFF9C4,stroke:#F9A825,color:#333
style G fill:#C7CEEA,stroke:#9FA8DA,color:#333
style H fill:#E8D5F5,stroke:#CE93D8,color:#333
style I fill:#FFDAB9,stroke:#FFAB76,color:#333
style J fill:#FFB3C6,stroke:#F48FB1,color:#3332.3 11 个设计问题 # 问题 影响 1 对象如何创建 和销毁 ? 构造函数、析构函数、内存分配 2 初始化 和赋值 有什么区别?构造 vs operator= 3 值传递 时意味着什么?拷贝构造的实现成本 4 什么是对象的合法值 ? 不变量、setter 检查 5 你的 class 是否需要继承 ? virtual 析构、virtual 函数 6 你的 class 需要哪些类型转换 ? operator T() 隐式转换 7 哪些运算符 和成员函数 合理? operator+、operator[] 8 哪些成员 应该 private?哪些 public? 封装、信息隐藏 9 未声明 的接口?拷贝、相等、swap、< 自实现 10 谁是你的用户 ? 嵌入式 / 库 / 应用 11 泛型 吗?是否需要模板?模板类
2.4 实战:设计一个 “Rational”(有理数)类 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 35 36 37 38 39 40 41 class Rational {public : Rational (int numerator = 0 , int denominator = 1 ); ~Rational () = default ; Rational operator +(const Rational& rhs) const ; Rational operator -(const Rational& rhs) const ; Rational operator *(const Rational& rhs) const ; Rational operator /(const Rational& rhs) const ; Rational (const Rational&) = default ; Rational (Rational&&) = default ; Rational& operator =(const Rational&) = default ; Rational& operator =(Rational&&) = default ; bool operator ==(const Rational& rhs) const ; bool operator !=(const Rational& rhs) const ; private : int numerator_; int denominator_; };
2.5 关键启示 C++ class 是”超类型的构造” ——几乎可以做任何事每次新 class 都要回答 11 个问题 ——这是 checklist未声明的接口同样重要 ——拷贝、相等、swap三、条款 20:宁以 pass-by-reference-to-const 替换 pass-by-value 3.1 经典反例 1 2 3 4 5 6 7 8 9 10 11 12 13 class Window { std::string name_; std::vector<Shape*> shapes_; }; void printWindow (Window w) { std::cout << w.name (); } printWindow (myWindow);
问题 :
性能差 ——string、vector 拷贝可能分配内存切断多态 ——void f(Window w); 传派生类会切片 (派生部分丢失)3.2 解决方案:pass-by-const-reference 1 2 3 4 void printWindow (const Window& w) { std::cout << w.name (); }
优势 :
维度 pass-by-value pass-by-const-ref 性能 拷贝(可能很贵) 传一个指针(8 bytes) 多态 切片(派生类变基类) 保留派生类型 安全性 副本可修改(不一定是优点) const 保护
3.3 什么时候用 pass-by-value? 并非永远 const&——这些场景 pass-by-value 更合理 :
场景 1:内置类型 1 2 3 void f (int x) ; void f (double x) ; void f (std::int64_t x) ;
场景 2:STL 迭代器 / 函数对象 1 2 3 4 5 6 template <typename Iter>void advance (Iter it, int n) ; std::for_each(v.begin (), v.end (), Compare{});
场景 3:明确需要”副本”的小型值类型 1 2 struct Point { int x, y; }; void draw (Point p) ;
3.4 性能实测对比 1 2 3 4 5 6 7 class Big { std::array<char , 1024> data_; }; void f1 (Big b) ; void f2 (const Big& b) ;
调用 f1 f2 1000 次循环 ~1ms(拷贝) ~0.001ms(传引用) 优势 直觉性 性能
3.5 “多态切片”问题 1 2 3 4 5 6 7 8 9 class Shape { public : virtual void draw () = 0 ; };class Circle : public Shape { double radius_; public : void draw () override { } }; void process (Shape s) ; void process (const Shape& s) ;
切片 (Object Slicing):
3.6 关键启示 默认用 const& ——直到测量显示传值更快内置类型、迭代器、函数对象用传值 ——性能可能更好多态基类必须传引用 ——否则切片小类型(< 16 字节)传值也合理 ——但 const& 也不亏四、条款 21:必须返回对象时,别妄想返回其 reference 4.1 经典陷阱:返回 local 对象的引用 1 2 3 4 5 6 7 8 const Rational& operator *(const Rational& lhs, const Rational& rhs) { Rational result (lhs.n() * rhs.n(), lhs.d() * rhs.d()) ; return result; } Rational a, b; Rational c = a * b;
4.2 常见”自作聪明”的反例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const Rational& operator *(const Rational& lhs, const Rational& rhs) { Rational* result = new Rational (lhs.n () * rhs.n (), lhs.d () * rhs.d ()); return *result; } const Rational& operator *(const Rational& lhs, const Rational& rhs) { static Rational result; result = Rational (lhs.n () * rhs.n (), lhs.d () * rhs.d ()); return result; } Rational a, b, c, d; (a * b) = c;
4.3 正确做法:返回”新对象”(值类型) 1 2 3 4 inline const Rational operator *(const Rational& lhs, const Rational& rhs) { return Rational (lhs.n () * rhs.n (), lhs.d () * rhs.d ()); }
为什么没问题?
编译器会做 RVO (Return Value Optimization)——直接构造到目标位置 即使没有 RVO,移动构造(C++11)也很快 编译器甚至能完全消除拷贝 4.4 RVO / NRVO 优化 1 2 3 4 5 6 Rational c = a * b;
RVO (Return Value Optimization):
1 2 3 Rational makeRational () { return Rational (1 , 2 ); }
NRVO (Named Return Value Optimization):
1 2 3 4 Rational makeRational () { Rational result (1 , 2 ) ; return result; }
4.5 何时”必须”返回 reference? 几乎没有——除了以下场景:
1 2 3 4 5 6 7 8 9 10 std::vector<int > v (10 ) ;v[0 ] = 42 ; class Window {public : const std::string& name () const { return name_; } Window& setName (const std::string& n) { name_ = n; return *this ; } };
4.6 关键启示 绝不要返回 local 对象的引用 ——UB绝不要返回 heap 对象的引用 ——所有权混乱绝不要返回 static 对象的引用 ——多线程不安全 + 共享值返回 + RVO + 移动构造 = 性能最优“返回引用”仅在”语义上必须” (如 operator[])五、条款 22:将成员变量声明为 private 5.1 三大”封装”层级 关键字 访问性 封装度 public谁都能访问 ❌ 无 protected派生类 + 自己的成员 ⚠️ 部分 private仅自己的成员 ✅ 最高
5.2 为什么”protected”不好? 1 2 3 4 5 6 7 8 9 10 11 12 class Base {protected : int x_; }; class Derived : public Base { void foo () { x_ = 10 ; } };
封装被破坏 :
改 x_ 的代码可以分布在任何派生类 中 Base 完全无法保证”修改后 x_ 仍然有效” 派生类数量增加时,不变量越来越难维护 public 同理 ——直接破坏封装。
5.3 private 的”封装力” 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Window {private : int x_, y_; std::string name_; public : void setX (int x) { if (x < 0 || x > 100 ) throw std::out_of_range ("x" ); x_ = x; } int x () const { return x_; } void setName (const std::string& n) { name_ = n; } };
优势 :
不变量 ——“x 在 0-100”在 setX 中强制扩展性 ——以后加日志、缓存、信号——只改一处向后兼容 ——x() 变 x() 接口不变同步 ——可以加锁5.4 实战对比 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Point {public : double x, y; }; class Point {private : double x_, y_; public : double x () const { return x_; } void setX (double x) { x_ = x; } };
5.5 关键启示 成员变量永远是 private ——别用 protected 或 publicprivate 是”封装”的最低门槛 ——给不变量、扩展点、向后兼容protected 不比 public 安全多少 ——派生类数量一多,就和 public 一样危险getter / setter 是封装的”接口” ——加验证、加日志、加同步都可以六、条款 23:宁以 non-member、non-friend 替换 member 函数 6.1 案例:WebBrowser 的”清理缓存”功能 1 2 3 4 5 6 7 8 9 10 11 12 13 class WebBrowser {public : void clearCache () ; void clearHistory () ; void clearCookies () ; void clearEverything () { clearCache (); clearHistory (); clearCookies (); } };
问题 :
clearEverything 必须能访问 clearCache、clearHistory、clearCookies但 clearEverything 和它们是”独立”的功能——不需要访问类的内部数据 用成员函数增加了”耦合”(也能访问私有成员) 6.2 方案 B:non-member、non-friend 函数 1 2 3 4 5 6 void clearBrowser (WebBrowser& browser) { browser.clearCache (); browser.clearHistory (); browser.clearCookies (); }
优势 :
更少耦合 ——clearBrowser 不能访问 WebBrowser 的私有成员更好封装 ——只通过”公开接口”操作 WebBrowser可扩展 ——可以放在不同头文件,按需 includeSTL 风格 ——std::begin、std::end、std::size 都是 non-member6.3 什么时候用 member? 如果函数必须 访问类的私有数据——用 member:
1 2 3 4 5 class Window {public : int x () const { return x_; } void setX (int x) { x_ = x; } };
6.4 决策表 维度 member non-member、non-friend 访问私有数据 ✅ ❌ 封装性 较弱 较强 耦合 较紧 较松 扩展性 难(要改类) 易(加新函数) namespace 组织 ❌ 必须在类内 ✅ 可以放 namespace 测试 较难 较易
6.5 实战:STL 的设计哲学 STL 中很多”看起来像成员”的函数都是 non-member:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 namespace std { template <typename T> void swap (T& a, T& b) { T tmp = std::move (a); a = std::move (b); b = std::move (tmp); } }
6.6 关键启示 优先 non-member、non-friend ——只要能这么做member 函数仅在”必须访问私有数据”时用 ——不要多把”工具函数”放在 namespace ——按模块化拆分STL 的 swap、begin、end 是典范 ——学习它七、条款 24:若所有参数皆需类型转换,请为此采用 non-member 函数 7.1 案例:Number 类 + operator* 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Number { int val_; public : Number (int val) : val_ (val) {} const Number operator *(const Number& rhs) const { return Number (val_ * rhs.val_); } }; Number n (5 ) ;n * 3 ; 3 * n;
问题 :
成员函数的”左侧”是 *this,只能从 this 隐式转换 3 * n 中 3 不会被自动转 Number(如果 operator* 是成员函数)7.2 解决方案:non-member 函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Number { int val_; public : Number (int val) : val_ (val) {} int val () const { return val_; } }; const Number operator *(const Number& lhs, const Number& rhs) { return Number (lhs.val () * rhs.val ()); } Number n (5 ) ;n * 3 ; 3 * n;
7.3 为什么 non-member 支持”所有参数转换”? C++ 的”隐式转换”规则 :
位置 是否支持隐式转换 函数参数 ✅ 是(non-member) 成员函数的 this ❌ 否(member)
7.4 类似的”类型转换”问题 操作符 应放哪? operator+ - * /non-member operator== != < >non-member (条款 25 配套)operator<< >>non-member (必须有左侧 ostream)operator= [] ()member (必须是成员)operator->member (必须是成员)operator++ --member (必须是成员)
7.5 关键启示 operator==/!=/</>/operator+ 等”对称”操作符用 non-member operator=/[]/()/-> 必须是 member 如果想让”两侧都支持隐式转换”,用 non-member “对称”操作符 = 两侧都该是合法类型 ——支持双向转换八、条款 25:考虑写一个不抛异常的 swap 函数 8.1 什么是 swap? 1 2 3 4 5 6 7 8 namespace std { template <typename T> void swap (T& a, T& b) { T tmp = std::move (a); a = std::move (b); b = std::move (tmp); } }
对内置类型和”小类”很高效 ——3 次移动。
8.2 问题:对于”重”的类,std::swap 效率低 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class String { char * data_; size_t size_; public : String (const String& other) : size_ (other.size_) { data_ = new char [size_ + 1 ]; std::memcpy (data_, other.data_, size_ + 1 ); } String& operator =(const String& rhs) { if (this != &rhs) { delete [] data_; data_ = new char [rhs.size_ + 1 ]; std::memcpy (data_, rhs.data_, rhs.size_ + 1 ); size_ = rhs.size_; } return *this ; } }; String s1 ("hello" ) , s2 ("world" ) ;std::swap (s1, s2);
8.3 解决方案:特化 std::swap 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 namespace std { template <> void swap <String>(String& a, String& b) { a.swap (b); } } class String { public : void swap (String& other) noexcept { std::swap (data_, other.data_); std::swap (size_, other.size_); } }; String s1 ("hello" ) , s2 ("world" ) ;std::swap (s1, s2);
8.4 pimpl 模式的 swap 实践 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Widget {public : Widget (const Widget& rhs); Widget& operator =(const Widget& rhs) { Widget temp (rhs); swap (temp); return *this ; } void swap (Widget& other) noexcept { using std::swap; swap (pImpl_, other.pImpl_); } private : struct Impl ; std::unique_ptr<Impl> pImpl_; }; void swap (Widget& a, Widget& b) noexcept { a.swap (b); }
C++11 之前 :必须特化 std::swap 或提供 WidgetSwap 命名空间函数。C++11 之后 :移动构造 + 移动赋值,std::swap 自动高效(用移动而非拷贝)。
8.5 关键启示 对于”重”的类,提供 swap ——避免 std::swap 的 3 次拷贝swap 应是 noexcept ——绝不能抛成员 swap + 非成员 swap ——C++11 后优先非成员C++11 后 :unique_ptr / shared_ptr / string / vector 都有”廉价 swap”pimpl 习惯用 swap 实现”copy assignment” ——copy-and-swap九、8 个条款的”接口设计”全景 graph TB
A["接口设计原则"] --> B["条款 18\n易用且难误用"]
A --> C["条款 19\nclass = type\n设计 11 问"]
A --> D["条款 20-21\n传值 vs 传 ref\n返回值 vs 引用"]
A --> E["条款 22-23\n封装:private\n非成员函数"]
A --> F["条款 24-25\n类型转换:non-member\n高效 swap"]
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 F fill:#FFF9C4,stroke:#F9A825,color:#333核心思路 :
对外 :易用、难误用、不易被破坏对内 :明确所有权、明确类型转换、明确性能十、常见误区与陷阱 10.1 误区 1:返回 local 对象的引用 1 2 3 4 5 const std::string& bad () { std::string s = "hello" ; return s; }
10.2 误区 2:传值大型对象 1 2 void process (std::vector<int > v) ; void process (const std::vector<int >& v) ;
10.3 误区 3:把成员变量设为 public 1 2 3 4 class Point {public : double x, y; };
10.4 误区 4:operator== 用 member 1 2 3 4 5 6 7 8 class Number {public : bool operator ==(const Number& rhs) const ; }; bool operator ==(const Number& lhs, const Number& rhs);
10.5 误区 5:忘记 noexcept swap 1 2 3 4 5 void swap (String& a, String& b) ; void swap (String& a, String& b) noexcept ;
十一、面试高频考点 11.1 必背题 题目 答案要点 pass-by-value vs pass-by-const-ref? 默认 const&;内置类型 / 迭代器 / 函数对象用值 为什么不要返回 local 对象的引用? UB——函数结束时对象析构 什么时候用 member / non-member? 必须访问私有数据用 member;对称操作符用 non-member 为什么 operator== 应该是 non-member? 支持两侧都隐式转换 成员变量应该 public / protected / private? 永远是 private 什么是 RVO? 编译器省略拷贝——直接在调用方位置构造 什么时候写 swap? “重”类(拷贝代价高),或 pimpl 模式
11.2 高频追问 追问 关键点 什么是”对象切片”? pass-by-value 多态会丢失派生部分 什么是”ADL”? Argument-Dependent Lookup,编译器通过参数类型找函数 友元 vs 公开接口? 友元破坏封装——慎用 如何设计易用的接口? 强类型、const 正确性、与内建类型行为一致 namespace vs class? namespace 用于”模块化”,class 用于”对象”
十二、配套实验 12.1 实验 1:pass-by-const-ref 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 35 36 37 38 39 40 41 42 43 44 45 46 #include <iostream> #include <string> #include <vector> #include <chrono> class BigObject { std::array<char , 1024> data_; public : void fill () { data_.fill ('x' ); } }; void processByValue (BigObject obj) { } void processByRef (const BigObject& obj) { } int main () { BigObject obj; obj.fill (); auto start = std::chrono::steady_clock::now (); for (int i = 0 ; i < 100000 ; ++i) { processByValue (obj); } auto end = std::chrono::steady_clock::now (); auto valueTime = std::chrono::duration_cast <std::chrono::microseconds>(end - start).count (); start = std::chrono::steady_clock::now (); for (int i = 0 ; i < 100000 ; ++i) { processByRef (obj); } end = std::chrono::steady_clock::now (); auto refTime = std::chrono::duration_cast <std::chrono::microseconds>(end - start).count (); std::cout << "pass-by-value: " << valueTime << " μs\n" ; std::cout << "pass-by-const-ref: " << refTime << " μs\n" ; std::cout << "Speedup: " << (double )valueTime / refTime << "x\n" ; return 0 ; }
12.2 实验 2:operator* 的成员 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 #include <iostream> class Number { int val_; public : Number (int val = 0 ) : val_ (val) {} int val () const { return val_; } const Number operator *(const Number& rhs) const { return Number (val_ * rhs.val_); } }; int main () { Number n (5 ) ; Number a = n * 3 ; std::cout << "n * 3 = " << a.val () << "\n" ; return 0 ; }
输出 :
12.3 实验 3:non-member operator* 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <iostream> class Number { int val_; public : Number (int val = 0 ) : val_ (val) {} int val () const { return val_; } }; const Number operator *(const Number& lhs, const Number& rhs) { return Number (lhs.val () * rhs.val ()); } int main () { Number n (5 ) ; Number a = n * 3 ; Number b = 3 * n; std::cout << "n * 3 = " << a.val () << "\n" ; std::cout << "3 * n = " << b.val () << "\n" ; return 0 ; }
十三、回到 8 条黄金法则 条款 黄金法则 18 接口要易用、难误用 ——强类型、限制值、const 正确性 19 C++ class = type——设计前回答 11 问 20 默认 pass-by-const-ref——除了内置类型 / 迭代器 / 函数对象 21 绝不要返回 local 对象的引用——值返回 + RVO 最优 22 成员变量永远 private——给不变量、扩展点、向后兼容 23 优先 non-member、non-friend——更少耦合、更好封装 24 对称操作符用 non-member——支持两侧都隐式转换 25 “重”类提供 noexcept swap——避免 3 次拷贝
十四、结尾思考题 思考题 1 :以下代码的输出是什么?为什么?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Number { int val_; public : Number (int val) : val_ (val) {} int val () const { return val_; } const Number operator +(const Number& rhs) const { return Number (val_ + rhs.val_); } }; int main () { Number n (5 ) ; Number result = 3 + n; return 0 ; }
思考题 2 :实现一个 String 类,包括:swap、operator+、operator==,其中 operator+ 和 operator== 应该是 non-member。
思考题 3 :为什么 STL 的 std::swap 用 3 次移动,而不是 1 次拷贝 + 2 次移动?
思考题 4 :pimpl 模式中,operator= 的 copy-and-swap 怎么用?和”普通类的 copy-and-swap”有什么区别?
思考题 5 :你的项目里,有哪些接口是”易误用”的?如何用类型系统改进?
十五、本篇速查表 主题 关键 API / 模式 适用场景 易用且难误用 强类型 + 限制值 API 设计 class = type 11 问 checklist 设计新类 pass-by-const-ref void f(const T&)默认参数传递 值返回 RVO + 移动 绝大多数场景 private 成员 永远不用 public/protected 封装 non-member、non-friend 工具函数、STL 风格 工具方法 对称操作符 non-member operator== 双向隐式转换 noexcept swap 成员 swap + 全局 swap “重”类 / pimpl
十六、系列导航 下一篇 :第 5 篇《实现:让类”用得顺手”》——条款 26-31 一起讲透实现细节:变量初始化顺序、隐式转换的利弊、名字查找、编译期依赖、复合 == 三原则、inline 的代价、降低编译依赖。
行动建议 :
今天 :把你项目的”传值大型对象”改成 const&今天 :把 operator== 改成 non-member 版本本周 :识别你项目中的”成员变量 public”代码——改成 private本周 :写一个”重”类的 swap 方法思考 :你的类设计 checklist 通过了 11 问吗?