【问题标题】:Code crashes when derived class' destructor is virtual and base class' dtor is not当派生类的析构函数是虚拟的而基类的 dtor 不是时代码崩溃
【发布时间】: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。问题是什么?
  • 我认为你应该停止移动鼠标。拔掉电源还会死机吗?

标签: c++ gcc crash undefined-behavior virtual-destructor


【解决方案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。执行的内容相当于写入/读取无效地址。

【讨论】:

    猜你喜欢
    • 2012-06-26
    • 2021-08-22
    • 2018-05-11
    • 2013-11-01
    • 2011-11-16
    • 1970-01-01
    • 2015-02-24
    • 2019-01-09
    • 2020-09-30
    相关资源
    最近更新 更多