【问题标题】:Virtual inheritance crashes application虚拟继承使应用程序崩溃
【发布时间】:2014-05-28 05:13:26
【问题描述】:

以下代码崩溃(访问冲突错误),因为我使用了虚拟继承。
AFAIK 虚拟继承通过强制使用类的单个实例来解决 Diamond 问题。在这种情况下,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;
}

【问题讨论】:

  • 为什么要使用虚拟继承?
  • 我没有。我正在重构我的代码,最终虚拟继承变得不必要了。但是在崩溃发生之前我没有注意到它,然后我将其删除。我仍然对原因感到好奇。
  • 这里没有钻石问题!!!
  • @barakmanos 我知道。我不明白为什么它会导致崩溃。
  • 您的代码运行良好,当我尝试使用 IObject* 而不是 void 并删除了强制转换时。我试过 VS2010 和 Ideone C+11

标签: c++ visual-studio visual-studio-2008 crash virtual-inheritance


【解决方案1】:

实际的崩溃来自于结合使用 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;
}

【讨论】:

  • 非常准确地说:对void* 唯一能做的就是将其转换回用于创建它的类型。在这里,它是从 Derived* 创建的,因此将其转换为 IObject* 并使用它是未定义的行为。 (出于您引用的原因;我并没有试图纠正您,而只是指出允许这种行为的正式理由。)
  • 你可能会补充一点,虽然它似乎在没有 virtual 的情况下工作,但它仍然是未定义的行为。
  • @JamesKanze Derived 继承 IObject,所以我应该能够将指针相互转换。我不明白为什么这是未定义的行为。
  • @stefaanv 打印出来对我没有帮助。函数中ptr的值与main中的值相同。所以我认为它应该有效。
  • @atoMerz Derived 实例及其IObject 子对象不一定从同一地址开始。
【解决方案2】:

崩溃是由 reinterpret_cast 引起的。

    void* ptr = new Derived();

其实是

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*后有什么办法可以将其转换回来?
  • 我不知道 - 在 IObject 中声明了很多虚函数吗?你能把它分成不同的文件吗?
  • @atoMez 首先从void* 转换为您想要的类型。 然后转换为void*。做任何其他事情都是危险的,即使它有效:通常是在某些狭窄情况下的 UB。您需要更加小心地处理从void* 的转换。 “它有效”还不够好,因为 UB 通常是“它有效”:会发生崩溃和远离变化的脆弱代码——我避免隐式转换到void*,并尝试隔离到-来自我的代码中的void*。
【解决方案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-&gt;__vfptr 和ptr3-&gt;__vfptr 正确指向地址 0x01236834,即类的 V-Table 所在的位置。该表第一项的值为0x012310f0,即函数Base::getType的地址。

另一方面,ptr1-&gt;__vfptr 指向地址 0x0123683C。 “此表中的第一个条目”的值是 0x00000000,这显然 不是任何函数的地址,因此您遇到了内存访问冲突。

【讨论】:

    【解决方案4】:

    问题在于转换链不正确:Derived* -&gt; void* -&gt; IObject* 是由于混合 C 和 C++ 概念而导致的未定义行为。更具体地说,void* 周围的规则是从 C 继承而来的,没有对对象和层次结构进行任何调整。

    因此,解决方案是确保通过void* 的任何循环都是T -&gt; void* -&gt; T 循环:始终通过相同的类型。因此,在您的情况下,您需要Derived* -&gt; IObject* -&gt; void* -&gt; IObject*。


    要了解为什么virtual 继承会导致问题,您必须了解它的具体表示方式具体(这是实现定义的)。让我们看一下可能的内存表示(大致基于 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
    

    在这种情况下,一般是&amp;derived == &amp;base 和&amp;superderived == &amp;derived(注意:如果一层没有虚拟表而下一层有,那么它会从屋顶掉下来)。

    具有多个基础的层次结构

    struct Base1 { int a; };
    struct Base2 { int b; };
    struct Derived: Base1, Base2 { int c; };
    
    +---+---+---+
    | a | b | c |
    +---+---+---+
    ^~~~~~~~~~~~~ Derived
            ^~~~~ Derived specific
        ^~~~~     Base2
    ^~~~~         Base1
    

    在这种情况下,&amp;derived == &amp;base1 但&amp;derived != &amp;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 到派生类。

    但是,请注意,在我们设计&amp;base1 != &amp;object、&amp;base2 != &amp;object 和&amp;derived != &amp;object 的情况下,因为object 被放在了尾部。

    这就是为什么使用 C++ 机器 执行转换很重要的原因,该机器知道如何静态或动态地(取决于情况)计算从一个基地到另一个基地时所需的指针调整。

    注意:C++ 机器知道计算是静态的还是动态的,例如static_cast&lt;Base1*&gt;(&amp;object) 是编译时错误,这里需要dynamic_cast。

    【讨论】:

    • 说得好,这就是我要找的。​​span>
    • @atoMerz:我很高兴你发现它有用:)
    猜你喜欢
    • 1970-01-01
    • 2013-12-18
    • 2016-10-26
    • 1970-01-01
    • 2013-08-24
    • 1970-01-01
    • 2016-03-26
    相关资源
    最近更新 更多