111
class A                     { public: void eat(){ cout<<"A";} }; 
class B: virtual public A   { public: void eat(){ cout<<"B";} }; 
class C: virtual public A   { public: void eat(){ cout<<"C";} }; 
class D: public         B,C { public: void eat(){ cout<<"D";} }; 

int main(){ 
    A *a = new D(); 
    a->eat(); 
} 

我了解钻石问题,上面的代码没有这个问题。

虚拟继承究竟是如何解决这个问题的?

我的理解: 当我说A *a = new D();时,编译器想知道一个类型的对象是否D可以分配给一个类型的指针A,但是它有两条路径可以遵循,但不能自行决定。

那么,虚拟继承如何解决这个问题(帮助编译器做出决定)?

4

5 回答 5

134

您想要:(可通过虚拟继承实现)

  A  
 / \  
B   C  
 \ /  
  D 

而不是:(没有虚拟继承会发生什么)

A   A  
|   |
B   C  
 \ /  
  D 

虚拟继承意味着只有 1 个基A类实例而不是 2 个。

您的类型D将有 2 个 vtable 指针(您可以在第一张图中看到它们),一个用于虚拟继承B,一个用于C虚拟继承AD的对象大小增加了,因为它现在存储了 2 个指针;但是现在只有一个A

所以B::AC::A是相同的,因此不会有来自 的模棱两可的调用D。如果你不使用虚拟继承,你有上面的第二张图。然后对 A 成员的任何调用都会变得模棱两可,您需要指定要采用的路径。

维基百科有另一个很好的纲要和例子在这里

于 2010-04-17T16:37:24.300 回答
83

为什么要另一个答案?

好吧,许多关于 SO 的帖子和外面的文章都说,钻石问题是通过创建单个实例A而不是两个(每个父级一个D)来解决的,从而解决了歧义。然而,这并没有让我对过程有全面的了解,我最终得到了更多的问题,比如

  1. 如果BC尝试创建不同的实例,A例如调用具有不同参数的参数化构造函数(D::D(int x, int y): C(x), B(y) {})怎么办?A将选择哪个实例成为其中的一部分D
  2. 如果我使用非虚拟继承B,而使用虚拟继承C呢?A创建in的单个实例是否足够D
  3. 从现在开始,我是否应该始终默认使用虚拟继承作为预防措施,因为它以较小的性能成本解决了可能的钻石问题并且没有其他缺点?

如果不尝试代码示例就无法预测行为意味着不理解这个概念。以下是帮助我了解虚拟继承的内容。

双A

首先,让我们从没有虚拟继承的代码开始:

#include<iostream>
using namespace std;
class A {
public:
    A()                { cout << "A::A() "; }
    A(int x) : m_x(x)  { cout << "A::A(" << x << ") "; }
    int getX() const   { return m_x; }
private:
    int m_x = 42;
};

class B : public A {
public:
    B(int x):A(x)   { cout << "B::B(" << x << ") "; }
};

class C : public A {
public:
    C(int x):A(x) { cout << "C::C(" << x << ") "; }
};

class D : public C, public B  {
public:
    D(int x, int y): C(x), B(y)   {
        cout << "D::D(" << x << ", " << y << ") "; }
};

int main()  {
    cout << "Create b(2): " << endl;
    B b(2); cout << endl << endl;

    cout << "Create c(3): " << endl;
    C c(3); cout << endl << endl;

    cout << "Create d(2,3): " << endl;
    D d(2, 3); cout << endl << endl;

    // error: request for member 'getX' is ambiguous
    //cout << "d.getX() = " << d.getX() << endl;

    // error: 'A' is an ambiguous base of 'D'
    //cout << "d.A::getX() = " << d.A::getX() << endl;

    cout << "d.B::getX() = " << d.B::getX() << endl;
    cout << "d.C::getX() = " << d.C::getX() << endl;
}

让我们通过输出。按预期执行B b(2);创建A(2),对于C c(3);

Create b(2): 
A::A(2) B::B(2) 

Create c(3): 
A::A(3) C::C(3) 

D d(2, 3);两者都需要Band C,它们每个都创建自己的A,所以我们有 double Ain d

Create d(2,3): 
A::A(2) C::C(2) A::A(3) B::B(3) D::D(2, 3) 

d.getX()这就是导致编译错误的原因,因为编译器无法选择A它应该为哪个实例调用方法。仍然可以直接为所选父类调用方法:

d.B::getX() = 3
d.C::getX() = 2

虚拟性

现在让我们添加虚拟继承。使用具有以下更改的相同代码示例:

class B : virtual public A
...
class C : virtual public A
...
cout << "d.getX() = " << d.getX() << endl; //uncommented
cout << "d.A::getX() = " << d.A::getX() << endl; //uncommented
...

让我们跳到创建d

Create d(2,3): 
A::A() C::C(2) B::B(3) D::D(2, 3) 

您可以看到,A是使用默认构造函数创建的,忽略从B和的构造函数传递的参数CgetX()随着歧义消失,所有返回相同值的调用:

d.getX() = 42
d.A::getX() = 42
d.B::getX() = 42
d.C::getX() = 42

但是如果我们想调用参数化的构造函数A呢?可以通过从以下构造函数显式调用它来完成D

D(int x, int y, int z): A(x), C(y), B(z)

通常,类只能显式使用直接父级的构造函数,但虚拟继承情况除外。发现这条规则对我来说是“点击”并有助于理解虚拟接口:

代码class B: virtual A意味着,任何继承自B的类现在都负责A自行创建,因为B不会自动创建。

考虑到这一点,很容易回答我的所有问题:

  1. D创建过程中,既不负责B也不C负责参数A,完全取决于D
  2. C将委托创建Ato D,但B将创建自己的实例,A从而将钻石问题带回
  3. 在孙子类而不是直接子类中定义基类参数不是一个好习惯,因此当存在菱形问题并且这种措施不可避免时应该容忍。
于 2018-07-14T21:49:22.160 回答
49

派生类的实例存储其基类的成员

如果没有虚拟继承,内存布局看起来像(注意class 中成员的两个副本):AD

class A: [A members]
class B: public A [A members|B members]
class C: public A [A members|C members]
class D: public B, public C [A members|B members|A members|C members|D members]

使用虚拟继承,内存布局看起来像(注意class 中成员的单个副本):AD

class A: [A members]
class B: virtual public A [B members|A members]
                           |         ^
                           v         |
                         virtual table B

class C: virtual public A [C members|A members]
                           |         ^
                           v         |
                         virtual table C

class D: public B, public C [B members|C members|D members|A members]
                             |         |                   ^
                             v         v                   |
                           virtual table D ----------------|

对于每个派生类,编译器创建一个虚拟表,其中包含指向其存储在派生类中的虚拟基类成员的指针,并在派生类中添加指向该虚拟表的指针。

于 2010-04-17T16:51:25.177 回答
13

问题不在于编译器必须遵循的路径。问题在于该路径的终点:演员阵容的结果。当涉及到类型转换时,路径无关紧要,只有最终结果才重要。

如果使用普通继承,每条路径都有自己独特的端点,这意味着强制转换的结果是不明确的,这就是问题所在。

如果你使用虚拟继承,你会得到一个菱形的层次结构:两条路径都指向同一个端点。在这种情况下,选择路径的问题不再存在(或者更准确地说,不再重要),因为两条路径导致相同的结果。结果不再模棱两可——这才是最重要的。确切的路径没有。

于 2010-04-17T17:14:19.710 回答
11

其实这个例子应该如下:

#include <iostream>

//THE DIAMOND PROBLEM SOLVED!!!
class A                     { public: virtual ~A(){ } virtual void eat(){ std::cout<<"EAT=>A";} }; 
class B: virtual public A   { public: virtual ~B(){ } virtual void eat(){ std::cout<<"EAT=>B";} }; 
class C: virtual public A   { public: virtual ~C(){ } virtual void eat(){ std::cout<<"EAT=>C";} }; 
class D: public         B,C { public: virtual ~D(){ } virtual void eat(){ std::cout<<"EAT=>D";} }; 

int main(int argc, char ** argv){
    A *a = new D(); 
    a->eat(); 
    delete a;
}

...这样输出将是正确的:“EAT=>D”

虚拟继承只解决了爷爷的重复!但是您仍然需要将方法指定为虚拟方法才能正确覆盖这些方法...

于 2016-01-14T23:37:11.160 回答