钻石继承与虚继承
首先,何为钻石继承,顾名思义,在类的继承过程中,继承结构是一个类似菱形(钻石)的结构就属于钻石继承,如下:
这是一个最简单的钻石继承。实际上,在复杂的继承表中,只要子类按不同的继承路径回溯到基类有菱形结构,均属钻石继承。下面先看一个例子,钻石继承在C++程序设计中带来的问题。
1 //diamond.cpp
2 #include<iostream>
3 using namespace std;
4 class A{
5 public:
6 A (int x) : m_x(x) {}
7 int m_x;
8 };
9 class B : public A {
10 public:
11 B (int x) : A(x) {}
12 void set(int x) {
13 this -> m_x = x;
14 }
15 };
16 class C : public A {
17 public:
18 C (int x) : A(x) {}
19 int get(void) {
20 return this -> m_x;
21 }
22 };
23 class D : public B,public C {
24 public:
25 D (int x) : B(x),C(x) {}
26 };
27 int main(void) {
28 D d(10);
29 d.set(20);
30 cout << d.get() << endl;
31 return 0;
32 }
33
这样的运行结果是10?还是20呢?结果是10,为什么?!明明sets的是20,为什么get的还是10呢?
要解释这个问题那酒必须要先搞清楚,d对象在内存中是如何存放的,是怎样布局的。每一个子类都会有一个内存视图,在子类里都包含了它的基类子对象,下面是创建是d对象时,d对象在内存中的存放形式。
包含一个B类的基类子对象和一个C类型基类子对象,而B和C里各自有一个A类型基类子对象,所以可以看到,在d的内存布局中有两个A类型基类子对象。
set函数是类B的成员函数,在执行set函数时,this指针指向B(其实也是指向A,B从A继承,A存在B中的首地址),所以set执行后,改变的是B里的A类基类子对象的数据成员的值。同理,get函数得到的是C里A类基类子对象的数据成员的值。这样就可以理解这样的运行结果了。所谓钻石继承问题,就是公共基类对象在我们最终的子类对象中有多个副本,多份拷贝,当我们沿着不同的继承路径去访问公共基类子对象时结果会出现不一致。
而我们应该怎样解决这样的问题呢?采用虚继承。我们所期望的d的存储形式:
我们需要按如下方式修改代码:
class B : virtual public A //虚继承
class C : virtual public A //虚继承
D(int x) : B(x),C(x),A(x) {}
这样就解决了。
在这个过程中,A对象只在D的初始化表中A(x)进行构造(虚基类最先被构造),而在B和C的初始化表中不再对A进行构造(实际上是都有一个指针指向了D中的A(x),来对A进行构造)。
钻石继承,在访问公共基类成员函数时,如果不是虚继承,还会引起二义性的错误。代码如下:
1 //diamond.cpp
2 #include<iostream>
3 using namespace std;
4 class A{
5 public:
6 A (int x) : m_x(x) {}
7 void foo() {
8 cout << "A::foo()" << endl;
9 }
10 int m_x;
11 };
12 class B : public A {
13 public:
14 B (int x) : A (x) {}
15 void set (int x) {
16 this -> m_x = x;
17 }
18 };
19 class C : public A {
20 public:
21 C (int x) : A (x) {}
22 int get (void) {
23 return this -> m_x;
24 }
25 };
26 class D : public B,public C {
27 public:
28 D (int x) : B(x), C(x), A(x) {}
29 };
30 int main(void) {
31 D d(10);
32 d.set (20);
33 cout << d.get() << endl;
34 d.foo();
35 return 0;
36 }
编译器会报错:对成员'foo()'的请求有歧义,备选为 void A::foo() void A::foo()
依旧用对象d的内存视图来理解,在构建d对象时,里面存在两个A类基类子对象,尽管成员函数不存放在类中而在代码段,并且只会有一份,但是编译器不知道,他会作为两个继承函数来处理,用d.foo()来访问时,编译器便不知道访问的是哪一个基类子对象里的foo(),所以备选项都是void A::foo()。
而通过虚继承可以避免通过最终子类访问其继承自公共基类的成员函数时引发的名字冲突问题。