本来打算这一篇不拆分了,不过想想还是拆开吧,这样更容易看一定,一篇文章太长,不好看。这一篇就接着上一篇没有分析完的继续分析。
3.1 "带有一个virtual Function"的Class
这个看着英语就懂了,比较简单。
就是一个类中如果存在一个虚函数,或者继承父类有虚函数,编译器都会合成一个默认的构造函数。
合成这个构造函数的时候,编译器又插入了什么代码?下面我们来一探究竟。
3.1.1 没有构造函数
还是原来的操作,上代码:
#include <iostream>
#include "stdio.h"
class A
{
public:
virtual int fun1(int a);
};
int A::fun1(int a)
{
return a;
}
int main()
{
A a;
return 0;
}
我们来反汇编:
main:
.LFB1022:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movq %fs:40, %rax
movq %rax, -8(%rbp)
xorl %eax, %eax
leaq -16(%rbp), %rax
movq %rax, %rdi
call _ZN1AC1Ev // 类A的构造函数
movl $0, %eax
movq -8(%rbp), %rdx
xorq %fs:40, %rdx
je .L6
call __stack_chk_fail
编译器会默认合成类A的构造函数,接下来我们看看构造函数里做了啥:
_ZN1AC2Ev:
.LFB1024:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movq %rdi, -8(%rbp)
movl $_ZTV1A+16, %edx // 这个是重点
movq -8(%rbp), %rax
movq %rdx, (%rax)
nop
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
_ZTV1A是类A的虚函数表,+16是在虚函数表的开始偏移16个字节,就是把虚函数表+16个字节,赋值给类中的vptr指针。
(gdb) p a
$18 = {_vptr.A = 0x4008e8 <vtable for A+16>}
可以用gdb来查看这个类的变量。
我们可以简单看一下虚函数表的结构:
_ZTV1A:
.quad 0 # 虚函数表开始,为0
.quad _ZTI1A # type info 记录对象指针的真是类型,用于运行时多态
.quad _ZN1A4fun1Ei # 就是我们的虚函数地址
.weak _ZTI1A
.section .rodata._ZTI1A,"aG",@progbits,_ZTI1A,comdat
.align 8
.type _ZTI1A, @object
.size _ZTI1A, 16
这里就简单看一下,以后再详细分析虚函数表。
3.1.2 有构造函数的时候
如果我们写了构造函数呢?按照之前的经验,编译器会在我们写的构造函数中,插入必要的代码,如果是有虚函数,插入的就是初始化vptr指针等代码。
我们就用代码来看看:
class A
{
public:
A() // 插入部分
{
}
virtual int fun1(int a);
};
反汇编查看:
_ZN1AC2Ev:
.LFB1022:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movq %rdi, -8(%rbp)
movl $_ZTV1A+16, %edx
movq -8(%rbp), %rax
movq %rdx, (%rax)
nop
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
跟之前的反汇编代码差不多。
3.2 "带有一个virtual Base class"的class
还有最后一种情况,就是带有虚基类,出现这个虚基类的情况,是在菱形继承中出现的。
3.2.1 没有构造函数
我们就来上代码试试:
// 带有虚基类
#include <iostream>
using namespace std;
// 整体是菱形继承
class X
{
};
class A : virtual public X
{
};
class B : virtual public X
{
};
class C : public A, public B
{
};
int main(int argc, char **argv)
{
C cc;
return 0;
}
继承关系:
反汇编的结果:
main:
.LFB1021:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $48, %rsp
movl %edi, -36(%rbp)
movq %rsi, -48(%rbp)
movq %fs:40, %rax
movq %rax, -8(%rbp)
xorl %eax, %eax
leaq -32(%rbp), %rax
movq %rax, %rdi
call _ZN1CC1Ev // 生成了类C的构造函数
movl $0, %eax
movq -8(%rbp), %rdx
xorq %fs:40, %rdx
je .L7
call __stack_chk_fail
查看反汇编代码,就知道这个有生成类C的构造函数,类C构造函数中的代码,可以简单认识一下:
_ZN1CC1Ev:
.LFB1032:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movq %rdi, -8(%rbp)
movq -8(%rbp), %rax
movq %rax, %rdi
call _ZN1XC2Ev // 类X构造函数
movl $_ZTT1C+8, %edx
movq -8(%rbp), %rax
movq %rdx, %rsi
movq %rax, %rdi
call _ZN1AC2Ev // 类A构造函数
movl $_ZTT1C+16, %edx
movq -8(%rbp), %rax
addq $8, %rax
movq %rdx, %rsi
movq %rax, %rdi
call _ZN1BC2Ev // 类B的构造函数
movl $_ZTV1C+24, %edx
movq -8(%rbp), %rax
movq %rdx, (%rax)
movl $_ZTV1C+48, %edx
movq -8(%rbp), %rax
movq %rdx, 8(%rax)
nop
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
关于虚基类的详细分析,我们后面才分析。
3.2.2 有构造函数
当有构造函数的时候,编译器会自动给我们插入编译器需要的代码,这个我们已经很熟悉了。
class C : public A, public B
{
public:
C(){} // 增加的部分
};
返汇编结果就跟上面的一样,就不写了吧,大家可以自己去试试。
3.3 总结
侯捷老师都有总结,我也来总结一波。
我们这一节和上一节总结列出了四种情况,这四种情况都会导致编译器必须为未声明构造函数的类合成一个缺省的构造函数。
现在是否理解了编译器需要和程序员需要了?
编译器合成默认构造函数都是编译器需要,并且合成的默认构造函数只满足编译器,编译器不知道程序员需要干啥,编译器只知道编译器需要什么。
从四种情况中发现,编译器合成的构造函数,一般都是为了调用父类的构造函数或者是类中的对象的构造函数,如果存在虚函数,则准备虚函数表和指向虚函数表的指针,如果存在虚基类,也是为了调用各个父类的构造函数和生产虚虚函数表等。
没看到这个文章之前,我们也有两个常见的误解:
- 任何class如果没有定义构造函数,就会被合成出一个来。
- 编译器合成出来的默认构造函数会明确设定类内每一个成员变量的初始值。