C++小实验测试:下面程序中main函数里a.a和b.b的输出值是多少?
#include <iostream> struct foo { foo() = default; int a; }; struct bar { bar(); int b; }; bar::bar() = default; int main() { foo a{}; bar b{}; std::cout << a.a << '\t' << b.b; }
答案是a.a是0,b.b是不确定值(不论你是gcc编译器,还是clang编译器,或者是微软的msvc++编译器)。为什么会这样?这是因为C++中的初始化已经开始畸形发展了。
接下来,我要探索一下为什么会这样。在我们知道原因之前,先给出一些初始化的概念:默认初始化,值初始化,零初始化。
T global; //T是我们的自定义类型,首先零初始化,然后默认初始化 void foo() { T i; //默认初始化 T j{}; //值初始化(C++11) T k = T(); //值初始化 T l = T{}; //值初始化(C++11) T m(); //函数声明 new T; //默认初始化 new T(); //值初始化 new T{}; //值初始化(C++11) } struct A { T t; A() : t() //t将值初始化 { //构造函数 } }; struct B { T t; B() : t{} //t将值初始化(C++11) { //构造函数 } }; struct C { T t; C() //t将默认初始化 { //构造函数 } };
上面这些不同形式的初始化方式有点复杂,我会对这些C++11的初始化做一下简化:
默认初始化:如果T是一个类,那么调用默认构造函数进行初始化;如果是一个数组,每个元素默认初始化,否则不进行初始化,其值未定义。至于合成的默认构造函数初始化数据成员的规则是:1.如果类数据成员存在类内初始值,则用该值初始化相应成员(c++11);2.否则,默认初始化数据成员。
值初始化:如果T是一个类,那么类的对象进行默认初始化(如果T类型的默认构造函数不是用户自定义的,默认初始化之前先进行零初始化);如果是一个数组,每个元素值初始化,否则进行零初始化。
零初始化:对于static或者thread_local变量将会在其他类型的初始化之前先初始化。如果T是算数、指针、枚举类型,将会初始化为0;如果是类类型,基类和数据成员会零初始化;如果是数组,数组元素也零初始化。
看一下上面的例子,如果T是int类型,那么global和那些T类型的使用值初始化形式的变量都会初始化为0(因为int是内置类型,不是类类型,也不是数组,将会零初始化,又因为int是算术类型,如果进行零初始化,则初始值为0)而其他的默认初始化都是未定义值。
回到开头的例子,现在我们已经有了搞明白这个例子所必要的基础知识。造成结果不同的根本原因是:foo和bar被它们不同位置的默认构造函数所影响。
foo的构造函数在起初声明时是要求默认合成,而不是我们自定义提供的,因此它属于编译器合成的默认构造函数。而bar的构造函数则不同,它是在定义时被要求合成,因此它属于我们用户自定义的默认构造函数。
前面提到的关于值初始化的规则时,有说明到:如果T类型的默认构造函数不是用户自定义的,默认初始化之前先进行零初始化。因为foo的默认构造函数不是我们自定义的,是编译器合成的,所以在对foo类型的对象进行值初始化时,会先进行一次零初始化,然后再调用默认构造函数,这导致a.a的值被初始化为0,而bar的默认构造函数是用户自定义的,所以不会进行零初始化,而是直接调用默认构造函数,从而导致b.b的值是未初始化的,因此每次都是随机值。
这个陷阱迫使我们注意:如果你不想要你的默认构造函数是用户自定义的,那么必须在类的内部声明处使用"=default",而不是在类外部定义处使用。