C++虚表与析构函数问题:派生类数组delete[]崩溃分析
以下C++代码执行delete[]时会触发段错误,且崩溃前未调用任何析构函数:
class X { public: virtual ~X() {} private: double m_a; }; class Y: public X { private: int m_b; }; int main() { X *xp = new Y[5]; delete[] xp; return 0; }
核心问题:未定义行为的触发
你遇到的崩溃本质是用基类指针delete[]派生类数组触发了C++98标准中的未定义行为——C++明确要求,delete[]的指针类型必须与new分配的数组元素类型完全匹配,数组场景下即使基类有虚析构函数,也不允许用基类指针销毁派生类数组。
虚表偏移量的计算逻辑
虚表的偏移量完全由编译期确定的固定索引决定,和函数名无关。每个虚函数在类的虚表中都有唯一的索引位置:比如基类X的虚析构函数是X虚表的第0个条目(假设是第一个虚函数)。派生类Y的虚表会继承基类的虚表结构,并用自己的虚函数覆盖对应索引的条目——这里Y的虚表第0个条目是~Y的地址,而非~X。
你观察到的崩溃不是因为虚表偏移计算错误,而是数组delete[]的本质问题:当用X*指向Y数组时,编译器会按X的对象大小来计算数组元素的步长(比如X占16字节,就每次+16),但实际Y的对象更大(比如24字节),导致后续“伪对象”的起始地址根本不是有效Y对象的位置,访问这些地址的虚表指针自然会触发段错误,而且因为地址无效,连析构函数都没机会调用。
疑问解答
1. 把Y的m_b改为double后程序不崩溃?
这是未定义行为的“侥幸”表现:当m_b改为double时,Y的大小可能刚好和X的大小形成某种内存布局巧合,比如在你的编译环境中,Y的大小是X的整数倍,或者错误步长访问的位置刚好是一段可访问的内存(虽然不是有效对象的虚表指针,但没触发段错误)。但这依然是未定义行为,换个环境或编译器版本可能还是会崩溃。
2. X的析构改为非虚时程序不崩溃?
当析构函数非虚时,delete[]会直接调用X的析构函数(而非通过虚表查找),且X的析构是空操作。此时即使编译器按X的步长遍历数组,也不会去访问虚表指针——既没有无效内存访问,也没有复杂的虚函数调用逻辑,所以不会触发段错误。但这依然是未定义行为,比如如果X/Y有需要释放的资源,会导致内存泄漏。
内容的提问来源于stack exchange,提问作者Alon Kalif

