在 C++ 的 Itanium ABI(应用二进制接口,被 GCC 和 Clang 等主流编译器广泛采用)中,为了处理复杂的继承(尤其是虚继承)和不同的对象生命周期管理需求,编译器会为同一个类生成三种主要的析构函数变体:D0、D1 和 D2。
这三种变体的主要区别在于:是否负责销毁虚基类 以及 是否负责释放内存。
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 中通常包含 D0 和 D1 的入口。当你通过基类指针
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关键字的正确语义。
优化小知识:
如果一个类没有虚基类(绝大多数情况),那么 D1 和 D2 的逻辑其实是一模一样的。在这种情况下,GCC 等编译器通常会进行优化,让 D1 和 D2 指向同一段代码(通过别名 Alias),以减少二进制文件的体积。