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いますが、たどることができる 2 つのパスがありますが、それ自体では決定できません。

では、仮想継承はどのように問題を解決するのでしょうか (コンパイラが決定を下すのに役立ちます)。

4

5 に答える 5

134

あなたが望む:(仮想継承で達成可能)

  A  
 / \  
B   C  
 \ /  
  D 

そうではありません:(仮想継承なしで何が起こるか)

A   A  
|   |
B   C  
 \ /  
  D 

仮想継承とは、基本Aクラスのインスタンスが 2 つではなく 1 つだけになることを意味します。

あなたの型Dには 2 つの vtable ポインター (最初の図で確認できます) があり、1つは仮想継承者用Bで、もう 1 つは継承者用です。 のオブジェクト サイズは、現在 2 つのポインタを格納しているため増加しています。ただし、現在は 1 つしかありません。 CADA

B::Aとは同じなのでC::A、 からのあいまいな呼び出しはありませんD。仮想継承を使用しない場合は、上の 2 番目の図になります。そして、 A のメンバーへの呼び出しはあいまいになり、どのパスを使用するかを指定する必要があります。

ウィキペディアには別の良い概要と例があります ここに

于 2010-04-17T16:37:24.300 に答える
49

派生クラスのインスタンスは、基本クラスのメンバーを格納します。

仮想継承がない場合、メモリレイアウトは次のようになります(クラスのメンバーの2つのコピーに注意してください)。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]

仮想継承を使用すると、メモリレイアウトは次のようになります(クラスのメンバーの単一コピーに注意してください)。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 に答える