构造/析构/赋值运算

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

当写下:

class Empty { };

就好像写下这样的代码:

class Empty {

public:

Empty() { ... } // default构造函数

Empty(const Empty& rhs) { ... } // copy构造函数

~Empty() { ... } // 析构函数,

// 编译器产出的是non-virtual,除非这个类的base class自身声明有virtual



Empty& operator=(const Empty& rhs) { ... } // copy assignment 操作符

};

唯有当这些函数被需要,它们才会被编译器创建出来。

1
2
3
4
5
6
7
8

Empty e1; // default 构造函数

// 析构函数

Empty e2(e1); // copy 构造函数

e2 = e1; // copy assignment 操作符

copy 构造函数和 copy assignment 操作符,编译器创建的版本只是单纯地将来源对象的每一个 non-static 成员变量拷贝到目标对象。

copy assignment 其行为基本与 copy 构造函数如出一辙,但一般而言只有当生出的代码合法且有适当机会证明它有意义,其表现才会如我先前所说。万一两个条件有一个不符合,编译器会拒绝为 class 生出 operator=

如果你打算在一个“内含reference成员”的 class 内支持赋值操作,你必须自己定义copy assignment 操作符。

面对“内含const成员”的 classes ,编译器的反应也是一样,更改 const成员使不合法的。

还有一种情况:如果某个base classescopy assignment操作符声明为private,编译器将拒绝为其derived classes生成一个copy assignment操作符。

小结:

  • 编译器可以暗自为 class 创建 default 构造函数、copy 构造函数、copy assignment 操作符,以及析构函数。

条款06:若不想使用编译器自动生成的函数,就该明确拒绝

怎么可以复制某些先天独一无二的东西呢?我们有时要阻止对象拷贝动作的发生。如果我们不声明对应函数,但是编译器会产出一份copy构造函数和copy assignment操作符。

一种方法是将copy构造函数或copy assignment操作符声明为private,藉由明确声明一个成员函数,你阻止了编译器暗自创建其专属版本;而令这些函数为 private ,得以成功阻止人们的调用它。但这样 member函数和friend函数还是可以调用private函数,除非只声明,而不定义它们,但是如果不慎调用到它们的话,会导致链接错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14

class HomeForSale {

public:

...

private:

HomeForSale(const HomeForSale&); //只有声明

HomeForSale& operator=(const HomeForSale&);

};

将链接错误移至编译期是可能的,越早侦测出错误越好,只要将copy构造函数和copy assignment操作符声明为private就可以做到,不过要在一个专门为了阻止copying动作而设计的base class内。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

class Uncopyable {

protected:

Uncopyable() {} // 允许 derived 对象构造和析构

~Uncopyable() {}

private:

Uncopyable(const Uncopyable&); // 但阻止 copying

Uncopyable& operator=(const Uncopyable&);

};

// 继承 Uncopyable

class HomeForSale : private Uncopyable {

... // class 不再声明 copy 构造函数和 copy assignment 操作符

};

这样的话,只要任何尝试拷贝 HomeForSale对象,编译器便试着生成一个 copy 构造函数和一个copy assignment 操作符,这些函数的“编译器生成版”会尝试调用其base classes的对应兄弟,因为base class 的拷贝函数是private ,所以那些调用会被编译器拒绝。

小结:

  • 为驳回编译器暗自提供的机能,可将相应的成员函数声明为private并且不予实现。使用像Uncopyable这样的base class也是一种做法。boost提供一个 noncopyableclass

条款07:为多态基类声明 virtual 析构函数

polymorphic(带多态性质的)base class ,这种 base class 的设计目的是为了用来“通过 base class 接口处理 derived class 对象”。

C++明确指出,当 derived class 对象经由一个 base class 指针被删除,而该 base class 带着一个 non-virtual 析构函数,其结果未有定义——实际执行时通常发生的是对象的derived成分(子类的成员变量)没被销毁。

解决方法是:给 base class 一个 virtual 析构函数,此后 delete 基类指针,子类对象也会析构掉。

任何 class 只要带有 virtual 函数几乎确定应该也有一个 virtual 析构函数。

如果并不打算被用做一个base class ,令其析构函数为 virtual 往往是个馊主意。

想实现出 virtual 函数,对象必须携带某些信息,主要用来在运行期决定哪一个 virtual 函数该被调用。这份信息通常是由一个所谓的vptr——虚表指针指出。vptr 指向一个由函数指针构成的数组,称为 vtbl——虚表;每一个带有 virtual 函数的类都有一个相应的 vtbl。当对象调用某一 virtual 函数,实际被调用的函数取决于该对象的 vtpr 所指的那个 vtbl——编译器在其中寻找适当的函数指针。

所以如果 class 内含虚函数,其对象的体积会增加,多一个 vtpr,这会带来一些问题。

析构函数的运作方式是,最深层派生的那个 class 其析构函数最先被调用,然后是其每一个 base class 的析构函数被调用。

如果企图继承一个标准容器或任何其他“带有 non-virtual 析构函数”的 class,都是错误的。

小结:

  • polymorphic(带多态性质的)base classes 应该声明为一个 virtual 析构函数。如果 class 带有任何 virtual 函数,它就应该拥有一个 virtual 析构函数。

  • Classes 的设计目的如果不是作为 base class 使用,或不是为了具备多态性,就不该声明 virtual 析构函数。

条款08:别让异常逃离析构函数

C++ 并不禁止析构函数吐出异常,但它不鼓励这样做。程序可能过早结束或出现不明确行为。

·

但是你的析构函数必须执行一个动作,而该动作可能会在失败时抛出异常,该怎么办?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

class DBConn {

public:

...

~DBConn() { // 确保数据库连接总是被关闭

db.close();

}

private:

DBConnection db;

};

比如上面这种情况,只要调用 close 成功,一切都美好。但如果该调用导致异常,DBConn 析构函数会传播该异常,也就是允许它离开这个析构函数。

第一个方法,如果抛出异常就结束程序,通常通过调用 abort 完成。如果程序遭遇一个“于析构期间发生的错误”后无法继续执行,“强迫结束程序”是个合理选项。毕竟它可以阻止异常从析构函数传播出去(那会导致不明确行为)。

第二个方法,吞下因调用 close 而发生的异常。一般而言,这是个坏主意。

这两种方法都不太好,问题在于两者都无法对“导致 close 抛出异常”的情况做出反应。一个较佳的策略是重新设计 DBConn 接口,使其客户有机会对可能出现的问题作出反应。比如下面这样改进,自己提供一个 close函数。

小结:

  • 析构函数绝对不能抛出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。

  • 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么 class 应该提供一个普通函数(而非在析构函数中)执行该操作。

条款09:绝不在构造和析构过程中调用 virtual 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

class Transaction { // 所有交易的 base class

public:

Transaction();

virtual void logTransaction() const = 0; // 做出一份因类型不同而不同的日志记录

...

};



Transaction::Transaction() {

...

logTransaction(); // 最后动作是log这笔交易

}



class BuyTransaction : public Transaction {

public:

virtual void logTransaction() const;

...

};



class SellTransaction : public Transaction {

public:

virtual void logTransaction() const;

...

};

构造函数的运作机制是,base class 构造函数的执行更早于 derived class 构造函数。base class 构造期间 virtual 函数绝不会下降到 derived class 阶层。在 base class 构造期间,virtual 函数不是 virtual 函数。

base class 构造函数执行时 derived class 的成员变量尚未初始化。如果此期间调用的 virtual 函数下降至 derived classes 阶层,要知道 derived class 的函数几乎必然取用 local 成员变量,而那些成员变量尚未初始化。

更根本的原因是:在 derived class 对象的 base class 构造期间,对象的类型是 base class 而不是 derived class。不只 virtual 函数会被编译器解析至 base class,若使用运行期类型信息,例如 dynamic_cast,也会把对象视为base class类型。这样对待是合理的,当子类对象的“子类专属成分”尚未初始化,所以面对它们,最安全的做法就是视它们不存在。对象在derived class构造函数开始执行前不会成为一个derived class` 对象。

相同道理也适用于析构函数。一旦 derived class 析构函数开始执行,对象内的 derived class 成员变量便呈现未定义值,所以 C++ 视它们仿佛不再存在。进入 base class 析构函数后对象就成为一个 base class 对象。

显然在 Transaction 构造函数内对对象调用 virtual 函数是一种错误做法。但如何确保每一次有 Tranaction 继承体系上的对象被创建,就会有适当版本的 logTransaction 被调用呢?

由于无法使用 virtual 函数从 base classes 向下调用,在构造期间,你可以藉由“令 derived classes 将必要的构造信息向上传递至 base class 构造函数” 替换之加以弥补。

小结:

  • 在构造和析构期间不要调用 virtual 函数,因为这类调用从不下降至 derived class (比起当前执行构造函数和析构函数的那层)。

条款10:令 operator= 返回一个 refernce to *this

关于赋值,可以把它们写成连锁形式:

1
2
3
4

int x, y, z;

x = y = z = 15;

为了实现“连锁赋值”,赋值操作符必须返回一个 reference 指向操作符的左侧实参。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

class Widget {

public:

...

Widget& operator=(const Widget& rhs) {

...

return *this; // 返回左侧对象

}

};

这个协议不仅适用于以上的标准赋值形式,也适用于所有赋值相关的运算,例如 +=、-=、*=等。

这只是个协议,并无强制性。这份协议被所有内置类型和标准程序库提供的类型共同遵守。

小结:

  • 令赋值(assignment)操作符返回一个 reference to *this。

条款11:在 operator= 中处理“自我赋值”

自我赋值发生在对象被赋值给自己时,这看起来有点愚蠢,但它合法。

有些自我赋值看起来并不明显,是“别名”带来的结果:所谓“别名”就是“有一个以上的方法指称某对象”。一般而言如果某段代码操作 pointers 或 reference 而它们被用来“指向多个相同类型的对象”,就需考虑这些对象是否为同一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

// 一个不具备 自我赋值安全 && 异常安全的例子

class Bitmap { ... }

class Widget {

...

private:

Bitmap* pb;

};



Widget& Widget::operator=(const Widget& rhs) {

delete pb;

pb = new Bitmap(*rhs.pb);

return *this;

}

这里的自我赋值问题是,*this 和 rhs 有可能是同一对象。delete 有可能也销毁了 rhs 的 bitmap。

传统做法是加一个“证同测试”。

1
2

if (this == &rhs) return *this;

这个版本解决了“自我赋值安全性”,但不具备“异常安全性”,如果 new导致异常(分配时内存不足或copy构造函数抛出异常),Widget 最终会持有一个指针指向一块被删除的 Bitmap。

令人高兴的是,让 operator= 具备“异常安全性”往往自动获得“自我赋值安全”的回报。我们只需注意在复制 pb 所指东西之前别删除 pb。

1
2
3
4
5
6
7
8
9
10
11
12

Widget& Widget::operator=(const Widget& rhs) {

Bitmap* pOrig = pb;

pb = new Bitmap(*rhs.pb);

delete pOrig;

return *this;

}

小结:

  • 确保当对象自我赋值时 operator= 有良好行为。其中技术包括比较“来源对象”和“目标对象”地址、精心周到的语句顺序、以及 copy-and-swap。

  • 确定任何函数如果操作一个以上对象,而其中多个对象是同一个对象时,其行为仍然正确。

条款12:复制对象时勿忘其每一个成分

条款5观察到编译器会在必要时为我们的 classes 创建 copying 函数,并说明这些“编译器生成版”的行为:将被拷对象的所有成员变量都做一份拷贝。

当编写一个 copying 函数时,请确保:第一种情况,复制所有 local 成员变量;第二种情况,调用所有 base class 内的适当的 copying 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

class PriorityCustomer : Customer {

public:

...

PriorityCustomer(const PriorityCustomer& rhs);

PriorityCustomer& operator=(const Priority& rhs);

...

private:

int priority;

};



PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)

: priority(rhs.priority) {

logCall("PriorityCustomer copy constructor");

}



PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs) {

logCall("PriorityCustomer assignment");

priority = rhs.priority;

return *this;

}

这种情况就是复制了 PriorityCustomer 声明的成员变量,但是每个 PriorityCustomer 还内含它所继承的 Customer 成员变量副本,而那些成员变量却未被复制。 PriorityCustomer 对象的 Customer 成分会被不带实参的 Customer 构造函数,即 default 构造函数初始化。

任何时候只要你承担起“为 derived class 撰写 copying 函数”的重责大任,必须很小心地复制其 base class 成分,那些成分往往是 private 的,所以你无法直接访问它们,应该让 derived class 的 copying 函数调用响应的 base class 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)

: Customer(rhs),

priority(rhs.priority) {

logCall("PriorityCustomer copy constructor");

}



PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs) {

logCall("PriorityCustomer assignment");

Customer::operator=(rhs);

priority = rhs.priority;

return *this;

}

这两个 copying 函数往往有近似相同的实现本体,这可能会诱使你让某个函数调用另一个函数以避免代码重复。但是令某个 copying 函数调用另一个 却无法达到想要的目标。

消除重复代码的做法是,建立一个新的成员函数给两者调用,这样的函数往往是 private 而且常被命名为 init

小结:

  • Copying 函数应该确保复制“对象内的所有成员变量”及“所有 base class 成分”。
  • 不要尝试以某个 copying 函数实现另一个 copying 函数,应该将共同机能放进第三个函数中,并由两个 copying 函数共同调用。
Donate comment here