第二章 构造函数语意学
2.1 Default Constructor的构造操作
默认构造函数在需要时被编译器合成出来。
这里的需要分为:程序的需要 和 编译器的需要。
- 程序的需要:即如果编译器不合成出来,不会发生错误,但程序语意不是自己想要的。
- 编译器的需要:即如果编译器不合成出来,将发生错误的时候。
当编译器需要的时候,默认构造函数会被合成出来,并且只执行编译器所需要的任务。(也不会帮你赋初始值)
1.类中带有 Default Constructor 的类对象成员
class Foo{ public: Foo(), Foo( int ) ... };
class Bar{ public: Foo foo; char *str; };
void foo_bar(){
Bar bar;
if( str ){ }....
}
如果一个class没有任何构造函数,但它内含一个对象成员,而后者有默认构造函数,那编译器需要为该class合成一个默认构造函数(如下)。
Bar::Bar(){
//被合成的默认构造函数只会满足编译器的需要,因此不会为你初始化str
foo.Foo::Foo();
}
若程序员定义了一个默认构造函数:
Bar::Bar(){
str = 0;
}
现在程序的需求满足了,但是编译器还需要初始化foo。由于默认构造函数已经被显式定义,编译器没法合成第二个。
那么编译器的行动是:
- 如果一个class 内含一个或者一个以上的类对象成员 ,那么class的每一个构造函数必须调用每一个类成员的默认构造函数 。
- 编译器会扩张已存在的构造函数们,在其中安插一些代码,使得在执行用户代码之前,先调用(调用顺序与对象成员在class 的声明次序一致)必要的默认构造函数们。
2.类继承于带有 Default Constructor 的基类
与前面道理相同:
- 若没有,则在类中没有默认构造函数,便合成;
- 若有,则在类中的默认构造函数们在执行用户代码之前,先调用(调用次序根据他们的声明次序)所有基类的默认构造函数。
3.“带有一个虚函数”的类
下面两种情况,同样需要合成默认构造函数:
- class 声明(或继承)一个虚函数;
- class派生自一个继承串链,其中一个或者更多的 virtual base class(虚基类);
因为一个类中若存在虚函数,那就少不了vptr与vtbl。
因此在编译期间发生:
一个虚函数表会被编译器产生出来,内放class 的虚函数们的地址;
-
在每一个类对象中,一个额外的pointer member(vptr)会被编译器合成出来,内含相关的虚函数表的地址;
【注】每一个类对象的意思是每一个有虚函数的类,就是说若基类与子类中都有虚函数,那么在构造函数中,都会执行这两步。
在合成的构造函数中,编译器会为每一个“带有一个虚函数”的类(或者其派生类)的对象实例设定vptr的初值,并在其中放置虚函数表的地址。
4.“带有一个虚基类”的类
这一小节有点笼统。
Virtual base class的实现法在不同编译器之间有很大差异。然而,<u>每一个实现的共同点在于必须使 虚基类 在其每一个 派生类 中的位置,能够在执行期准备妥当。</u>
对于class所定义的每一个constructor ,编译器都会安插那些“允许每一个virtual base class 的执行期存取操作”的代码。
意思是:因为没有办法在编译期确定 “虚基类中成员” 的实际偏移位置,因此只能在构造函数中加一些代码,合法化 “每一个虚基类的执行期存取操作”。
总结
以上4种情况,会导致“编译器必须为未声明构造函数的类合成一个默认构造函数 ”,这只是编译器(而非程序)的需要。
至于没有存在这4种情况,而又没有声明构造函数的class ,默认构造函数不会被合成出来的。
所有其他的非静态变量 ,如整数,整数指针,整数数组等是不会被初始化的,这些初始化操作对程序是必须的,但对编译器则并非需要的。
C++新手一般有两个误解:
- 任何class 如果没有定义默认构造函数,就会被合成出来一个;
- 编译器合成出来的默认构造函数会明确设定 class 内每一个data member的默认值;
2.2 Copy Constructor的构造操作
有三种情况,会以一个类的内容作为另一类对象的初值。
- 最明显的当然是对一个object做明确的初始化操作;(
X xx = x;
) - 当class object被当做参数交给某个函数;
- 当函数返回一个class object;
1.Default Memberwise Initialization(逐个成员初始化)
如果class 没有提供一个显式拷贝构造函数时,当class object以 “相同class的另一个object” 作为初值时,其内部是以所谓的default memberwise initialization(逐个成员初始化)方式完成的。
【注】这里没提拷贝构造函数。
逐个成员初始化:也就是把每一个内建的或派生的数据成员(例如一个数组或指针)的值,从某个object拷贝一份到另一个object上,但不拷贝其具体内容。例如只拷贝指针地址,不拷贝一份新的指针指向的对象,这也就是浅拷贝,不过它并不会拷贝其中member class object,而是以递归的方式实行memberwise initialization(就是再到这个member中,进行浅拷贝)。
memberwise initialization是如何实现的呢?
答案就是Bitwise Copy Semantics和default copy constructor。如果class展现了Bitwise Copy Semantics,则使用bitwise copy,否则编译器会生成默认拷贝函数。
也就是说:判断是否要合成拷贝构造函数的标准,是在于class是否展现出所谓的“bitwise copy semantics”(逐位拷贝语意)。
2.bitwise copy semantics(逐位初始化)
那什么情况下class不展现Bitwise Copy Semantics呢?有四种情况:
(等价于什么时候要用默认拷贝构造函数?)
当class内含有一个类对象成员,而这个类成员内有一个默认的copy 构造函数(不论是class设计者明确声明,或者被编译器合成);
当class 继承自一个基类,而基类内有copy构造函数(不论是class设计者明确声明,或者被编译器合成);
当一个类声明了一个或多个virtual 函数
当class派生自一个继承串链,其中一个或者多个virtual base class
在前2种情况下,编译器必须将“类对象成员”或者“基类”的 copy constructor的调用操作 安插到 被合成的copy constructor中。
后两种单独拿出来讲。
3.重新设定Virtual Table 的指针
第一章也提到,因为class 包含virtual function, 编译时需要做扩张操作:
- 增加virtual function table,内含有一个有作用的virtual function的地址;
- 创建一个指向virtual function table的指针,安插在class object内。
编译器对于每一个新产生的class object的vptr都必须被正确地赋值,否则将跑去执行其他对象的function了,其后果是很严重的(如图2)。因此,编译器导入一个vptr到class之中时,该class 就不在展现bitwise copy semantics,必须合成copy Constructor并将vptr适当地初始化。
下图这种同个类,若按照bitwise copy,没有错误:
但如下图这种,若按照bitwise copy,vptr有错误:
【注】图中,虽然Bear对draw()和animate()函数重写,但虚函数重写后也是虚函数,因此在虚函数表里。
4.处理Virtual Base Class Subobject
virtual base class的存在需要特别处理。一个class object 如果以另一个 virtual base class subobject那么也会使“bitwise copy semantics”失效。
每一个编译器对于虚拟继承的支持承诺,都是表示必须让 “derived class object 中的virtual base class subobject 位置” 在执行期就准备妥当,维护 “位置的完整性” 是编译器的责任。Bitwise copy semantics 可能会破坏这个位置,所以编译器必须自己合成出copy constructor。
这也就是说,拷贝构造函数和默认构造函数一样,需要的时候会进行构建,而并非程序员不写编译器就帮着构建。
2.4 初始化列表
下面四种情况必须使用初始化列表来初始化class 的成员:
- 当初始化一个reference member时;
- 当初始化一个const member时;
- 当调用一个base class 的 constructor ,而它拥有一组参数(其实就是自定义的构造函数)时;
- 当调用一个 member class 的 constructor,而它拥有一组参数时。
编译器会一一操作初始化列表,以适当顺序在构造函数内安插舒适化操作,并且在任何用户代码之前。
不过,初始化的顺序是class members声明次序决定的,不是由初始化列表决定的。