源码
#include <iostream>
class Base1
{
public:
virtual void func_1_1(){ std::cout << "Base1::func_1_1()" << std::endl; }
virtual void func_1_2(){ std::cout << "Base1::func_1_2()" << std::endl; }
virtual ~Base1() {}
};
class Base2
{
public:
virtual void func_2_1(){ std::cout << "Base2::func_2_1()" << std::endl; }
virtual void func_2_2(){ std::cout << "Base1::func_2_2()" << std::endl; }
virtual ~Base2() {}
};
class Son :public Base1, public Base2
{
public:
virtual void func_s(){ std::cout << "Son::func_s()" << std::endl; }
virtual void func_2_1(){ std::cout << "Son::func_2_1()" << std::endl; }
};
int main()
{
Base2 *pb2 = new Son();
delete pb2;
return 0;
}
分析
Son类的内存布局如下所示:
在代码 delete pb2; 处打下断点,进入汇编,如下图所示:
总结过程,如下:
delete pb2 时,根据 Base2 的虚函数表,找到 trunk 项,从而获得了子类对象的首地址,并且调用子类对象的虚析构函数。然后先后执行了 Base2、Base1和Son类的虚构函数,最后完成 new Son 对象的释放。
若是如下代码:
Base1 *pb1 = new Son();
delete pb1;
则 Base1 虚函数表中的 trunk 项对应的代码中就不包含了调整 pb1 指针的位置,因为 pb1 本身就指向了 Son 类对象的首地址,所以直接调用了 Son 类对象的析构函数。
拓展:
1、虚函数表中的 trunk 项,实际上一段汇编代码的首地址,执行的内容如下:
- 将 pb2 偏移到子类对象的首地址。
- 执行子类对象的虚析构函数。
2、上述释放内存的成功实现的前提是,基类的析构函数必须是虚函数。若不是,则直接执行了 Base2 那部分的析构函数,这就导致了 Son 类对象时只释放 Base2 对应的那部分。
对于操作系统的内存分配机制来说,相对于内存块前面的固定位置会记录着该内存块的信息,包括该内存块的字节数。由于上述操作中,释放 Base2 对应的部分是时候,没有办法找到这部分的内存信息,从而导致了释放内存失败,程序运行崩溃。
(SAW:Game Over!)