c++ 析构函数的三种变体

在 C++ 的 Itanium ABI(应用二进制接口,被 GCC 和 Clang 等主流编译器广泛采用)中,为了处理复杂的继承(尤其是虚继承)和不同的对象生命周期管理需求,编译器会为同一个类生成三种主要的析构函数变体:D0D1D2

这三种变体的主要区别在于:是否负责销毁虚基类 以及 是否负责释放内存


1. D2:基础对象析构函数 (Base Object Destructor)

  • 别名not-in-charge 析构函数。
  • 职责:销毁对象本身的成员变量和非虚基类子对象。
  • 不负责的内容:它不销毁虚基类(Virtual Base Classes),也不调用 delete 释放内存。
  • 使用场景:当一个类作为其他类的基类被销毁时使用。在派生类的析构过程中,派生类会负责销毁虚基类,而只要求基类析构函数清理基类本身的部分,此时调用的就是 D2。

2. D1:完整对象析构函数 (Complete Object Destructor)

  • 别名in-charge 析构函数。
  • 职责:销毁整个对象,包括所有的成员变量、非虚基类,以及虚基类(Virtual Base Classes)。
  • 不负责的内容:它不调用 delete 释放内存。
  • 使用场景:用于销毁具有自动存储期(栈对象)或静态存储期(全局/静态对象)的完整对象。因为这些对象的内存不是通过 new 分配的,所以只需要调用析构逻辑,不需要释放内存。

3. D0:删除析构函数 (Deleting Destructor)

  • 职责:执行与 D1 完全相同的销毁逻辑(包括虚基类),并在最后调用 operator delete 释放该对象占用的内存。
  • 使用场景:用于销毁通过 delete 表达式删除的动态分配对象(堆对象)。
  • **虚函数表 (vtable)**:对于带有虚析构函数的类,vtable 中通常包含 D0D1 的入口。当你通过基类指针 delete ptr; 时,程序会通过虚表找到 D0 并执行。

总结对比表

变体 名称 销毁成员/非虚基类 销毁虚基类 调用 delete 典型应用场景
D2 Base Object 作为基类子对象被析构时
D1 Complete Object 栈对象、静态对象销毁
D0 Deleting delete 堆对象时

为什么需要这么复杂?

这种设计的核心矛盾在于 **虚继承 (Virtual Inheritance)**:

  • 在虚继承中,虚基类是由“最派生类”(Most Derived Class)负责构造和析构的。
  • 如果基类的析构函数总是去销毁虚基类,那么在多重继承路径下,虚基类会被销毁多次。
  • 因此,ABI 定义了 D2(不碰虚基类,给子类调用)和 D1(销毁虚基类,完整对象用)。而 D0 则是在 D1 的基础上增加了对 operator delete 的调用,以便支持 delete 关键字的正确语义。

优化小知识:
如果一个类没有虚基类(绝大多数情况),那么 D1D2 的逻辑其实是一模一样的。在这种情况下,GCC 等编译器通常会进行优化,让 D1 和 D2 指向同一段代码(通过别名 Alias),以减少二进制文件的体积。