让自己习惯 C++
条款1:视 C++ 为一个语言联邦
- C。说到底 C++ 仍是以 C 为基础。区块(blocks)、语句(statements)、预处理器(preprocessor)、内置数据类型(built-in data types)、数组(arrays)、指针(pointers)等统统来自 C 解法,但当你以 C++ 内的 C 成分工作时,高效编程守则映照出 C 语言的局限:没有模板(templates),没有异常(execptions),没有重载(overloading)…
- Object-Oriented C++。这部分也就是 C with Classes 所诉求的:classes(包括构造函数和析构函数),封装(encapsulation)、继承(inheritance)、多态(polymorphism)、virtual 函数(动态绑定)…等等。这一部分是面向对象设计之古典守则在 C++ 上的最直接实施。
- Template C++。这是 C++ 的泛型编程(generic programming)部分,也是大多数程序员经验最少的部分。Template 相关考虑与设计已经弥漫整个 C++,良好编程守则中“惟 template 适用”的特殊条款并不罕见。实际上由于 templates 威力强大,它们带来崭新的编程范型(programming paradigm),也就是所谓的 template metaprogramming(TMP,模板元编程)。
- STL。STL 是一个 template 程序库,看名称也知道,但它是一个非常特殊的一个。它对容器(containers)、迭代器(iterators)、算法(algorithms)以及函数对象(function objects)的规约有极佳的紧密配合和协调,然而 templates 及程序库也可以其他想法建置出来。STL 有自己特殊的办事方式,当你伙同 STL 一起工作,你必须遵守它的规约。
- C++ 高效编译守则视情况而变化,取决于你使用的 C++ 的哪一部分、
条款2:尽量以 const、enum、inline 替换 #define
- 这个条款或许改为“宁可以编译器替换预处理器”比较好,因为或许 #define 不被视为语言的一部分。
- 对于单纯常量,最好以 const 对象或 enums 替换 #defines。
- 对于形似函数的宏(macros),最好改用 inline 函数替换 #defines。
条款3:尽可能使用 const
- 关键词 const 多才多艺。你可以用它在 classes 外部修饰 global 或 namespace 作用域中常量,或修饰文件、函数、或区块作用域(block scope)中被声明为 static 的对象。你也可以用它修饰 classes 内部的 static 和 non-static 成员变量。面对指针,你也可以指出指针自身、指针所指物,或两者都(或都不)是 const。
- const 语法虽然变化多端,但并不高深莫测。如果关键字 const 出现星号左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量;如果出现在星号两边,表示被指物和指针两者都是常量。
- const 最具威力的用法是面对函数声明时的应用。在一个函数声明式内,const 可以和函数返回值、各参数、函数自身产生关联。令函数返回一个常量值,往往可以降低因客户错误而造成的意外,而又不至于放弃安全性和高效性。
- 将 const 实施于成员函数的目的,是为了确认该成员函数可作用于 const 对象身上。这一类成员函数之所以重要,基于两个理由。第一,它们使 class 接口比较容易被理解。这是因为,得知哪个函数可以改动对象内容而哪个函数不行,很是重要。第二,它们使“操作 const 对象”成为可能。这对编写高效代码是个关键,因为,改善 C++ 程序效率的一个根本办法是以 pass by reference-to-const 方式传递对象,而此技术可行的前提是,我们有 const 成员函数可通来处理取得(并经修饰而成)的 cosnt 对象。
- 将某些东西声明为 const 可以帮助编译器侦测出错误方法。const 可被施加于在任何作用域的对象、函数参数、函数返回类型、成员函数本体。
- 编译器强制实施 bitwise constness,但你编写程序时应该使用“概念上的常量性”(conceptual constness)。
- 当 const 和 non-const 成员函数有着实质等价的实现时,令 non-const 版本调用 const 版本可避免代码重复。
条款4:确定对象使用前已先被初始化
- 构造函数的一个较佳的写法是,使用所谓的 member initialization list(成员初始列)替换赋值动作。
- 如果成员变量是 const 或 references,它们就一定需要初值,不能被赋值。为避免需要记住成员变量何时必须在成员初值列中初始化,何时不需要,最简单的做法就是:总是使用成员初始值。这样做有时候绝对必要,且又往往比赋值更高效。
- 许多 classes 拥有多个构造函数,每个构造函数有自己的成员初值列。如果这种 classes 存在许多成员变量和/或 base classes,多份成员初值列的存在就会导致不受欢迎的重复(在初值列内)和无聊的工作(对程序员而言)。这种情况下可以合理地在初值列中遗漏那些“赋值表现像初始化一样好”的成员变量,改用它们的赋值操作,并将那些赋值操作移往某个函数(通常是 private),供所有构造函数调用。这种做法在“成员变量的初值系由文件或者数据库读入”时特别有用。然而,比起经由赋值操作完成的“伪初始化”(pseudo-initialization),通过成员初值列(member initialization list)完成的“真正初始化”通常更加可取。
- 所谓 static 对象,其寿命从被构造出来直到程序结束为止,因此 stack 和 heap-based 对象都被排除。这种对象包括 global 对象、定义于 namespace 作用域内的对象、在 classes 内、在函数内、以及在 file 作用域内被声明为 static 的对象。函数内的 static 对象被称为 local static 对象,其他 static 对象成为 non-local static 对象。程序结束时 static 对象会被自动销毁,也就是它们的析构函数会在 main() 结束时被自动调用。
- 将每个 non-local static 对象搬到自己的专属函数内(该对象在此函数内被声明为 static)。这些函数返回一个 reference 指向它所含的对象。然后用户调用这些函数,而不直接指涉这些对象。换句话说,non-local static 对象被 local static 对象替换了。
- 为内置型对象进行手工初始化,因为 C++ 不保证初始化它们。
- 构造函数最好使用成员初始列(menber initialization list),而不要在构造函数本体内使用赋值操作(assignment)。初值列列出的成员变量,其排序次序应该和它们在 class 中的声明次序相同。
- 为免除“跨编译单元之初始化次序”问题,请以 local static 对象替换 non-local static 对象。
构造/析构/赋值运算
条款5:了解 C++ 默认编写并调用哪些函数
- 如果自己没声明,编译器就会为它声明一个 copy 构造函数、一个 copy assignment 操作符和一个析构函数。此外如果你没有声明任何构造函数,编译器也会为你声明一个 default 构造函数。所有的这些函数都是 public 且 inline。
- 如果你打算在一个“内含 reference 成员”的 class 内支持赋值操作(assignment),你必须自己定义 copy assignment 操作符。面对“内含 const 成员”的 classes,编译器和反应也一样。更改 const 成员是不合法的,所以编译器不知道如何在它自己生成的赋值函数内面对它们。最后还有一种情况:如果某个 八涩、 classes 将 copy assignment 操作符声明为 private,编译器将拒绝为其 derived classes 生成一个 copy assignment 操作符。
- 编译器可以暗自为 class 创建 default 构造函数、copy 构造函数、copy assignment 操作符,以及析构函数。
条款6:若不想使用编译器自动生成的函数,就该明确拒绝
- 所有编译器产出的函数都是 public。为阻止这些函数被创建出来,你得自行声明它们,但这里并没有什么需求使你必须将它们声明为 public。因此你可以将 copy 构造函数或 copy assignment 函数声明为 private。
- 一般而言这个做法并不绝对安全,因为 member 函数和 friend 函数还是可以调用你的 private 函数。除非你够聪明,不去定义它们,那么如果某些人不慎调用任何一个,会获得一个连接错误。
- 为驳回编译器自动(暗自)提供给的机制,可将响应的成员函数声明为 private 并且不予实现。使用像 Uncopyable 这样的 base class 也是一种做法。
条款7:为多态基类声明 virtual 析构函数
- 预实现出 virtual 函数,对象必须携带某些信息,主要用来在运行期决定哪一个 virtual 函数该被调用。这份信息通常是由一个所谓 vptr(virtual table pointer)指针指出。vptr 指向一个函数指针构成的数组,成为 vtbl(virtual table);每一个带有 virtual 函数的 class 都有一个相应的 vtbl。当对象的调用某一 virtual 函数,实际被调用的函数取决于该对象的 vptr 所指的那个 vtbl --编译器在其中寻找适当的函数指针。因此添加一个 vptr 会增加其对象大小达 50%~100%!因此,无端的将所有 classes 的析构函数声明为 virtual,就像从未声明它们为 virtual 一样,都是错误的。
- 析构函数的运作方式是,最深层派生(most derived)的那个 class 其析构函数最先被调用,然后是其每一个 base class 的析构函数被调用。这个规则只适用于 polymorphic (带多态性质的)base classes 身上。这种 base classes 的设计目的是为了用来“通过 base class 接口处理 derived class 对象”。
- 并非所有的 base classes 的设计目的都是为了多态用途。例如标准 string 和 STL 容器都不被设计为 base classes 使用,更别提多态用途。因此它们不需要 virtual 析构函数。
- polymorphic (带多态性质的)base classes 应该声明一个 virtual 析构函数。如果 class 代替任何 virtual 函数,它就应该拥有一个 virtual 析构函数。
- Classes 的设计目的如果不是作为 base classes 使用,或不是为了具备多态性(polymorphically),就不该声明 virtual 析构函数。
条款8:别让异常逃离析构函数
- 析构函数绝不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
- 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么 class 应该提供一个普通函数(而非在析构函数中)执行操作。
条款9:绝不在构造和析构过程中调用 virtual 函数
- base class 构造期间 virtual 函数绝不会下降至 derived classes 阶层。取而代之的是,对象的作为就像隶属 base 类型一样。非正式的说法或许比较传神:在 base class 构造期间,virtual 函数不是 virtual 函数。由于 base class 构造函数的执行更早于 derived class 构造函数,当 base class 构造函数执行时 derived class 的成员变量尚未初始化。如果此期间调用 virtual 函数下降至 derived classes 阶层,要知道 derived class 的函数几乎必然取用 local 成员变量,而那些成员变量尚未初始化。
- 在 derived class 对象的 base class 构造期间,对象的类型是 base class 而不是 derived class。不只 virtual 函数会被编译器解析至 (resolve to)base class,若使用运行期类型信息(runtime type information,例如 dynamic_cast 和 typeid),也会把对象视为 base class 类型。
- 在构造和析构期间不要调用 virtual 函数,因为这类调用从不下降至 derived class (比起当前执行构造函数和析构函数的那层)。
条款10:令 operator= 返回一个 reference to *this
- 这只是个协议,并无强制性。如果不遵循它,代码一样可以编译通过。然而这份协议被所有内置类型和标准程序库提供的类型如 string,vector,complex,tr1::shared_ptr 或即将提供的类型共同遵守。
- 令赋值(assignment)操作符返回一个 reference to *this。
条款11:在 operator= 中实现“自我赋值”
- 确保当对象自我赋值时 operator= 有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及 copy-and-swap。
- 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
条款12:复制对象时勿忘其每一部分
- 设计良好之面向对象系统(OO-systems)会将对象的内部封装起来,只留两个函数负责对象拷贝(复制),那便是带着适切名称的 copy 构造函数和 copy assignment 操作符,我称它们为 copying 函数。
- 当你编写一个 copying 函数,请确保(1)复制所有 local 成员变量,(2)调用所以 base classes 内适当的 copying 函数。
- Copying 函数应该确保复制“对象内所有成员变量”及“所有 base class 成分”。
- 不要尝试以某个 copying 函数实现另一个 copying 函数。应该将公共机能放进第三个函数中,并由两个 coping 函数共同调用。
资源管理
条款13:以对象管理资源
- 获得资源后立即放进管理对象(managing object)内。
- 管理对象(managing object)运用析构函数确保资源被释放。
- 为防止资源泄漏,请使用 RAII 对象,它们在构造函数中获得资源并在析构函数中释放资源。
- 两个常被使用的 RAII classes 分别是 tr1::shared_ptr 和 auto_ptr 。前者通常是较佳选择,因为其 copy 行为比较直观。若选择 auto_ptr,复制动作会使它(被复制物)指向 null。
条款14:在资源管理类中小心 coping 行为
条款15:在资源管理类中提供对原始资源的访问
条款16:成对使用 new 和 delete 时要采取相同形式
条款17:以独立语句将 newed 对象置入智能指针
设计和声明
条款18:让接口容易被正确使用,不容易被误用
- 好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质。
- “促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容。
- “防止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
- tr1::shared_ptr 支持定制型删除器(custom deleter)。这可防范 DLL 问题,可被用来自动解除互斥锁(mutexes)等等。
条款19:设计 class 犹如设计 type
条款20:宁以 pass-by-reference-to-const 替换 pass-by-value
by value 方式传递一个 Strudent (包含两个 string 字段)对象,总体成本是“六次构造函数合六次析构函数”。
C++ 编译器的底层,references 往往以指针实现出来,因此 pass by reference 通常意味真正传递的是指针。
- 尽量以 pass-by-reference-to-const 替换 pass-by-value。前者通常比较高效,并可避免切割问题(slicing problem)。
条款21:必须返回对象时,别妄想返回其 reference
绝不要返回 pointer 或 reference 指向一个 local stack 对象,或返回 reference 指向一个 heao-allocated 对象,或返回 pointer 或 reference 指向一个 local static 对象而有可能同时需要多个这样的对象。
条款22:将成员变量声明为 private
- 切记将成员变量声明为 private 。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供 class 作者以充分的实现弹性。
- protected 并不比 public 更具封装性。
条款23:宁以 non-member、non-friend 替换 member 函数
第一,这个论述只适用于 non-member non-friend 函数。friends 函数对 class private 成员的访问权力合 member 函数相同,因此两者对封装的冲击力道也相同。
第二件值得注意的事情是,只因在意封装性而让函数“成为 class 的 non-member”,并不意味它“不可以是另一个 class 的 member”。
条款24:若所有参数皆需要类型转换,请为此采用 non-member 函数
如果你需要为某个函数的所有参数(包括被 this 指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个 non-member。
条款25:考虑写出一个不抛出异常的 swap 函数
- 当 std::swap 对你的类型效率不高时,提供一个 swap 成员函数,并确定这个函数不抛出异常。
- 如果你提供一个 member swap,也该提供一个 non-member swap 用来调用前者。对于 classes(而非 templates),也请特化 std::swap。
- 调用 swap 时应针对 std::swap 使用 using 声明式,然后调用 swap 并且不带任何“命名空间资格修饰”。
- 为“用户定义类型”进行 std templates 全特化是好的,但千万不要尝试在 std 内加入某些对 std 而言全新的东西。
实现
条款26:尽可能延后变量定义式的出现时间
尽可能延后变量定义式的出现。这样做可以增加程序的清晰度并改善程序效率。
条款27:尽量少做转型动作
- 如果可以,尽量避免转型,特别是在注重效率的代码中避免 dynamic_casts。如果有个设计需要转型动作,试着发展无需转型的替代方案。
- 如果转型是必要的,试着将它隐藏于某个函数的背后。客户随后可以调用该函数,而不需要将转型放进他们自己的代码中。
- 宁可使用 C++ Style (新式)转型,不要使用旧时转型。前者很容易辨识出来,而且也比较有着分门别类的执掌。
条款28:避免返回 handles 指向对象内部成分
避免返回 handles(包括 references、指针、迭代器)指向对象内部。遵守这条可增加封装性,帮助 const 成员函数的行为像个 const,并将发生“虚吊号码牌”的可能性降至最低。
条款29:为“异常安全”而努力是值得的
- 异常安全函数(Exception-safe functions)即使发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。
- “强烈保证”往往能够以 copy-and-swap 实现出来,但是“强烈保证”并非对所有函数都可实现或具备现实意义。
- 函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。
条款30:透彻了解 inlining 的里里外外
- 将大多数 inlining 限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
- 不要只因为 function templates 出现在头文件,就将他们声明为 inline 。
条款31:将文件间的编译依存关系降至最低
- 支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段式 Handle classes 和 Interface classes。
- 程序库头文件应该以“完全且具有声明式”的形式存在。这种做法不论是否涉及 templates 都适用。
继承和面向对象设计
条款32:确定你的 public 继承塑模出 is-a 关系
“public 继承”意味 is-a。适用于 base classes 身上的每一件事情一定也适用于 derived classes 身上,因为每一个 derived class 对象也都是一个 base class 对象。
条款33:避免遮掩继承而来的名称
- derived classes 内的名称会遮掩 base classes 内的名称。在 public 继承下从来没有人希望如此。
- 为了让被遮掩的名称再见天日,可适用 using 声明式或转交函数(forwarding functions)。
条款34:区分接口继承和实现继承
- 接口继承和实现继承不同。在 public 继承之下,derived classes 总是继承 base class 的接口。
- pure virtual 函数只具体指定接口继承。
- 简朴的(非纯)impure virtual 函数具体指定接口继承及缺省实现继承。
- non-virtual 函数具体指定接口继承以及强制性实现继承。
条款35:考虑 virtual 函数以外的其他选择
- virtual 函数的替代方案包括 NVI 手法及 Strategy 设计模式的多种形式。NVI 手法自身是一个特殊形式的 Template Method 设计模式。
- 将机能从成员函数移到 class 外部函数,带来的一个缺点是,非成员函数无法访问 class 的 non-public 成员。
- tr1::function 对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式兼容”的所有可调用五。
条款36:绝不重新定义继承而来的 non-virtual 函数
条款37:绝不要重新定义继承而来的缺省参数值
绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而 virtual 函数——你唯一应该复写的东西——却是动态绑定。
条款38:通过复合塑模树 has-a 或“根据某物实现出”
- 复合的意义和 public 继承完全不同。
- 在应用域,符合意味 has-a。在实际域,符合意味 is-implemented-in-terms-of。
条款39:明智而审慎的使用 private 继承
条款40:明智而审慎的使用多重继承
模板和泛型编程
条款41:了解隐式接口和编译期多态
- classes 和 templates 都支持接口和多态。
- 对 classes 而言接口是显示的,以函数签名为中心。多态则是通过 virtual 函数发生于运行期。
- 对 template 参数而言,接口时隐式的,基于有效表达式。多态则是通过 template 具现化和函数重载解析发生于编译器。
条款42:了解 typename 的双重意义
- 声明 templaate 参数时,前缀关键字 class 和 typename 可互换。
- 请使用关键字 typename 标识嵌套从属类型名称;但不得在 base class lists(基类列)或 member initialization list (成员初值列)内以它作为 base class 修饰符。