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
的指针,但它有两条路径可以跟随,但不能自己决定。
那么,虚拟继承如何解决问题(帮助编译器做出决定)?
答案 0 :(得分:86)
您想要:(可通过虚拟继承获得)
A
/ \
B C
\ /
D
而不是:(没有虚拟继承会发生什么)
A A
| |
B C
\ /
D
虚拟继承意味着只有1个基本A
类的实例不是2.
您的类型D
将有2个vtable指针(您可以在第一个图表中看到它们),一个用于B
,另一个用于C
几乎继承A
。 D
的对象大小增加,因为它现在存储2个指针;但是现在只有一个A
。
所以B::A
和C::A
是相同的,因此D
不会发出含糊不清的电话。如果您不使用虚拟继承,则可以使用上面的第二个图表。然后,对A成员的任何调用都会变得模棱两可,您需要指定要采用的路径。
答案 1 :(得分:39)
派生类的实例“包含”基类的实例,因此它们在内存中看起来像这样:
class A: [A fields]
class B: [A fields | B fields]
class C: [A fields | C fields]
因此,如果没有虚拟继承,D类的实例将如下所示:
class D: [A fields | B fields | A fields | C fields | D fields]
'- derived from B -' '- derived from C -'
因此,请注意A数据的两个“副本”。虚继承意味着内部派生类在运行时设置了一个vtable指针,指向基类的数据,因此B,C和D类的实例看起来像:
class B: [A fields | B fields]
^---------- pointer to A
class C: [A fields | C fields]
^---------- pointer to A
class D: [A fields | B fields | C fields | D fields]
^---------- pointer to B::A
^--------------------- pointer to C::A
答案 2 :(得分:13)
好吧,关于SO的许多帖子以及外界的文章都说,钻石问题是通过创建A
的一个实例而不是两个实例(每个D
的父实例)解决的,从而解决了歧义。但是,这并没有使我对过程有全面的了解,最终我遇到了更多问题,例如
B
和C
试图创建A
的不同实例,例如调用具有不同参数(D::D(int x, int y): C(x), B(y) {}
)的参数化构造函数?将选择A
的哪个实例成为D
的一部分?B
使用非虚拟继承,而对C
使用虚拟继承怎么办?在A
中创建D
的单个实例是否足够?如果不尝试代码示例就无法预测行为,这意味着不了解该概念。以下是帮助我解决虚拟继承问题的原因。
首先,让我们从没有虚拟继承的代码开始:
#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);
同时需要B
和C
,它们每个都创建自己的A
,因此我们在A
中有两个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
和C
的构造函数传递的参数。随着歧义消失,所有对getX()
的调用都返回相同的值:
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
不会这样做自动。
考虑到这一点,很容易回答我遇到的所有问题:
D
创建期间,B
和C
都不负责A
的参数,它完全取决于D
。C
将A
的创建委托给D
,但是B
将创建其自己的A
实例,从而使钻石问题再次出现答案 3 :(得分:9)
问题不在于编译器必须遵循的路径。问题是该路径的端点:强制转换的结果。当涉及类型转换时,路径无关紧要,只有最终结果。
如果使用普通继承,每条路径都有自己独特的端点,这意味着强制转换的结果是不明确的,这就是问题所在。
如果使用虚拟继承,则会获得菱形层次结构:两个路径都指向同一个端点。在这种情况下,选择路径的问题不再存在(或者更确切地说,不再重要),因为两条路径都会产生相同的结果。结果不再含糊 - 这才是最重要的。确切的路径没有。
答案 4 :(得分:7)
实际上这个例子应该如下:
#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 =&gt; D”
虚拟继承只能解决祖父的重复问题! 但是你仍然需要指定方法是虚拟的,以便正确地覆盖方法...
答案 5 :(得分:0)
这里有正确的代码示例。钻石问题:
#include <iostream>
// Here you have the diamond problem : there is B::eat() and C::eat()
// because they both inherit from A and contain independent copies of A::eat()
// So what is D::eat()? Is it B::eat() or C::eat() ?
class A { public: void eat(){ std::cout << "CHROME-CHROME" << endl; } };
class B: public A { };
class C: public A { };
class D: public B,C { };
int main(int argc, char ** argv){
A *a = new D();
a->eat();
delete a;
}
解决方案:
#include <iostream>
// Virtual inheritance to ensure B::eat() and C::eat() to be the same
class A { public: void eat(){ std::cout<< "CHROME-CHROME" << endl; } };
class B: virtual public A { };
class C: virtual public A { };
class D: public B,C { };
int main(int argc, char ** argv){
A *a = new D();
a->eat();
delete a;
}
答案 6 :(得分:-1)
使用Virtual关键字可以解决此问题。
A
/ \
B C
\ /
D
钻石问题的例子。
#include<stdio.h>
using namespace std;
class AA
{
public:
int a;
AA()
{
a=10;
}
};
class BB: virtual public AA
{
public:
int b;
BB()
{
b=20;
}
};
class CC:virtual public AA
{
public:
int c;
CC()
{
c=30;
}
};
class DD:public BB,CC
{
public:
int d;
DD()
{
d=40;
printf("Value of A=%d\n",a);
}
};
int main()
{
DD dobj;
return 0;
}