0、前言
《The C++ ARM》告诉我们:“默认构造函数会在需要的时候自动生成(被编译器)。”然后“在需要的时候”是一个很抽象的概念,本文主要描述的问题也正是这些需要的情况。
我们看看下面的代码片段:
class Foo
{
public:
int val;
Foo *pnext;
};
void foo_bar()
{
Foo bar;
if (bar.val || bar.pnext)
{
cout << bar.val << endl;
cout << bar.pnext << endl;
}
}
用户并没有显示地定义默认构造函数,编译器会为它自动生成一个无关紧要(trivial)的默认构造函数,生成的默认构造函数什么也不错,既不会讲其成员变量置零,也不会做其他的任何事情,只是为了保证程序能够正确运行而已,这就是所谓的“需要”,如果还需要给初始化成员变量,这件事情还是交给程序员做吧!
一、非平凡(non-trivival)默认构造函数
C++标准描述了哪些情况,这样的隐式默认构造函数是无关紧要的。一个非平凡(non-trivival)的默认构造函数是ARM中所说的被实现所“需要”,并在必要的时候被编译器自动生成。下面来看看默认构造函数是非平凡的四种情况:
1.1 含有包含默认构造函数的成员类对象
如果该类包含一个成员类对象,它有默认的构造函数,那么这个类的隐式构造函数是非平凡的,并且编译器需要为包含的这个成员类对象生成一个默认构造函数。然后,这个编译器生成的默认构造函数只有在实际上被调用时才会被真正的生成。
class Foo
{
public:
Foo(){ _i = 1; }
int _i;
};
class Bar
{
public:
Foo foo;
char *str;
};
void foo_bar()
{
Bar bar;
}
在这个程序片段中Bar的成员foo含有默认构造函数,它初始化自己的类成员_i为1而Bar本身并没有定义默认的构造函数,这个构造函数的目的是为了初始化它的成员变量foo,实际上就是调用Bar::foo的默认构造函数,但它并不会做一丁点关于另外一个变量str的初始化和赋值工作,初始化Bar::foo是编译器的责任,二初始化str是程序员的。我们可以用以下代码来大致描述一下编译器的工作:
inline
Bar::Bar()
{
// Pseudo C++ Code
foo.Foo::Foo();
}
如果这里的Bar含有默认构造函数呢?我们可以从编译器和程序员的责任划分来考虑这个问题:
1. Bar的默认构造函数调用foo的默认构造函数,那么编译器啥也不用做了。
2. Bar的默认构造函数中没有去foo这个成员变量,那么编译器需要去帮助程序员把这件事情做完,插入一条类似“foo.Foo::Foo();”的代码。
注:如果Bar含有多个成员类变量,则编译器会按照这些变量的声明顺序去做以上处理。
1.2 含有包含默认构造函数的基类
类似的,要是一个继承的基类包含默认构造函数而该类本身没有任务的构造函数,那么编译器会生成一个默认构造函数,目的是初始化它的基类。
当程序员为该类定义了多个构造函数,就是没定义默认构造函数呢?
在这种情况下,编译器会在每一个构造函数中增加(augment)有关调用基类的默认构造函数部分代码。
1.3 含有虚函数
生成的默认构造函数是必须的当另外两个额外条件(满足其一):
1.该类定义了(或继承了)虚函数。
2.在该类的继承关系中,有一个或更多的虚基类。
可以参考一下的类继承关系:
class Widget
{
public:
virtual void flip() = 0;
};
class Bell: public Widget
{
public:
void flip(){ cout <<"Bell." << endl; }
};
class Whistle: public Widget
{
public:
void flip(){ cout <<"Whistle." << endl; }
};
void flip(Widget &widget)
{
widget.flip();
}
void foo()
{
Bell b;
Whistle w;
flip(b);
flip(w);
}
在编译时,在默认构造函数中会发生下面的两个类扩充(augmentation):
1.虚表会被产生,其内容被这个类的活动(active)虚函数填充。
2.编译器为每个类对象生成一个虚指针(vtbl)。
1.4 含有一个虚基类
关于虚基类的内容,本文暂不详述,需要记住的是:编译器会为该类添加一个类似虚指针的东西——“_vbcX”。
二、参考文献
《Inside The C++ Object Model》