C++(二) Effective C++(上)
让自己习惯 C++
条款 03:尽可能使用 const
如果关键字 const 出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量;如果出现在星号两边,表示被指物和指针两者都是常量。
1 | char greeting[] = "Hello"; |
条款 04:确定对象被使用前已先被初始化
- 为内置型对象进行手工初始化,因为 C++不保证初始化它们。
- 构造函数最好使用成员初值列(member initialization list),而不要在构造函数本体内使用赋值操作(assignment)。初值列列出的成员变量,其排列次序应该和它们在 class 中的声明次序相同。
- 为免除“跨编译单元之初始化次序”问题,请以 local static 对象替换 non-local static 对象。
成员初始化列表和构造函数的函数体都可以为我们的类数据成员指定一些初值,但是两者在给成员指定初值的方式上是不同的。成员初始化列表使用初始化的方式来为数据成员指定初值,而构造函数的函数体是通过赋值的方式来给数据成员指定初值。也就是说,成员初始化列表是在数据成员定义的同时赋初值,但是构造函的函数体是采用先定义后赋值的方式来做。这样的区别就造成了,在有些场景下,是必须要使用成员初始化列表。
构造/析构/赋值运算
条款 05:了解 C++默默编写并调用哪些函数
举个例子,假设 Namedobject 定义如下,其中 namevalue 是个 reference to string,objectValue 是个 const T:
1 | template<class T> |
现在考虑下面会发生什么事:
1 | std:string newDog("Persephone"); |
因为 C++并不允许“让 reference 改指向不同对象”。面对这个难题,C++的响应是拒绝编译那一行赋值动作。如果你打算在一个“内含 reference 成员”的 class 内支持赋值操作(assignment),你必须自己定义 copy assignment 操作符。
面对“内含 const 成员”的 classes,编译器的反应也一样。更改 const 成员是不合法的,所以编译器不知道如何在它自已生成的赋值函数内面对它们。
最后还有一种情况:如果某个 base classes 将 copy assignment 操作符声明为 private,编译器将拒绝为其 derived classes 生成一个 copy assignment 操作符。
条款 06:若不想使用编译器自动生成的函数,就该明确拒绝
所有编译器产出的函数都是 public。为阻止这些函数被创建出来,你得自行声明它们,但这里并没有什么需求使你必须将它们声明为 public。因此你可以将 copy 构造函数或 copy assignment 操作符声明为 private。藉由明确声明一个成员函数,你阻止了编译器暗自创建其专属版本;而令这些函数为 private,使你得以成功阻止人们调用它。
请记住
- 为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为 private 并且不予实现。
条款 07:为多态基类声明虚析构函数
- polymorphic(带多态性质的)base classes 应该声明一个 virtual 析构函数。如果 class 带有任何 virtual 函数,它就应该拥有一个 virtual 析构函数,因为 C++明白指出,当 derived class 对象经由一个 base class 指针被删除,而该 base class 带着一个 non-virtual 析构函数,其结果未有定义一实际执行时通常发生的是对象的 derived 成分没被销毁。
- Classes 的设计目的如果不是作为 base classes 使用,或不是为了具备多态性(polymorphically),就不该声明 virtual 析构函数。当 class 不企图被当作 base class,令其析构函数为 virtual 往往是个馊主意。额外的虚表指针和虚函数表生成,导致占空间增加以及不兼容性。
条款 08:别让异常逃离析构函数
- 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
- 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么 class 应该提供一个普通函数(而非在析构函数中)执行该操作。
条款 09:绝不在构造和析构过程中调用虚函数
在 base class 构造期间,virtual 函数不是 virtual 函数。 由于 base class 构造函数的执行更早于 derived class 构造函数,当 base class 构造函数执行时 derived class 的成员变量尚未初始化。如果此期间调用的 virtual 函数下降至 derived classes 阶层,要知道 derived class 的函数几乎必然取用 local 成员变量,而那些成员变量尚未初始化。所以 C++不让你走这条路。
其实还有比上述理由更根本的原因:在 derived class 对象的 base class 构造期间,对象的类型是 base class 而不是 derived class。不只 virtual 函数会被编译器解析至(resolve to)base class:,若使用运行期类型信息(runtime type information,例如 dynamic_cast 和 typeid),也会把对象视为 base class 类型。
相同道理也适用于析构函数。一旦 derived class 析构函数开始执行,对象内的 derived class 成员变量便呈现未定义值,所以 C++视它们仿佛不再存在。进入 baseclass 析构函数后对象就成为一个 base class 对象,而 C++的任何部分包括 virtual 函数、dynamic_casts 等等也就那么看待它。
条款 10:令 operator= 返回一个* this 引用
关于赋值,有趣的是你可以把它们写成连锁形式:
1 | int x,y,z; |
同样有趣的是,赋值采用右结合律,所以上述连锁赋值被解析为:
1 | x = (y = (z = 15)); |
这里 15 先被赋值给 z,然后其结果(更新后的 z)再被赋值给 y,然后其结果(更新后的 y)再被赋值给 x。为了实现“连锁赋值”,赋值操作符必须返回一个 reference 指向操作符的左侧实参。这是你为 classes 实现赋值操作符时应该遵循的协议:
1 | class Widget |
条款 12:复制对象时勿忘其每一个成分
当你编写一个 copying 函数,请确保:
- 复制所有 local 成员变量。
- 调用所有 base classes 内的适当的 copying 函数。
令 copy assignment 操作符调用 copy 构造函数是不合理的,因为这就像试图构造个已经存在的对象。这件事如此荒谬,乃至于根本没有相关语法。是有一些看似如你所愿的语法,但其实不是;也的确有些语法背后真正做了它,但它们在某些情况下会造成你的对象败坏,所以我不打算将那些语法呈现给你看。单纯地接受这个叙述吧:你不该令 copy assignment 操作符调用 copy 构造函数。
反方向一令 copy 构造函数调用 copy assignment 操作符一同样无意义。构造函数用来初始化新对象,而 assignment 操作符只施行于已初始化对象身上。对一个尚未构造好的对象赋值,就像在一个尚未初始化的对象身上做“只对已初始化对象才有意义”的事一样。无聊嘛!别尝试。
如果你发现你的 copy 构造函数和 copy assignment 操作符有相近的代码,消除重复代码的做法是,建立一个新的成员函数给两者调用。这样的函数往往是 private 而且常被命名为 init。这个策略可以安全消除 copy 构造函数和 copy assignment 操作符之间的代码重复。
参考文献
《Effective C++》