Default constructor的构造操作
1. 问:什么时候编译器才会自动合成一个默认的构造函数?
答:当编译器需要的时候。也就是说编译器合成的默认构造函数只完成编译器需要的操作,不会做其他程序员需要做的事。主要在四种情况下,编译器会合成比较有用的默认构造函数,其他情况即使合成,也是没多大用途的(有可能就不会合成了),四种情况是:
- 该类内含一个对象,该对象有一个显式或隐式的构造函数。
- 该类继承自一个类,基类有一个显式的或隐式的构造函数。
- 该类含有虚方法。那么编译器需要给对象合成vptr,所以编译器需要做一些构造函数的事。
- 该类的继承体系中存在虚拟继承。编译器需要做一些工作指出基类子对象的位置。
2. 问:因为编译器要做一些它需要的工作,那么编译器怎么做?
答:当程序员有自己定义的构造函数时,编译器会在程序员定义的构造函数里添加代码,添加的代码在程序员的代码之前。当程序员没有自己定义构造函数时,编译器就添加一个构造函数完成编译器需要的工作。总结就是扩张或添加。
看例子学知识:
例1. 编译器不会为你初始化
class Foo{ public: int val; Foo *pnext; };
void foo_bar(){
//程序要求bar's 的members 都被清0
Foo bar;
if (bar.val || bar.pnext)
//... do something
//...
}
这个例子旨在检验,如果用户不自己定义默认构造函数的话,会是个什么情况。首先把这个例子补全
#include <iostream>
using namespace std;
class Foo{ public: int val; Foo *pnext; };
void foo_bar(){
//程序要求bar's 的members 都被清0
Foo bar;
if (bar.val || bar.pnext){
//... do something
cout << "用户未定义构造函数,编译器不会去初始化这些members" << endl;
}
//...
}
int main(){
foo_bar();
return 0;
}
在ubuntu 14.04运行结果是:
这就说明,user(指初级程序员)不去定义构造函数来初始化members,以为编译器会有默认的构造函数来初始化members,那么纠错了,此例证明编译器并没有去为user初始化这些成员。
而且,结合问题1,即使编译器去初始化了一些变量,编译器也不会初始化除编译器需要的以外的变量,即user自己定义的简单变量必须有user显式的初始化。
例2. 编译器对默认构造函数的合成或扩张
class Foo {public: Foo(), Foo(int) ... };
class Bar {public: Foo foo; char *str; }; //此处是内含,不是继承
void foo_bar()
{
Bar bar; //Bar::foo bar的这个成员必须在此处初始化
//译注:Bar::foo 是一个member object, 而其
//class Foo 拥有default constructor,编译器会对其进行初始化。
if(str) { } ...
}
此例旨在说明,内含一个对象,且这个成员对象有一个默认的构造函数。则此时编译器会合成一个默认构造函数,在这个默认构造函数里调用成员对象的构造函数来初始化成员对象。但是请注意,对于member str,编译器依旧不会初始化它,它的初始化必须有user来完成。
编译器在调用到时候合成一个默认构造函数,对于上例子,合成的默认构造函数很可能长这样:
inline Bar::bar()
{
//C++ 伪码
foo.Foo::Foo();
}
刚才说了,编译器只做它该做的事(估计是C++标准委员会要求编译器实现的一些事吧),他不会去替user初始化str,所以程序需要有显式的初始化str操作。假设程序定义了如下初始化str的操作:
Bar::Bar() { str = 0; }
这是user自己定义的默认构造函数,并在该构造函数里初始化了str。这是正确的,程序员应该这么做。接下来该编译器做了,编译器做什么,因为程序还不能满足C++的运行要求,编译器需要做的就是把bar里的成员对象给一个一个的初始化。所以编译器会扩充user写的这个默认的构造函数。大致如下:
//扩张后的default constructor
//C++ 伪码
Bar::Bar()
{
foo.Foo::Foo(); //附加上的 compiler code
str = 0; //显式的 user code
}
编译器扩张的代码,会在user code之前执行。
例3. 如果有多个成员对象要constructor初始化操作,编译器按照其声明顺序逐个调用其constructor
class Dopey { public: Dopey(); ... };
class Sneezy { public: Sneezy(int); Sneezy(); ... };
class Bashful { ublic: Bashful(); ...};
class Snow_White{
public:
Dopey dopey;
Sneezy sneezy;
Bashful bashful; //dopey, sneezy, bashful 是三个成员对象
// ...
private:
int mumble;
};
对这个例子,首先看类是否显式定义了默认构造函数,若没有,(在需要时)编译器会为其合成默认构造函数,并按照在类中的声明顺序依次调用成员对象对应类的构造函数进行初始化。
当然,这个类因为有普通成员mumble,该成员编译器是无论如何也不会帮忙初始化的,所以程序员需要自己写代码显式初始化。假设程序员定义了如下的构造函数:
//程序员所写的 default constructor
Snow_White::Snow_White():Sneeze(1024)
{
mumble = 2048;
}
这样的话,它可能会被扩张为如下的样子:
//编译器扩张后的 default constructor
//C++ 伪码
Snow_White::Snow_White()
{
//插入的member object,调用其constructor
dopey.Dpoey::Dopey();
sneezy.Sneezy::Sneezy(1024);
bashful.Bashful::Bashful();
//explicit user code
mumble = 2048;
}
例4. 含有虚方法时,编译器做那些事
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);
}
看这个例子的话,编译器会做什么呢。
无论如何编译器都会对user的代码进行扩张,来满足虚方法的支持。具体来说,编译器会做以下两件事扩张。
- 为每个类创建一个虚方法表(virtual table,简称vtbl),虚方法表里放置相关虚方法的地址。
- 为含有虚方法的类的每一个对象,安插一个vptr(指向虚方法表的指针)。
此外,编译器会改写flip的调用代码。因为flip是虚方法,所以它会被改写成
(*widget.vptr[1])(&widget);
//1 表示flip在virtual table 里的固定索引
//&widget 代表要交给“被调用的flip函数实例的”this指针。
- 因为编译器必须为每一个类对象安插vptr指针,其指向对应类的virtual table。
- 所以如果类定义的有构造函数,编译器会对类的所有构造函数都进行扩张,以加入vptr,来支持虚方法机制。
- 如果类没有定义构造函数,编译器就给其合成一个构造函数,为类对象加入vptr。
例5. 含虚基类时,编译器做哪些事
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; };
//无法在编译期决定(resolve)出 pa->X::i 的位置
void foo(const A* pa) { pa->i = 1024; }
main()
{
foo( new A );
foo( new C );
// ...
}
在这个例子中,编译器要做的是保证virtual base class 在derived class object 中的位置,在执行期准备妥当。也就是说编译器要在派生类的每个子对象里,安插一个指向虚基类的指针。这样函数foo(const A* pa)可以转化为:
void foo(const A* pa) { pa->vbcX->i = 1024; }
(根据第四章方法语意学,指向派生类中的基类子对象的指针有可能在虚方法表的-1位置)。
总结
1. 编译器只做编译器自己的事。
2. 编译器会帮助初始化成员对象。
3. 编译器会帮助初始化基类(子对象)。
4. 编译器会帮助实现虚方法机制(虚表和虚指针)。
5. 编译器会帮助实现虚基类机制(虚基类子对象指针或偏移)。