C++對象記憶體布局測試總結
http://hi.baidu.com/%D6%F2%C7%EF/blog/item/826d38ff13c32e3a5d6008e8.html
上文是半年前對虛函數、虛擬繼承的理解。可能有一些錯漏。而且只是理解了比較簡單的部分,表達也不夠清晰,這次決定花的時間再做一次總結。
對於普通的C++對象記憶體布局,簡單得不得了,就不做總結了。這裡只總結涉及到虛擬繼承的情況。
因為不同編譯器對虛擬繼承的實現採用不同的方式,所以要完整的分析是不可能的。這裡只考慮VS2005/2008,還有簡單涉及GCC編譯器。
1、 單個虛擬繼承
只是為了分析而已,實際中並沒有太大的作用。跟虛擬繼承相關的衍生類別對象的記憶體布局跟具體的編譯器相關。
(1)VS編譯器:無論有無虛函數,必然含有虛基類表指標。虛基類表中的內容為本類執行個體的位移和基類執行個體的相對位移值。如果有虛函數,那麼基類的虛函數表跟衍生類別的虛函數表是分開的。
在記憶體布局上,地址從低到高,順序如下:衍生類別的虛函數表指標+虛基類表指標+衍生類別的成員變數+“間隔”(4個位元組)+基類的虛函數表指標+基類的成員變數。衍生類別跟基類執行個體的位置關係跟普通繼承正好相反。
說明:“間隔”產生的原因是衍生類別重寫了基類的虛函數。如果沒重寫,則這一項沒有。"本類地址"指的是包含有虛基類的對象(或部分對象),也就是繼承鏈上的直接子類對象的地址,本例比較簡單,就是衍生類別對象地址。“本類地址跟虛基類表指標地址只差”,這個值經常是-4、0,-4表明“本類”還有一個虛函數表指標;0則表明“本類”的第一個4位元組儲存的就是虛基類表指標,沒有虛函數表指標。
圖 1 VS編譯器—單個虛擬繼承
(2)GNU的GCC編譯器:跟VS的編譯器類似,有不同的地方是,虛基類表跟衍生類別的虛函數表合并。另外通過虛基類表指標往正負兩個方向定址,可以獲得不同位移值,也就是說有兩個功能一樣的虛函數表。不過在實際應用的時候,不知道虛基類表是否真的有用,測試了簡單的情況發現編譯器做了最佳化,根本就沒有用虛基類表來定址虛基類執行個體。
圖 2 GCC編譯器—單個虛擬繼承
2、 虛擬繼承多個基類
虛基類表要增加內容,有N個虛基類就有N項基類執行個體位移值,再加上1項本類執行個體的位移值,也就是N+1。
假設C虛擬繼承了A類和B類,考慮最複雜的情況(都有虛函數),那麼C類對象的記憶體布局如下
(VS編譯器):
C類虛函數表指標+虛基類表指標+C類成員變數+A類間隔(4個位元組) + A類虛函數表指標+ A類成員變數+ B類間隔(4個位元組)+B類虛函數表指標+ B類成員變數。
說明:當衍生類別重寫了該基類的虛函數,才會有“間隔”。“間隔”屬於虛函數被重新實現了的虛基類,可能是一個標誌,也有可能是在函數調用的時候用上。不是很清楚。
圖 3 VS編譯器—虛擬繼承多個基類
(GCC編譯器):
C類虛函數表指標(包含虛基類表) + C類成員變數 + A類虛函數表指標 + A類成員變數 + B類虛函數表指標 + B類成員變數。
相比較執行,使用GCC編譯器,衍生類別對象小一些。(圖略)
3、 虛擬繼承之菱形繼承
這裡的菱形繼承指的是:B、C虛擬繼承A,然後D普通繼承B、C。
D類的對象的記憶體布局如下
(VS編譯器)
B類虛函數表指標(該虛函數表包含D類專屬的虛函數的地址)+B類虛基類表指標+B類成員變數+C類虛函數表指標+C類虛基類表指標+C類成員變數+D類成員變數+“間隔”+A類虛函數表指標+A類成員變數。
說明:如果A類的虛函數沒有被重寫,那麼就沒有“間隔”。
圖 4 VS編譯器—菱形繼承
(GCC編譯器)
把B、C類的虛函數表跟虛基類表合并就是了。(圖略)
4、VS編譯器,“間隔”的疑問
“間隔”的問題,在沒有虛函數的情況下,重寫是沒有“間隔”的,所以覺得可能跟虛函數有關,也就是說是為了實現多態,具體是用在哪個地方,做了簡單的反組譯碼調試(父類指標指向子類對象,調用被子類重寫了的虛函數),並沒有發現哪裡用到了“間隔”,可能要在複雜的調用才會用上吧,目前搞不清楚。
5、虛基類表的問題
通過反組譯碼調試發現在使用多態的時候,VS編譯器會去使用虛基類表,用於定址虛基類地址。而GCC編譯器則沒有這麼做,測試了比較簡單的情況,發現它做了最佳化,並沒有利用虛基類表,而是直接在衍生類別對象地址上加上一個常數,獲得虛基類執行個體的地址。
學習參考:
http://blog.csdn.net/haoel/archive/2008/10/15/3081328.aspx
http://blog.csdn.net/BlueDog/archive/2009/10/22/4711169.aspx
《深度探索C++物件模型》也是非常好的資料,只是該書較舊,而類物件版面配置又是編譯器的個性屬性,手上的編譯器有可能不是按照書裡的來實現(譬如書中強調虛函數表指標放在類對象的結尾,以便相容C,而實際上編譯器沒有這麼做,還有,資料成員的地址也沒有像書中所說的那樣),所以動手實踐才有真理。
2010.8.19
cs_wuyg@126.com