最近在看《深度探索C++对象模型》,看的头大。对于c++默认构造函数,一直有两个错误的认识。
1,没有定义默认构造函数的类都会被编译器生成一个默认构造函数。
2,编译器生成的默认构造函数会初始化所有数据成员。
而lippman在书中告诉我们 以上两个理解是错误的。
先看第2条,举个例子。
#include <iostream>
using namespace std;
class Base {
public:
~Base() {}
int val;
};
int main()
{
Base b;
cout << b.val << endl;
return 0;
}
编译运行上述代码,可以得到结果
-1218625548, 显然这是未初始化。所以编译器生成的默认构造函数并不初始化所有成员,比如int,char等。
C++标准规定:如果类的设计者并未为类定义任何构造函数,那么会有一个默认 构造函数被暗中生成,这个暗中生成的默认构造函数通常是不做什么事的,而我们通常所说的默认构造函数是有用的,也就是所谓的 nontrivial default constructor。
那么伟大的编译器会啥时候善解人意的产生有用的默认构造函数呢,lippman提到了四点。
1,如果类的某个数据成员有 有用的 默认构造函数,则编译器就会为该类产生nontrivial default constructor.
如果一个类A没有定义一个构造函数,但是它的一些数据成员有默认构造函数,则此时编译器就会为它合成默认构造函数,该合成的默认构造函数会调用对象数据成员的默认构造函数来进行初始化。但是对那些没有默认构造函数的数据成员,合成的默认构造函数不会进行初始化,比如说,A的对象数据成员有string类型,和int类型,这个时候编译器会合成默认构造函数只会调用string类的构造函数,但是不会初始化int类型。如果用户自己定义了默认构造函数,初始化了int类型, 但是没有“理会”string类型成员变量,这个时候,编译器就会对用户自定义的默认构造函数扩展,插入string默认构造函数的代码。
#include <iostream>
using namespace std;
class Base {
public:
Base():val(5) {cout << "user define" << endl;}
~Base() {}
int val;
};
class A {
public:
A() : A_val(3) {} //用户自定义的构造函数,只明确初始化A_val; 方式1
~A() {}
int A_val;
Base b;
};
int main()
{
A a;
cout << a.A_val << endl;
return 0;
}
如果方式1不注释掉,则输出(实际上对用户自定义的默认构造函数进行了扩充)
user define
3
方式1注释掉,则输出
user define
134514777
可以看出,实际上默认构造函数没有初始化A_val;只调用了Base默认构造函数。
2, 继承自带有默认构造函数的基类的类。
如果一个没有类没有定义默认构造函数,但是它派生于一个带有默认构造函数的基类,于是乎,编译器又为该类合成默认构造函数。若该类定义了其他构造函数但是没有定义默认构造函数,则编译器扩充所有构造函数,在所有构造函数中加入基类构造函数。且将基类默认构造函数放在成员变量默认构造函数之前。
3,带有虚函数的类。
有虚函数的类,类的结构里面多了一个vptr,而vptr是由编译器设定的,因此编译器会为带有虚函数的类合成默认构造函数,以此来初始化vptr。不管你是否自定义默认构造函数,都会有初始化vptr的步骤。
4,带有虚基类的类。
编译器要将虚基类在类中的位置准备妥当,以此提供支持虚基类的机 制。也就是说要在所有构造函数中加入实现虚基类的的代码。没有构造函数将合成默认构造函数以完成上述工作。
C++ standard将合成的默认构造函数分为 trivial 和 notrivial 两种,前面所讲的四种情况对应于notrivial默认构造函数,其它情况都属于trivial。对于一个trivial默认构造函数,编译器的态度是,既然它全无用处,干脆就不合成它。