C++類內存布局圖(成員函數和成員變量分開討論)


一、成員函數

成員函數可以被看作是類作用域的全局函數,不在對象分配的空間里,只有虛函數才會在類對象里有一個指針,存放虛函數的地址等相關信息。

成員函數的地址,編譯期就已確定,並靜態綁定或動態的綁定在對應的對象上。
對象調用成員函數時,早在編譯期間,編譯器就可以確定這些函數的地址,並通過傳入this指針和其他參數,完成函數的調用,所以類中就沒有必要存儲成員函數的信息。

 

二、成員變量

轉自:http://www.cnblogs.com/jerry19880126/p/3616999.html

      書上類繼承相關章節到這里就結束了,這里不妨說下C++內存分布結構,我們來看看編譯器是怎么處理類成員內存分布的,特別是在繼承、虛函數存在的情況下。

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

 

 

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

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

復制代碼
1 class Base
2 {
3     int a;
4     int b;
5 public:
6     void CommonFunction();
7 };
復制代碼

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

這里不想花精力在內存對齊因素上,所以成員變量都設為int型。

從這里可以看到普通類的排布方式,成員變量依據聲明的順序進行排列(類內偏移為0開始),成員函數不占內存空間。

 

再看下繼承,往后面添加如下代碼:

復制代碼
1 class DerivedClass: public Base
2 {
3     int c;
4 public:
5     void DerivedCommonFunction();
6 };
復制代碼

編譯,然后看到如下的內存分布(父類的內存分布不變,這里只討論子類成員變量的內存分布):

可以看到子類繼承了父類的成員變量,在內存排布上,先是排布了父類的成員變量,接着排布子類的成員變量,同樣,成員函數不占字節。

 

下面給基類加上虛函數,暫時注釋掉DerivedClass,看一下這時的內存排布:

復制代碼
1 class Base
2 {
3     int a;
4     int b;
5 public:
6     void CommonFunction();
7     void virtual VirtualFunction();
8 };
復制代碼

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

編譯器是在構造函數創建這個虛表指針以及虛表的。

那么編譯器是如何利用虛表指針與虛表來實現多態的呢?是這樣的,當創建一個含有虛函數的父類的對象時,編譯器在對象構造時將虛表指針指向父類的虛函數;同樣,當創建子類的對象時,編譯器在構造函數里將虛表指針(子類只有一個虛表指針,它來自父類)指向子類的虛表(這個虛表里面的虛函數入口地址是子類的)。

所以,如果是調用Base *p = new Derived();生成的是子類的對象,在構造時,子類對象的虛指針指向的是子類的虛表,接着由Derived*到Base*的轉換並沒有改變虛表指針,所以這時候p->VirtualFunction,實際上是p->vfptr->VirtualFunction,它在構造的時候就已經指向了子類的VirtualFunction,所以調用的是子類的虛函數,這就是多態了。

 

下面加上子類,並在子類中添加虛函數,像下面這樣:

復制代碼
1 class DerivedClass: public Base
2 {
3     int c;
4 public:
5     void DerivedCommonFunction();
6     void virtual VirtualFunction();
7 };
復制代碼

可以看到子類內存的排布如下:

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

 

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

復制代碼
1 class DerivedClass1 : public Base
2 {
3     int c;
4 public:
5     void DerivedCommonFunction();
6     void virtual VirtualFunction2();
7 };
復制代碼

注意到這時我們並沒有覆寫父類的虛方法,而是重聲明了一個新的子類虛方法,內存分布如下:

還是只有一個虛表指針,但是下方虛表的內容變化了,虛表的0號是父類的VirtualFunction,而1號放的是子類的VirtualFunction2。也就是說,如果定義了DerivedClass的對象,那么在構造時,虛表指針就會指向這個虛表,以后如果調用的是VirtualFunction,那么會從父類中尋找對應的虛函數,如果調用的是VirtualFunction2,那么會從子類中尋找對應的虛函數。

 

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

復制代碼
1 class DerivedClass1 : public Base
2 {
3     int c;
4 public:
5     void DerivedCommonFunction();
6     void virtual VirtualFunction();
7     void virtual VirtualFunction2();
8 };
復制代碼

我們既覆寫父類的虛函數,也有新添的虛函數,那么可以料想的到,是下面的這種內存分布:

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

復制代碼
 1 class Base
 2 {
 3     int a;
 4     int b;
 5 public:
 6     void CommonFunction();
 7     void virtual VirtualFunction();
 8 };
 9 
10 
11 class DerivedClass1: public Base
12 {
13     int c;
14 public:
15     void DerivedCommonFunction();
16     void virtual VirtualFunction();
17 };
18 
19 class DerivedClass2 : public Base
20 {
21     int d;
22 public:
23     void DerivedCommonFunction();
24     void virtual VirtualFunction();
25 };
26 
27 class DerivedDerivedClass : public DerivedClass1, public DerivedClass2
28 {
29     int e;
30 public:
31     void DerivedDerivedCommonFunction();
32     void virtual VirtualFunction();
33 };
復制代碼

內存分布從父類到子類,依次如下:

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的內存偏移。

 

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

復制代碼
 1 class DerivedClass1: virtual public Base
 2 {
 3     int c;
 4 public:
 5     void DerivedCommonFunction();
 6     void virtual VirtualFunction();
 7 };
 8 
 9 class DerivedClass2 : virtual public Base
10 {
11     int d;
12 public:
13     void DerivedCommonFunction();
14     void virtual VirtualFunction();
15 };
16 
17 class DerivedDerivedClass :  public DerivedClass1, public DerivedClass2
18 {
19     int e;
20 public:
21     void DerivedDerivedCommonFunction();
22     void virtual VirtualFunction();
23 };
復制代碼

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. 如果是虛繼承,那么子類會有兩份虛指針,一份指向自己的虛表,另一份指向虛基表,多重繼承時虛基表與虛基表指針有且只有一份。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM