以下代码崩溃(访问冲突错误),因为我使用了虚拟继承
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;
}
答案 0 :(得分:3)
实际的崩溃来自于使用void *和cast与多重继承相结合(在这种情况下只使用虚拟继承)。正如James Kanze所指出的那样,因为转换为void *而不是回退到原始类型是未定义的行为,所以一切皆有可能。
使用c ++样式转换时,指针实际上已更改为允许使用多重继承。通过使用c style(和void *),编译器无法执行此操作,因此您使用不同的虚拟表访问IObject(如barak manos的答案所示)。
工作解决方案(没有空*):
#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;
}
答案 1 :(得分: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->__vfptr
和ptr3->__vfptr
正确指向地址0x01236834,该类的V-Table位于该地址。此表中第一个条目的值为0x012310f0,这是函数Base::getType
的地址。
另一方面,ptr1->__vfptr
指向地址0x0123683C。 &#34;该表中的第一个条目&#34;是0x00000000,显然不任何函数的地址,因此您正在经历的内存访问冲突。
答案 2 :(得分:3)
问题在于转换链是不正确的:Derived* -> void* -> IObject*
是由于混合C和C ++概念而导致的未定义行为。更具体地说,void*
周围的规则是从C继承的,没有任何对象和层次结构的适应。
因此,解决方案是确保void*
周期内的任何周期都是T -> void* -> T
周期:始终采用相同的类型。因此,在您的情况下,您需要Derived* -> IObject* -> void* -> 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
在这种情况下,&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<Base1*>(&object)
是编译时错误,这里需要dynamic_cast
。
答案 3 :(得分:1)
崩溃是由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();
}