当你构造一个派生类的实例时,过程会像下面这样:
- 第一步:构造最顶层的基类部分
- 让实例指向基类的虚函数表
- 构造基类实例成员变量
- 执行基类构造函数
- 第二步:构造派生部分(递归的)
- 让实例指向派生类的虚函数表
- 构造派生类实例成员变量
- 执行派生类构造函数
析构时则是按相反的顺序,就像这样:
- 第一步:析构派生部分(递归的)
- (实例已经指向派生类的虚函数表)
- 执行派生类析构函数
- 析构派生类实例成员变量
- 第二步:析构基类部分(递归的)
- 让实例指向基类的虚函数表
- 执行基类析构函数
- 析构基类实例成员变量
示例代码
#include <stdio.h>
class base
{
public:
base(){}
~base(){}
virtual void foo() = 0;
};
class derived : public base
{
public:
derived(){}
~derived(){}
void foo(){ printf("hello world\n");}
};
int main(void)
{
derived son;
return 0;
}
用它的汇编代码证明:
派生类构造的时候
Dump of assembler code for function derived::derived():
0x000000000040089e <+0>: push %rbp
0x000000000040089f <+1>: mov %rsp,%rbp
0x00000000004008a2 <+4>: sub $0x10,%rsp
0x00000000004008a6 <+8>: mov %rdi,-0x8(%rbp) # 一般x64 this指针用rdi传参
0x00000000004008aa <+12>: mov -0x8(%rbp),%rax
0x00000000004008ae <+16>: mov %rax,%rdi
0x00000000004008b1 <+19>: callq 0x400834 <base::base()>
0x00000000004008b6 <+24>: mov -0x8(%rbp),%rax # rax = this
0x00000000004008ba <+28>: movq $0x4009f0,(%rax) # this[0] = 0x4007f0,实例内存的第一项为虚表指针,这里即修改虚表指针
0x00000000004008c1 <+35>: leaveq
0x00000000004008c2 <+36>: retq
End of assembler dump.
(gdb) x/gx 0x4009f0 # 打印出来是derived的虚表偏移2个宽字,虚表前面的两个宽字用于存储RTTI信息
0x4009f0 <_ZTV7derived+16>: 0x00000000004008c4
(gdb) disassemble base::base
Dump of assembler code for function base::base():
0x0000000000400834 <+0>: push %rbp
0x0000000000400835 <+1>: mov %rsp,%rbp
0x0000000000400838 <+4>: mov %rdi,-0x8(%rbp)
0x000000000040083c <+8>: mov -0x8(%rbp),%rax
0x0000000000400840 <+12>: movq $0x400a30,(%rax) # 基类的构造函数也会设置虚表指针
0x0000000000400847 <+19>: pop %rbp
0x0000000000400848 <+20>: retq
End of assembler dump.
派生类析构的时候
(gdb) disassemble derived::~derived
Dump of assembler code for function derived::~derived():
0x00000000004008c4 <+0>: push %rbp
0x00000000004008c5 <+1>: mov %rsp,%rbp
0x00000000004008c8 <+4>: sub $0x10,%rsp
0x00000000004008cc <+8>: mov %rdi,-0x8(%rbp) # rdi 存着this
0x00000000004008d0 <+12>: mov -0x8(%rbp),%rax
0x00000000004008d4 <+16>: movq $0x4009f0,(%rax) # 派生类会先设置虚表指针为自己的
0x00000000004008db <+23>: mov -0x8(%rbp),%rax
0x00000000004008df <+27>: mov %rax,%rdi
0x00000000004008e2 <+30>: callq 0x40084a <base::~base()> # 然后调用基类析构,同样基类析构中也会修改虚表指针为自己的
0x00000000004008e7 <+35>: mov $0x0,%eax
0x00000000004008ec <+40>: test %eax,%eax
0x00000000004008ee <+42>: je 0x4008fc <derived::~derived()+56>
0x00000000004008f0 <+44>: mov -0x8(%rbp),%rax
0x00000000004008f4 <+48>: mov %rax,%rdi
0x00000000004008f7 <+51>: callq 0x4006a0 <_ZdlPv@plt>
0x00000000004008fc <+56>: leaveq
0x00000000004008fd <+57>: retq
End of assembler dump.
(gdb) disassemble base::~base
Dump of assembler code for function base::~base():
0x000000000040084a <+0>: push %rbp
0x000000000040084b <+1>: mov %rsp,%rbp
0x000000000040084e <+4>: sub $0x10,%rsp
0x0000000000400852 <+8>: mov %rdi,-0x8(%rbp)
0x0000000000400856 <+12>: mov -0x8(%rbp),%rax
0x000000000040085a <+16>: movq $0x400a30,(%rax) # 这里基类修改了虚表指针
0x0000000000400861 <+23>: mov $0x0,%eax
0x0000000000400866 <+28>: test %eax,%eax
0x0000000000400868 <+30>: je 0x400876 <base::~base()+44>
0x000000000040086a <+32>: mov -0x8(%rbp),%rax
0x000000000040086e <+36>: mov %rax,%rdi
0x0000000000400871 <+39>: callq 0x4006a0 <_ZdlPv@plt>
0x0000000000400876 <+44>: leaveq
0x0000000000400877 <+45>: retq
End of assembler dump.
经典多线程问题__cxa_pure_virtual导致进程崩溃
- __cxa_pure_virtual是c++为纯虚函数默认创建的一个实现,调用它程序会主动core。
- 抽象类的虚表项中的相关函数存放的是__cxa_pure_virtual
在上面分析中,如果一个线程正执行到基类析构,并将this指针的虚表改为了父类的虚表,此时切换线程,而另一个线程继续以this指针访问成员函数,结果将导致访问的是基类的成员函数,即纯虚函数__cxa_pure_virtual