在以下四种情况中,如果类中没有定义一个默认构造函数,则编译器会自动生成一个nontrivial的默认构造函数,而不是一个不做事情的trivial默认构造函数:
1.内含一个成员变量,而这个成员变量所属的类中含有默认构造函数,则此时需要为此类生成一个implicit default constructor(隐式的默认构造函数),这个implicit default constructor是nontrivial的,因为内含的成员变量需要进行默认构造操作。如果内含一个成员变量a,且该成员变量a含有默认构造函数。同时已经对该类定义了默认构造函数但未对a进行操作,则编译器会在自己定义的默认构造函数的开始部分插入一个a所属类的默认构造函数,不然这个自己定义的默认构造函数将会忽视掉a的nontrival默认构造函数。
2.继承自一个类,且该类中有默认构造函数。派生类中没有定义默认构造函数,则编译器会为派生类提供一个上一层基类的默认构造函数。
3.class中声明了一个虚函数。声明了虚函数代表该class中将出现vptr,并需要为虚函数构造一张虚函数表vtbl,这个操作是必须的,因此使该class的默认构造函数成为nontrivial的,因此需要为了这个vptr和vtbl构建默认构造函数,进行初始化操作。
4.带有虚基类的class。因为虚基类的引入,必须要有一个指针或者类似索引的东西来指向虚基类的区域,以使虚基类的派生类们能找到共享的虚基类的存储区域。
如:
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};
main()
{
............
}
其中,因为pa的类型可以改变,它可以是A*,也可以是C*,因此,在编译期并不能确定pa指向的对象到底是什么,而只能等到执行期才能固定pa->i的具体位置(因为pa可能不同,而虚基类一般是存在最底部的,因此pa->i相对于类的起始地址的偏移会随着pa的不同而不同)。因此,为了实现在执行期能确定i的位置,需要在类中在引入一个指针或类似索引的东西,这样,在编译期就可以转化为 pa->_vbcX->i=1024;而_vbcX是由编译期所产生的指针。这样无论pa是A*类还是C*类,他们都是经由_vbcX找到的i,只要在A中与C中都引入_vbcX,这样他们在执行期都能通过同样的方式来确定i的位置。
而正因为需要引入_vbcX这种形式的指针,因此使该类的初始化变得nontrivial,因此需要引入一个nontrivial的默认构造函数,使类在创建时期就构建一个_vbcX指针。
C++新手一般有两个常见的误解:
1.任何ckass如果没有定义default constructor,就会被合成一个出来。
实际上,只有上述四种情况中才会生成默认构造函数,因为只有上述四种情况下默认构造函数才是nontrivial的,而诸如类中int a;的初始化,这对编译期来说是trivial的,它的初始化是应该交由程序猿们来实现的。
2.编译期合成出来的default constructor会明确设定"class内每一个data member的默认值"。
实际上,只有nontrivial的成员才需要default constructor对他进行初始化,如类中有默认构造函数的成员变量,vptr.....这种情况。
而trivial的成员的初始化工作不是编译期的职责而是程序设计者的职责。
相反,如果class设计的默认构造函数只对trivial的成员初始化了,编译期则会自己在所定义的默认构造函数中插入对nontrivial的初始化操作(按照声明优先顺序)。