第2
章:构造函数语意学
在这一章中,主要介绍编译器对于“对象构造过程”的干涉,以及对于“程序形式”和“程序效率”的冲击。
2.1
Default Constructor
的构造操作
对于未声明构造函数的类,只有在以下四种情况下编译器才会为它们合成默认构造函数:
- 类有一个类对象(
Member Class Object
)成员,且该成员含有默认构造函数(Default Constructor
) - 继承自带有默认构造函数(
Default Constructor
)的基类(Base class
) - 带有虚函数(
Virtual function
)的类 - 继承自虚基类(
Virtual base class
)的类
对于以上四种情况,C++
标准把合成的默认构造函数叫隐式的有意义默认构造函数(implicit nontrivial default constructors
)。被合成的构造函数只能满足编译器(而非程序)的需要,它之所以能够完成任务,是借着调用成员对象或基类的默认构造函数(情况1/2
),或是为每一个对象初始化其虚函数机制或虚基类机制(情况3/4
)。
至于没有存在上述四种情况,而又没有声明任何构造函数的类,那么它们拥有的是隐式无意义默认构造函数(implicit trivial default constructors
),实际上它们并不会被合成出来。
Default Constructor
会在编译器需要的时候被产生出来,被合成出来的Constructor
只执行编译器所需要的行为。如果程序有需要,定制Default Constructor
的行为是程序员的责任。
“带有
Default Constructor
”的Member Class Object
如果有多个
class member objects
都要求constructor
初始化操作,将如何呢?C++
语言要求以“member objects
在class
中的声明次序”来调用各个constructors
。这一点由编译器完成,它为每一个constructor
安插程序代码,以“member
声明次序”调用每一个member
所关联的default constructors
。这些码将被安插在explicit user code
之前。举个例子: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
33class Dopey
{
public:
Dopey();
...
};
class Sneezy
{
public:
Sneezy(int);
Sneezy();
...
};
class Bashful
{
public:
Bashful();
...
};
class Snow_White
{
...
private:
Dopey dopey;
Sneezy sneezy;
Bashful bashful;
int mumble;
};如果
Snow_White
没有定义default constructor
,就会有一个nontrivial constructor
被合成出来,依序调用Dopey
、Sneezy
、Bashful
的default constructors
。然而如果Snow_White
定义了下面这样的default constructor
:1
2
3
4
5// 程序员所写的 default constructor
Snow_White::Snow_White() : sneezy(1024)
{
mumble = 2048;
}它会被扩张为:
1
2
3
4
5
6
7
8
9
10
11// 编译器扩张后的 C++ 伪码
Snow_White::Snow_White()
{
// 调用其 constructor
dopey.Dopey::Dopey();
Sneezy.Sneezy::Sneezy(1024);
bashful.Bashful::Bashful();
// explicit user code
mumble = 2048;
}“带有
Default Constructor
”的Base Class
类似的道理,如果一个没有任何
constructors
的class
派生自一个“带有default constructor
”的base class
,那么这个derived class
的default constructor
会被视为nontrivial
,并因此需要被合成出来。它将调用上一层base classes
的default constructor
(根据它们的声明次序)。对一个后继派生的class
而言,这个合成的constructor
和一个“被明确提供的default constructor
”没有什么差异。如果设计者提供多个
constructors
,但其中都没有default constructor
呢?编译器会扩张现有的每一个constructors
,将“用以调用所有必要之default constructors
”的程序代码加进去。它不会合成一个新的default constructor
,这是因为其它“由user
所提供的constructors
”存在的缘故。如果同时亦存在着“带有default constructors
”的member class objects
,那些default constructor
也会被调用。“带有(声明或继承)一个
Virtual Function
”的Class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class Widget
{
public:
virtual void flip() = 0;
...
};
void flip(const Widget& widget)
{
widget.flip();
}
// 假设 Bell 和 Whistle 都派生自 Widget
void foo()
{
Bell b;
Whistle w;
flip(b);
flip(w);
}下面两个扩张操作会在编译期间发生:
一个
virtual function table
会被编译器产生出来,内放class
的virtual functions
地址。在每一个
class object
中,一个额外的pointer member
会被编译器合成出来,内含相关的class vtbl
的地址。此外,
widget.flip()
的虚拟引发操作会被重新改写,以使用widget
的vptr
和vtbl
中的flip()
条目:1
2
3// 1 表示 flip() 在 virtual table 的固定索引
// &widget 代表要交给"被调用的某个 flip() 实体"的 this 指针
(*widget.vptr[1])(&widget));为了让这个机制发挥功效,编译器必须为每一个
Widget
(或其派生类之)object
的vptr
设定初值,放置适当的virtual table
地址。对于class
所定义的每一个constructor
,编译器会安插一些码来做这样的事情。对于那些未声明任何constructors
的classes
,编译器会为它们合成一个default constructor
,以便正确地初始化每一个class object
的vptr
。
“带有一个
Virtual Base Class
”的Class
编译器必须使
virtual base class
在其每一个derived class object
中的位置,能够在执行期准备妥当。一种可能的做法是在derived class
中安插一个指向每一个virtual base class
的指针,所有“经由reference
和pointer
来存取其中virtual base class
中数据的操作”都通过此指针完成。同样地,这个
virtual base class
指针是在class object
构建期间完成的,编译器的默认行为和3
中的vptr
的处理方式一样。
2.2
Copy Constructor
的构造操作
有三种情况会调用copy constructor
:
对一个
class object
做明确的初始化操作1
2
3
4
5class X {...};
X x;
X xx(x);
X xx = x;当
class object
被当作参数交给某个函数时1
2
3
4
5
6
7extern void foo(X x);
void bar()
{
X xx;
foo(xx);
...
}当函数返回一个
class object
1
2
3
4
5
6X foo_bar()
{
X xx;
...
return xx;
}
Default Memberwise Initialization
如果
class
没有提供一个explicit copy constructor
又当如何?当class object
以“相同class
的另一个object
”作为初值时,其内部是以所谓的default memberwise initialization
完成的,也就是把每一个内建的或派生的data member
的值,从某个object
拷贝一份到另一个object
身上。不过它并不会拷贝其中的member class object
,而是以递归的方式施行memberwise initialization
。一个良好的编译器可以为大部分
class objects
产生bitwise copies
,因为它们有bitwise copy semantics
。copy constructors
在必要的时候才由编译器产生出来。这个句子中的“必要”意指当class
不展现bitwise copy semantics
时。Bitwise Copy Semantics
C++ Standard
把copy constructor
区分为trivial
和nontrivial
两种。只有nontrivial
的实体才会被合成于程序之中。决定一个copy constructor
是否为trivial
的标准在于class
是否展现出所谓的“bitwise copy semantics
”。什么时候一个class不展现出
Bitwise Copy Semantics
呢?当
class
内含一个member object
而后者的class
声明有一个copy constructor
时(不论是被class
设计者明确地声明;或是被编译器合成)。当
class
继承自一个base class
而后者存在有一个copy constructor
(再次强调,不论是被明确声明或是被合成而得)。当
class
声明了一个或多个virtual functions
时。当
class
派生自一个继承串链,其中有一个或多个virtual base classes
时。有一点很值得注意:在被合成出来的
copy constructor
中,如整数、指针、数组等等的nonclass members
也都会被复制。
重新设定
virtual table
的指针vptr
当编译器导入一个
vptr
到class
之中时,该class
就不再展现bitwise semantics
了。编译器需要合成出一个copy constructor
,以求将vptr
适当地初始化。base class object
以另一个base class object
作为初值,或derived class object
以另一个derived class object
作为初值,都可以直接靠“bitwise copy semantics
”完成。当一个base class object
以其derived class
的object
内容做初始化操作时,其vptr
复制操作必须保证安全。此时,合成出来的base copy constructor
会明确设定object
的vptr
指向base class
的virtual table
,而不是直接从右手边的class object
中将其vptr
现值拷贝过来。处理
virtual base class subject
和
3
同理,需要合成copy constructor
来明确的设定virtual base class pointer
的初值。
2.3
程序转化语意学
已知有这样的定义:
1 | X x0; |
下面有三个定义,每一个都明显地以x0
来初始化其class object
:
1 | void foo_bar() |
必要的程序转化有两个阶段:
- 重写每一个定义,其中的初始化操作会被剥除。
class
的copy constructor
调用操作会被安插进去。
举个例子,在明确的双阶段转化之后,foo_bar()
可能看起来像这样:
1 | // 可能的程序转换 |
参数的初始化
把一个
class object
当做参数传给一个函数,相当于以下形式的初始化操作:1
2// xx 代表形式参数, arg 代表真正的参数值
X xx = arg;返回值的初始化
已知下面这个函数定义:
1
2
3
4
5
6X bar()
{
X xx;
// 处理 xx ...
return xx;
}这里有一个双阶段转化:
首先加上一个额外参数,类型是
class object
的一个reference
。这个参数将用来放置被“拷贝建构(copy constructed
)”而得的返回值。在
return
指令之前安插一个copy constructor
调用操作,以便将欲传回之object
的内容当做上述新增参数的初值。根据这样的算法,
bar()
转换如下:1
2
3
4
5
6
7
8
9
10void bar(X &_result)
{
X xx;
// 编译器所产生的 default constructor 调用操作
xx.X::X();
// ... 处理 xx
// 编译器所产生的 copy constructor
result.X::X(xx);
return;
}现在编译器必须转换每一个
bar()
调用操作,以反映其新定义。例如:1
X xx = bar();
将被转换为下列两个指令句:
1
2
3// 注意, 不必施行 default constructor
X xx;
bar(xx);
NRV
优化已知下面这个函数定义:
1
2
3
4
5
6X bar()
{
X xx;
// 处理 xx ...
return xx;
}编译器会把它优化为:
1
2
3
4
5
6
7
8
9void bar(X &_result)
{
// default constructor 调用操作
_result.X::X();
// ... 直接处理 _result
return;
}只有当程序提供了
explicit copy constructor
,编译器才会实施NRV
优化。没有提供的话(编译器自己合成的),是不会实施NRV
优化的。copy constructor
要还是不要?对于下面的
3D
坐标点类,这个class
的设计者应该提供一个explicit copy constructor
吗?1
2
3
4
5
6
7
8class Point3d
{
public:
Point3d(float x, float y, float z);
// ...
private:
float _x, _y, _z;
};除非你预见该
class
需要大量的memberwise
初始化操作,例如函数以传值(by value
)的方式传回objects
,那么提供一个copy constructor
的explicit inline
函数实体就非常合理——可以激活编译器提供的NRV
优化。否则,不提供explicit copy constructor
既快速又安全。
2.4
Member initialization list
为了让你的程序能够被顺利编译,必须使用member initialization list
的情况:
- 当初始化一个
refercnce member
时; - 当初始化一个
const member
时; - 当调用一个
base class
的constructor
,而它拥有一组参数时; - 当调用一个
member class
的constructor
,而它拥有一组参数时。
在这四种情况之外,不使用member initialization list
,程序可以被正确编译并执行,但是效率不彰。例如:
1 | class Word |
下面是编译器对constructor
可能的内部扩张结果:
1 | // C++ 伪码 |
更有效率的实现方法:
1 | // 较佳的方式 |
它会被扩张成这个样子:
1 | // C++ 伪码 |
编译器会一一操作initialization list
,以member
声明次序(不是由initialization list
中的排列次序决定的)在constructor
之内的explicit user code
之前安插初始化操作。
调用一个member function
以设定一个member
的初值是可以的,因为此时this
指针已经被构造妥当。
1 | // X::xfoo() 被调用 |
注意:如果一个derived class member function
被调用,其返回值被当做base class constructor
的一个参数,这不是一个好主意!!!