1 让自己习惯C++
常量指针的定义
由于常量定义式通常被放在头文件中以便被不同的源文件含入,因此有必要将常量指针声明为const(即声明为指针常量)。例如若要在头文件内定义一个常量的 char*-based 字符串,你必须写 const 两次:
const char* const AUTHOR_NAME = "Scott Meyers";
“the enum hack”技术
即在类内部定义枚举量来当整型常量使用。例如:
class Player {
private:
enum { NumTurns = 5 };
int scores[NumTurns]; //NumTurns作为整数常量使用
}
2 构造,析构,赋值运算符
条款05 了解C++默默编写并调用哪些函数
编译器在需要时可以暗自为 class 创建 default 构造函数、copy 构造函数、operator=操作符函数,以及析构函数。
条款06 若不想使用编译器自动生成的函数,就该明确拒绝
class Uncopyable { protected: Uncopyable(void) { } //允许derived对象构造和析构 ~Uncopyable(void) { } //该类不是为了具备多态性,所以析构函数不该声明为virtual private: Uncopyable(const Uncopyable&); //但阻止copying Uncopyable& operator=(const Uncopyable&); };
现在,为阻止对象被拷贝,我们唯一需要做的就是继承Uncopyable。
条款07 为多态基类声明virtual析构函数
C++明白指出,当 derived class 对象经由一个 base class 指针被删除,而该 base class 带着一个 non-virtual 析构函数,其结果未有定义 —— 实际执行时通常发生的是对象的 derived 成分没被销毁。
所以 polymorphic(带多态性质的)base classes 应该声明一个 virtual 析构函数。如果 class 带有任何 virtual 函数,它就应该拥有一个 virtual 析构函数。但 classes 的设计目的如果不是作为 base class 使用(如 std::string等),或不是为了具备多态性(如 Uncopyable类),就不该声明 virtual 析构函数。
条款08 别让异常逃离析构函数
C++中,析构函数抛出异常会导致未定义的行为。
所以析构函数应该捕获任何异常,然后吞下它们(不传播)或结束程序。
条款09 绝不在构造和析构过程中调用 virtual 函数
C++中,派生对象在 derived class 构造函数开始执行前不会成为一个 derived class 对象。相同地,一旦 derived class 析构函数开始执行,对象内的 derived class 成员变量便呈现未定义值,所以 C++ 视它们仿佛不再存在。进入 base class 析构函数后对象就成为一个 base class 对象,而 C++ 的任何部分包括 virtual 函数、dynamic_casts 等等也就那么看待它。
条款10 令赋值操作符函数返回一个 reference to *this
赋值操作符包括 =、+=、-=等等。这么做是为了支持连锁赋值,即 x = y = z; 或 x += y += z;
条款11 在 operator= 中处理“自我赋值”
确保当对象自我赋值时 operator= 有良好行为。其中技术包括比较“源对象”和“目标对象”的地址、精心周到的语句顺序(避免资源重复释放和资源破坏),以及 copy-and-swap技术。
确保任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
条款12 复制对象时勿忘其每一个成分
当你编写一个 copying 函数,请确保①复制所有 local 成员变量,②调用所有 base classes 内的适当的 copying 函数。
不要尝试以某个 copying 函数实现另一个 copying 函数。应该将共同技能放进第三个函数中,并由两个 copying 函数共同调用。
3 资源管理
条款13 以对象管理资源
“以对象管理资源”也称为“资源取得时机便是初始化时机”(Resource Acquisition Is Initialization; RAII)。
关键想法是:①获得资源后立刻放进管理对象内;②管理对象运用析构函数确保资源被释放。
条款14 在资源管理类中小心 copying 行为
复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。
普遍而常用的 RAII class copying 行为是:禁止 copying、施行引用计数法。不过其他行为也都可能被实现。
条款15 在资源管理类中提供对原始资源的访问
因为会有很多API需要访问原始资源。
条款16 成对使用 new 和 delete 时要采取相同形式
如果你在 new 表达式中使用[],必须在相应的 delete 表达式中也使用[]。如果你在 new 表达式中不使用[],一定不要在相应的 delete 表达式中使用[]。否则会导致未定义行为。
条款17 以独立语句将 newed 对象置入智能指针
如果不这样做,一旦异常抛出,有可能导致难以察觉的资源泄漏。
4 设计与声明
条款18 让接口容易被正确使用,不易被误用
条款19 设计 class 犹如设计 type
条款20 优先考虑 pass-by-reference-to-const 而非 pass-by-value
条款21 必须返回对象时,别妄想返回其 reference
条款22 将成员变量声明为 private
public 和 protected 意味不封装,而几乎可以说,不封装意味不可改变,特别是对被广泛使用的 classes 而言。一旦你将一个成员变量声明为 public 或 protected 而客户开始使用它,就很难改变那个成员变量所涉及的一切。太多代码需要重写、重新测试、重新编写文档、重新编译。从封装的角度观之,其实只有两种访问权限:private(提供封装)和其他(不提供封装)。
[封装是为了降低改变导致的影响,使改变成为可能,使改变更容易执行。]
条款23 优先考虑 non-member and non-friend 函数而非 member 函数
这样做可以增加封装性、包裹弹性和功能扩充性。
[面向对象守则要求数据应该尽可能被封装,而不是说所有操作数据的函数都应该和数据捆绑在同一个类中。和直观相反,member 函数带来的封装性比 non-member 函数低。]
条款24 若所有参数皆需类型转换,请为此采用 non-member 函数
如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个 non-member。
条款25 考虑写出一个不抛异常的 swap 函数
5 实现
条款26 尽可能延后变量定义式的出现时间
条款27 尽量少做转型动作
条款28 避免返回 handles 指向对象内部成分
条款29 为异常安全而努力是值得的
“异常安全”有两个条件,当异常被抛出时,带有异常安全性的函数会:①不泄漏任何资源;②不允许数据破坏。
有一个一般化的设计策略很典型地会导致异常安全的强烈保证,这个策略被称为 copy and swap。原则很简单:为你打算修改的对象(原件)做出一份副本,然后在那副本身上做一切必要修改。若有任何修改动作抛出异常,原对象仍保持未改变状态。待所有改变都成功后,再将修改过的那个副本和原对象在一个不抛出异常的操作中置换(swap)。
条款30 透彻了解 inlining 的里里外外
条款31 将文件间的编译依存关系降至最低
支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是 Handle classes 和 Interface classes。
6 继承与面向对象设计
条款32 public继承
public继承意味 work-like-a。适用于 base classes 身上的每一件事情一定也适用于 derived classes 身上(Liskov Substitution Principle)。
条款33 避免隐藏继承而来的名称
derived classes 内的名称会隐藏 base classes 内的名称。在public继承下从来没有人希望如此。
为了让被隐藏的名称再见天日,可使用 using 声明式或转交函数(forwarding functions)。
条款34 区分接口继承和实现继承
pure virtual 函数只具体指定接口继承。
impure virtual 函数具体指定接口继承及缺省实现继承。
non-virtual 函数具体指定接口继承以及强制性实现继承。
条款35 考虑 virtual 函数以外的其他选择
其他选择有:
①藉由 Non-Virtual Interface 手法实现 Template Method 模式
②藉由 Function Pointers 实现 Strategy 模式
③藉由 tr1::function 完成 Strategy 模式
④古典的 Strategy 模式
条款36 绝不重新定义继承而来的 non-virtual 函数
条款37 绝不重新定义继承而来的缺省参数值
条款38 通过组合塑模出 has-a 或 “根据某物实现出”
条款39 明智而审慎地使用 private 继承
private 继承意味着只有实现部分被继承,接口部分应略去。如果 D 以 private 形式继承 B,意思是 D 对象根据 B 对象实现而得,再没有其他意涵了。private 继承在软件“设计”层面上没有意义,其意义只系于软件实现层面。
条款40 明智而审慎地使用多重继承
7 模板与泛型编程
条款41 了解隐式接口和编译期多态
classes 和 templates 都支持接口和多态。
对 classes 而言接口是显式的(explicit),以函数签名为中心。多态则是通过 virtual 函数发生于运行期。
对 templates 参数而言,接口是隐式的(implicit),奠基于有效表达式。多态则是通过 templates 具现化和函数重载解析(function overloading resolution)发生于编译期。
条款42 了解 typename 的双重意义
①声明 templates 参数时,前缀关键字 class 和 typename 可互换。
②请使用关键字 typename 标识嵌套从属类型名称;但不得在 base class lists 或 member initialization list 内以它作为 base class 修饰符。
条款43 学习处理模板化基类内的名称
条款44 将与参数无关的代码抽离 templates
条款45 运用成员函数模板接受所有兼容类型
小结:
第二次读这本书了,但还是有很多收获。参加工作后再读这本书,给我最大的感受是:
很多时候看到一种设计模式或技术会感觉很抽象,这是因为自己没有理解这种模式或技术是为了解决什么具体问题而产生的。所以就算知道了模式或技术背后的实现细节,却不知道它的应用背景和应用场合。而没有具体使用过的东西是很容易忘记的。所以,读书的时候并不需要把每个细节都一次性搞定,更不能读过一遍之后就束之高阁(毕竟不是在看小说:)),而是应该在需要时多读几遍,常读常新。