C++支持多重继承,这和现实生活很类似,任何一个物体都不可能单一的属于某一个类型。就像马,第一想到的就是它派生自动物这个基类,但是它在某系地方可不可以说也派生自交通工具这一个基类呢?所以C++的多重继承很有用,但是又引入了一个问题(专业术语叫做菱形继承?)。动物和交通工具都是从最根本的基类——“事物”继承而来,事物包含了两个最基本的属性,体积和质量。那么动物和交通工具都保存了基类成员变量——体积和质量的副本。而马有继承了这两个类,那么马就有两份体积和质量,这是不合理的,编译器无法确定使用哪一个,所以就会报错。JAVA中不存在这样的问题,因为JAVA不允许多重继承,它只可能实现多个接口,而接口里面只包含一些函数声明不包含成员变量,所以也不存在这样的问题。
这个问题用具体代码表述如下所示:
class A { public: int a; }; class B : public A { }; class C : public A { }; class D : public B, public C { }; int main() { D d; d.a = 1; return 0; }
这个代码会报错,因为d中保存了两份A的副本,即有两个成员变量a,一般不会报错,但是一旦对D中的a使用,就会报一个“对a的访问不明确”。虚继承就可以解决这个问题。在探讨虚函数之前,先来一个sizeof的问题。
#include <stdio.h> class A { public: int a; }; class B : virtual public A { }; int main() { printf("%d\n", sizeof(B)); return 0; }
B的大小是?首先回答0的是绝对错的,理由我之前都说了。1也是错的,不解释。4也是错的,如果B不是虚继承自A的,那么4就是对的。正确答案是8,B虚继承A了之后,比预想中的多了4个字节,这是怎么回事呢?这个通过调试是看不出来的,因为看不到类似于vftable的成员变量(实际上编译器生成了一个类似的东西,但是调试时看不到,但是在观察反汇编的时候,可以见到vbtable的字样,应该是virtual base table的意思)。
虚继承的提出就是为了解决多重继承时,可能会保存两份副本的问题,也就是说用了虚继承就只保留了一份副本,但是这个副本是被多重继承的基类所共享的,该怎么实现这个机制呢?编译器会在派生类B的实例中保存一个A的实例,然后在B中加入一个变量,这个变量是A的实例在实际B实例中的偏移量,实际上B中并不直接保存offset的值,而是保存的一个指针,这个指针指向一个表vbtable,vbtable表中保存着所有虚继承的基类在实例中的offset值,多个B的实例共享这个表,每个实例有个单独的指针指向这个表,这样就很好理解为什么多了4个字节了。用代码表示就像下面这样。
class A { public: ... }; int vbtable_of_B[] = { offset(B::_a), ... }; class B :virtual public A{ private: const int* vbtable = vbtable_of_B; A _a; };
每一个A的虚派生类,都会有自己的vbtable表,这个派生类的所有实例共享这个表,然后每个实例各自保存了一个指向vbtable表的指针。假如还有一个类C虚继承了A,那么编译器就会为它自动生成一个vbtable_of_C的表,然后C的实例都会有一个指向这个vbtable表的指针。
假如有多级的虚继承会发生什么情况,就像下面这段代码一样:
#include <stdio.h> class A { public: int a; }; class B : virtual public A {
public:
int b; }; class C : virtual public B { }; int main() { printf("%d\n", sizeof(C)); return 0; }
程序运行的结果是16,按照之前的理论,大概会这么想。基类A里有1个变量,4个字节。B类虚继承了A,所以它有一个A的副本和一个vbtable,还有自己的一个变量,那就是12字节。然后C类又虚继承了B类,那么它有一个B的副本,一个vbtable,16字节。但实际上通过调试和反汇编发现,C中保存分别保存了A和B的副本(不包括B类的vbtable),8字节。然后有一个vbtable指针,4字节,表里面包含了A副本和B副本的偏移量。最后还有一个无用的4字节(?),一共16字节。不仅是这样,每经过一层的虚继承,便会多出4字节。这个多出来的四字节在反汇编中没发现实际用途,所以这个有待探讨,不管是编译器不够智能,还是有待其它作用,虚继承和多重继承都应该谨慎使用。
还是以上面的例子,假如C类是直接继承B类,而不是使用虚继承,那么C类的大小为12字节。它里面是直接保存了A和B的副本(不包含B的vbtable),然后还有一个自己的vbtable指针,所以一共12字节,没有了上一段所说的最后的4个字节。
但是如果想下面一种继承,会是什么情况?
#include <stdio.h> class A { public: int a; }; class B : virtual public A { }; class C : virtual public A { }; class D : public B, public C{ }; int main() { printf("%d\n", sizeof(D)); return 0; }
D从B,C类派生出来,而B和C又同时虚继承了A。输出的结构是12,实际调试反汇编的时候发现,D中继承了B和C的vbtable,这就是8字节,而同时还保存了一个A的副本,4字节,总共12字节。它和上面的多重虚继承例子里的12字节是不一样的。之前一个例子中只有一个vbtable,一个A的实例,末尾还有一个未知的4字节。而这个例子中是有两个仅挨着的vbtable(都有效)和一个A的实例。