一、default constructor的构造操作
先看一个小例子:
class Foo { public: int val; Foo *pnext; };
此处正确的程序语意是要求Foo有一个默认构造函数,能将它的两个成员初始化为0,那么编译器会为我们合成这样的默认构造函数吗?答案是不会!(事实上,这里合成的默认构造函数是trival的构造函数,也就是它什么也不干,连初始化也不干。在概念上我们把这种构造函数叫做implicit trival default constructors,但实际上这种构造函数根本不会被合成出来)。这是因为初始化成员是程序的需要,而不是编译器的需要,本例中要承担成员初始化责任的是设计者。
在四种情况下,non-trival default constructor会被编译器合成出来,下面一一讨论这四种情况。
1. 带有Default constructor的Member Class Object
如果一个class没有任何constructor,但它内含一个member object,而后者有default constructor,那么编译器就需要为该class合成出一个default constructor,不过这个合成操作只有在constructor真正需要被调用时才会发生。
这就引出一个问题,在C++的各个不同的文件中,编译器如何避免合成出多个default constructor?解决办法是把合成的default constructor、copy constructor、deconstructor,copy assignment operator都以inline方式完成,一个inline函数具有静态链接,不会被文件以外者看到。如果函数太复杂不适合做成inline,就会合成出一个explicit non-inline static实例。(这一段没太明白)
下面是一个小例子:
class Foo { public: Foo(), Foo(int) ... };
class Bar { public: Foo foo; char *str; };
void foo_bar()
{
Bar bar; //Bar::foo在这里必须被初始化
if( str ) { }...
}
在本例中,编译器将为class Bar合成一个default constructor,内含必要的代码,能够调用class Foo的default constructor来处理member object Bar::foo,但是要注意,这个default constructor依然不会负责str的初始化!
但是如果class Bar已经有了构造函数,那么编译器会怎么做呢?由于Bar有构造函数,所以编译器不能再为class Bar合成一个默认构造函数。编译器的做法是:如果class A内含一个或一个以上的member class objects,那么class A的每一个constructor必须调用每一个member class的default constructor。编译器会扩张已存在的constructors,在其中安插一些代码,使得user code被执行之前,先调用必要的default constructors。值得注意的是,调用的default constructor的顺序和member objects在class中的声明顺序相同。看下面这个例子:
class Dopey { public: Dopey(); ... };
class Sneezy { public: Sneezy( int ); Sneezy(); ... };
class Bashful { public: Bashful(); ... };
class Snow_White{
public:
Dopey dopey;
Sneezy sneezy;
Bashful bashful;
private:
int mumble;
};// Snow_White 包含dopey,sneezy,bashful三个类的对象成员
假设class Snow_white有这样的构造函数:
Snow_White::Snow_White() : sneezy( 1024 )
{
mumble = 2048;
}
那么它会被编译器扩张为:
Snow_White::Snow_White() : sneezy( 1024 )
{
//插入member class objects
dopey.Dopey::Dopey();
sneezy.Sneezy::Sneezy(1024);
bashful.Bashful::Bashful();
mumble = 2048;
}
值得注意的是,以上这个例子除了体现了上面的知识点外,还能看到sneezy既存在于初始化列表中,又存在于隐式构造函数的调用中,那么这两者之间有什么联系吗?这个问题将在后面的内容中回答。
2. “带有Default Constructor”的Base Class
这里默认构造函数合成的逻辑与1中很相似,因为class里面包含一个类的对象成员和class里面有一个base class的part这两种情况在内存布局上就很相似。
这里需要注意的是,如果一个class里面既有基类的部分也有类的对象成员,那么先调用基类的默认构造函数,再调用类的对象成员的默认构造函数。
3. “带有一个Virtual Function”的Class
假设一个class声明(或继承)了一个virtual function,那么以下两个扩张行动会在编译期间发生:(1) 一个virtual function table会被编译器产生出来,里面包含class的virtual functions地址。(2) 在每一个class object中,一个额外的vptr会被编译器合成出来,指向相关的class vtbl的地址。
所以编译器必须为继承体系中的每一个类的对象中的vptr设定初值。如果class定义了构造函数,那么编译器会在构造函数中安插代码做这些事情;如果class没有定义构造函数,那么编译器就会生成一个默认构造函数,用以正确地初始化每一个class object的vptr。
4. “带有一个Virtual Base Class”的Class(这一部分理解起来有点tricky)
(这一部分对构造函数影响的逻辑和3有点类似,都是因为class具有某些特性,所以编译器必须为我们多做一些事情,要么是在已有的构造函数中安插代码,要么是为我们合成一个默认构造函数。至于这个特性到底是什么,编译器需要为我们做些什么,可以抽象出来考虑。以下就来聊一聊,对于有虚基类的class,编译器需要额外做点什么。)
virtual base class的实现法在不同编译器之间有着极大的差异。然而,每一种实现法的共同点在于必须使virtual base class在其每一个derived class object中的位置,能够于执行期准备妥当。看以下这个例子:
class X { public: int i; };
class A : public virtual X { public: int j; };
class B : public virtual X { public: double d; };
class C : public A, public B { public: int k; };
void foo( const A* pa ) { pa->i = 1024 };
编译器在编译期间无法确定经由pa存取的X::i的实际偏移位置,因为pa的实际类型可以改变,所以编译器必须改变执行存取操作的那些代码,从而使X::i延迟到执行期再确定下来。
二、Copy Constructor的构造操作
什么时候会调用一个类的拷贝构造函数呢?当使用一个类的对象对另一个类的对象进行初始化时。
1. Default Memberwise Initialization
当class object以相同class的另一个object作为初值,且class没有提供一个显式的拷贝构造函数时,其内部是以所谓的“逐成员初始化”来完成的,也就是把每一个内建的或派生的data member的值,从一个object拷贝一份到另一个object身上。不过它并不会拷贝其中的member class object,而是以递归的形式实行“逐成员初始化”。
合成默认拷贝构造函数的逻辑和合成默认构造函数的逻辑是类似的,也分为trivial和non-trivial两种,像只有逐成员初始化这种就是trivial的,只有non-trivial的实例才会被合成于程序之中。那么怎么判断是否trivial的情况呢?这取决于class是否展现出所谓的“bitwise copy semantics(位逐次拷贝)”
2. Bitwise Copy Semantics
在以下4中情况下,一个class不展现出“bitwise copy semantics”
(1) 当class内含一个member object而后者的class声明有一个copy constructor时,不论是被class设计者显式地声明还是被编译器合成(也就是说,递归地执行逐成员初始化这种情况也是non-trivial的?)
(2) 当class继承自一个base class而后者存在一个copy constructor(再次强调,无论是显式声明还是被编译器合成)
(3) 当class声明了一个或多个virtual function时
(4) 当class派生自一个继承串链,其中有一个或多个virtual base class时
接下来重点讨论情况(3)和(4)
3. 重新设定virtual table的指针
如果编译器对于每一个新产生的class object的vptr不能成功且正确地设好初值,将导致可怕的后果,所以当编译器导入一个vptr到class之中时,class就不再呈现出bitwise semantics了。
所以在这种情况下,编译器会合成default copy constructor,指定object的vptr指向正确class的virtual table。因为copy constructor的参数是一个引用,所以也可能是派生类的类型。
4. 处理Virtual Base Class Subobject
一个class object如果以另一个object作为初值,而后者有一个virtual base class subobject,那么也会使bitwise semantics失效。
当用相同class的object初始化另一个object时,bitwise copy是足够使用的,但是如果使用派生类的object初始化基类的object时,编译器就必须合成一个copy constructor,安插一些代码以设定virtual base class pointer/offset 的初值,对每一个member执行必要的memberwise初始化,以及执行其他的内存相关工作。
这里还存在一个有趣的问题:当一个初始化操作存在并保持着bitwise copy semantics时,编译器是否应该抑制copy constructor的调用?下一个部分会专门讨论一下这个问题。
三、程序转化语意学(Program Transformation Semantics)
(这部分内容不做总结)
四、成员们的初始化队伍(Member Initialization List)
在下列4中情况下,为了保证程序正确编译,必须使用member initialization list:
(1) 初始化一个reference member时
(2) 初始化一个const member时
(3) 当调用一个base class的constructor,而它拥有一组参数时
(4) 当调用一个member class的constructor,而它拥有一组参数时
除这4种情况之外,如果不使用member initialization list也可以正确编译,但是效率不高,比如:
class Word{
String _name;
int _cnt;
public:
Word(){
_name = 0;
_cnt = 0;
}
}
这里Word的构造函数会先产生一个临时的String对象,然后将它初始化,之后以一个赋值运算符将临时对象指定给_name,然后再销毁这个临时性对象。
成员初始化列表中到底发生了什么?编译器会一一操作初始化列表,以适当顺序在构造函数之内安插初始化操作,并且在任何explicit user code之前。并且要注意的是:成员初始化的顺序是由class中members的声明顺序决定的,不是由Initialization list中的排列顺序决定的。