一般的C++书籍上会告诉你,当一个类没有声明任何构造函数时,编译器会自动的生成默认构造函数。但实际的情况好像不是这样的,当一个类没有声明任何构造函数,且在必要的时候编译器才会帮助合成默认构造器,在一些非必要的时候,编译是不会帮助合成默认构造器的。
下面通过把C++编译成汇编的方式,分析哪四种常见情况下,编译器会帮助合成默认构造器。
环境
https://godbolt.org/,这个网站可以方便的将C++代码编译成汇编,同时使用颜色标识某条C++语句对应的汇编代码。
在使用之前需要可以根据自己的习惯配置一下。
使用的gcc版本是10.2
-m32
:将C++代码编译成32位的汇编
-O0
:不要进行编译优化
Intel asm syntax
:编译成intel格式的汇编,默认是AT&T格式的汇编。
Demangle identifiers
:反命名倾轧,在汇编代码中使用C++中函数的名字,这样更便于阅读。
编译器并没有帮助合成默认构造器
此时类A并没有手动提供任意构造器,且编译器也没有帮助合成默认构造器。
类内数据成员是类对象
- 该类没有手动提供任何构造器
- 类内部有一个数据成员是类对象
- 该类对象有默认构造函数(手动提供或编译器帮助合成的)
先看一个反例,此时类B内部虽然有类A对象a,但类A内没有默认构造函数,所以编译器并不会给类B合成默认构造器。
给类A手工提供了一个默认的构造器,此时类B内部在定义类A对象a时,需要调用类A的默认构造器。又因为类B没有默认构造器,所以编译器帮助它合成一个默认的构造器,并在这个构造器中调用了类A的默认构造器。
类B虽然没有手动提供默认构造器,但其内部数据成员类A有默认构造器,类B为了调用类A的默认构造器,所以编译器为其合成了一个默认构造器,且内部调用了类A的默认构造器。此时类B有了编译器合成的默认构造器,类B有又作为类C的数据成员,且类C内部没有手动提供默认构造器,为了调用类B的默认构造器,编译器会为类C合成一个默认构造器,并在合成的构造器中调用类B的默认构造器。
父类有默认构造器
- 父类有默认构造器(手动提供的或编译器帮助合成的)
- 子类内部没有任何构造器
当创建一个子类对象时,父类的默认构造器需要被调用,又因为子类中没有默认构造器,所以为了调用父类的默认构造器,编译器会为子类合成一个默认的构造器,并在其中调用父类的默认构造器。
类内有虚函数
- 类内有虚函数
- 类内没有任何构造器
因为类内有虚函数,所以这个类需要有一个指向虚函数表的指针,为了初始化这个指针,编译器会为这个类合成一个默认构造器,在这个构造器中会对类对象的虚函数表指针进行初始化,使其指向类的虚函数表。
A::A() [base object constructor]:
push ebp
mov ebp, esp
mov edx, OFFSET FLAT:vtable for A+8 #获取指向虚函数表的指针
mov eax, DWORD PTR [ebp+8] #获取对象a的地址
mov DWORD PTR [eax], edx #将虚函数表指针写入对象a的前四个字节
nop
pop ebp
ret
main:
lea ecx, [esp+4]
and esp, -16
push DWORD PTR [ecx-4]
push ebp
mov ebp, esp
push ecx
sub esp, 20
sub esp, 12
lea eax, [ebp-16] #获取对象a的地址
push eax #对象a的地址,作为A::A()调用前的参数,压栈。
call A::A() [complete object constructor]
add esp, 16
mov eax, 0
mov ecx, DWORD PTR [ebp-4]
leave
lea esp, [ecx-4]
ret
有虚继承
- 类带有虚基类
- 类内没有任何构造器
类使用了虚继承,那么需要为类初始虚基表指针,此时编译器会合成一个默认构造器,再提供相应的代码,以用于类对象内虚基表指针的初始化。