【穿越Effective C++】Scott Meyers的《Effective C++》逻辑框架概要汇总--各条款是什么?为什么?怎么做?
第一部分:让自己习惯C++
条款01:视C++为一个语言联邦
- 是什么: C++是由多种“子语言”组成的联邦,包括C、面向对象C++、模板C++、STL。
- 为什么: 不同子语言有各自的编程范式。理解这点能帮助你在不同情境下切换合适的规则和最佳实践。
- 怎么做: 编写C风格代码时遵循C的规则;使用面向对象特性时遵循类设计的规则;使用模板时掌握模板元编程规则;使用STL时遵循容器和迭代器的规则。
条款02:尽量以const, enum, inline替换#define
- 是什么: 用编译器替代预处理器。
- 为什么:
#define定义的宏在预处理阶段被替换,不进符号表,不利于调试,且易产生错误。 - 怎么做:
- 用
const常量替换宏常量。 - 用
enum替换类中的静态整型常量(尤其在旧标准中)。 - 用
inline函数替换宏函数。
- 用
条款03:尽可能使用const
- 是什么:
const是一个语义约束,指定对象不可被修改。 - 为什么: 增强代码可读性,帮助编译器检测错误,并在某些情况下产生更优化的代码。
- 怎么做:
- 修饰指针、迭代器(
const_iterator)。 - 修饰函数返回值,避免被意外修改。
- 修饰成员函数,表明该函数不修改对象状态(注意
mutable成员变量的特例)。
- 修饰指针、迭代器(
条款04:确定对象被使用前已先被初始化
- 是什么: 确保所有对象在使用前都有确定的初始值。
- 为什么: 读取未初始化值是未定义行为,可能导致程序崩溃或随机错误。
- 怎么做:
- 对于内置类型,手动初始化。
- 对于类类型,使用构造函数初始化列表(总是使用初始化列表,而非在构造函数体内赋值)。
- 警惕“跨编译单元之初始化次序问题”,使用局部静态变量(C++11后是线程安全的)替代非局部静态变量。
第二部分:构造/析构/赋值运算
条款05:了解C++默默编写并调用哪些函数
- 是什么: 编译器会为类自动生成默认构造函数、拷贝构造函数、拷贝赋值操作符和析构函数。
- 为什么: 如果你未声明,编译器会自动生成这些函数,但可能不是你想要的行为。
- 怎么做: 了解编译器自动生成的函数在什么条件下会被创建(例如,若你声明了带参构造,则默认构造不会自动生成),并明确是否需要自定义它们。
条款06:若不想使用编译器自动生成的函数,就该明确拒绝
- 是什么: 禁止类的拷贝或赋值等操作。
- 为什么: 防止对象被意外拷贝(如
std::unique_ptr)。 - 怎么做: C++11前:将不想使用的函数声明为
private且不实现。C++11后:使用= delete。
条款07:为多态基类声明virtual析构函数
- 是什么: 如果一个类被设计为多态基类(有虚函数,意图通过基类指针来操作派生类对象),其析构函数必须是虚函数。
- 为什么: 如果基类指针指向派生类对象,且通过该指针
delete,若基类析构非虚,则派生部分的析构不会被调用,导致资源泄漏。 - 怎么做: 将多态基类的析构函数声明为
virtual。反之,如果一个类不是设计为基类,就不要声明虚析构函数,避免引入虚表指针的开销。
条款08:别让异常逃离析构函数
- 是什么: 析构函数不应抛出异常。
- 为什么: 如果析构函数抛出异常,且正处于栈展开过程(因另一个异常导致),程序通常会终止。
- 怎么做:
- 析构函数捕获所有异常并吞下它们(记录日志)或结束程序。
- 提供一个普通函数(如
close())让用户有机会处理异常,析构函数作为备用路径调用它并处理异常。
条款09:绝不在构造和析构过程中调用virtual函数
- 是什么: 在构造/析构期间,对象的动态类型被视为当前类的类型,而非派生类类型。
- 为什么: 在基类构造期间,派生类部分尚未初始化/在基类析构期间,派生类部分已销毁。此时调用虚函数不会下降到派生类层,可能导致未定义行为。
- 怎么做: 确保构造函数和析构函数内调用的函数都是非虚的。如果需要在构造时获取派生类信息,可将派生类信息作为参数传递给基类构造函数。
*条款10:令operator=返回一个reference to this
- 是什么: 赋值操作符(
=,+=,-=等)应返回一个指向左操作数的引用。 - 为什么: 为了支持连锁赋值(
a = b = c)。 - 怎么做:
ClassName& operator=(const ClassName& rhs) { ... return *this; }
条款11:在operator=中处理“自我赋值”
- 是什么: 确保对象自己给自己赋值时也能正确工作。
- 为什么: 自我赋值时,如果先释放资源再拷贝,会导致访问已释放内存。
- 怎么做:
- 证同测试:
if (this == &rhs) return *this; - 精心排列语句:先拷贝副本,再删除原对象,最后用副本赋值(拷贝并交换技术)。
- 使用copy-and-swap。
- 证同测试:
条款12:复制对象时勿忘其每一个成分
- 是什么: 编写拷贝构造函数和拷贝赋值操作符时,要复制所有成员变量和基类部分。
- 为什么: 编译器生成的版本会自动复制所有成员,但当你自己定义时,编译器不再帮忙,遗漏复制会导致局部拷贝。
- 怎么做:
- 在拷贝构造函数中,调用基类的拷贝构造函数。
- 在拷贝赋值操作符中,调用基类的拷贝赋值操作符。
- 确保复制所有成员变量。
- 不要让拷贝赋值操作符调用拷贝构造函数,反之亦然。如果发现代码重复,可提取到
private init函数中。
第三部分:资源管理
条款13:以对象管理资源
- 是什么: 使用RAII(资源获取即初始化)对象来管理动态分配的资源。
- 为什么: 防止因异常、过早
return或忘记delete而导致的内存泄漏。 - 怎么做: 将资源在构造时获取,在析构时释放。使用智能指针(如
std::unique_ptr,std::shared_ptr)或自定义RAII类。
条款14:在资源管理类中小心copying行为
- 是什么: RAII类在被拷贝时的行为需要仔细设计。
- 为什么: 默认的拷贝行为(浅拷贝)可能导致同一资源被释放多次。
- 怎么做:
- 禁止复制:继承
noncopyable或使用= delete(条款06)。 - 引用计数:使用
std::shared_ptr并指定删除器。 - 深拷贝:拷贝底层资源。
- 转移资源所有权:如
std::unique_ptr。
- 禁止复制:继承
条款15:在资源管理类中提供对原始资源的访问
- 是什么: RAII类需要提供获取其管理的原始资源的方法。
- 为什么: 很多API需要原始资源指针或引用。
- 怎么做:
- 显式转换:提供
get()成员函数。 - 隐式转换:提供转换操作符(如
operator T*()),但需谨慎,可能引发意外类型转换。
- 显式转换:提供
条款16:成对使用new和delete时要采取相同形式
- 是什么:
new对应delete,new[]对应delete[]。 - 为什么: 使用不匹配的形式是未定义行为。
- 怎么做: 如果使用
new[]分配数组,一定要使用delete[]释放。优先使用std::vector,std::string等容器来避免直接管理数组。
条款17:以独立语句将newed对象置入智能指针
- 是什么: 在一条语句中执行
new和将结果存入智能指针。 - 为什么: 防止因编译器优化导致指令重排,在资源创建和资源被托管之间发生异常,导致资源泄漏。
- 怎么做:
std::unique_ptr<Widget> pw(std::make_unique<Widget>());或分两步写:先new,再置入智能指针(但最好用std::make_unique)。
第四部分:设计与声明
条款18:让接口容易被正确使用,不易被误用
- 是什么: 设计接口时应考虑用户会如何错误地使用它,并加以防范。
- 为什么: 减少客户错误,提高代码健壮性。
- 怎么做:
- 保持接口的一致性(如STL接口)。
- 使用智能指针防止内存泄漏。
- 定义新的类型(类)来限制参数(如
Day,Month,Year而非int)。 - 约束对象值(通过构造函数等)。
条款19:设计class犹如设计type
- 是什么: 设计一个类就是设计一个新的类型,需要周全考虑。
- 为什么: 类的设计质量直接影响代码的可用性、可维护性和效率。
- 怎么做: 考虑:对象如何创建/销毁、初始化和赋值的区别、按值传递的含义、合法值、继承关系、类型转换、操作符和函数、访问控制、未声明接口等。
条款20:宁以pass-by-reference-to-const替换pass-by-value
- 是什么: 函数参数传递时,用
const T&代替T。 - 为什么:
- 效率:避免不必要的拷贝构造和析构,尤其对于大型对象。
- 正确性:避免对象切割(Slicing):当派生类对象按值传递给期望基类对象的函数时,派生类部分会被“切割”掉。
- 怎么做: 对于内置类型、STL迭代器、函数对象,按值传递通常更合适。其他情况优先考虑
pass-by-reference-to-const。
条款21:必须返回对象时,别妄想返回其reference
- 是什么: 函数返回一个局部对象时,必须按值返回。
- 为什么: 绝不能返回指向局部栈对象的指针或引用(函数结束即销毁),也不能返回指向堆上对象的引用(谁负责
delete?),更不能返回静态局部对象的引用(线程安全问题,且逻辑错误)。 - 怎么做: 安心地按值返回。编译器可能会使用RVO(返回值优化)来消除临时对象的开销。
条款22:将成员变量声明为private
- 是什么: 成员变量应该是私有的。
- 为什么:
- 语法一致性:客户访问成员唯一的方式是通过函数。
- 精确控制:可以轻松实现只读、只写、读写、甚至通知(观察者模式)等访问控制。
- 封装性:以后可以改变实现而不影响客户代码。
- 怎么做: 将所有数据成员声明为
private。protected并不比public更具封装性。
条款23:宁以non-member、non-friend替换member函数
- 是什么: 如果一个功能可以通过类的公有接口实现,那么把它实现为非成员非友元函数,而不是成员函数。
- 为什么: 增强封装性。非成员非友元函数不能访问类的私有成员,意味着类的封装性没有被破坏。同时,它还可以提高包装弹性,允许更好的代码组织。
- 怎么做: 在同一个命名空间下,将相关功能作为工具函数提供,而不是塞进类里。例如
std::string的getline函数。
条款24:若所有参数皆需类型转换,请为此采用non-member函数
- 是什么: 如果一个操作符函数(如
operator*)的所有参数(包括被this指向的隐式参数)都可能需要隐式类型转换,那么它应该是非成员函数。 - 为什么: 成员函数不允许隐式转换其第一个参数(即
this指向的对象)。 - 怎么做: 将操作符声明为非成员函数。如果它需要访问类的非公有成员,可以声明为
friend。
条款25:考虑写出一个不抛异常的swap函数
- 是什么: 为自定义类型提供高效的、不抛异常的
swap特化版本。 - 为什么: 标准库的
swap通过三次拷贝实现,对于资源管理类效率低下。std::swap被假定为不抛异常,某些算法(如容器排序)需要这一点。 - 怎么做:
- 在类内提供一个
public swap成员函数,高效地交换成员(通常只是交换指针),并保证noexcept。 - 在类所在的命名空间提供一个非成员
swap函数,调用成员swap。 - 如果要为标准库模板(如
std::vector<T>)特化swap,在std命名空间内特化它(但只能特化模板,不能重载)。
- 在类内提供一个
第五部分:实现
条款26:尽可能延后变量定义式的出现时间
- 是什么: 直到真正需要使用时才定义变量。
- 为什么:
- 效率:避免构造和析构不必要的对象。
- 清晰:变量定义与其使用点靠近,代码更易读。
- 怎么做: 不仅延后定义,最好在定义时直接用参数初始化,而不是先默认构造再赋值。
条款27:尽量少做转型动作
- 是什么: 避免使用C风格和旧式C++转型,使用新式转型(
const_cast,dynamic_cast,static_cast,reinterpret_cast)。 - 为什么:
- 新式转型目标明确,易于识别和排错。
- 转型可能产生运行时成本(如
dynamic_cast)或导致对象代码布局改变(如多重继承中)。
- 怎么做:
- 使用新式转型。
- 注重设计,避免在派生类和基类之间来回转型。
- 如果必须转型,考虑是否有更好的设计可以避免它。
条款28:避免返回handles指向对象内部成分
- 是什么: “handles”指引用、指针、迭代器。不要返回它们指向对象的私有成员。
- 为什么:
- 破坏封装性,客户可以修改私有数据。
- 可能导致悬空句柄(dangling handles):如果对象被销毁,返回的handle就悬空了。
- 怎么做: 返回副本(by value)或
const引用/指针。但即使返回const,也要注意悬空问题。
条款29:为“异常安全”而努力是值得的
- 是什么: 当异常被抛出时,函数需要保证:1. 不泄漏资源。2. 不允许数据结构被破坏。
- 为什么: 异常安全的代码能带来更健壮的程序。
- 怎么做: 异常安全函数提供以下三个保证之一:
- 基本承诺: 程序状态保持有效,但可能是任何有效状态(对象仍处于一致状态)。
- 强烈保证: 操作要么成功,要么失败(如同没调用过该函数)。常通过copy-and-swap实现。
- 不抛掷保证: 承诺绝不抛出异常。析构函数和内存释放函数应提供此保证。
条款30:透彻了解inlining的里里外外
- 是什么:
inline是对编译器的请求,建议将函数调用替换为函数体。 - 为什么:
- 优点:消除函数调用开销,可能生成更小的目标代码。
- 缺点: 增加目标代码大小,从而可能导致换页行为增加,降低指令缓存命中率。
- 怎么做:
- 将
inline限制在小型、频繁调用的函数上。 - 不要对构造函数、析构函数或虚函数随意使用
inline。 - 模板实例化与
inline无关。inline函数通常放在头文件。
- 将
条款31:将文件间的编译依存关系降至最低
- 是什么: 减少头文件间的依赖,从而加速编译,并使得类的修改影响范围最小。
- 为什么: C++的编译模型是“分离编译,但头文件依赖严重”,一个头文件改变,所有包含它的文件都要重新编译。
- 怎么做:
- “声明的依存性”替换“定义的依存性”: 使用前向声明(forward declaration)而非包含头文件。
- 接口与实现分离:
- Handle Classes: 使用指针指向一个实现类(Pimpl Idiom)。
- Interface Classes: 定义抽象基类接口,客户通过指针操作。
第六部分:继承与面向对象设计
条款32:确定你的public继承塑模出is-a关系
- 是什么: 公有继承意味着“is-a”关系。派生类对象必须适用于基类对象能出现的任何地方(Liskov替换原则)。
- 为什么: 这是面向对象设计的基石。如果违反,客户代码将无法正确工作。
- 怎么做: 在设计继承体系时,问自己:“是否所有派生类对象都是基类对象?” 例如,
Penguinis-aBird,但Penguin不能fly,所以Bird类不应有默认的fly虚函数。
条款33:避免遮掩继承而来的名称
- 是什么: 派生类作用域中的名称会遮掩基类作用域中的同名名称(无论参数是否相同)。
- 为什么: C++的名称查找规则(由内而外,找到即止)。
- 怎么做:
- 使用
using声明式将基类名称引入派生类作用域:using Base::mf1; - 或者使用转发函数(forwarding function),特别是私有继承时。
- 使用
条款34:区分接口继承和实现继承
- 是什么: 公有继承下,派生类继承的成员函数有不同的继承方式。
- 为什么: 不同的设计意图需要用不同的语法来表达。
- 怎么做:
- 只继承接口: 声明为
pure virtual函数。 - 继承接口和默认实现: 声明为
virtual函数,并提供默认实现。(注意:默认实现可能导致意外,可将其分离为独立的protected函数)。 - 继承接口和强制实现: 声明为
non-virtual函数。
- 只继承接口: 声明为
条款35:考虑virtual函数以外的其他选择
- 是什么: 使用非虚接口(NVI)、函数指针、
std::function、策略模式等替代虚函数。 - 为什么: 提供更大的灵活性、解耦和编译时绑定带来的性能优势。
- 怎么做:
- NVI Idiom: 用
public non-virtual函数调用private virtual函数。 - 函数指针/
std::function成员变量: 将功能委托给外部可调用对象。 - 策略模式: 将算法封装在独立的策略类中。
- NVI Idiom: 用
条款36:绝不重新定义继承而来的non-virtual函数
- 是什么: 不要在派生类中重写(override)基类的非虚函数。
- 为什么: 非虚函数是静态绑定的。通过基类指针调用的是基类版本,通过派生类指针调用的是派生类版本,这违反了公有继承的“is-a”关系,导致行为不一致。
- 怎么做: 如果函数需要在派生类中有不同行为,就把它声明为
virtual。
条款37:绝不重新定义继承而来的缺省参数值
- 是什么: 虚函数可以重写,但不要改变它的默认参数。
- 为什么: 虚函数是动态绑定,但默认参数是静态绑定。通过基类指针调用派生类虚函数时,使用的是基类定义的默认参数。
- 怎么做: 遵守约定,绝不重新定义默认参数。如果需要,考虑使用NVI手法,在非虚函数中指定默认参数。
条款38:通过复合塑模出has-a或“根据某物实现出”
- 是什么: 复合(Composition)是类型之间的一种关系,一个对象包含另一个对象。
- 为什么: 有两种含义:
- has-a: 应用域对象,如
Personhas-aAddress。 - is-implemented-in-terms-of: 实现域,如
std::set根据std::vector实现(虽然不好,但举例)。
- has-a: 应用域对象,如
- 怎么做: 在应用域,使用复合表示“有一个”的关系。在实现域,当需要利用已有类的功能来实现新类时,使用复合而非公有继承。
条款39:明智而审慎地使用私有继承
- 是什么: 私有继承意味着“根据某物实现出”,基类的
public和protected成员在派生类中变成private。 - 为什么: 私有继承不是“is-a”关系。它通常比复合级别低。
- 怎么做:
- 尽可能使用复合。
- 使用私有继承当:
- 需要重写基类的虚函数。
- 需要访问基类的
protected成员。 - 需要极致的空间优化(EBO,空基类优化)。
条款40:明智而审慎地使用多重继承
- 是什么: 一个类从多个基类继承。
- 为什么: MI可能带来复杂性,尤其是“菱形继承”导致的命名冲突和数据冗余。
- 怎么做:
- 对接口类的多重继承通常是安全的。
- 对实现类的多重继承要小心。
- 使用虚继承解决菱形继承问题,但要明白虚继承有大小和速度成本。
- 复杂MI下,让接口是
public虚继承,实现是private非虚继承。
第七部分:模板与泛型编程
条款41:了解隐式接口和编译期多态
- 是什么:
- 隐式接口: 模板中,类型
T需要支持的操作集合(由模板中的使用方式决定),而非一个显式的基类。 - 编译期多态: 通过模板实例化和函数重载在编译期决定调用哪个函数。
- 隐式接口: 模板中,类型
- 为什么: 模板泛型编程与面向对象编程的核心区别。
- 怎么做: 编写模板时,思考的是“对类型T有什么样的约束(概念)”,而非“T继承自哪个基类”。
条款42:了解typename的双重意义
- 是什么:
typename关键字有两个用途:- 声明模板类型参数(可和
class互换)。 - 标识一个嵌套从属类型名称(而非非类型成员)。
- 声明模板类型参数(可和
- 为什么: 编译器在解析模板时,默认假设从属名称(依赖于模板参数的名称)不是类型。必须用
typename明确告诉编译器它是类型。 - 怎么做: 在模板中,对于嵌套从属类型名称(如
T::const_iterator),前面必须加typename。但不能在基类列表和成员初始化列表中用它。
条款43:学习处理模板化基类内的名称
- 是什么: 派生类模板无法直接调用基类模板的成员函数。
- 为什么: 编译器在解析派生类模板时,不知道基类模板会被实例化成什么(因为模板参数可能特化),所以不会去基类模板中查找名称。
- 怎么做: 三种方式让编译器“看到”基类模板的名称:
- 使用
this->:this->baseFunc(); - 使用
using声明:using Base<T>::baseFunc; - 显式调用:
Base<T>::baseFunc();(但若该函数是虚函数,则会关闭虚绑定)。
- 使用
条款44:将与参数无关的代码抽离templates
- 是什么: 模板可能会造成代码膨胀(因为每个实例化都会生成一份代码)。
- 为什么: 避免目标代码过大。
- 怎么做:
- 因非类型模板参数造成的膨胀: 使用函数参数或类成员变量替换模板参数。
- 因类型参数造成的膨胀: 让具有相同二进制表示的实例化类型(如
int和long)共享代码。例如,让vector<T*>调用void*的底层实现。
条款45:运用成员函数模板接受所有兼容类型
- 是什么: 为智能指针类编写成员函数模板,以实现跨类型的拷贝和赋值。
- 为什么: 即使
Dervied*可以隐式转换为Base*,shared_ptr<Derived>和shared_ptr<Base>也是完全不同的类,没有继承关系。 - 怎么做: 在智能指针类中定义泛化拷贝构造函数和赋值操作符。同时,也需要提供正常的拷贝构造函数,否则编译器会自动生成。
条款46:需要类型转换时请为模板定义非成员函数
- 是什么: 模板实参推导不会考虑通过构造函数进行的隐式类型转换。
- 为什么: 条款24在模板中不适用。
operator*作为成员函数时,rhs不能隐式转换。 - 怎么做: 将操作符声明为友元函数,并定义在类内部(利用模板类内友元函数特化的特性,它们会在类实例化时被具体化)。
条款47:请使用traits classes表现类型信息
- 是什么: Traits技术,在编译期获取类型的相关信息。
- 为什么: 为了让泛型代码能根据类型的不同而选择不同的算法(如
std::advance对随机访问迭代器用i += d,对双向迭代器用++i/--i)。 - 怎么做:
- 建立一组traits类(如
iterator_traits),通过特化来包含类型信息(如iterator_category)。 - 提供一个泛型模板和一系列特化版本。
- 在算法中使用重载或
if constexpr(C++17)根据traits信息分派。
- 建立一组traits类(如
条款48:认识template元编程
- 是什么: TMP是在C++编译期执行的编程,使用模板、特化和递归。
- 为什么:
- 将工作从运行时转移到编译期。
- 实现自定义的编译期条件判断和循环。
- 生成基于策略选择的代码。
- 怎么做: TMP是函数式编程,主要工具是模板特化和递归。例如,通过递归模板实例化实现循环,通过特化实现条件判断。常用于生成高效代码、进行编译期计算等。
第八部分:定制new和delete
条款49:了解new-handler的行为
- 是什么:
new-handler是当operator new无法满足内存分配请求时调用的函数。 - 为什么: 通过设置
new-handler,你可以在内存分配失败时采取一些措施(如释放部分内存、抛出bad_alloc异常等)。 - 怎么做:
- 使用
std::set_new_handler设置全局new-handler。 - 为类提供专属的
new-handler:重写set_new_handler和operator new,在分配时使用类的handler,分配后恢复全局handler。
- 使用
条款50:了解new和delete的合理替换时机
- 是什么: 替换编译器提供的全局
operator new和operator delete。 - 为什么:
- 检测内存错误(内存泄漏、越界)。
- 提升性能(特定大小的对象分配池)。
- 收集使用统计信息。
- 实现特殊对齐要求。
- 怎么做: 确保你提供的版本满足对齐要求,并提供
nothrow版本。注意替换影响整个程序。
条款51:编写new和delete时需固守常规
- 是什么: 自定义
operator new和operator delete需要遵守一些规则。 - 怎么做:
operator new:- 循环尝试分配,失败时调用
new-handler。 - 处理0字节请求。
- 避免遮掩全局的
nothrow new。
- 循环尝试分配,失败时调用
operator delete:- 处理
nullptr。 - 与
operator new匹配。
- 处理
条款52:写了placement new也要写placement delete
- 是什么: “placement new”是除了
void* size_t外还接受其他参数的operator new。 - 为什么: 如果placement new的构造函数抛出异常,编译器会自动调用对应签名的placement delete来释放内存。如果没有提供,会导致内存泄漏。
- 怎么做: 成对提供placement new和placement delete。注意这会遮掩全局的普通版本,可能需要用
using引入它们。
第九部分:杂项讨论
条款53:不要轻忽编译器的警告
- 是什么: 认真对待编译器给出的警告信息。
- 为什么: 警告往往预示着潜在的错误或未定义行为。
- 怎么做: 争取在最高警告级别下编译无警告。理解警告的含义,并修复它,不要简单地忽略或压制。
条款54:让自己熟悉包括TR1在内的标准程序库
- 是什么: 熟悉C++标准库及其扩展(TR1是C++11标准库的前身)。
- 为什么: 避免重复造轮子,提高开发效率和代码质量。
- 怎么做: 学习并掌握STL容器、算法、迭代器、智能指针、
std::function/bind、哈希容器、正则表达式等。
条款55:让自己熟悉Boost
- 是什么: Boost是一个高质量的、可移植的、源码开放的C++库集合。
- 为什么: Boost是C++标准库的试验场,很多Boost库最终进入了C++标准(如智能指针、线程、正则表达式等)。它提供了大量经过实战检验的组件。
- 怎么做: 浏览Boost网站,了解其提供的库。在项目中选择合适的Boost库来使用。
这个总结涵盖了《Effective C++》的核心思想,每一条都是C++程序员在实践中需要时刻铭记的准则。掌握它们,你将能写出更安全、更高效、更易维护的C++代码。
