我记得我在学校的时候,以及当初看学习视频的时候,总是被告诉C++对象构造的顺序,先调用父类的构造函数,后按照顺序调用各成员的构造函数,最后再调用当前类型的构造函数。反过来析构的时候,先调用当前类型析构函数,然后从下到上调用成员的析构函数,最后调用父类。
当初乍一听,并且在每个类的构造器和析构器中加打印,感觉老师说的没有问题!
但是我当时有一个疑问,C++编译器是如何控制这一套流程的呢?后来我发现老师说的并非全对。
看下面的例子:
普通构造,析构分析, 零优化的汇编代码,栈上创建对象的情况
#include <cstdio>
class Data {
public:
Data() { num = 10; }
~Data() {}
private:
int num = 0;
};
class Data1 {
public:
Data1() { num = 10; }
~Data1() {}
private:
int num = 0;
};
class Base {
public:
Base(char ch) : c{ch} {}
~Base() {}
private:
int num = 20;
char c = 'c';
};
class Derived : Base {
public:
Derived() : Base('a') {
::printf("hello world\n");
}
~Derived() {
::printf("hello world again\n");
}
Data data_;
Data1 data2_;
};
int main() {
Derived d;
return 0;
}
类Derived 继承了Base 类(暂时不考虑写虚析构的问题 ),并有两个数据成员,他们的顺序为Data ,然后Data1 ,在类Derived 的构造器中,有一侧打印。
编译环境是gcc x86-64 经过编译器生成汇编代码:
Data::Data() [base object constructor]:
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-8], rdi
mov rax, QWORD PTR [rbp-8]
mov DWORD PTR [rax], 0
mov rax, QWORD PTR [rbp-8]
mov DWORD PTR [rax], 10
nop
pop rbp
ret
Data::~Data() [base object destructor]:
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-8], rdi
nop
pop rbp
ret
Data1::Data1() [base object constructor]:
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-8], rdi
mov rax, QWORD PTR [rbp-8]
mov DWORD PTR [rax], 0
mov rax, QWORD PTR [rbp-8]
mov DWORD PTR [rax], 10
nop
pop rbp
ret
Data1::~Data1() [base object destructor]:
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-8], rdi
nop
pop rbp
ret
Base::Base(char) [base object constructor]:
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-8], rdi
mov eax, esi
mov BYTE PTR [rbp-12], al
mov rax, QWORD PTR [rbp-8]
mov DWORD PTR [rax], 20
mov rax, QWORD PTR [rbp-8]
movzx edx, BYTE PTR [rbp-12]
mov BYTE PTR [rax+4], dl
nop
pop rbp
ret
Base::~Base() [base object destructor]:
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-8], rdi
nop
pop rbp
ret
.LC0:
.string "hello world"
Derived::Derived() [base object constructor]:
push rbp
mov rbp, rsp
push rbx
sub rsp, 24
mov QWORD PTR [rbp-24], rdi
mov rax, QWORD PTR [rbp-24]
mov esi, 97
mov rdi, rax
call Base::Base(char) [base object constructor]
mov rax, QWORD PTR [rbp-24]
add rax, 8
mov rdi, rax
call Data::Data() [complete object constructor]
mov rax, QWORD PTR [rbp-24]
add rax, 12
mov rdi, rax
call Data1::Data1() [complete object constructor]
mov edi, OFFSET FLAT:.LC0
call puts
jmp .L10
mov rbx, rax
mov rax, QWORD PTR [rbp-24]
add rax, 12
mov rdi, rax
call Data1::~Data1() [complete object destructor]
mov rax, QWORD PTR [rbp-24]
add rax, 8
mov rdi, rax
call Data::~Data() [complete object destructor]
mov rax, QWORD PTR [rbp-24]
mov rdi, rax
call Base::~Base() [base object destructor]
mov rax, rbx
mov rdi, rax
call _Unwind_Resume
.L10:
mov rbx, QWORD PTR [rbp-8]
leave
ret
.LC1:
.string "hello world again"
Derived::~Derived() [base object destructor]:
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], rdi
mov edi, OFFSET FLAT:.LC1
call puts
mov rax, QWORD PTR [rbp-8]
add rax, 12
mov rdi, rax
call Data1::~Data1() [complete object destructor]
mov rax, QWORD PTR [rbp-8]
add rax, 8
mov rdi, rax
call Data::~Data() [complete object destructor]
mov rax, QWORD PTR [rbp-8]
mov rdi, rax
call Base::~Base() [base object destructor]
nop
leave
ret
main:
push rbp
mov rbp, rsp
push rbx
sub rsp, 24
lea rax, [rbp-32]
mov rdi, rax
call Derived::Derived() [complete object constructor]
mov ebx, 0
lea rax, [rbp-32]
mov rdi, rax
call Derived::~Derived() [complete object destructor]
mov eax, ebx
mov rbx, QWORD PTR [rbp-8]
leave
ret
可以看到Derived 的构造函数和析构函数内,实际上会按照老师说的调用调用顺序去调用数据成员和基类的构造函数和析构函数,但是他们的入口函数都是外层类的构造器和析构器
即,并不是CPU自动按照顺序调用,而是CPU调用Derived 的函数,在函数内再按照约定好的顺序调用其他的构造器和析构器。这一点在main函数的汇编指令中可以清晰地看到。
|