C++标准提出:默认构造函数会在需要的时候被编译器生成。那什么时候被需要?被谁需要?做什么呢?
class Foo{
public:
int val;
Foo *next;
};
void foo_bar(){
Foo bar; // 这里要求bar的成员都被置于0
if(bar.val || bar.next){
}
}
上面程序语义是要求Foo有一个默认构造,可以将它的两个成员初始化为0。那这个时候是不是“需要的时候”?答案是no,其间的差别在于一个是程序员需要,一个是编译器需要。程序员如果需要是程序员的责任,所以上面片段并不会合成出一个默认构造函数
那么,什么时候才会合成出一个默认构造函数呢?当编译器需要它的时候!此外,被合成的构造函数只执行编译器所需要的行动。也就是说,即使有为class Foo合成一个默认构造函数,这个函数也不会将两个数据成员初始化为0。因此,类的设计者最好提供一个显式的默认构造函数,将两个成员适当的初始化。
C++95标准中说:对于class X,如果没有任何用户声明的构造函数,那么会有一个默认构造函数被暗中(implicitly)声明出来。而且这个被暗中声明出来的默认构造函数是一个没啥用的(trival)构造函数(平凡构造函数)
有四种情况会生成不平凡(nontrivial)默认构造函数
- 带有默认构造函数的类对象成员
- 带有默认构造函数的基类
- 带有虚函数的类
- 带有虚基类的类
带有默认构造函数的类对象成员
- 如果一个类没有任何的构造函数,但它包含一个成员对象,而后者有默认构造函数,那么这个类的隐式构造函数(implicit default constructor)就是不平凡(nontrivial)的,编译器需要为此类合成出一个默认构造函数。不过这个合成操作只有在构造函数真正需要被调用时才会发生
那么:在C++各个不同的编译模块中,编译器如何避免合成出多个默认构造函数?
- 解决方法是把合成的默认构造函数、拷贝构造函数、析构函数、赋值运算符(assignment copy operator)都以内联(inline)方式完成。
- 一个内联函数都有静态链接,不会被档案以外者看到。
- 如果函数太复杂,不适合做成内联,就会合成出一个显式非内联静态实体(explicit non-inline static)
举个例子,下面编译器将为类Bar合成一个默认构造函数:
class Foo{
public:
Foo(); Foo(int);
};
class Bar{
public:
Foo foo; // 注意,不是继承,是包含
char *str;
};
void foo_bar(){
Bar bar; // Bar::foo必须在此初始化
// Bar::for是一个成员对象,而其class fii
// 有默认构造函数
if(str){
}
}
- 被合成的Bar默认构造函数内含必要的代码,能够调用类Foo的默认构造函数来处理成员对象Bar::foo,但它并不产生任何代码来初始化Bar::str
- 编译器只负责初始化Bar::for,不会初始化Bar::str,Bar::str必须由调用者初始化
被合成的默认构造函数可能是这样:
inline Bar::Bar(){
foo.Foo::Foo();
}
- 注意,被合成的默认构造函数只满足编译器的需要,而不是程序的需要。
- 为了能让这个程序片段能够正确执行,字符指针str也需要被初始化。
假设程序员已经自己定义了一个默认构造函数用来初始化str:
// 用户自定义的默认构造函数
Bar::Bar(){str = 0;};
因为默认构造函数已经被明确定义了出来,编译器就没有办法合成第二个了,但是成员对象foo还没有被初始化,怎么办呢?
编译器是这样做的:
- 如果类A内含一个或一个以上的成员类对象,那么类A的每一个构造函数必须调用每一个成员类的默认构造函数。
- 编译器会扩张已存在的构造函数,在其中安插一些码,使得用户代码在被执行之前,先调用必要的默认构造函数:
// 扩张后的默认构造函数
Bar::Bar(){
foo.Foo::Foo(); // 附加上的编译器代码
str = 0; // 显式定义的用户代码(explicit user code)
}
如果有多个类成员对象都要求构造函数初始化操作,将如何呢?
- C++语言要求以类对象在类中的声明次序来调用各个构造函数。
- 这一点由编译器完成。它为每一个构造函数安插程序代码,以成员声明次序调用每一个成员所关联的默认构造函数。这些代码将被安插在显式用户代码(explicit user code)之前。
举个例子:
class Dopey {public: Dopey();};
class Sneezy {public: Sneezy(int); Sneezy();};
class Baseful{public: Baseful();};
class Snow_White{
public:
Dopey dopey;
Sneezy sneery;
Baseful baseful; // dopey、sneery、baseful是三个成员对象
private:
int numble;
};
上面Snow_White没有定义构造函数,就会有一个非凡构造函数(nontrivial constructor)被合成出来,依次调用Dopey、Sneezy、Baseful 的默认构造函数。
如果Snow_White定义了下面的默认构造函数:
// 用户自定义的默认构造函数
Snow_White::Snow_White() : sneezy(1024){
numble = 2048;
};
它会被扩张为:
// 编译器扩张后的默认构造函数
Snow_White::Snow_White() : sneezy(1024){
// 插入成员类对象
// 调用其构造函数
dopey.Dopey::Dopey();
sneezy.Sneezy::Sneezy(1024);
baseful.Baseful::Baseful();
// explicit user code
numble = 2048;
}
带有默认构造函数的基类
如果一个没有任何构造函数的类派生自一个“带有默认构造函数”的基类,那么这个派生类的默认构造函数将被视为非凡的(nontrivial),并因此需要被合成出来。
- 它将调用上一层基类(base classes)的默认构造函数(根据它们的声明次序)。
- 对一个后继派生的类而言,这个合成的构造函数和一个被明确提供的默认构造函数没有什么差异
如果设计者提供多个构造函数,但是其中没有默认构造函数呢?
- 编译器会扩张现有的每一个构造函数,将"用以调用所有必要的默认构造函数"的程序代码加进去。
- 它不会合成一个新的默认构造函数,这是因为其他由用户所提供的构造函数存在的缘故
- 如果同时存在着带有默认构造函数的成员类对象,那么默认构造函数也会被调用–在所有基类构造函数都被调用之后。
带有一个虚函数的类
另外有两种情况,也需要合成出默认构造函数:
- 类声明(或者继承)一个虚函数
- 类派生自一个继承串链,其中有一个或者更多的虚基类。
不管哪一种情况,由于缺乏有用户声明的构造函数,编译器会详细记录合成一个默认构造函数的必要信息。
举个例子:
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);
}
下面两个扩张操作会在编译期间产生
- 编译器产生一个虚函数表(vtbl),里面放置了类的虚函数地址
- 在每一个类对象中,编译器会合成一个成员指针(vptr),里面放置了类vtbl的地址。
此外,widget.flip()的虚拟引发操作(virtual invocation)会被重新改写,以使用widget的vptr和vtbl的flip()条目。
// widget.flip()的**虚拟引发操作**(virtual invocation)的转变
(*widget.vptr[1])(&widget)
其中:
- 1表示flip()在虚函数表中的固定索引
- &widget代表要交给被调用的某个flip()函数实体的this指针
为了让这个机制发挥功效,编译器必须为每一个widget(或其派生类)对象的vptr设定初值,放置适当的虚函数表地址。
对于类所定义的每一个构造函数,编译器会安插一些代码来做这样的事。
对于那些未声明任何构造的类,编译器会为它们合成一个默认构造函数,以便正确的初始化每一个类对象的vptr。
带有一个虚基类的类
虚基类的实现在不同的编译器之间有极大的差异,但是,每一个实现的共同点在于必须使虚基类在其每一个派生类对象中的位置,能够于执行期准备妥当。比如:
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;};
// 无法在编译实际决定出pa->X::i的位置
void foo(const A*pa){ pa->i = 1024;}
int main(){
foo(new A);
foo(new B);
}
编译器无法固定住foo()之中的"经由pa而存取的X::i"的实际偏移位置,因为pa的真正类型可以改变。编译器必须改变"执行存取操作"的那些码,使X::i可以延迟到执行期才决定下来。
原先cfront的做法是靠“在继承类对象的每一个虚基类中安插一个指针”完成。所有经由引用或指针来存取一个虚基类的操作都可以通过相关指针完成:
// 可能的编译器转变操作
void foo(const A * pa) {pa->_vbcX->x = 1024;};
- 其中_vbcX表示由编译器产生的指针,指向虚基类X
- _vbcX是在类对象构建期间完成的。
- 对于类所定义的每一个构造函数,编译器都会安插那些"允许每一个虚基类的执行期存取操作"的码。
- 如果类没有声明任何构造函数,编译器必须未它合成一个默认构造函数
总结
- 有四种情况,会导致编译器必须为未声明构造函数的类合成一个默认构造函数
- C++标准将这些合成物叫做隐式非凡默认构造函数(implicit nontrivial default constructors)
- 它之所以能够完成任务,是借着调用成员对象或者基类的默认构造函数或者为每一个对象初始化其虚函数机制或者虚基类机制完成
- 置于那些没有存在四种情况而又没有声明任何构造函数的类,它们拥有的是implicit trivial default constructors,它们实际上并不会被合成出来。
- 在合成的默认构造函数中,只有基类子对象(base class subobjects)和成员类对象(member class object)会被初始化。所有其他的非静态数据成员,比如整数、整数指针、整数数组等都不会被初始化。
C++新手的两个误解:
- 任何类如果没有定义默认构造函数,就会被合成出一个来
- 编译器合成出来的默认构造函数,会明确设定类中每一个数据成员的默认值
这两个都不是真的!!!