虛函數和虛擬繼承的內存分佈

一.虛函數

(1)C++中的虛函數的主要作用:實現了多態的機制

(2)多態:用父類型的指針指向其子類的實例,然後通過父類的指針調用實際子類的成員函數。這種技術可以讓父類的指針有“多種形態”,這是一種泛型技術。所謂泛型技術,說白了就是試圖使用不變的代碼來實現可變的算法。比如:模板技術,RTTI技術,虛函數技術,要麼是試圖做到在編譯時決議,要麼試圖做到運行時決議。

(3)多態要基於函數重載,所以如果子類沒有重載父類的虛函數那是一件毫無意義的事情。


二.虛函數表

1.虛函數表:虛函數表C++的編譯器應該是保證虛函數表的指針存在於對象實例中最前面的位置(這是爲了保證取到虛函數表的有最高的性能——如果有多層繼承或是多重繼承的情況下)。 這意味着我們通過對象實例的地址得到這張虛函數表,然後就可以遍歷其中函數指針,並調用相應的函數。

如圖:

wKioL1dvlO7BRHsiAACKttf2LPU093.png

虛函數表的最後多加了一個結點,這是虛函數表的結束結點,就像字符串的結束符“/0”一樣,其標誌了虛函數表的結束。這個結束標誌的值在不同的編譯器下是不同的。在WinXP+VS2003下,這個值是NULL。而在Ubuntu 7.10 + Linux 2.6.22 + GCC 4.1.3下,這個值是如果1,表示還有下一個虛函數表,如果值是0,表示是最後一個虛函數表。


2.虛函數在虛函數表中的存放原則

1)虛函數按照其聲明順序放於表中。

2)父類的虛函數在子類的虛函數前面          

3)覆蓋的f()函數被放到了虛表中原來父類虛函數的位置。

4)沒有被覆蓋的函數依舊。

wKiom1dvnojTjUt8AADku2hdpTQ926.png

3.多重繼承

1) 每個父類都有自己的虛表。

2) 子類的成員函數被放到了第一個父類的表中。(所謂的第一個父類是按照聲明順序來判斷的)     這樣做就是爲了解決不同的父類類型的指針指向同一個子類實例,而能夠調用到實際的函數。

wKioL1dvrZDS8UPpAAPBCruBM1E485.png

三.進階

(1)單一的一般繼承

1)虛函數表在最前面的位置。

2)成員變量根據其繼承和聲明順序依次放在後面。

3)在單一的繼承中,被重寫的虛函數在虛函數表中得到了更新。

wKiom1dvtIvQ07OpAAKCaNHq6VE359.pngx

(2)多重繼承(只是比上面的多重繼承多了個成員變量)

1)每個父類都有自己的虛表。

2)子類的成員函數被放到了第一個父類的表中。

3)內存佈局中,其父類佈局依次按聲明順序排列。

4)每個父類的虛表中的f()函數都被重寫成了子類的f()。這樣做就是爲了解決不同的父類類型的指針指向同一個子類實例,而能夠調用到實際的函數。

wKiom1dvtlTh_oD6AAMe4HLSnic017.png

(3)重複繼承

wKioL1dvul6xQfEiAANrwO8DalY536.png

最頂端的父類B其成員變量存在於B1和B2中,並被D給繼承下去了。而在D中,其有B1和B2的實例,於是B的成員在D的實例中存在兩份,一份是B1繼承而來的,另一份是B2繼承而來的。所以,如果我們使用以下語句,則會產生二義性編譯錯誤:

D d;
d.ib = 0; //二義性錯誤
d.B1::ib = 1; //正確
d.B2::ib = 2; //正確

注意,上面例程中的最後兩條語句存取的是兩個變量。雖然我們消除了二義性的編譯錯誤,但B類在D中還是有兩個實例,這種繼承造成了數據的重複,我們叫這種繼承爲重複繼承。重複的基類數據成員可能並不是我們想要的。所以,C++引入了虛基類的概念。


(4)重複虛擬繼承-把上述的“重複繼承”的B1和B2繼承B的語法中加上virtual 關鍵,就成了虛擬繼承

wKiom1dv1Z3QU5lTAANZafcMnok149.png

四.利用虛函數表做的壞事兒

(1)任何試圖使用父類指針調用子類中的未覆蓋父類的成員函數的行爲都會被編譯器視爲非法,但我們可以通過指針的方式訪問虛函數表來達到違反C++語義的行爲。

(2)如果父類的虛函數是private或是protected的,但這些非public的虛函數同樣會存在於虛函數表中,所以,我們同樣可以使用訪問虛函數表的方式來訪問這些non-public的虛函數。


<有個疑惑:爲什麼最後一個多重虛繼承B的地址在最後?請大家指教一下,我也下去再好好研究一下>

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章