当派生类的析构函数是虚拟的而基类的 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
,但是由于没有数据可以指向derived
到base
的演员表被处理为无操作(很好,它指向 vptr ......但无论如何你都不能取消引用它)。这有意义吗?
@Saurabh Manchanda:当您delete
时,会发生两件事:编译器调用适当的析构函数(根据您的代码,在这种情况下,它远非适当) ,然后释放内存。为了释放内存,它使用它知道的最派生析构函数的类型(在本例中为base
)来获取分配函数返回的指针的地址,但该指针是错误的。测试起来相当简单:int main() derived *d = new derived; base *b = d; std::cout << d << "," << b << std::endl; delete b;
.
... 编译器在delete
中所做的是:b->~base(); deallocate(b);
(其中deallocate
通常是free
,但不是必须的)。这就是它正在死去的地方:当你获得内存时,你得到了地址d
,但你正在释放地址b
,它从未分配过。请注意,即使它仍然是未定义的行为,any 虚拟方法的存在也会修改 g++ 对类的布局方式,并将 base._vptr
放在前面,这将导致 d == b
通过,程序不会崩溃。不过,它仍然是 UB。【参考方案2】:
两种类型的大小不匹配,您的示例中的布局应该不同。
您正在比较 pod 类型与具有 vtable 的类型(布局和偏移量由实现定义)。当调用析构函数时,隐式 this 的地址被假定为base
的布局,但实际上这就是derived
。执行的内容相当于写入/读取无效地址。
【讨论】:
以上是关于当派生类的析构函数是虚拟的而基类的 dtor 不是时代码崩溃的主要内容,如果未能解决你的问题,请参考以下文章