虚拟继承导致应用程序崩溃

4
以下代码由于使用虚继承而崩溃(访问冲突错误)。 据我所知,虚继承通过强制使用一个类的单个实例来解决钻石问题。在这种情况下,Derived类仅继承了一个IObject类的实例,因此不应该有问题,但是它却崩溃了。
class IObject
{
public:
    virtual int getType()=0;
};
class Base : public IObject
{
protected:
    int val;
public:
    Base() { val = 1; }
    virtual int getType();
};
int Base::getType() { return val; }

class Derived : public virtual Base //If I remove the virtual keyword here problem is solved.
{
public:
    Derived() { val = 2; }
};

int getVal( void* ptr ) 
{
    return ((IObject*)ptr)->getType();
}

int main()
{
    void* ptr = new Derived();
    cout << getVal(ptr) << endl;
    return 0;
}

为什么想要使用虚继承? - 4pie0
我不知道。我正在重构我的代码,最终虚拟继承变得不必要了。然而,在崩溃发生之前我没有注意到它,然后我将其删除了。但我仍然对原因很好奇。 - atoMerz
这里没有钻石问题!!! - barak manos
@barakmanos 我知道。我不明白的是为什么它会导致崩溃。 - atoMerz
您的代码在我试用 IObject* 替换 void 并删除强制转换后,在 VS2010 和 Ideone C+11 上都运行良好。 - sajas
显示剩余2条评论
4个回答

5
问题在于类型转换链是错误的:Derived* -> void* -> IObject*,这是由于混合使用了 C 和 C++ 的概念导致的未定义行为。更具体地说,关于void*的规则是继承自C语言的,没有任何对象和层次结构的适配。
因此,解决方案是确保通过void*的任何循环都是T -> void* -> T循环:始终通过相同的类型。因此,在您的情况下,您需要Derived* -> IObject* -> void* -> IObject*
要理解为什么虚拟继承会引起问题,您必须了解它如何被具体表示(这是实现定义的)。让我们看一些可能的内存表示示例 (基于Itanium ABI)。
线性非虚拟层次结构是按组合方式实现的:
struct Base { int a; };
struct Derived: Base { int b; };
struct SuperDerived: Derived { int c; };

+---+---+
| a | b |
+---+---+
^~~~~~~~~ Derived
    ^~~~~ Derived specific
^~~~~         Base

+---+---+---+
| a | b | c |
+---+---+---+
^~~~~~~~~~~~~ SuperDerived
        ^~~~~ SuperDerived specific
^~~~~~~~~     Derived
^~~~~         Base

在这种情况下,通常情况下&derived == &base并且&superderived == &derived(注意:如果其中一层没有虚拟表而下一层有,则会出现问题)。
具有多个基类的继承体系。
struct Base1 { int a; };
struct Base2 { int b; };
struct Derived: Base1, Base2 { int c; };

+---+---+---+
| a | b | c |
+---+---+---+
^~~~~~~~~~~~~ Derived
        ^~~~~ Derived specific
    ^~~~~     Base2
^~~~~         Base1

在这种情况下,&derived == &base1 但是 &derived != &base2 ,因此我们已经注意到基类不一定具有其派生类相同的地址。
最后,让我们介绍虚拟继承:
struct Object { int a; };
struct Base1: virtual Object { int b; };
struct Base2: virtual Object { int c; };
struct Derived: Base1, Base2 { int d; };

+---+---+
| b | a |
+---+---+
^~~~~~~~~ Complete Base1
^~~~~     Base1 specific
    ^~~~~ Object

+---+---+
| c | a |
+---+---+
^~~~~~~~~ Complete Base2
^~~~~     Base2 specific
    ^~~~~ Object

+---+---+---+---+
| b | c | d | a |
+---+---+---+---+
^~~~~~~~~~~~~~~~~ Complete Derived
        ^~~~~     Derived specific
^~~~~             Incomplete Base1
    ^~~~~         Incomplete Base2
            ^~~~~ Object

挑战在于虚基类的单个实例应该在所有潜在基类之间共享。由于只有完整对象知道哪些基类将参与其中,一个简单的选择是让完整对象负责放置虚基类(它将其放置在尾部),并且让虚表提供机制,在运行时从Object导航到派生类。
然而,请注意,在我们设计中,&base1 != &object、&base2 != &object和&derived != &object,因为object被放置在尾部。
这就是为什么使用C++机制执行强制转换很重要,它知道如何静态或动态(取决于情况)计算从一个基类到另一个基类时所需的指针调整。
注意:C++机制知道计算是静态还是动态的,例如static_cast(&object)会导致编译时错误,这里需要使用dynamic_cast。

非常好的表达,这正是我在寻找的。 - atoMerz
@atoMerz:我很高兴你觉得它有用 :) - Matthieu M.

3
实际的崩溃来自于使用void*,并与多重继承结合使用时(在这种情况下仅使用虚拟继承)。正如James Kanze指出的那样,因为将类型转换为void*,然后不将其转换回原始类型是未定义行为,所以任何结果都有可能发生。
当使用c++风格的强制类型转换时,指针实际上被改变以便于处理多重继承。通过使用c风格(和void*),编译器无法这样做,因此您正在访问具有不同虚拟表的IObject(如barak manos的答案中所示)。
一个有效的解决方案(没有void*):
#include <iostream>

class IObject
{
public:
    virtual int getType()=0;
};
class Base : public IObject
{
protected:
    int val;
public:
    Base() { val = 1; }
    virtual int getType();
};
int Base::getType() { return val; }

class Derived : public virtual Base
{
public:
    Derived() { val = 2; }
};

int getVal( IObject* ptr ) 
{
    return ptr->getType();
}

int main()
{
    IObject* ptr = new Derived();
    std::cout << getVal(ptr) << std::endl;
    return 0;
}

4
确切地说,您可以使用 void* 的唯一操作是将其强制转换回用于创建它的类型。在这里,它是从 Derived* 创建的,因此将其强制转换为 IObject* 并对其进行使用会导致未定义的行为。(出于您所述的原因;我不是在纠正您,而只是指出正式的理由,允许此行为。) - James Kanze
你可以补充说明,虽然没有使用 virtual 似乎也能正常工作,但这仍然是未定义的行为。 - James Kanze
@stefaanv 打印输出对我没有帮助。函数中ptr的值与main中的值相同。所以我认为它应该可以工作。 - atoMerz
@atoMerz Derived 实例以及它的 IObject 子对象不一定从相同的地址开始。 - molbdnilo
在将对象转换为 void* 后,有没有办法获取继承信息? - atoMerz
显示剩余5条评论

3

这里有一些可能帮助您理解问题的内容:

void* ptr = new Derived();
IObject* ptr1 = (IObject*)ptr;
IObject* ptr2 = new Derived();
IObject* ptr3 = new Derived();

当您在调试器中查看IObject指针时,您大致会看到以下内容:
每个Derived类的实例都有一个额外的成员变量(由编译器隐式添加到类定义中),它指向该类的虚函数表(也称为V-Table)。使用Visual Studio调试器,您可以在任何此类实例下查看其名称为__vfptr的虚函数表。
如上图所示,ptr2->__vfptrptr3->__vfptr正确地指向位于0x01236834地址处的类的V-Table。该表中第一个条目的值为0x012310f0,这是Base::getType函数的地址。
另一方面,ptr1->__vfptr指向地址0x0123683C。该表中“第一个条目”的值为0x00000000,显然不是任何函数的地址,因此您遇到了内存访问冲突。

1

崩溃是由 reinterpret_cast 引起的。

    void* ptr = new Derived();

is actually

void* ptr = reinterpret_cast<void*>(new Derived());

当您这样做时,指针ptr不知道它所指向的内容。 当您这样做时:
return ((IObject*)ptr)->getType();

它的意思是

return (reinterpret_cast<IObject*>(ptr))->getType();

这里调用了IObject::getType,但该函数未定义,因此程序崩溃。为了解决这个问题,使用IObject*代替void*

IObject* ptr = new Derived();
...
int getval(IObject* ptr)
{
    return ptr->getType();
}

在将其转换为void*后,我是否有任何方法可以将其强制转换回来? - atoMerz
我不知道 - IObject 中有很多虚函数声明吗?你能把它拆分成一个不同的文件吗? - cup
@atoMez 首先将 void* 转换为所需类型,然后再转换为 void*。其他任何操作都是危险的,即使它能正常工作:通常在某些狭窄情况下会出现 UB。您需要更加小心地处理 void* 的转换。"它能正常工作" 不足以说明问题,因为经常会出现 UB:崩溃和易碎的代码会在变更远离时发生——我避免隐式转换为 void*,并尝试在我的代码中隔离 void* 的转换。 - Yakk - Adam Nevraumont

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接