目录
何时需要析构函数
编译器合成析构函数的情况
对象析构的流程
接下来我将持续更新“深度解读《深度探索C++对象模型》”系列,敬请期待,欢迎关注!也可以关注公众号:iShare爱分享,或文章末尾扫描二维码,自动获得推文和全部的文章列表。
深入分析C++对象的构造过程系列:
深度解读《深度探索C++对象模型》之C++对象的构造过程(一)
深度解读《深度探索C++对象模型》之C++对象的构造过程(二)
深度解读《深度探索C++对象模型》之C++对象的构造过程(三)
何时需要析构函数
何时需要一个析构函数?在对象的生命周期结束时必须要调用一个析构函数来销毁它吗?还是说有定义了构造函数的情况下就必须要定义一个析构函数,以实现资源申请和释放的对称性策略?以何为标准来决定是否要提供一个析构函数,我们接下来将来探讨这个问题。
是否需要提供一个析构函数,实际上要看程序是否需要,而非感觉上是否需要。对象被销毁时是否一定要调用一个析构函数?比如局部对象在生命周期结束时或者new出来的对象在被delete时,这时需要析构函数吗?答案是视情况而定,如果在类的构造函数或者其他的成员函数中没有申请系统资源(如内存、硬件资源等),那么这时候不需要析构函数,对象本身所占用的内存空间会被系统释放掉,不需要析构函数来完成。
类中有构造函数的时候需要对称的需要一个析构函数吗?这个也不一定,如果构造函数中有申请了系统资源,如申请了内存,那么在对象销毁时则需要归还给系统,否则会导致内存泄漏,这时就需要一个析构函数来完成这个事情。如果提供构造函数的作用只是想要在对象被构造时为它分配一个唯一的ID号,这种情况在对象销毁时并不需要归还资源给系统,所以不需要一个析构函数,除非你想要在对象析构时做好记录,那么才需要实现一个析构函数来完成这个功能。构造函数和析构函数的作用并不完全对称,在大多数情况下,构造函数是必要的,但析构函数却不一定需要。如以下的类定义:
class Object {
public:
Object(): x(0), y(0) {}
private:
int x;
int y;
};
类中的构造函数是必要的,否则往往程序就会遭遇错误,因为类中数据成员未被正确初始化,直接使用它们将引起不可预期的结果,但是析构函数却不是必须的,如下的析构函数的定义:
~Object() {
x = 0;
y = 0;
}
是不必要的。没有规定说在对象被销毁时要把它的内容清除干净,这时也没有资源需要归还给系统。这时为它提供一个析构函数反而是低效的,因为在每个对象被销毁时都要调用它。
因此,是否提供需要提供析构函数取决于程序的需求,当程序有需要时就定义它,没有需要则忽略它。但有时程序中没有需要,编译器却需要一个析构函数,所以这时编译器就会合成一个析构函数出来。
编译器合成析构函数的情况
编译器需要合成出来析构函数的情况一般有两种,一种是类的父类中有定义了析构函数,另一种是类中含有类类型的成员且定义了析构函数。在这两种情况,如果类中没有定义析构函数,编译器就会为它合成一个析构函数,如果类中有定义了析构函数,编译器则会扩充它,目的就是去调用父类的或者类类型成员的析构函数。如以下的例子:
class Base {
public:
Base(): b(0) {}
~Base() {}
private:
int b;
};
class Object: public Base {
public:
Object(): d(0) {}
private:
int d;
};
int main() {
Object obj;
return 0;
}
Base类中有定义了析构函数,Object类中没有定义析构函数,但是编译器为它生成了一个析构函数,来看看编译器生成的汇编代码:
Object::~Object() [base object destructor]: # @Object::~Object() [base object destructor]
push rbp
mov rbp, rsp
sub rsp, 16
mov qword ptr [rbp - 8], rdi
mov rdi, qword ptr [rbp - 8]
call Base::~Base() [base object destructor]
add rsp, 16
pop rbp
ret
main: # @main
# 略...
call Object::Object() [base object constructor]
# 略...
call Object::~Object() [base object destructor]
# 略...
main函数中调用了Object类的构造函数后就马上调用了它的析构函数,Object类的析构函数特意贴出它的全部代码,这个是编译器生成的代码,它的目的主要是去调用Base类的析构函数,除此之外并无它用,没有看到它会去清除类中的数据成员,因为不需要。类中含有类类型成员的情况和此差不多,就不再一一分析了。
需要注意的是,当类中有虚函数时或者父类中有一个虚基类时,如果类中没有定义任何构造函数,编译器会为类合成一个默认构造函数,但是这时编译器是不会为类合成析构函数的。
对象析构的流程
如果类中有析构函数(无论是自定义的还是编译器合成的),当对象的生命结束时,它的析构函数将会被调用,对象析构的流程为:
- 如果对象中含有虚函数表指针vptr,它的值会被重设,其实就是重新赋值,所指的还是这个类的虚函数表。
- 执行析构函数体内的代码(程序员所写的代码)。
- 如果类中含有类类型的成员且有定义了析构函数,那么它的析构函数会被调用,以声明它们的顺序的相反顺序调用之。
- 如果有一个直接基类且定义了析构函数,那么基类的析构函数会被调用,如果基类之上还有基类,那么将依次调用它们的析构函数;如果是多重继承的情况,那么将按照声明顺序的相反顺序依次调用基类的析构函数。
- 如果有虚基类且有析构函数,如果此类是最派生类(即继承体系的最末端),那么将会以声明顺序的相反顺序调用它们的析构函数。
我们以一个具体的例子来详细分析上面的流程:
#include <cstdio>
class Grand {
public:
Grand(int i): g(i) { printf("%s: g = %d\n", __PRETTY_FUNCTION__, g); }
virtual ~Grand() {
printf("%s\n", __PRETTY_FUNCTION__);
print();
}
virtual void print() { printf("%s\n", __PRETTY_FUNCTION__); }
private:
int g;
};
class Base1: virtual public Grand {
public:
Base1(int a, int b): Grand(a), b1(b) { printf("%s: b1 = %d\n", __PRETTY_FUNCTION__, b1); }
virtual ~Base1() {
printf("%s\n", __PRETTY_FUNCTION__);
print();
}
void print() override { printf("%s\n", __PRETTY_FUNCTION__); }
private:
int b1;
};
class Base2: virtual public Grand {
public:
Base2(int a, int b): Grand(a), b2(b) { printf("%s: b2 = %d\n", __PRETTY_FUNCTION__, b2); }
virtual ~Base2() {
printf("%s\n", __PRETTY_FUNCTION__);
print();
}
void print() override { printf("%s\n", __PRETTY_FUNCTION__); }
private:
int b2;
};
class Derived: public Base1, public Base2 {
public:
Derived(): Grand(0), Base1(1, 2), Base2(3, 4), d{0} { printf("%s\n", __PRETTY_FUNCTION__); }
virtual ~Derived() {
printf("%s\n", __PRETTY_FUNCTION__);
print();
}
void print() override { printf("%s\n", __PRETTY_FUNCTION__); }
private:
int d;
};
int main() {
Derived d;
return 0;
}
代码的输出结果:
Grand::Grand(int): g = 0
Base1::Base1(int, int): b1 = 2
Base2::Base2(int, int): b2 = 4
Derived::Derived()
virtual Derived::~Derived()
virtual void Derived::print()
virtual Base2::~Base2()
virtual void Base2::print()
virtual Base1::~Base1()
virtual void Base1::print()
virtual Grand::~Grand()
virtual void Grand::print()
对比对象的构造过程,对象的析构过程并不完全是构造过程的相反顺序,如构造过程是先构造虚基类、再构造基类,然后设置vptr,初始化类成员(如类类型成员有构造函数会调用之),最后执行构造函数的本体代码。但是对象的析构过程是先设置vptr,这个vptr也是指向本类的虚函数表(跟构造函数设置的一样),这样是保证在析构函数中调用虚函数能正确调用到这个类的虚函数,或者在析构函数中能正确地访问虚基类的成员。如上面代码中在各自析构函数中调用虚函数print,都是调用到对应类的虚函数实例,见上面的输出结果。我们来看看析构函数的汇编代码:
Derived::~Derived() [complete object destructor]: # @Derived::~Derived() [complete object destructor]
# 略...
lea rsi, [rip + VTT for Derived]
call Derived::~Derived() [base object destructor]
mov rdi, qword ptr [rbp - 16] # 8-byte Reload
add rdi, 32
call Grand::~Grand() [base object destructor]
# 略...
Derived::~Derived() [base object destructor]: # @Derived::~Derived() [base object destructor]
# 略...
mov qword ptr [rbp - 8], rdi
mov qword ptr [rbp - 16], rsi
mov rax, qword ptr [rbp - 8]
mov qword ptr [rbp - 32], rax # 8-byte Spill
mov rcx, qword ptr [rbp - 16]
mov qword ptr [rbp - 24], rcx # 8-byte Spill
mov rdx, qword ptr [rcx]
mov qword ptr [rax], rdx
mov rsi, qword ptr [rcx + 40]
mov rdx, qword ptr [rax]
mov rdx, qword ptr [rdx - 24]
mov qword ptr [rax + rdx], rsi
mov rcx, qword ptr [rcx + 48]
mov qword ptr [rax + 16], rcx
lea rdi, [rip + .L.str]
lea rsi, [rip + .L__PRETTY_FUNCTION__._ZN7DerivedD2Ev [base object destructor]]
xor eax, eax
call printf@PLT
jmp .LBB32_1
.LBB32_1:
mov rdi, qword ptr [rbp - 32] # 8-byte Reload
mov rax, qword ptr [rdi]
mov rax, qword ptr [rax + 16]
call rax
jmp .LBB32_2
.LBB32_2:
mov rdi, qword ptr [rbp - 32] # 8-byte Reload
mov rsi, qword ptr [rbp - 24] # 8-byte Reload
add rdi, 16
add rsi, 24
call Base2::~Base2() [base object destructor]
mov rsi, qword ptr [rbp - 24] # 8-byte Reload
mov rdi, qword ptr [rbp - 32] # 8-byte Reload
add rsi, 8
call Base1::~Base1() [base object destructor]
# 略...
编译器会生成两个版本的析构函数:“complete object destructor”和“base object destructor”,complete版本完成的是全功能的析构函数,是当一个完整的对象析构时被调用,base版本是当子对象析构时被调用的。Base1类和Base2类同样也是有两个版本,因为Base1和Base2作为Derived对象的子对象,所以这里调用的是它们的base版本(见上面的代码第41和45行)。complete版本会去析构虚基类,在上面的代码里,complete版本的析构函数先调用base版本的析构函数完成基本的工作,然后调用虚基类Grand的析构函数,在base版本的析构函数里调用了Base1类和Base2类的base版本的析构函数,在这两个析构函数里则不会调用虚基类Grand的析构函数。
上面代码的第11到24行就是设置虚函数表指针vptr的值,第11行的rdi寄存器保存的是对象d的首地址,第12行的rsi寄存器保存的是虚表的首地址(见第3行代码),这里先将它们保存到栈空间[rbp - 8]和[rbp - 16]中。这里会设置三个vptr值,分别是Derived对象/Base1子对象(这两者共用一个)(第15到18行)、Grand虚基类子对象(第19到22行)和Base2子对象(第23、24行)。
接着是执行析构函数体内的代码,首先调用printf函数(第25到28行),接着是代码的第31到34行是调用虚函数print,这里使用的是虚拟调用的机制,虚函数表指针在之前的代码中已设置完毕。
然后是代码中的第37到45行是以声明顺序的相反顺序调用Base2和Base1的析构函数。之后base版本的析构函数结束并返回到complete版本的析构函数,最后调用虚基类Grand的析构函数。