析构函数
析构函数和构造函数一样,也分完整析构函数和基类析构函数两种。而且析构函数的工作流程和构造函数相反。
如果一个类的基类有未实现的析构函数(未提供定义或者是纯虚函数),会导致链接失败。这是因为子类的析构函数有调用基类析构函数的逻辑!
什么时候需要虚析构函数
https://stackoverflow.com/a/461237
Declare destructors virtual in polymorphic base classes. This is Item 7 in Scott Meyers’ Effective C++. Meyers goes on to summarize that if a class has any virtual function, it should have a virtual destructor, and that classes not designed to be base classes or not designed to be used polymorphically should not declare virtual destructors.
**自动生成的析构函数不是虚函数,但是可以显式声明其为 virtual 然后用 = default
引入。**从基类继承的析构函数后虚实性质保持不变。
有了虚析构函数,才能正确地通过非本类的指针 / 引用析构或 delete 某个对象。
虚析构函数如何工作?
虚析构函数是给类型增加 vptr 的一种方法。有了虚析构函数就能通过不匹配的指针和 delete 关键字删除曾经 new 出的对象。
- complete object destuctor:该类型对象退出作用域时调用(由于可能会通过指针 / 引用人为调用析构函数——比如说 STL 里面——所以完整析构函数也会记录在虚表)。
- base object destructor:该类型对象被子类处理的时候调用。(在一些场景下和完整析构函数是同一个)
- deleting destructor:该类型指针被使用 delete 时调用的、通过虚表机制查找到的虚函数。deleting destructor 要负责修正 this 指针,调用 complete object destructor 来析构对象本身,并最终使用无类型 new 归还空间。
可以观察这段代码:https://godbolt.org/z/ohePxP9qd
#include <iostream>
#include <cassert>
using namespace std;
struct A { virtual ~A() {} };
struct B { virtual ~B() {} };
struct C: A, B { };
int main() {
C *p = new C;
A *pa = (A *)p;
B *pb = (B *)p;
assert((void *)pb != (void *)p);
delete pb; // pb 是 B* 类型,而 B 又有虚表,因此做偏移可以找到真正的指针起始位置
}
通过基类指针 delete 子类对象
基类必须有虚析构函数!这样才能正确找到应该被析构的对象。
反例(对象没有被正确析构) 下面代码是虚拟继承,由于 A 和 C 不能重合 vptr,C 需要一个新的 vptr。delete 时指针的地址和申请时并不匹配,这能被 -fsanitize=address 检查出来。
#include <cstdio> struct A { int a; A() = default; virtual void f() {} }; struct C : virtual A { int c; C() = default; }; // Compile with gcc 13.1 and option -fsanitize=address, // and an error will occur. int main() { C *pc = new C; A *pa = pc; fprintf(stderr, "pc=%p\n", pc); fprintf(stderr, "pa=%p\n", pa); delete pa; }
下面这段代码中 A 没有虚析构函数。 A 类型是多态类,delete 时也和原来申请时地址一致(单具体继承就是这样),所以 -fsanitize=address 检查不出来问题。但是调用 ~A() 时没有通过虚拟机制调用到 ~C(),这样 C 类中的数据没有被正确析构!编译下面的代码可以发现 ~C() 中的打印语句没有生效。
#include <cstdio> struct A { int a; A() = default; virtual void f() {} }; struct C : A { int c; C() = default; virtual ~C() { puts("~C()"); } }; int main() { C *pc = new C; A *pa = pc; fprintf(stderr, "pc=%p\n", pc); fprintf(stderr, "pa=%p\n", pa); delete pa; }
接下来增加 C 类的体积:
#include <cstdio> struct A { int a; A() = default; virtual void f() {} }; struct C : A { int c; int d; // <-- C() = default; virtual ~C() { puts("~C()"); } }; int main() { C *pc = new C; A *pa = pc; fprintf(stderr, "pc=%p\n", pc); fprintf(stderr, "pa=%p\n", pa); delete pa; }
现在 A 的大小是 12 字节,C 的大小是 20 字节。new 和 delete 操作符都将大小对齐到 8 字节再操作,那么用 -fsanitize=address 编译就能够检查出来析构类型不匹配的问题。