当派生类的析构函数是虚拟的而基类的 dtor 不是时代码崩溃

Posted

技术标签:

【中文标题】当派生类的析构函数是虚拟的而基类的 dtor 不是时代码崩溃【英文标题】:Code crashes when derived class' destructor is virtual and base class' dtor is not 【发布时间】:2011-09-03 13:26:01 【问题描述】:

我在 gcc 4.4.5 上尝试了以下代码。

如果成员 'data' 不存在,则代码可以正常执行,但如果存在,则会崩溃。当派生类的 dtor 不是虚拟的时,它也不会崩溃。

我知道 在这两种情况下,行为都是未定义的,如 C++03 (5.3.5 / 3) 中所列,但仍然有人可以向我解释为什么会这样在后一种情况下它会崩溃?

是的,我知道 UB 意味着任何事情都可能发生,但仍然我想知道特定于实现的细节。

#include<iostream>    
using std::cout;

struct base
int data;
   base()
      cout << "ctor of base\n";
   
   ~base()
      cout << "dtor of base\n";
   
;

struct derived :  base
   derived()
      cout << "ctor of derived\n";
   
   virtual ~derived()
      cout << "dtor of derived\n";
   
;

int main()
   base *p = new derived;
   delete p;

【问题讨论】:

由于这是 UB,“诊断”的唯一方法是查看为您的特定平台生成的程序集。我不确定这样做会得到什么! 几乎不可能说 - 在我的 g++ 4.5.1 安装中,例如它似乎运行正常。关于 UB 的全部意义在于它就是 UB。 了解崩溃的发生方式/原因将给您带来什么好处?您将无法在任何其他编译器版本中可靠地使用该信息;您可能无法在其他平台上使用相同的编译器版本。 “它似乎有时会起作用”,但已知是 UB。问题是什么? 我认为你应该停止移动鼠标。拔掉电源还会死机吗? 【参考方案1】:

假设我的系统(gcc 4.6.0,linux x86_64)上发生的事情与你的系统上发生的事情相同(它也会与data 一起崩溃并且没有运行),实现细节是p 确实 not指向为derived类型的对象分配的内存块的开头。

正如valgrind 告诉我的,

Address 0x595c048 is 8 bytes inside a block of size 16 alloc'd

如果您打印指针的值,您可以自己看到:

derived * d = new derived;
std::cout << d << '\n';
base *p = d;
std::cout << p << '\n';

原因是 gcc 中的对象布局是 vtable, base, derived

当 base 为空时,vtable, base, derived 和 base 的大小恰好相同,因为分配空类的对象占用非零字节数,这两种情况恰好相等。

当派生没有虚函数时,vtable不存在,地址再次相同,删除成功。

【讨论】:

所以,这意味着当派生对象是delete-d时,基dtor被调用并且因为基类没有任何关于vtable的信息,所以它像'p'一样删除它指基础对象的起始地址。由于“p”处存在 vtable 而不是基础对象的开头,因此会发生崩溃。我说的对吗? 下一个问题是为什么有或没有字段会影响编译器布局对象的方式。我的猜测是它不会改变布局,所以它仍然是 vptr, [empty base], derived ,但是由于没有数据可以指向derivedbase 的演员表被处理为无操作(很好,它指向 vptr ......但无论如何你都不能取消引用它)。这有意义吗? @Saurabh Manchanda:当您delete 时,会发生两件事:编译器调用适当的析构函数(根据您的代码,在这种情况下,它远非适当) ,然后释放内存。为了释放内存,它使用它知道的最派生析构函数的类型(在本例中为base)来获取分配函数返回的指针的地址,但该指针是错误的。测试起来相当简单:int main() derived *d = new derived; base *b = d; std::cout &lt;&lt; d &lt;&lt; "," &lt;&lt; b &lt;&lt; std::endl; delete b; . ... 编译器在delete 中所做的是:b-&gt;~base(); deallocate(b);(其中deallocate 通常是free,但不是必须的)。这就是它正在死去的地方:当你获得内存时,你得到了地址d,但你正在释放地址b,它从未分配过。请注意,即使它仍然是未定义的行为,any 虚拟方法的存在也会修改 g++ 对类的布局方式,并将 base._vptr 放在前面,这将导致 d == b通过,程序不会崩溃。不过,它仍然是 UB。【参考方案2】:

两种类型的大小不匹配,您的示例中的布局应该不同。

您正在比较 pod 类型与具有 vtable 的类型(布局和偏移量由实现定义)。当调用析构函数时,隐式 this 的地址被假定为base 的布局,但实际上这就是derived。执行的内容相当于写入/读取无效地址。

【讨论】:

以上是关于当派生类的析构函数是虚拟的而基类的 dtor 不是时代码崩溃的主要内容,如果未能解决你的问题,请参考以下文章

虚析构函数

基类的析构函数写成virtual虚析构函数

在一个派生类对象结束其生命周期时析构函数的调用顺序

C++ 设置基类的析构函数为虚函数

虚析构函数与纯虚函数

为什么基类的析构函数要写成虚函数?