Effective C++是 Scott Meyers 最经典的 C++ 经验总结之一,内容不追求面面俱到,而是聚焦在日常开发里最容易踩坑、也最值得反复思考的语言细节。- 这篇文章作为阅读笔记,按条款整理其中的核心观点,尽量用简洁的方式记录每条建议背后的关注点,方便后续回顾和查阅。
- 它不适合作为 C++ 入门教材,但很适合已经写过一段时间 C++ 之后,用来系统修正常见编码习惯。
Effective C++ - Scott Meyers
条款 01:视 C++ 为一个语言联邦
C++ 不是单一语言,而是四种子语言的组合:
- C:保留了过程式语言的高效与底层能力。
- Object-Oriented C++:类、继承、多态。
- Template C++:模板元编程与泛型编程。
- STL:容器、迭代器、算法、函数对象。
这一条的核心意思是:不同子语言有不同规则,写代码时要知道自己正在使用哪一套语义。
条款 02:尽量以 const、enum、inline 替换 #define
#define 本质上只是预处理阶段的文本替换,缺点很明显:
- 没有作用域
- 没有类型检查
- 调试信息不友好
- 容易出现副作用
因此更推荐:
- 常量:使用
const或enum - 类内整型常量:优先
static const或enum - 函数宏:使用
inline函数
1 | const int MaxSize = 100; |
一句话总结:尽量让编译器替你工作,而不是依赖预处理器。
条款 03:尽可能使用 const
const 的价值不只是“不可修改”,更重要的是它能明确表达接口意图。
常见场景:
- 修饰变量:避免误改
- 修饰指针:区分“指针本身不可变”还是“指向内容不可变”
- 修饰成员函数:说明该函数不会修改对象状态
- 修饰参数和返回值:提升接口可读性
1 | const char* p; // 指向常量的指针 |
对于成员函数:
1 | class TextBlock { |
原则很简单:只要某个值不该被改,就尽早加上 const。
条款 04:确定对象被使用前已先被初始化
C++ 不保证内置类型一定被自动初始化,所以最稳妥的做法是:声明时就初始化。
对于类成员,优先使用成员初始化列表,而不是在构造函数体内赋值:
1 | class Widget { |
原因有两个:
- 更高效:直接初始化,而不是先默认构造再赋值
- 更安全:避免未初始化状态
另外,跨编译单元的 non-local static 对象存在初始化顺序问题。常见解法是把它改成函数内的 local static:
1 | Logger& globalLogger() { |
条款 05:了解 C++ 默默编写并调用哪些函数
如果你没有显式声明,编译器可能会为类自动生成这些函数:
- 默认构造函数
- 拷贝构造函数
- 拷贝赋值运算符
- 析构函数
1 | class Empty {}; |
大致会被视作:
1 | class Empty { |
这条的重点是:编译器会帮你生成成员函数,但生成出来的不一定符合你的语义要求。
条款 06:若不想使用编译器自动生成的函数,就该明确拒绝
有些类天生就不该被拷贝,例如:
- 管理互斥锁的类
- 文件句柄封装类
- 单例类
这种情况下,不要依赖“大家自觉不拷贝”,而要从语法层面禁止。
现代 C++ 最推荐的写法是:
1 | class Uncopyable { |
老式写法是声明为 private 且不提供定义,但现在 = delete 更直接清晰。
条款 07:为多态基类声明 virtual 析构函数
如果一个类打算作为基类,并且会通过基类指针操作派生类对象,那么析构函数必须是 virtual。
否则会出现“只析构基类部分、不析构派生类部分”的问题,最终导致资源泄漏。
1 | class Base { |
只有一种情况通常不需要虚析构:类不作为多态基类使用。
条款 08:别让异常逃离析构函数
析构函数通常用于资源释放,而资源释放往往发生在异常传播过程中。如果析构函数自己再抛异常,程序很容易直接 terminate。
因此析构函数应遵循两个原则:
- 自己内部吞掉异常
- 或者把可能失败的操作挪到普通成员函数里,让调用者显式处理
1 | class DBConn { |
一句话:析构函数应该尽最大努力善后,但不要把异常往外丢。
条款 09:绝不在构造和析构过程中调用 virtual 函数
在构造基类阶段,派生类部分尚未完成初始化;在析构基类阶段,派生类部分已经被销毁。
所以此时调用 virtual 函数,并不会表现出你想要的“多态行为”,而是只会调用当前构造/析构阶段所属类的版本。
1 | class Transaction { |
如果你希望不同派生类执行不同逻辑,应改成:
- 让派生类把必要信息先准备好
- 再把这些信息传给基类构造函数
- 或者在对象构造完成后再调用普通接口
条款 10:令 operator= 返回一个 reference to *this
赋值运算符应返回当前对象的引用,这样才能支持连锁赋值,并保持与内置类型一致的行为。
1 | class Widget { |
这样才能写出:
1 | x = y = z; |
不只是 operator=,像 +=、-=、*= 这类赋值相关运算符,通常也都应该返回 *this 的引用。