0%

C++(二) Effective C++(上)

原图

让自己习惯C++

条款03:尽可能使用const

如果关键字const出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量;如果出现在星号两边,表示被指物和指针两者都是常量。

1
2
3
4
5
char greeting[] = "Hello";
char* p = greeting; //non-const pointer,non-const data
const char* p greeting; //non-const pointer,const data
char* const p = greeting; //const pointer,non-const data
const char* const p = greeting; //const pointer,const data

条款04:确定对象被使用前已先被初始化

  • 为内置型对象进行手工初始化,因为C++不保证初始化它们
  • 构造函数最好使用成员初值列(member initialization list),而不要在构造函数本体内使用赋值操作(assignment)。初值列列出的成员变量,其排列次序应该和它们在class中的声明次序相同
  • 为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-local static对象

成员初始化列表和构造函数的函数体都可以为我们的类数据成员指定一些初值,但是两者在给成员指定初值的方式上是不同的。成员初始化列表使用初始化的方式来为数据成员指定初值,而构造函数的函数体是通过赋值的方式来给数据成员指定初值。也就是说,成员初始化列表是在数据成员定义的同时赋初值,但是构造函的函数体是采用先定义后赋值的方式来做。这样的区别就造成了,在有些场景下,是必须要使用成员初始化列表。

构造/析构/赋值运算

条款05:了解C++默默编写并调用哪些函数

举个例子,假设Namedobject定义如下,其中namevalue是个reference to string,objectValue是个const T:

1
2
3
4
5
6
7
8
9
template<class T>
class Namedobject
{
public:
Namedobject (std:string& name,const T&value);
//如前,假设并未声明operator=
private:
std::string& namevalue;// referenceconst T objectvalue;
};

现在考虑下面会发生什么事:

1
2
3
4
5
6
7
std:string newDog("Persephone");
std:string oldDog ("Satch");
Namedobject<int> p(newDog,2); //当初撰写至此,我们的狗Persephone
//即将度过其第二个生日。
Namedobject<int> s(oldDog,36); //我小时候养的狗Satch则是36岁,

p = s; //现在p的成员变量该发生什么事?

因为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
2
int x,y,z;
x = y = z = 15; //赋值连锁形式

同样有趣的是,赋值采用右结合律,所以上述连锁赋值被解析为:

1
x = (y = (z = 15));

这里15先被赋值给z,然后其结果(更新后的z)再被赋值给y,然后其结果(更新后的y)再被赋值给x。为了实现“连锁赋值”,赋值操作符必须返回一个reference指向操作符的左侧实参。这是你为classes实现赋值操作符时应该遵循的协议:

1
2
3
4
5
6
7
8
9
class Widget
{
public:
Widget& operator=(const Widget& rhs) //返回类型是个reference,
{ //指向当前对象。
return* this; //返回左侧对象
}
...
};

条款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++》