请考虑以下代码:
class A1
{
virtual void a() = 0;
};
class A2
{
virtual int a(int x) = 0;
};
class B : public A1, public A2
{
void a() {}
int a(int x) { return x; }
};
int main()
{
A1* pa1;
pa1 = new B;
delete pa1;
A2* pa2;
pa2 = new B;
delete pa2;
return 0;
}
类A1和A2只是纯抽象,因此多重继承应该没有坏处。现在,上面的代码将在析构函数调用期间导致崩溃,但是什么是特殊的,仅针对一个对象:pa2。解决这个问题似乎非常明显 - 使用虚拟析构函数~A1()和~A2()。但是,仍有两个问题:
为什么虚拟析构函数是必需的,因为我们在这些类中没有任何数据?
为什么pa1和pa2的行为不同?我发现这与类放在父列表中的顺序有关。如果您将其更改为:
class B : public A2, public A1
然后
delete pa1;
会导致崩溃。
答案 0 :(得分:8)
可能的典型内存布局:
+-A1---+ | vptr | +------+ +-A2---+ | vptr | +------+ +-B------------------+ | +-A1---+ +-A2---+ | | | vptr | | vptr | | | +------+ +------+ | +--------------------+
vptr
是一个指针,指向有关最派生类型的一些信息,例如虚函数表,RTTI等(参见例如Itanium C++ ABI vtable layout)
所以,当你写A2* p = new B
时,你最终会得到:
+-B------------------+ | +-A1---+ +-A2---+ | | | vptr | | vptr | | | +------+ +------+ | +-----------^--------+ ^ | p | new B
当你现在delete p;
时,这可能会导致免费存储解除分配器出现问题,因为p
中存储的地址与您从分配器接收的地址不同({{1 }})。如果您转向new B
,即A1
,则不会发生这种情况,因为在这种情况下没有偏移。
您可以通过A1* p = new B
恢复原始指针,避免尝试避免此特定问题:
dynamic_cast
但不依赖于此。它仍然是未定义的行为(见Barry的回答)。
答案 1 :(得分:7)
来自[expr.delete]:
在第一个替代(删除对象)中,如果要删除的对象的静态类型与其不同 动态类型,静态类型应该是要删除的对象的动态类型的基类 static类型应具有虚拟析构函数或行为未定义。
未定义未定义的行为。虚拟析构函数是必要的,因为标准是这样说的(参见dyp's answer)
使用警告进行编译也有助于:
main.cpp: In function 'int main()':
main.cpp:22:12: warning: deleting object of abstract class type 'A1' which has non-virtual destructor will cause undefined behaviour [-Wdelete-non-virtual-dtor]
delete pa1;
^
main.cpp:26:12: warning: deleting object of abstract class type 'A2' which has non-virtual destructor will cause undefined behaviour [-Wdelete-non-virtual-dtor]
delete pa2;
^
答案 2 :(得分:0)
订单是相关的,因为析构函数的顺序与声明顺序相反。但是,它实际上是“幸运的”它甚至适用于pa1,因为使用非虚拟析构函数删除abstact类类型的对象会导致未定义的行为。总是需要为抽象类添加虚拟析构函数。