多重繼承首先咱們考慮一個(非虛擬)多重繼承的相對簡單的例子。看看下面的C++類層次結構。程序員
注意Top被繼承了兩次(在Eiffel語言中這被稱做重複繼承)。這意味着類型Bottom的一個實例bottom將有兩個叫作a的元素(分別爲bottom.Left::a和bottom.Right::a)。api |
Left、Right和Bottom在內存中是如何佈局的?讓咱們先看一個簡單的例子。Left和Right擁有以下的結構:函數
請注意第一個屬性是從Top繼承下來的。這意味着在下面兩條語句後佈局
若是咱們提高Bottom指針,會發生什麼事呢?
這段代碼工做正常。咱們能夠把一個Bottom的對象看成一個Left對象來使用,由於兩個類的內存部局是同樣的。那麼,若是將其提高爲Right呢?會發生什麼事?性能
|
虛擬繼承爲了不重複繼承Top,咱們必須虛擬繼承Top:測試
雖然從程序員的角度看,這也許更加的明顯和簡便,但從編譯器的角度看,這就變得很是的複雜。從新考慮下Bottom的佈局,其中的一個(也許沒有)多是:網站 Bottom Left::Top::a Left::b Right::c Bottom::d |
這個佈局的優勢是,佈局的第一部分與Left的佈局重疊了,這樣咱們就能夠很容易的經過一個Left指針訪問 Bottom類。但是咱們怎麼處理
咱們將哪一個地址賦給right呢? 通過這個賦值,若是right是指向一個普通的Right對象,咱們應該就能使用 right了。可是這是不可能的!Right自己的內存佈局是徹底不一樣的,這樣咱們就沒法像訪問一個"真正的"Right對象同樣,來訪問升級的Bottom對象。並且,也沒有其它(簡單的)能夠正常運做的Bottom佈局。 解決辦法是複雜的。咱們先給出解決方案,以後再來解釋它。 你應該注意到了這個圖中的兩個地方。第一,字段的順序是徹底不一樣的(事實上,差很少是相反的)。第二,有幾個vptr指針。這些屬性是由編譯器根據須要自動插入的(使用虛擬繼承,或者使用虛擬函數的時候)。編譯器也在構造器中插入了代碼,來初始化這些指針。 |
vptr (virtual pointers)指向一個 「虛擬表」。類的每一個虛擬基類都有一個vptr指針。要想知道這個虛擬表 (vtable)是怎樣運用的,看看下面的C++ 代碼。
第二個賦值使left指向了bottom的所在地址(即,它指向了Bottom對象的「頂部」)。咱們想一想最後一條賦值語句的編譯狀況(稍微簡化了):
用語言來描述的話,就是咱們用left指向虛擬表,而且由它得到了「虛擬基類偏移」(vbase)。這個偏移以後就加到了left,而後left就用來指向Bottom對象的Top部分。從這張圖你能夠看到Left的虛擬基類偏移是20;若是假設Bottom中的全部字段都是4個字節,那麼給left加上20字節將會確實指向a字段。 |
通過這個設置,咱們就能夠一樣的方法訪問Right部分。按這樣
以後right將指向Bottom對象的合適的部位:
對top的賦值如今能夠編譯成像前面Left一樣的方式。惟一的不一樣就是如今的vptr是指向了虛擬表的不一樣部位:取得的虛擬表偏移是12,這徹底正確(肯定!)。咱們能夠將其圖示歸納: 固然,這個例子的目的就是要像訪問真正Right對象同樣訪問升級的Bottom對象。所以,咱們必須也要給Right(和Left)佈局引入vptrs: 如今咱們就能夠經過一個Right指針,一點也不費事的訪問Bottom對象了。不過,這是付出了至關大的代價:咱們要引入虛擬表,類須要擴展一個或更多個虛擬指針,對一個對象的一個簡單屬性的查詢如今須要兩次間接的經過虛擬表(即便編譯器某種程度上能夠減少這個代價)。 |
向下轉換如咱們所見,將一個派生類的指針轉換爲一個父類的指針(或者說,向上轉換)可能涉及到給指針增添一個偏移。有人可能會想了,這樣向下轉換(反方向的)就能夠簡單的經過減去一樣的偏移來實現。確實,對非虛擬繼承來講是這樣的。但是,虛擬繼承(絕不奇怪的!)帶來了另外一種複雜性。 假設咱們像下面這個類這樣擴展繼承層次。
繼承層次如今看起來是這樣 如今考慮一下下面的代碼。
下圖顯示了Bottom和AnotherBottom的佈局,並且在最後一個賦值後面顯示了指向top的指針。
|
如今考慮一下怎麼去實現從top1到left的靜態轉換,同時要想到,咱們並不知道top1是否指向一個Bottom類型的對象,或者是指向一個AnotherBottom類型的對象。因此這辦不到!這個重要的偏移依賴於top1運行時的類型(Bottom則20,AnotherBottom則24)。編譯器將報錯:
由於咱們須要運行時的信息,因此應該用一個動態轉換來替代實現:
但是,編譯器仍然不滿意:
(注:polymorphic多態的) 問題在於,動態轉換(轉換中使用到typeid)須要top1所指向對象的運行時類型信息。可是,若是你看看這張圖,你就會發現,在top1指向的位置,咱們僅僅只有一個integer (a)而已。編譯器沒有包含指向Top的虛擬指針,由於它不認爲這是必需的。爲了強制編譯器包含進這個vptr指針,咱們能夠給Top增長一個虛擬的析構器:
這個修改須要指向Top的vptr指針。Bottom的新佈局是 ![]() (固然相似的其它類也有一個新的指向Top的vptr指針)。如今編譯器爲動態轉換插進了一個庫調用:
這個函數__dynamic_cast定義在stdc++庫中(相應的頭文件是cxxabi.h);參數爲Top的類型信息,Left和Bottom(經過vptr.Top),這個轉換能夠執行。 (參數 -1 標示出Left和Top之間的關係如今仍是未知)。更多詳細資料,請參考tinfo.cc 的具體實現 。 |
總結語最後,咱們來看看一些沒告終的部分。 指針的指針這裏出現了一點使人迷惑的問題,可是若是你仔細思考下一的話它其實很簡單。咱們來看一個例子。假設使用上一節用到的類層次結構(向下類型轉換).在前面的小節咱們已經看到了它的結果:
編譯器會接受這樣的形式嗎?咱們快速測試一下,編譯器會報錯:
所以,bb和rr都指向b,而且b和r指向Bottom對象的正確的章節。如今考慮當咱們賦值給*rr時會發生什麼(注意*rr的類型時Right*,所以這個賦值是有效的):
|
這樣的賦值和上面的賦值給r在根本上是一致的。所以,編譯器會用一樣的方式實現它!特別地,它會在賦值給*rr以前將b的值調整8個字節。辦事*rr指向的是b!咱們再一次圖示化這個結果: 只要咱們經過*rr來訪問Bottom對象這都是正確的,可是隻要咱們經過b自身來訪問它,全部的內存引用都會有8個字節的偏移---明顯這是個不理想的狀況。 所以,總的來講,及時*a 和*b經過一些子類型相關,**aa和**bb倒是不相關的。 |
虛擬基類的構造函數編譯器必須確保對象的全部虛指針都被正確的初始化。特別是,編譯器確保了類的全部虛基類都被調用,而且只被調用一次。若是你不顯示地調用虛擬超類(無論他們在繼承層次結構中的距離有多遠),編譯器都會自動地插入調用他們缺省構造函數。 這樣也會引來一些不能夠預期的錯誤。以上面給出的類層次結構做爲示例,並添加上構造函數的部分:
爲了不這種狀況,你應該顯示的調用虛基類的構造函數:
|
指針等價再假設一樣的(虛擬)類繼承等級,你但願這樣就打印「相等」嗎?
記住這兩個地址並不實際相等(r偏移了8個字節)。可是這應該對用戶徹底透明;所以,實際上編譯器在r與b比較以前,就給r減去了8個字節;這樣,這兩個地址就被認爲是相等的了。 |
轉換爲void類型的指針最後,咱們來思考一下當將一個對象轉換爲void類型的指針時會發生什麼事情。編譯器必須保證一個指針轉換爲void類型的指針時指向對象的頂部。使用虛函數表這很容易實現。你可能已經想到了指向top域的偏移量是什麼。它是虛函數指針到對象頂部的偏移量。所以,轉化爲void類型的指針操做可使用查詢虛函數表的方式來實現。然而必定要確保使用動態類型轉換,以下: dynamic_cast<void*>(b); 參考文獻[1] CodeSourcery, 特別是C++ ABI Summary, Itanium C++ ABI(不考慮名字,這些文檔是在平臺無關的上下文中引用的;特別低,structure of the vtables給出了虛函數表的詳細信息)。 libstdc++實現的動態類型轉化,和同RTTI和命名調整定義在 tinfo.cc中。 [2]libstdc++ 網站,特別是 C++ Standard Library API這一章節。 [3]Jan Gray 寫的C++: Under the Hood [4]Bruce Eckel的Thinking in C++(第二卷) 第9章"多重繼承"。 做者容許下載這本書download. |