Effective C++ 第三版笔记
书籍信息
- 书名: Effective C++, Third Edition: 55 Specific Ways to Improve Your Programs and Designs
- 作者: Scott Meyers
- 页数: 321页
- 出版: Addison-Wesley, 2005
- ISBN: 0-321-33487-6
核心观点
本书是C++程序员必读的”第二本书”(在第一本入门教材之后)。全书以55个独立条目(Item)的形式,系统性地讲解了如何写出更有效、更安全、更高效C++代码的实践指南。每个条目聚焦一个具体主题,解释”怎么做”以及”为什么这样做”。
章节结构与关键要点
第1章:让自己习惯C++ (Accustoming Yourself to C++)
Item 1: 将C++视为一个语言联邦
C++不是单一语言,而是由四个子语言组成的联邦:
- C: 过程式编程的基础
- Object-Oriented C++: 基于类的面向对象编程(Classes, Constructors, Destructors, Encapsulation, Inheritance, Polymorphism, Virtual Functions)
- Template C++: 泛型编程(Templates, STL)
- STL: 标准模板库(Containers, Iterators, Algorithms, Function Objects)
不同子语言有不同的规则和行为模式,理解这种分层是有效使用C++的前提。
Item 2: 优先使用const、enum、inline替代#define
#define宏缺乏类型检查,容易造成意外行为。推荐替代方案:
- 常量用
const或enum - 内联函数用
inline(而非宏函数) - 类内常量用
static const(C++11起可用= 0)
Item 3: 尽可能使用const
const不仅是编译期检查工具,更是设计契约:
- 声明
const成员函数表明不修改对象状态 const迭代器指向只读数据const引用参数防止意外修改const指针区分指向内容和指针本身是否可变
Item 4: 确保对象在使用前已被初始化
C++对”未初始化”的定义比想象中更复杂:
- 默认构造函数保证内置类型不被初始化
- 成员初始化列表(Member Initialization List)是正确初始化成员的唯一方式(尤其对于const和引用成员)
- 不要在构造函数体内给成员赋值,应使用初始化列表
第2章:构造函数、析构函数和赋值运算符
Item 5: 了解C++默默编写并调用的函数
编译器自动生成四个特殊成员函数:
- 默认构造函数(Default Constructor)
- 复制构造函数(Copy Constructor)
- 复制赋值运算符(Copy Assignment Operator)
- 析构函数(Destructor)
理解这些函数的生成规则和调用时机,是控制对象行为的基础。
Item 6: 显式禁止编译器生成我们不想要的函数
通过将复制构造函数和赋值运算符声明为private(不实现),可以防止对象被复制:
class Uncopyable {
protected:
Uncopyable() {}
~Uncopyable() {}
private:
Uncopyable(const Uncopyable&);
Uncopyable& operator=(const Uncopyable&);
};Item 7: 在多态基类中声明虚析构函数
当类包含至少一个虚函数时,析构函数必须是虚的。否则通过基类指针删除派生类对象会导致未定义行为(派生类资源泄漏)。
Item 8: 不让异常从析构函数中抛出
析构函数抛出异常会导致程序终止(调用std::terminate)。解决方案:
- 捕获析构函数内的异常并忽略
- 设计一个普通成员函数来执行可能失败的操作
- 使用”退出/操作”(cleanup/operation)模式分离
Item 9: 绝不在构造和析构过程中调用虚函数
在构造/析构期间,虚函数不会调用派生类版本,而是调用当前正在构造/析构的类的版本。因为此时派生类部分尚未构造/已析构。
Item 10: 让赋值运算符返回this的引用
支持链式赋值:a = b = c;
Widget& operator=(const Widget& rhs) {
// ... 实现 ...
return *this;
}Item 11: 在operator=中处理”自我赋值”
使用copy-and-swap惯用法或先保存源对象再释放目标资源。
Item 12: 复制对象的所有部分
operator=应复制所有成员变量,包括基类部分。调用基类的operator=确保基类数据也被正确复制。
第3章:资源管理
Item 13: 以对象管理资源
RAII(Resource Acquisition Is Initialization)是C++资源管理的核心范式:
- 资源在构造函数中获取
- 资源在析构函数中释放
- 使用智能指针(
std::auto_ptr、std::shared_ptr、std::unique_ptr)管理动态分配的资源
Item 14: 在资源管理类中考虑拷贝行为
需要明确资源的所有权语义:
- 禁止拷贝(如Item 6)
- 引用计数(
shared_ptr) - 深度拷贝
- 转移所有权(
unique_ptr的移动语义)
Item 15: 在资源管理类中提供对原始资源的访问
为了与旧API兼容,资源管理类应提供获取原始资源的接口(如get()方法),同时保持所有权管理。
Item 16: 成对使用new和delete时要采用相同形式
int* p = new int; // 单对象
delete p;
int* arr = new int[10]; // 数组
delete[] arr;形式不匹配导致未定义行为。
Item 17: 以独立语句将newed对象置入智能指针
避免以下代码:
processWidget(std::shared_ptr<Widget>(new Widget), prioritize());因为函数参数求值顺序未定义,可能先执行prioritize()再执行new,若prioritize()抛出异常则内存泄漏。正确做法:
std::shared_ptr<Widget> pw(new Widget);
processWidget(pw, prioritize());第4章:设计与声明
Item 18: 让接口容易被正确使用,难以被错误使用
设计API时应考虑:
- 类型安全(使用强类型)
- 约束条件的显式化
- 如果某些参数组合无效,通过类型系统阻止而非运行时检查
Item 19: 将类设计为类型设计
好的类设计应像好的数据类型设计:
- 一致性
- 最小化暴露
- 清晰的语义
Item 20: 优先使用const引用传参而非值传递
- 避免不必要的拷贝开销
const保证不修改参数- 对于内置类型和小型对象,值传递可能更高效
Item 21: 必须返回对象时不要返回引用
返回值优化(RVO/NRVO)可以消除临时对象的开销,无需担心性能。
Item 22: 声明成员变量为private
封装是面向对象的基本原则。保护数据不被外部随意修改。
Item 23: 优先使用非成员非友元函数而非成员函数
当函数不需要访问类的私有成员时,应声明为非成员函数,以保持类的封装性。这也有助于实现类型转换。
Item 24: 当所有参数都需要类型转换时,声明为非成员函数
成员函数的第一个参数(this)不参与隐式类型转换。若需要将类型转换应用于所有参数,应使用非成员函数。
Item 25: 考虑支持非抛出异常的swap
std::swap应保证不抛出异常。自定义类型可提供特化的swap成员函数,并通过ADL(Argument-Dependent Lookup)发现。
第5章:实现
Item 26: 尽可能推迟变量定义
扩大变量的作用域会降低代码可读性。在第一次使用前再定义变量。
Item 27: 尽可能避免强制类型转换
static_cast: 编译期检查,用于明确的类型转换dynamic_cast: 运行时检查,用于多态类型的向下转换reinterpret_cast: 危险的底层转换,尽量避免const_cast: 仅用于添加/移除const
Item 28: 避免返回”句柄”指向对象内部
返回引用或指针指向对象内部数据会破坏封装,使对象状态可能被外部意外修改。
Item 29: 追求异常安全的代码
异常安全三个级别:
- 基本保证:无资源泄漏,数据一致性
- 强烈保证:操作成功或恢复到原始状态
- 不抛出保证:永远不会抛出异常
Item 30: 理解inline的内在
inline仅是建议,编译器可忽略- 头文件中的函数通常应为inline
- 虚函数通常不是inline候选
- 类内定义的函数自动成为inline
Item 31: 最小化编译依赖性
- 使用句柄类(Handle Class)或接口类减少头文件依赖
#include只包含必要的头文件- 前向声明(Forward Declaration)替代完整包含
第6章:继承与面向对象设计
Item 32: 确认公共继承建模”is-a”关系
公共继承意味着”is-a”(是一种)。派生类对象可以替换基类对象。如果不是”is-a”关系,考虑组合(Composition)或”has-a”关系。
Item 33: 避免隐藏继承的名称
派生类重新声明与基类同名的函数会隐藏基类版本,即使签名不同。使用using声明引入基类名称。
Item 34: 区分继承接口与继承实现
- 纯虚函数:继承接口
- 虚函数(有默认实现):继承接口+可选实现
- 非虚函数:继承接口+强制实现
Item 35: 考虑虚函数以外的替代方案
- Non-virtual Interface (NVI) 惯用法:将虚函数设为private,公开非虚函数调用它
- 策略模式(Strategy Pattern)
- 函数对象(Function Objects)
- 模板方法模式
第7章:模板与泛型编程
Item 36: 区分模板参数递推与显式指定
模板参数推导有时不符合预期,可使用显式指定或辅助函数解决。
Item 37: 使用参数推导处理”可接受值组合”
通过函数模板的多个重载处理不同类型组合。
Item 38: 通过”出参指针模型化reference-to-pointer”转换
C++中引用不能作为模板参数直接传递指针类型,需要特殊处理。
Item 39: 理解隐式接口和编译期多态
模板不要求显式接口(如虚函数),而是要求”满足特定操作的隐式接口”。编译期多态通过模板特化和重载解析实现。
Item 40: 明智使用CRTP获得类型无关的性能
Curiously Recurring Template Pattern(奇异的递归模板模式)允许在编译期获得类似虚函数的多态行为,避免运行时虚函数调用开销。
第8章:定制内存管理
Item 41: 理解自定义new-handler的行为
当operator new无法分配内存时,会调用global new-handler。可通过std::set_new_handler设置自定义处理函数。
Item 42: 理解malloc/free与new/delete的对应关系
new=operator new+ 构造函数delete= 析构函数 +operator delete- C风格的
malloc/free不调用构造/析构函数
Item 43: 思考malloc-free与new-delete的区别
operator new可被重载以实现定制内存分配策略(池化、栈分配等)。
Item 44: 通过operator new/delete实现高性能小内存分配
对于频繁分配/释放的小对象,使用自定义分配器可以减少内存碎片和分配开销。
Item 45: 遵守”一致使用new和delete的形式”的规则
确保new/delete、new[]/delete[]、malloc/free成对使用。
第9章:谈票证
Item 46: 如果不想使用编译器生成的函数,应显式拒绝
再次强调Item 6的原则。
Item 47: 通过非成员函数处理类型转换
再次强调Item 24的原则。
Item 48: 通过traits classes实现类型相关行为
Traits类提供编译期类型信息,用于模板特化和SFINAE(Substitution Failure Is Not An Error)。
Item 49: 理解iostream的类型系统
iostream库使用显式类型,不同于STL的泛型设计。理解其设计决策有助于避免误用。
Item 50: 熟悉标准库的类型工具
std::tr1::result_of: 查询函数调用结果类型std::mem_fun/std::bind1st/std::bind2nd: 绑定成员函数和参数std::iterator_traits: 迭代器特性
Item 51: 通过non-member函数加入类型转换
再次强调Item 23-24的原则。
Item 52: 写好toString时考虑operator<<
自定义类型的字符串表示应通过operator<<实现,而非toString方法,以保持与iostream生态的一致性。
Item 53: 避免隐式转换的危险
- 单一参数构造函数可能导致隐式转换
- 使用
explicit关键字阻止不需要的隐式转换 - 仔细设计类型转换路径
Item 54: 理解operator new和operator delete的实现细节
operator new可能抛出std::bad_allocoperator new可以被重载nothrow版本的operator new不抛出异常,返回nullptr
Item 55: 编写placement new时小心不要遗漏delete
placement new在已分配内存上构造对象,不会自动调用全局operator delete。需要显式调用析构函数和正确的delete形式。
行动点
- 日常编码: 每写一个新类,对照第2章的12个Item检查构造函数、析构函数、赋值运算符的实现
- 代码审查: 使用第3章的RAII原则审查所有资源管理代码
- 接口设计: 参考第4章的原则设计API,特别关注Item 18的”易用难错”原则
- 性能优化: 理解第8章的内存管理技巧,在热点路径中使用合适的分配策略
- 模板编程: 掌握第7章的泛型编程技术,特别是traits和SFINAE的使用
与其他知识的关联
- Modern-Cpp-Design-泛型编程与设计模式应用-Andrei-Alexandrescu - Policy-Based Design与Effective C++的互补视角
- C++-Concurrency-in-Action-2nd-Edition - 并发编程中的资源管理和异常安全
- A-Tour-of-C++-2nd-Edition-Bjarne-Stroustrup - C++语言 Tour 的深入补充
- Effective-Modern-C++-Scott-Meyers - Scott Meyers后续的Modern C++作品