0%

ICS Effective C++ Reading Note

  • 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 本质上只是预处理阶段的文本替换,缺点很明显:

  • 没有作用域
  • 没有类型检查
  • 调试信息不友好
  • 容易出现副作用

因此更推荐:

  • 常量:使用 constenum
  • 类内整型常量:优先 static constenum
  • 函数宏:使用 inline 函数
1
2
3
4
5
6
const int MaxSize = 100;
enum { NumTurns = 5 };

inline int square(int x) {
return x * x;
}

一句话总结:尽量让编译器替你工作,而不是依赖预处理器。

条款 03:尽可能使用 const

const 的价值不只是“不可修改”,更重要的是它能明确表达接口意图。

常见场景:

  • 修饰变量:避免误改
  • 修饰指针:区分“指针本身不可变”还是“指向内容不可变”
  • 修饰成员函数:说明该函数不会修改对象状态
  • 修饰参数和返回值:提升接口可读性
1
2
3
const char* p;       // 指向常量的指针
char* const p2 = p0; // 常量指针
const char* const p3 = p0;

对于成员函数:

1
2
3
4
5
class TextBlock {
public:
const char& operator[](std::size_t pos) const;
char& operator[](std::size_t pos);
};

原则很简单:只要某个值不该被改,就尽早加上 const

条款 04:确定对象被使用前已先被初始化

C++ 不保证内置类型一定被自动初始化,所以最稳妥的做法是:声明时就初始化。

对于类成员,优先使用成员初始化列表,而不是在构造函数体内赋值:

1
2
3
4
5
6
7
class Widget {
public:
Widget() : x(0), y(0) {}
private:
int x;
int y;
};

原因有两个:

  • 更高效:直接初始化,而不是先默认构造再赋值
  • 更安全:避免未初始化状态

另外,跨编译单元的 non-local static 对象存在初始化顺序问题。常见解法是把它改成函数内的 local static:

1
2
3
4
Logger& globalLogger() {
static Logger logger;
return logger;
}

条款 05:了解 C++ 默默编写并调用哪些函数

如果你没有显式声明,编译器可能会为类自动生成这些函数:

  • 默认构造函数
  • 拷贝构造函数
  • 拷贝赋值运算符
  • 析构函数
1
class Empty {};

大致会被视作:

1
2
3
4
5
6
7
class Empty {
public:
Empty();
Empty(const Empty&);
Empty& operator=(const Empty&);
~Empty();
};

这条的重点是:编译器会帮你生成成员函数,但生成出来的不一定符合你的语义要求。

条款 06:若不想使用编译器自动生成的函数,就该明确拒绝

有些类天生就不该被拷贝,例如:

  • 管理互斥锁的类
  • 文件句柄封装类
  • 单例类

这种情况下,不要依赖“大家自觉不拷贝”,而要从语法层面禁止。

现代 C++ 最推荐的写法是:

1
2
3
4
5
6
7
8
class Uncopyable {
public:
Uncopyable() = default;
~Uncopyable() = default;

Uncopyable(const Uncopyable&) = delete;
Uncopyable& operator=(const Uncopyable&) = delete;
};

老式写法是声明为 private 且不提供定义,但现在 = delete 更直接清晰。

条款 07:为多态基类声明 virtual 析构函数

如果一个类打算作为基类,并且会通过基类指针操作派生类对象,那么析构函数必须是 virtual

否则会出现“只析构基类部分、不析构派生类部分”的问题,最终导致资源泄漏。

1
2
3
4
5
6
7
8
9
10
11
class Base {
public:
virtual ~Base() = default;
};

class Derived : public Base {
public:
~Derived() {
// 释放资源
}
};

只有一种情况通常不需要虚析构:类不作为多态基类使用。

条款 08:别让异常逃离析构函数

析构函数通常用于资源释放,而资源释放往往发生在异常传播过程中。如果析构函数自己再抛异常,程序很容易直接 terminate

因此析构函数应遵循两个原则:

  • 自己内部吞掉异常
  • 或者把可能失败的操作挪到普通成员函数里,让调用者显式处理
1
2
3
4
5
6
7
8
9
10
11
12
class DBConn {
public:
~DBConn() {
try {
close();
} catch (...) {
// 记录错误,禁止异常继续向外传播
}
}

void close();
};

一句话:析构函数应该尽最大努力善后,但不要把异常往外丢。

条款 09:绝不在构造和析构过程中调用 virtual 函数

在构造基类阶段,派生类部分尚未完成初始化;在析构基类阶段,派生类部分已经被销毁。

所以此时调用 virtual 函数,并不会表现出你想要的“多态行为”,而是只会调用当前构造/析构阶段所属类的版本。

1
2
3
4
5
6
7
8
class Transaction {
public:
Transaction() {
logTransaction(); // 不安全
}

virtual void logTransaction() const;
};

如果你希望不同派生类执行不同逻辑,应改成:

  • 让派生类把必要信息先准备好
  • 再把这些信息传给基类构造函数
  • 或者在对象构造完成后再调用普通接口

条款 10:令 operator= 返回一个 reference to *this

赋值运算符应返回当前对象的引用,这样才能支持连锁赋值,并保持与内置类型一致的行为。

1
2
3
4
5
6
7
8
9
class Widget {
public:
Widget& operator=(const Widget& rhs) {
if (this != &rhs) {
// 复制 rhs 的内容
}
return *this;
}
};

这样才能写出:

1
x = y = z;

不只是 operator=,像 +=-=*= 这类赋值相关运算符,通常也都应该返回 *this 的引用。