天天看點

C++ 虛基類的定義、功能、規定

原文聲明:http://blog.sina.com.cn/s/blog_93b45b0f01011pkz.html

虛繼承和虛基類的定義是非常的簡單的,同時也是非常容易判斷一個繼承是否是虛繼承的,雖然這兩個概念的定義是非常的簡單明确的,但是在C++語言中虛繼承作為一個比較生僻的但是又是絕對必要的組成部份而存在着,并且其行為和模型均表現出和一般的繼承體系之間的巨大的差異(包括通路性能上的差異),現在我們就來徹底的從語言、模型、性能和應用等多個方面對虛繼承和虛基類進行研究。

首先還是先給出虛繼承和虛基類的定義。

    虛繼承:在繼承定義中包含了virtual關鍵字的繼承關系;

    虛基類:在虛繼承體系中的通過virtual繼承而來的基類,需要注意的是:class Base1 : public virtual Base0 {}; 其中Base0稱之為Base1的虛基類,而不是說Base0就是個虛基類,因為Base0還可以不不是虛繼承體系中的基類。

    有了上面的定義後,就可以開始虛繼承和虛基類的本質研究了,下面按照文法、語義、模型、性能和應用五個方面進行全面的描述。

1. 文法

       文法有語言的本身的定義所決定,總體上來說非常的簡單,如下:

class Base1 : public virtual Base0 {};
           

其中可以采用public、protected、private三種不同的繼承關鍵字進行修飾,隻要確定包含virtual就可以了,這樣一來就形成了虛繼承體系,同時Base0就成為了Base1的虛基類了。其實并沒有那麼的簡單,如果出現虛繼承體系的進一步繼承會出現什麼樣的狀況呢?

       如下所示:

class Base0
{
public:
    Base0(int i) : m_val( i ) {}
    int m_val;
};
class Base1 : public virtual Base0
{
public:
    Base1 (int i) : Base0( i ) {}
};   
         
class Base2 : public virtual Base0
{
public:
    Base2(int i) : Base0( i ) {}
};     
       
class Base3 : public Base1, public Base2
{
public:
    Base3(int i) : Base0( i ), Base1 ( i ), Base2 ( i ) {}
}; 
           
class Base4: public Base3
{
public:
    Base4 (int i) : Base0( i ), Base3( i ) {}
};
           

注意上面代碼中的Base3和Base4兩個類的構造函數初始化清單中的内容。可以發現其中均包含了虛基類Base0的初始化工作,如果沒有這個初始化語句就會導緻編譯時錯誤,為什麼會這樣呢?一般情況下不是隻要在Base1和Base2中包含初始化就可以了麼?要解釋該問題必須要明白虛繼承的語義特征,是以參看下面語義部分的解釋。

2. 語義

       從語義上來講什麼是虛繼承和虛基類呢?上面僅僅是從如何在C++語言中書寫合法的虛繼承類定義而已。

       首先來了解一下virtual這個關鍵字在C++中的公共含義,在C++語言中僅僅有兩個地方可以使用virtual這個關鍵字,一個就是類成員虛函數和這裡所讨論的虛繼承。不要看這兩種應用場合好像沒什麼關系,其實他們在背景語義上具有virtual這個詞所代表的共同的含義,是以才會在這兩種場合使用相同的關鍵字。

       那麼virtual這個詞的含義是什麼呢?virtual在《美國傳統詞典[雙解]》中是這樣定義的:

           adj.(形容詞)

           1. Existing or resulting in essence or effect though not in actual fact, form, or name:

              實質上的,實際上的:雖然沒有實際的事實、形式或名義,但在實際上或效果上存在或産生的;

           2. Existing in the mind, especially as a product of the imagination. Used in literary criticism of text.

              虛的,内心的:在頭腦中存在的,尤指意想的産物。用于文學批評中。

       我們采用第一個定義,也就是說被virtual所修飾的事物或現象在本質上是存在的,但是沒有直覺的形式表現,無法直接描述或定義,需要通過其他的間接方式或手段才能夠展現出其實際上的效果。

       那麼在C++中就是采用了這個詞意,不可以在語言模型中直接調用或展現的,但是确實是存在可以被間接的方式進行調用或展現的。比如:虛函數必須要通過一種間接的運作時(而不是編譯時)機制才能夠激活(調用)的函數,而虛繼承也是必須在運作時才能夠進行定位通路的一種體制。存在,但間接。其中關鍵就在于存在、間接和共享這三種特征。

       對于虛函數而言,這三個特征是很好了解的,間接性表明了他必須在運作時根據實際的對象來完成函數尋址(C++中利用類指針*與引用&來完成多樣性的重載),共享性表象在基類會共享被子類重載後的虛函數,其實指向相同的函數入口。

       對于虛繼承而言,這三個特征如何了解呢?存在即表示虛繼承體系和虛基類确實存在,間接性表明了在通路虛基類的成員時同樣也必須通過某種間接機制來完成(下面模型中會講到),共享性表象在虛基類會在虛繼承體系中被共享,而不會出現多份拷貝。

       那現在可以解釋文法小節中留下來的那個問題了,“為什麼一旦出現了虛基類,就必須在每一個繼承類中都必須包含虛基類的初始化語句”。

       由上面的分析可以知道,虛基類是被共享的,也就是在繼承體系中無論被繼承多少次,對象記憶體模型中均隻會出現一個虛基類的子對象(這和多繼承是完全不同的),這樣一來既然是共享的那麼每一個子類都不會獨占,但是總還是必須要有一個類來完成基類的初始化過程(因為所有的對象都必須被初始化,哪怕是預設的),同時還不能夠重複進行初始化,那到底誰應該負責完成初始化呢?C++标準中(也是很自然的)選擇在每一次繼承子類中都必須書寫初始化語句(因為每一次繼承子類可能都會用來定義對象),而在最下層繼承子類中實際執行初始化過程。是以上面在每一個繼承類中都要書寫初始化語句,但是在建立對象時,而僅僅會在建立對象用的類構造函數中實際的執行初始化語句,其他的初始化語句都會被壓制不調用。

 3. 模型

       為了實作上面所說的三種語義含義,在考慮對象的實作模型(也就是記憶體模型)時就很自然了。在C++中對象實際上就是一個連續的位址空間的語義代表,我們來分析虛繼承下的記憶體模型。

       3.1. 存在

           也就是說在對象記憶體中必須要包含虛基類的完整子對象,以便能夠完成通過位址完成對象的辨別。那麼至于虛基類的子對象會存放在對象的那個位置(頭、中間、尾部)則由各個編譯器選擇,沒有差别。(在VC8中無論虛基類被聲明在什麼位置,虛基類的子對象都會被放置在對象記憶體的尾部)

       3.2. 間接

           間接性表明了在直接虛基承子類中一定包含了某種指針(偏移或表格)來完成通過子類通路虛基類子對象(或成員)的間接手段(因為虛基類子對象是共享的,沒有确定關系),至于采用何種手段由編譯器選擇。(在VC8中在子類中放置了一個虛基類指針vbc,該指針指向虛函數表中的一個slot,該slot中存放着虛基類子對象的偏移量的負值,實際上就是個以補碼表示的int類型的值,在計算虛基類子對象首位址時,需要将該偏移量取絕對值相加,這個主要是為了和虛表中隻能存放虛函數位址這一要求相差別,因為位址是原碼表示的無符号int類型的值)

       3.3. 共享

           共享表明了在對象的記憶體空間中僅僅能夠包含一份虛基類的子對象,并且通過某種間接的機制來完成共享的引用關系。在介紹完整個内容後會附上測試代碼,展現這些内容。

    4. 性能

       由于有了間接性和共享性兩個特征,是以決定了虛繼承體系下的對象在通路時必然會在時間和空間上與一般情況有較大不同。

       4.1. 時間

           在通過繼承類對象通路虛基類對象中的成員(包括資料成員和函數成員)時,都必須通過某種間接引用來完成,這樣會增加引用尋址時間(就和虛函數一樣),其實就是調整this指針以指向虛基類對象,隻不過這個調整是運作時間接完成的。

           (在VC8中通過打開彙編輸出,可以檢視*.cod檔案中的内容,在通路虛基類對象成員時會形成三條mov間接尋址語句,而在通路一般繼承類對象時僅僅隻有一條mov常量直接尋址語句)

       4.2. 空間

           由于共享是以不同在對象記憶體中儲存多份虛基類子對象的拷貝,這樣較之多繼承節省空間。

    5. 應用

       談了那麼多語言特性和内容,那麼在什麼情況下需要使用虛繼承,而一般應該如何使用呢?

       這個問題其實很難有答案,一般情況下如果你确性出現多繼承沒有必要,必須要共享基類子對象的時候可以考慮采用虛繼承關系(C++标準ios體系就是這樣的)。由于每 一個繼承類都必須包含初始化語句而又僅僅隻在最底層子類中調用,這樣可能就會使得某些上層子類得到的虛基類子對象的狀态不是自己所期望的(因為自己的初始化語句被壓制了),是以一般建議不要在虛基類中包含任何資料成員(不要有狀态),隻可以作為接口類來提供。

最後聲明:c++中的虛基類對應java中的接口,Java中的接口則沒有任何實作代碼,而且接口裡面的屬性預設都是public static, 所有方法都是public 的。是以java用起來确實又很多友善之處。

繼續閱讀