C++虛擬多重繼承對象模型討論
作者:magictong
調試環境:Windows7VS2005
概述
記得剛開始寫C++程序時,那還是大學時光,感覺這玩意比C強大多了,怎麼就實現了多態,RTTI這些牛逼的玩意呢?當時沒有深究,後來零零散散看過一些介紹的文章,也看了一些相關的書籍,總覺得說得不甚清楚。而這些問題的本質還是在於C++對象的內存模型問題,數據結構決定了你的算法嘛,在這裡也是基本適用的。網上有很多講C++對象模型的文章,但是大部分都是涉及基本繼承,多重繼承等等,而對於虛擬多重繼承的情況則涉及不多,這篇文章則主要講述這種情況下C++的對象模型情況,希望能夠起到拋磚引玉的作用。
本文分三個步驟由淺入深的討論這個問題。
一、先看一個最簡單的例子
KVBase有一個虛函數Run(),KA從KVBase虛擬繼承並且覆蓋Run()函數。
源代碼
#include"stdafx.h"
#include
usingnamespacestd;
//先看一個簡單的例子
classKVBase
{
public:
KVBase():m_nBase(1){}
virtualvoidRun()
{
cout <<"KVBase::Run()is called." < } private: intm_nBase; }; classKA :virtualpublic
KVBase { public: KA():m_nb(2){} virtualvoidRun() { cout <<"KA::Run()is called." < } private: intm_nb; }; int_tmain(intargc,_TCHAR*argv[]) { KAa; KVBase*pBase = &a; cout<<"The Base Address:"< cout<<"//=============>ObjectInfomation: " < cout< cout< cout< cout< cout< pBase->Run(); return0; } 輸出: 參考一下調試的結果(第一個圖是視圖,直觀反映成員情況但不是具體的內存對象模型,第二張圖才是內存中的真正數據排布)。 a對象內存分布情況: 0x00403238是a對象的虛基類指針,注意不是虛表指針,我開始也以為是虛表指針,但是根據後面的分析,在這種情況下,a對象因為沒有自己的獨特虛函數,實際上它不需要專門的虛指針了,共用虛基類的就可以了(至少我認為它是這麼設計的^_^)。它裡面的兩個成員第一個成員是目前只發現有兩個可能的值0和-4,如果當前類沒有獨特的虛函數,則值是0,否則是-4,我把它簡單稱之為一個標志位,第二個成員則是一個當前位置到虛基類地址的偏移量,單位是字節(0x00ff30+0x0c=0x00ff3c)。 0x00403234則是虛基類的虛表指針,0x004015d0實際就是KA::Run的地址。 這種情況下,虛表結構圖大概是這樣的(虛基類在最後面): 二、繼承類有自己特有的虛函數 稍微變動一下,給KA加一個自己的獨特的虛函數RunKA(),分析方法同上,此時你會發現內存布局發生了一些小變化,最大的變化是KA類有了自己的虛表。 classKA :virtualpublic
KVBase { public: KA():m_nb(2){} virtualvoidRun() { cout <<"KA::Run()is called." < } virtualvoidRunKA() { cout <<"KA::RunKA()is called." < } private: intm_nb; }; 輸出: 其它部分不變,再次調試發現,內存是這樣了: 視圖: 0x00403258是KA類的虛表指針,裡面的0x00401120正是KA::RunKA()的地址。 0x00403264是虛基類指針,存放著-4(0xfffffffc的補碼,上面討論過這種情況下表示當前類有自己的特有虛函數)和0x0c(偏移)。 0x403260是虛基類的虛表指針,裡面存放這KA::Run()地址。 因此,這種情況下,虛表圖大概是這樣的(虛基類依然在最後面): 三、鑽石型繼承 好吧,分析一個復雜的鑽石型繼承情況之後收工,為了說明的完整性,我貼一下全部的代碼。 #include"stdafx.h" #include usingnamespacestd; classKVBase { public: KVBase():m_nBase(1){} virtualvoidRun() { cout <<"KVBase::Run()is called." < } private: intm_nBase; }; classKA :virtualpublic
KVBase { public: KA():m_na(2){} virtualvoidRun() { cout <<"KA::Run()is called." < } virtualvoidRunKA() { cout <<"KA::RunKA()is called." < } private: intm_na; }; classKB :virtualpublic
KVBase { public: KB():m_nb(3),m_nb2(0x1022){} virtualvoidRun() { cout <<"KB::Run()is called." < } virtualvoidRunKB() { cout <<"KB::RunKB()is called." < } virtualvoidFuncKB() { cout <<"KB::FuncKB()is called." < } private: intm_nb; intm_nb2; }; classDChild :publicKA,public
KB { public: DChild():m_ndChild(4){} virtualvoidRun() { cout <<"DChild::Run()is called." < } virtualvoidRunKA() { cout <<"DChild::RunKA()is called." < } virtualvoidRunKB() { cout <<"DChild::RunKB()is called." < } virtualvoidFuncDChild() { cout <<"DChild::FuncDChild()is called." < } private: intm_ndChild; }; int_tmain(intargc,_TCHAR*argv[]) { DChilda; KVBase*pBase = &a; cout<<"The Base Address:"< cout<<"//=============>ObjectInfomation: " < cout< cout< cout< cout< cout< cout< cout< cout< cout< cout< cout< pBase->Run(); return0; } 運行一下: 現在開始變得有些有趣了,先看一下視圖,大概了解一下整個布局。 然後一個個的看打印出來的那些地址都代表著什麼。 0x00403384是KA的虛表指針,其中0x00401330是DChild::RunKA()地址,0x00401390是DChild::FuncDChild()地址,很明顯,DChild類的虛函數是放在這個虛表裡面的。 0x004033a0是KA的虛基類指針,標志位依然是-4,偏移則是0x20,算一下,0x0012FF1C+0x20果然是0x0012FF3C。 跟KB類相關的兩個地址,0x00403390和0x004033a8的地址內容跟KA類是相似的(不過虛表就沒有DChild的虛函數指針了),聰明的小伙伴可以自己去看下。 至於0x0040339c則依然是虛基類虛表指針,現在虛表裡面是函數DChild::Run()地址。 直接上圖,這種情況下,虛表結構圖大概是這樣的: 總結 其實C++的對象內存模型在不同的編譯器下面是有差異的,有興趣的同學可以在GCC之類的環境下測試一下,但是整體的設計思想其實都是大同小異的,我們也許沒有必要把所有的細節都弄得極其清楚,但是學習這個思想才是最根本的,思考一下前人為什麼要這麼設計?!這麼設計的好處是什麼?!他們想解決什麼問題?! 參考文獻 [1] 虛函數表解析http://blog.csdn.net/haoel/article/details/1948051 [2] C++對象的內存布局http://blog.csdn.net/haoel/article/details/3081328 [3] 《深度探索C++對象模型》侯捷 樣例代碼下載