引言
結合網上的一些資料,通過自己的一番摸索,得出了一點個人見解。現在寫下來,希望與各位同學共同探討,共同進步。
以下所有代碼均是在VS2012下測試。
一個普通的基類
1: #include <iostream>
2: using namespace std;
3:
4: class Base
5: {
6: public:
7: Base():
8: i(0)
9: {
10: }
11: void test(){
12: cout << "Base::test" << " i = " << i << endl;
13: }
14: virtual void virtualTest()
15: {
16: cout << "Base::virtualTest" << " i = " << i << endl;
17: }
18: static void staticTest()
19: {
20: cout << "Bae::staticTest" << endl;
21: }
22:
23:
24:
25: private:
26: int i;
27:
28: };
29:
30: int main()
31: {
32: Base b;
33: b.test();
34: b.virtualTest();
35: Base::staticTest();
36:
37: return 0;
38: }
我們定義了一個Base類,其類成員不言自明。在第31行處打斷點,調試模式下運行。通過觀察b對象,可以得到下圖:
此時b還未初始化,我們可以在watch窗口中,雙擊b,在b前面加上”&”符號,得到b的地址。同理,將i也添加如watch窗口中,然后可得如下圖:
我們可以看到,對象b的地址為0x003afc00,變量i的地址為0x003afc04。同時,我們可以通過Type列看到其類型的變化(通過這個,我們很容易寫出相應的代碼來驗證其顯示的數據)。按f10運行至第32行。
我們可以看到b的結構里包含了兩個成員及其他們的地址:_vfptr 和 i。我們先來聊聊i。
數據成員
我們通過觀察比較對象b的地址與i的地址,得出一個結論:i的地址是對象b的地址加上一個int*單位(即4字節)。現在,我們來驗證我們的結論。
添加37、38兩行代碼,將b的地址強制轉換為int*,然后將1賦值給b的地址增加一個int*單位后的地址。我們調試運行至39行,可以看到如下圖:
i的值確實變了!證明我們的結論沒錯。數據成員的值存放在對象地址的+4偏移位置上。但是還有些疑問,如果i是個char類型的,內存布局會怎么樣? 是不是存放在&b加一個char*單位(即1字節)地址上呢?如果有多個數據成員呢?這些請大家自行驗證。通過觀察他們的地址,我們得出最后的結論如下:不管數據成員的類型是什么,第一個定義的數據成員的地址總是與對象的地址相差四個字節,如果有多個數據成員,后面的數據成員的地址將根據第一個數據成員的地址加上其自身的類型的指針單位長度(比如char,則+1,float則+4)。
成員函數
在watch窗口中,對象b包含的成員,除了i,就只有一個_vfptr了,它是什么呢?那么多成員函數呢?我們仔細觀察_vfptr的結構:
發現_vfptr好像是一個數組一樣的結構,但是又不盡然。從圖上我們可以看出,它本身是一個void**類型的結構,其“0下標”處存放的是一個void*類型,而且看起來像是virtualTest這個函數。根據圖,我們有以下猜想:普通的成員函數和靜態成員函數的存放位置,在類的對象中並沒有保存。類對象中僅保存了虛函數的信息。又依據35行調用靜態成員函數的代碼,我們可以得到:在類中定義的靜態成員函數,等同於在一個普通的函數上面包了一層命名空間。至於普通的成員函數,在這篇里暫且不表。下面我們來驗證_vtptr是否是保存着對象的虛函數信息,以及是如何保存的。
我們通過圖,可以看到三個地址信息:&b即對象的地址為0x0046fa10,_vfptr的地址為0x0024dc74,可能是表示虛函數的“[0]”的地址為0x002410d7,直觀上看,他們毫無物理上聯系,他們的地址相隔很大。為了稱呼方便,我們用虛表(Virtual Table)來指稱_vfptr,用虛函數virtualTest來指稱“[0]”。
首先,我們看一下0x0046fa10地址上到底存的是什么,通過按Alt+6,我們可以呼出memory窗口,該窗口顯示了相應內存地址存放的信息。
該窗口大概的內容布局如紅色部分所示。通過在address欄輸入0x002af8f4(下圖b的地址),我們可以看到其中存放的是 74 dc f6 00。現在再結合虛表的地址0x00f6dc74來看,是不是有那么點聯系?腦中是否立馬浮現了一些大端機、小端機之類的信息?沒錯,因為我的機器是小端機,所以0x002af8f4中存的數據是0x00f6dc74。即虛表的地址。現在兩者的邏輯關系很明顯了,只要這樣寫即可得到虛表的地址:
如37行代碼所示,我們將b的地址強制轉換成int*,然后解引用,即可得到一個int值。我們將vbAdd添加到watch窗口中查看(注意將value調成16進制顯示),得到結果如下:
是的,我們的確得到了虛表的地址!我們按照前面的步驟,看是否能得到虛函數virtualTest的地址。在內存窗口中查詢得到如下結果:
果真如此,通過虛表地址存放的值,即可找到虛函數virtualTest的地址。下面我們用代碼驗證一下。
不出所料。我們得到了虛函數virtualTest的地址。現在三個地址(對象b、虛表、虛函數virtualTest)之間的邏輯關系很清晰了:對象b地址上存放的是指向虛表地址的數據,虛表地址上存放的是指向虛函數地址的數據。
既然我們得到了虛函數的地址,那么我們是不是可以手工來調用他們?如同前面通過地址來給類的私有數據成員賦值一樣。讓我們來寫代碼驗證一下
我們可以看到,確實能夠通過函數指針調用虛函數virtualTest,但是他的輸出卻出乎我們的意料。why?很遺憾,我目前也不知道原因。
總結
通過此篇,我們可以了解到在一個無繼承關系的普通的類中,其虛函數和數據成員的內存布局,它們之間的內在物理上和邏輯上的關系是怎么樣的。此外,還有一個問題:為什么通過地址調用虛函數,其輸出的值為什么不是預期的呢?
由於個人水平有限,寫的不是很詳盡正確。如果同學們發現了錯誤,煩請指正。也請有能力答復上述疑問的同學,不吝筆墨。
參考資料
- 韓宏.老碼識途.北京:電子工業出版社,2012
- 陳皓博客:http://blog.csdn.net/haoel/article/details/1948051/