“default constructors...在需要的时候被编译器产生出来“。被谁需要?做什么事情?看如下代码:
class Foo{
public:
int val;
Foo* pnext;
};
void foo_bar()
{
//程序要求bar’s members都被清零
Foo bar;
if(bar.val || bar.pnext)
//...do something
//...
}
例子中,正确的程序予以是要求Foo有一个default constructor,可以将它的两个members初始化为0。上述只会隐式声明出不会初始化对象的default constructor,初始化对象是程序员的责任。
对于class X而言,如果没有任何user-declared constructor,那么会有一个default constructor被隐式(implicitly)声明出来,一个被隐式声明出来的default constructor将是一个trivial(无用)constructor。
C++ Standard然后会一一叙述什么情况下这个implicit default constructor会被视为他trival。一个nontrivial default constructor在ARM语义中就是编译器需要的那种,必须的话会被编译器合成。下面4小结分别讨论nontrivial default constructor的4种情况。
“带有Default Constructor”的Member Class Object
如果一个class没有任何constructor,但它内含一个member object,而后者有default constructor,那么这个class的implicit default constructor就是“nontrivial”,编译器需要为该class合成一个default constructor。不过这个合成操作只有在constructor真正需要被调用的时候才会发生。
于是有个问题:在C++各个不同的编译模块中,编译器如何避免合成出多个default constructor(比如一个在A.C文件合成,一个B.C文件合成)呢?解决方法是把合成的default constructor、copy constructor、destructor、assignment copy constructor都以inline方法完成。一个inline函数有静态连接,不会被文件以外者看到。如果函数太复杂,不适合做inline,就会合成一个explicit non-inline static实例。
如下例子,下面的程序片段,编译器为class Bar合成一个default constructor:
class Foo{
public:
Foo();
Foo(int);
...
};
class Bar
{
public:
Foo foo;
char* str;
};
void foo_bar
{
Bar bar;//Bar::foo必须在此处出书哈
if(str)
{
}
...
}
被合成的Bar 的default constructor内含必要的代码,能够调用class Foo的default constructor来处理member object Bar::foo,但它不会产生任何代码来处理Bar::str,将Bar::foo初始化时编译器的责任,将Bar::str初始化时程序员的责任。被合成的default constructor看起来像这样:
Bar::Bar()
{
//C++伪代码
foo.Foo::Foo();
}
被合成的default constructor只满足编译器的需要,而不是程序的需要。如果程序员在default constructor中初始化了str,编译器还是要初始化member object foo。由于default constructor已经被显示定义出来了,编译器无法合成第二个。
编译器采取的行动是“如果class A内含一个或一个以上的member class object,那么class A的每一个constructor必须调用每一个member classes的default constructor”。编译器会扩张已存在的constructors,在其中安插代码,使得user code被执行之前,先调用必要的default constructor。
如果有多个class member objects都要求constructor初始化操作,C++语言要求“member objects 在class中的声明顺序”来调用各个constructors。这一点由编译器你完成,它为每一个constructor安插代码,以“member声明顺序”调用每一个member所关联的default constructors。这些代码将被安插在explicit user code之前。
如果像上面的有explicit default constructor,程序员只初始化了内置的成员,那么编译器还是要在explicit default constructor中安插初始化member object的代码,按照它们声明的顺序来安插、
“带有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,编译器会扩展每一个现在的constructor,将“用以调用所有必要之default constructors”的程序代码加进入。它不会合成一个新的default constructor,因为其他“由user所提供的constructors”存在的缘故。如果同时亦存在“带有default constructor”的member class objects,那么default constructor也会被调用——在所有base classconstructor都被调用之后。
“带有一个Virtual Function”的Class
另有两种情况,也需要合成default constructor:
-
class声明(或继承)一个virtual function。
-
class派生自一个继承串链,其中有一个或更多的virtual base classes。
不管哪种情况,由于缺乏由user声明的constructors,编译器会详细记录合成一个default constructor的必要信息。如下的程序片段:
class 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 function地址上。
-
在每一个class object中,一个额外的pointer member(也就是vptr)会被编译器和好处能出来,内含相关的vtbl的地址。
此外,widget.flip()的虚拟调用操作(virtual invocation)会被重写。以使用widget的vptr和vtbl的flip()条目:
//widget.flip()的虚拟调用操作的转变
(*widget.vptr[1])(&widget);
- 1表示flip()所在的virtual table中的固定索引;
- &widget代表要交给“被调用的某个flip()函数实例”的this指针。
为了让这个机制发挥功效,编译器必须为每一个Widget(或其派生类的)object的vptr设定初值,放置适当的virtual table地址。对于class所定义的每一个constructor,编译器会安插一些代码来做这些事情。对于那些未声明的constructor的classes,编译器会为它们合成一个default constructor,以便正确初始化每一个class object的vptr。
“带有一个Virtual Base Class”的Class
Virtual base classs的实现手法在不同的编译器之间极大的差异。然而,每一种实现法的共同点在于必须使virtual base class在其每一个derived class object中的位置,能够执行期准备妥当。例如下面的程序代码中:
class X
{
public:
int i;
};
class A: virtual public X
{
public:
int j;
};
class B: virtual public X
{
public:
double d;
};
class C: public A,public B
{
public:
int k;
};
//无法在编译器决定(resolve)出pa->X::i的位置
void foo(const A* pa)
{
pa->i = 1024;
}
main
{
foo(new A);
foo(new C);
}
编译器无法固定住foo()之中“经由pa而存取的X::i”的实际偏移位置,因为pa的真正类型可以改变。编译器必须改变“执行存取操作”的那么码,使X::i可以延迟到执行期才决定下来。原先的做法是靠“在derived class object”的每一个virtual base classes中安插一个指针来完成。所有“经由reference货pointer来存取一个virtual base class”的操作都可以相关指针完成。在这个例子中,foo()可以被改下如下,以符合这样的策略:
//可能的编译转变操作
void foo(const A* pa)
{
pa->__vbcX->i = 1024;
}
其中__vbcX表示编译器所产生的指针,指向virtual base class X。
正如你臆想的那样,__vbcX(或编译器所做出的某个什么东西)是在class object建构期间完成的。对于class所定义每一个constructor,编译器会安插那些“允许每一个virtual base class的执行期存取操作”的码。如果class没有声明任何constructors,编译器必须为它合成一个default constructor。
总结
有四种情况,会导致”编译器必须为未声明的constructor之classes合成一个default constructor“。C++ Stardand把那些合成物称为implicit nontrivial default constructors。被合成出来的constructor只能满足编译器(非程序)的需要。它之所以能完成任务,是借着”调用member object或base class的default constructor“或是”为每一个object初始化其virtual function机制或virtual base class机制“而完成。至于没有存在那四种情况又没有声明任何constructor的classes,我们说它们拥有的是implicit trivial default constructors,它们实际上并不会被合成出来。
在合成的default constructor中,只有base class subobjects和member class objects会被初始化。所有其他的nonstatic data member,如整数、整数指针、整数数组等等不会被初始化。这些初始化操作对程序而言或许是需要的,但是对编译器并非必要。如果程序需要一个”把某指针设为0“的default constructor,那么提供它的人应该是程序员。
C++新手一般两个常见的误解:
- 任何class如果没有定义default constructor,就会被合成一个出来。
- 编译器合成出来的default constructor会明确设定”class 内每一data member 的默认值“。
如你所见,没一个是真的。