1. 程式人生 > >C++ 類記憶體分佈

C++ 類記憶體分佈

工欲善其事,必先利其器,我們先用好Visual Studio工具,像下面這樣一步一步來:

先選擇左側的C/C++->命令列,然後在其他選項這裡寫上/d1 reportAllClassLayout,它可以看到所有相關類的記憶體佈局,如果寫上/d1 reportSingleClassLayoutXXX(XXX為類名),則只會打出指定類XXX的記憶體佈局。近期的VS版本都支援這樣配置。

下面可以定義一個類,像下面這樣:

1234567 classBase{inta;intb;public:voidCommonFunction();};

然後編譯一下,可以看到輸出框裡面有這樣的排布:

這裡不想花精力在記憶體對齊因素上,所以成員變數都設為int型。

從這裡可以看到普通類的排布方式,成員變數依據宣告的順序進行排列(類內偏移為0開始),成員函式不佔記憶體空間。

再看下繼承,往後面新增如下程式碼:

123456 classDerivedClass:publicBase{intc;public:voidDerivedCommonFunction();};

編譯,然後看到如下的記憶體分佈(父類的記憶體分佈不變,這裡只討論子類成員變數的記憶體分佈):

可以看到子類繼承了父類的成員變數,在記憶體排布上,先是排布了父類的成員變數,接著排布子類的成員變數,同樣,成員函式不佔位元組。

下面給基類加上虛擬函式,暫時註釋掉DerivedClass,看一下這時的記憶體排布:

12345678 classBase{inta;intb;public:voidCommonFunction();voidvirtual VirtualFunction();};

這個記憶體結構圖分成了兩個部分,上面是記憶體分佈,下面是虛表,我們逐個看。VS所帶編譯器是把虛表指標放在了記憶體的開始處(0地址偏移),然後再是成員變數;下面生成了虛表,緊跟在&Base1_meta後面的0表示,這張虛表對應的虛指標在記憶體中的分佈,下面列出了虛擬函式,左側的0是這個虛擬函式的序號,這裡只有一個虛擬函式,所以只有一項,如果有多個虛擬函式,會有序號為1,為2的虛擬函式列出來。

編譯器是在建構函式建立這個虛表指標以及虛表的。

那麼編譯器是如何利用虛表指標與虛表來實現多型的呢?是這樣的,當建立一個含有虛擬函式的父類的物件時,編譯器在物件構造時將虛表指標指向父類的虛擬函式;同樣,當建立子類的物件時,編譯器在建構函式裡將虛表指標(子類只有一個虛表指標,它來自父類)指向子類的虛表(這個虛表裡面的虛擬函式入口地址是子類的)。

所以,如果是呼叫Base *p = new Derived();生成的是子類的物件,在構造時,子類物件的虛指標指向的是子類的虛表,接著由Derived*到Base*的轉換並沒有改變虛表指標,所以這時候p->VirtualFunction,實際上是p->vfptr->VirtualFunction,它在構造的時候就已經指向了子類的VirtualFunction,所以呼叫的是子類的虛擬函式,這就是多型了。

下面加上子類,並在子類中新增虛擬函式,像下面這樣:

1234567 classDerivedClass:publicBase{intc;public:voidDerivedCommonFunction();voidvirtual VirtualFunction();};

可以看到子類記憶體的排布如下:

上半部是記憶體分佈,可以看到,虛表指標被繼承了,且仍位於記憶體排布的起始處,下面是父類的成員變數a和b,最後是子類的成員變數c,注意虛表指標只有一個,子類並沒有再生成虛表指標了;下半部的虛表情況與父類是一樣的。

我們把子類換個程式碼,像這樣:

1234567 classDerivedClass1:publicBase{intc;public:voidDerivedCommonFunction();voidvirtual VirtualFunction2();};

注意到這時我們並沒有覆寫父類的虛方法,而是重聲明瞭一個新的子類虛方法,記憶體分佈如下:

還是隻有一個虛表指標,但是下方虛表的內容變化了,虛表的0號是父類的VirtualFunction,而1號放的是子類的VirtualFunction2。也就是說,如果定義了DerivedClass的物件,那麼在構造時,虛表指標就會指向這個虛表,以後如果呼叫的是VirtualFunction,那麼會從父類中尋找對應的虛擬函式,如果呼叫的是VirtualFunction2,那麼會從子類中尋找對應的虛擬函式。

我們再改造一下子類,像這樣:

12345678 classDerivedClass1:publicBase{intc;public:voidDerivedCommonFunction();voidvirtual VirtualFunction();voidvirtual VirtualFunction2();};

我們既覆寫父類的虛擬函式,也有新添的虛擬函式,那麼可以料想的到,是下面的這種記憶體分佈:

下面來討論多重繼承,程式碼如下:

123456789101112131415161718192021222324252627282930313233 classBase{inta;intb;public:voidCommonFunction();voidvirtual VirtualFunction();};classDerivedClass1:publicBase{intc;public:voidDerivedCommonFunction();voidvirtual VirtualFunction();};classDerivedClass2:publicBase{intd;public:voidDerivedCommonFunction();voidvirtual VirtualFunction();};classDerivedDerivedClass:publicDerivedClass1,publicDerivedClass2{inte;public:voidDerivedDerivedCommonFunction();voidvirtual VirtualFunction();};

記憶體分佈從父類到子類,依次如下:

Base中有一個虛表指標,地址偏移為0

DerivedClass1繼承了Base,記憶體排布是先父類後子類。

DerivedClass2的情況是類似於DerivedClass1的。

下面我們重點看看這個類DerivedDerivedClass,由外向內看,它並列地排布著繼承而來的兩個父類DerivedClass1與DerivedClass2,還有自身的成員變數e。DerivedClass1包含了它的成員變數c,以及Base,Base有一個0地址偏移的虛表指標,然後是成員變數a和b;DerivedClass2的記憶體排布類似於DerivedClass1,注意到DerivedClass2裡面竟然也有一份Base。

這裡有兩份虛表了,分別針對DerivedClass1與DerivedClass2,在&DerivedDericedClass_meta下方的數字是首地址偏移量,靠下面的虛表的那個-16表示指向這個虛表的虛指標的記憶體偏移,這正是DerivedClass2中的{vfptr}在DerivedDerivedClass的記憶體偏移。

如果採用虛繼承,像下面這樣:

1234567891011121314151617181920212223 classDerivedClass1:virtualpublicBase{intc;public:voidDerivedCommonFunction();voidvirtual VirtualFunction();};classDerivedClass2:virtualpublicBase{intd;public:voidDerivedCommonFunction();voidvirtual VirtualFunction();};classDerivedDerivedClass:publicDerivedClass1,publicDerivedClass2{inte;public:voidDerivedDerivedCommonFunction();voidvirtual VirtualFunction();};

Base類沒有變化,但往下看:

DerivedClass1就已經有變化了,原來是先排虛表指標與Base成員變數,vfptr位於0地址偏移處;但現在有兩個虛表指標了,一個是vbptr,另一個是vfptr。vbptr是這個DerivedClass1對應的虛表指標,它指向DerivedClass1的虛表vbtable,另一個vfptr是虛基類表對應的虛指標,它指向vftable。

下面列出了兩張虛表,第一張表是vbptr指向的表,8表示{vbptr}與{vfptr}的偏移;第二張表是vfptr指向的表,-8指明瞭這張表所對應的虛指標位於記憶體的偏移量。

DerivedClass2的記憶體分佈類似於DerivedClass1,同樣會有兩個虛指標,分別指向兩張虛表(第二張是虛基類表)。

下面來仔細看一下DerivedDerivedClass的記憶體分佈,這裡面有三個虛指標了,但base卻只有一份。第一張虛表是內含DerivedClass1的,20表示它的虛指標{vbptr}離虛基表指標{vfptr}的距離,第二張虛表是內含DerivedClass2的,12表示它的虛指標{vbptr}離虛基表指標{vfptr}的距離,最後一張表是虛基表,-20指明瞭它對應的虛指標{vfptr}在記憶體中的偏移。

虛繼承的作用是減少了對基類的重複,代價是增加了虛表指標的負擔(更多的虛表指標)。

下面總結一下(當基類有虛擬函式時):

1. 每個類都有虛指標和虛表;

2. 如果不是虛繼承,那麼子類將父類的虛指標繼承下來,並指向自身的虛表(發生在物件構造時)。有多少個虛擬函式,虛表裡面的項就會有多少。多重繼承時,可能存在多個的基類虛表與虛指標;

3. 如果是虛繼承,那麼子類會有兩份虛指標,一份指向自己的虛表,另一份指向虛基表,多重繼承時虛基表與虛基表指標有且只有一份。