Effective C++

让自己习惯 C++
条款1:视 C++ 为一个语言联邦
  1. C。说到底 C++ 仍是以 C 为基础。区块(blocks)、语句(statements)、预处理器(preprocessor)、内置数据类型(built-in data types)、数组(arrays)、指针(pointers)等统统来自 C 解法,但当你以 C++ 内的 C 成分工作时,高效编程守则映照出 C 语言的局限:没有模板(templates),没有异常(execptions),没有重载(overloading)…
  2. Object-Oriented C++。这部分也就是 C with Classes 所诉求的:classes(包括构造函数和析构函数),封装(encapsulation)、继承(inheritance)、多态(polymorphism)、virtual 函数(动态绑定)…等等。这一部分是面向对象设计之古典守则在 C++ 上的最直接实施。
  3. Template C++。这是 C++ 的泛型编程(generic programming)部分,也是大多数程序员经验最少的部分。Template 相关考虑与设计已经弥漫整个 C++,良好编程守则中“惟 template 适用”的特殊条款并不罕见。实际上由于 templates 威力强大,它们带来崭新的编程范型(programming paradigm),也就是所谓的 template metaprogramming(TMP,模板元编程)。
  4. STL。STL 是一个 template 程序库,看名称也知道,但它是一个非常特殊的一个。它对容器(containers)、迭代器(iterators)、算法(algorithms)以及函数对象(function objects)的规约有极佳的紧密配合和协调,然而 templates 及程序库也可以其他想法建置出来。STL 有自己特殊的办事方式,当你伙同 STL 一起工作,你必须遵守它的规约。
  • C++ 高效编译守则视情况而变化,取决于你使用的 C++ 的哪一部分、
条款2:尽量以 const、enum、inline 替换 #define
  1. 这个条款或许改为“宁可以编译器替换预处理器”比较好,因为或许 #define 不被视为语言的一部分。
  • 对于单纯常量,最好以 const 对象或 enums 替换 #defines。
  • 对于形似函数的宏(macros),最好改用 inline 函数替换 #defines。
条款3:尽可能使用 const
  1. 关键词 const 多才多艺。你可以用它在 classes 外部修饰 global 或 namespace 作用域中常量,或修饰文件、函数、或区块作用域(block scope)中被声明为 static 的对象。你也可以用它修饰 classes 内部的 static 和 non-static 成员变量。面对指针,你也可以指出指针自身、指针所指物,或两者都(或都不)是 const。
  2. const 语法虽然变化多端,但并不高深莫测。如果关键字 const 出现星号左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量;如果出现在星号两边,表示被指物和指针两者都是常量。
  3. const 最具威力的用法是面对函数声明时的应用。在一个函数声明式内,const 可以和函数返回值、各参数、函数自身产生关联。令函数返回一个常量值,往往可以降低因客户错误而造成的意外,而又不至于放弃安全性和高效性。
  4. 将 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:确定对象使用前已先被初始化
  1. 构造函数的一个较佳的写法是,使用所谓的 member initialization list(成员初始列)替换赋值动作。
  2. 如果成员变量是 const 或 references,它们就一定需要初值,不能被赋值。为避免需要记住成员变量何时必须在成员初值列中初始化,何时不需要,最简单的做法就是:总是使用成员初始值。这样做有时候绝对必要,且又往往比赋值更高效。
  3. 许多 classes 拥有多个构造函数,每个构造函数有自己的成员初值列。如果这种 classes 存在许多成员变量和/或 base classes,多份成员初值列的存在就会导致不受欢迎的重复(在初值列内)和无聊的工作(对程序员而言)。这种情况下可以合理地在初值列中遗漏那些“赋值表现像初始化一样好”的成员变量,改用它们的赋值操作,并将那些赋值操作移往某个函数(通常是 private),供所有构造函数调用。这种做法在“成员变量的初值系由文件或者数据库读入”时特别有用。然而,比起经由赋值操作完成的“伪初始化”(pseudo-initialization),通过成员初值列(member initialization list)完成的“真正初始化”通常更加可取。
  4. 所谓 static 对象,其寿命从被构造出来直到程序结束为止,因此 stack 和 heap-based 对象都被排除。这种对象包括 global 对象、定义于 namespace 作用域内的对象、在 classes 内、在函数内、以及在 file 作用域内被声明为 static 的对象。函数内的 static 对象被称为 local static 对象,其他 static 对象成为 non-local static 对象。程序结束时 static 对象会被自动销毁,也就是它们的析构函数会在 main() 结束时被自动调用。
  5. 将每个 non-local static 对象搬到自己的专属函数内(该对象在此函数内被声明为 static)。这些函数返回一个 reference 指向它所含的对象。然后用户调用这些函数,而不直接指涉这些对象。换句话说,non-local static 对象被 local static 对象替换了。
  • 为内置型对象进行手工初始化,因为 C++ 不保证初始化它们。
  • 构造函数最好使用成员初始列(menber initialization list),而不要在构造函数本体内使用赋值操作(assignment)。初值列列出的成员变量,其排序次序应该和它们在 class 中的声明次序相同。
  • 为免除“跨编译单元之初始化次序”问题,请以 local static 对象替换 non-local static 对象。
构造/析构/赋值运算
条款5:了解 C++ 默认编写并调用哪些函数
  1. 如果自己没声明,编译器就会为它声明一个 copy 构造函数、一个 copy assignment 操作符和一个析构函数。此外如果你没有声明任何构造函数,编译器也会为你声明一个 default 构造函数。所有的这些函数都是 public 且 inline。
  2. 如果你打算在一个“内含 reference 成员”的 class 内支持赋值操作(assignment),你必须自己定义 copy assignment 操作符。面对“内含 const 成员”的 classes,编译器和反应也一样。更改 const 成员是不合法的,所以编译器不知道如何在它自己生成的赋值函数内面对它们。最后还有一种情况:如果某个 八涩、 classes 将 copy assignment 操作符声明为 private,编译器将拒绝为其 derived classes 生成一个 copy assignment 操作符。
  • 编译器可以暗自为 class 创建 default 构造函数、copy 构造函数、copy assignment 操作符,以及析构函数。
条款6:若不想使用编译器自动生成的函数,就该明确拒绝
  1. 所有编译器产出的函数都是 public。为阻止这些函数被创建出来,你得自行声明它们,但这里并没有什么需求使你必须将它们声明为 public。因此你可以将 copy 构造函数或 copy assignment 函数声明为 private。
  2. 一般而言这个做法并不绝对安全,因为 member 函数和 friend 函数还是可以调用你的 private 函数。除非你够聪明,不去定义它们,那么如果某些人不慎调用任何一个,会获得一个连接错误。
  • 为驳回编译器自动(暗自)提供给的机制,可将响应的成员函数声明为 private 并且不予实现。使用像 Uncopyable 这样的 base class 也是一种做法。
条款7:为多态基类声明 virtual 析构函数
  1. 预实现出 virtual 函数,对象必须携带某些信息,主要用来在运行期决定哪一个 virtual 函数该被调用。这份信息通常是由一个所谓 vptr(virtual table pointer)指针指出。vptr 指向一个函数指针构成的数组,成为 vtbl(virtual table);每一个带有 virtual 函数的 class 都有一个相应的 vtbl。当对象的调用某一 virtual 函数,实际被调用的函数取决于该对象的 vptr 所指的那个 vtbl --编译器在其中寻找适当的函数指针。因此添加一个 vptr 会增加其对象大小达 50%~100%!因此,无端的将所有 classes 的析构函数声明为 virtual,就像从未声明它们为 virtual 一样,都是错误的。
  2. 析构函数的运作方式是,最深层派生(most derived)的那个 class 其析构函数最先被调用,然后是其每一个 base class 的析构函数被调用。这个规则只适用于 polymorphic (带多态性质的)base classes 身上。这种 base classes 的设计目的是为了用来“通过 base class 接口处理 derived class 对象”。
  3. 并非所有的 base classes 的设计目的都是为了多态用途。例如标准 string 和 STL 容器都不被设计为 base classes 使用,更别提多态用途。因此它们不需要 virtual 析构函数。
  • polymorphic (带多态性质的)base classes 应该声明一个 virtual 析构函数。如果 class 代替任何 virtual 函数,它就应该拥有一个 virtual 析构函数。
  • Classes 的设计目的如果不是作为 base classes 使用,或不是为了具备多态性(polymorphically),就不该声明 virtual 析构函数。
条款8:别让异常逃离析构函数
  • 析构函数绝不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
  • 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么 class 应该提供一个普通函数(而非在析构函数中)执行操作。
条款9:绝不在构造和析构过程中调用 virtual 函数
  1. base class 构造期间 virtual 函数绝不会下降至 derived classes 阶层。取而代之的是,对象的作为就像隶属 base 类型一样。非正式的说法或许比较传神:在 base class 构造期间,virtual 函数不是 virtual 函数。由于 base class 构造函数的执行更早于 derived class 构造函数,当 base class 构造函数执行时 derived class 的成员变量尚未初始化。如果此期间调用 virtual 函数下降至 derived classes 阶层,要知道 derived class 的函数几乎必然取用 local 成员变量,而那些成员变量尚未初始化。
  2. 在 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
  1. 这只是个协议,并无强制性。如果不遵循它,代码一样可以编译通过。然而这份协议被所有内置类型和标准程序库提供的类型如 string,vector,complex,tr1::shared_ptr 或即将提供的类型共同遵守。
  • 令赋值(assignment)操作符返回一个 reference to *this。
条款11:在 operator= 中实现“自我赋值”
  • 确保当对象自我赋值时 operator= 有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及 copy-and-swap。
  • 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
条款12:复制对象时勿忘其每一部分
  1. 设计良好之面向对象系统(OO-systems)会将对象的内部封装起来,只留两个函数负责对象拷贝(复制),那便是带着适切名称的 copy 构造函数和 copy assignment 操作符,我称它们为 copying 函数。
  2. 当你编写一个 copying 函数,请确保(1)复制所有 local 成员变量,(2)调用所以 base classes 内适当的 copying 函数。
  • Copying 函数应该确保复制“对象内所有成员变量”及“所有 base class 成分”。
  • 不要尝试以某个 copying 函数实现另一个 copying 函数。应该将公共机能放进第三个函数中,并由两个 coping 函数共同调用。
资源管理
条款13:以对象管理资源
  1. 获得资源后立即放进管理对象(managing object)内。
  2. 管理对象(managing object)运用析构函数确保资源被释放。
  • 为防止资源泄漏,请使用 RAII 对象,它们在构造函数中获得资源并在析构函数中释放资源。
  • 两个常被使用的 RAII classes 分别是 tr1::shared_ptr 和 auto_ptr 。前者通常是较佳选择,因为其 copy 行为比较直观。若选择 auto_ptr,复制动作会使它(被复制物)指向 null。
条款14:在资源管理类中小心 coping 行为
条款15:在资源管理类中提供对原始资源的访问
条款16:成对使用 new 和 delete 时要采取相同形式
条款17:以独立语句将 newed 对象置入智能指针
设计和声明
条款18:让接口容易被正确使用,不容易被误用
  1. 好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质。
  2. “促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容。
  3. “防止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
  4. 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 通常意味真正传递的是指针。

  1. 尽量以 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
  1. 切记将成员变量声明为 private 。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供 class 作者以充分的实现弹性。
  2. 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 函数
  1. 当 std::swap 对你的类型效率不高时,提供一个 swap 成员函数,并确定这个函数不抛出异常。
  2. 如果你提供一个 member swap,也该提供一个 non-member swap 用来调用前者。对于 classes(而非 templates),也请特化 std::swap。
  3. 调用 swap 时应针对 std::swap 使用 using 声明式,然后调用 swap 并且不带任何“命名空间资格修饰”。
  4. 为“用户定义类型”进行 std templates 全特化是好的,但千万不要尝试在 std 内加入某些对 std 而言全新的东西。
实现
条款26:尽可能延后变量定义式的出现时间

尽可能延后变量定义式的出现。这样做可以增加程序的清晰度并改善程序效率。

条款27:尽量少做转型动作
  1. 如果可以,尽量避免转型,特别是在注重效率的代码中避免 dynamic_casts。如果有个设计需要转型动作,试着发展无需转型的替代方案。
  2. 如果转型是必要的,试着将它隐藏于某个函数的背后。客户随后可以调用该函数,而不需要将转型放进他们自己的代码中。
  3. 宁可使用 C++ Style (新式)转型,不要使用旧时转型。前者很容易辨识出来,而且也比较有着分门别类的执掌。
条款28:避免返回 handles 指向对象内部成分

避免返回 handles(包括 references、指针、迭代器)指向对象内部。遵守这条可增加封装性,帮助 const 成员函数的行为像个 const,并将发生“虚吊号码牌”的可能性降至最低。

条款29:为“异常安全”而努力是值得的
  1. 异常安全函数(Exception-safe functions)即使发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。
  2. “强烈保证”往往能够以 copy-and-swap 实现出来,但是“强烈保证”并非对所有函数都可实现或具备现实意义。
  3. 函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。
条款30:透彻了解 inlining 的里里外外
  1. 将大多数 inlining 限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
  2. 不要只因为 function templates 出现在头文件,就将他们声明为 inline 。
条款31:将文件间的编译依存关系降至最低
  1. 支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段式 Handle classes 和 Interface classes。
  2. 程序库头文件应该以“完全且具有声明式”的形式存在。这种做法不论是否涉及 templates 都适用。
继承和面向对象设计
条款32:确定你的 public 继承塑模出 is-a 关系

“public 继承”意味 is-a。适用于 base classes 身上的每一件事情一定也适用于 derived classes 身上,因为每一个 derived class 对象也都是一个 base class 对象。

条款33:避免遮掩继承而来的名称
  1. derived classes 内的名称会遮掩 base classes 内的名称。在 public 继承下从来没有人希望如此。
  2. 为了让被遮掩的名称再见天日,可适用 using 声明式或转交函数(forwarding functions)。
条款34:区分接口继承和实现继承
  1. 接口继承和实现继承不同。在 public 继承之下,derived classes 总是继承 base class 的接口。
  2. pure virtual 函数只具体指定接口继承。
  3. 简朴的(非纯)impure virtual 函数具体指定接口继承及缺省实现继承。
  4. non-virtual 函数具体指定接口继承以及强制性实现继承。
条款35:考虑 virtual 函数以外的其他选择
  1. virtual 函数的替代方案包括 NVI 手法及 Strategy 设计模式的多种形式。NVI 手法自身是一个特殊形式的 Template Method 设计模式。
  2. 将机能从成员函数移到 class 外部函数,带来的一个缺点是,非成员函数无法访问 class 的 non-public 成员。
  3. tr1::function 对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式兼容”的所有可调用五。
条款36:绝不重新定义继承而来的 non-virtual 函数
条款37:绝不要重新定义继承而来的缺省参数值

绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而 virtual 函数——你唯一应该复写的东西——却是动态绑定。

条款38:通过复合塑模树 has-a 或“根据某物实现出”
  1. 复合的意义和 public 继承完全不同。
  2. 在应用域,符合意味 has-a。在实际域,符合意味 is-implemented-in-terms-of。
条款39:明智而审慎的使用 private 继承
条款40:明智而审慎的使用多重继承
模板和泛型编程
条款41:了解隐式接口和编译期多态
  1. classes 和 templates 都支持接口和多态。
  2. 对 classes 而言接口是显示的,以函数签名为中心。多态则是通过 virtual 函数发生于运行期。
  3. 对 template 参数而言,接口时隐式的,基于有效表达式。多态则是通过 template 具现化和函数重载解析发生于编译器。
条款42:了解 typename 的双重意义
  1. 声明 templaate 参数时,前缀关键字 class 和 typename 可互换。
  2. 请使用关键字 typename 标识嵌套从属类型名称;但不得在 base class lists(基类列)或 member initialization list (成员初值列)内以它作为 base class 修饰符。
条款43:学习处理模板化基类内的名称
条款44:将与参数无关的代码抽离 templates
条款45:运用成员函数模板接受所有兼容类型
条款46:需要类型转换时请为模板定义非成员函数
条款47:请使用 traits class 表现类型信息
条款48:认识 template 元编程
定义 new 和 delete
条款49:了解 new-handler 的行为
条款50:了解new和delete的合理替换时机
条款51:编写 new 和 delete 时需固守常规
条款52:写了 placement new 也要写 placement delete
杂项讨论
条款53:不要轻忽编译器的警告
条款54:让自己熟悉包括 TR1 在内的标准程序库
条款55:让自己熟悉 Boost
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值