C++虚继承(六) --- 虚继承浅析
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的實例。
總結
以上是生活随笔為你收集整理的C++虚继承(六) --- 虚继承浅析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C++虚继承(五) --- 虚拟继承的概
- 下一篇: C++虚继承(七) --- 虚继承对基类