前言
之前阿里面試的時候有個面試官就問了我會不會"什么什么的內存模型",當時自己還不知道這個名詞(知道概念,但確確實實不知道叫這個名字.....),所以就回了是問關於大小端存儲么?面試官就問下一個問題了.....
后來在《程序員的自我修養》這本書中,看了相關的概念,在這里整理一下:
Visual Studio查看虛函數表
在這里首先插一個話題,講解一下如何查看虛函數表。
我們通過調試去查看變量的分布的時候,會發現只能顯示出來基類的虛函數表,而派生類的虛函數表卻是被隱藏的;我們想查看這個怎么辦?下面是步驟:
先選擇左側的C/C++->命令行,然后在其他選項這里寫上/d1 reportAllClassLayout,它可以看到所有相關類的內存布局,如果寫上/d1 reportSingleClassLayoutXXX(XXX為類名),則只會打出指定類XXX的內存布局。近期的VS版本都支持這樣配置。
運行程序的話就會自動生成一張虛函數表了:
這個內存結構圖分成了兩個部分,上面是內存分布,下面是虛表;就可以簡單進行查看了。
C++內存模型(內存布局)
內存區域
這部分經友人提醒,可以從C++標准的"內存"概念中出發,后面會更新這部分內容。
HERE
C++內存分為5個區域:
堆 heap :
由new分配的內存塊,其釋放編譯器不去管,由我們程序自己控制(一個new對應一個delete)。如果程序員沒有釋放掉,在程序結束時OS會自動回收。涉及的問題:“緩沖區溢出”、“內存泄露”
棧 stack :
是那些編譯器在需要時分配,在不需要時自動清除的存儲區。存放局部變量、函數參數。
存放在棧中的數據只在當前函數及下一層函數中有效,一旦函數返回了,這些數據也就自動釋放了。
全局/靜態存儲區 (.bss段和.data段) :
全局和靜態變量被分配到同一塊內存中。在C語言中,未初始化的放在.bss段中,初始化的放在.data段中;在C++里則不區分了。
常量存儲區 (.rodata段) :
存放常量,不允許修改(通過非正當手段也可以修改)
代碼區 (.text段) :
存放代碼(如函數),不允許修改(類似常量存儲區),但可以執行(不同於常量存儲區)
根據C++對象生命周期不同,C++的內存模型有三種不同的內存區域:
1.自由存儲區,動態區、靜態區局部非靜態變量的存儲區域(棧)
2.動態區:用operator new,malloc分配的內存(堆)
3.靜態區:全局變量、靜態變量、字符串常量存在位置
內存布局
介紹完了內存區域,那么在C++中類對象的內存布局是如何分布的呢?
回顧一下,我們寫class的時候,會有成員變量、成員函數、靜態成員變量、靜態成員函數、虛函數與純虛函數這幾個元素,他們都分布在內存中,后文會詳細介紹這些分布;在這里,影響對象大小的有哪些因素呢?成員變量的類型與數量、虛函數表的指針(_vftptr)
、虛基類表指針(_vbtptr)
-->產生虛函數表、單一繼承、多重繼承、重復繼承、虛擬繼承,當然也會有編譯器的優化與內存對齊的影響,不過這里重點講一下類的成員變量與虛函數表相關的內存布局。
單一類
1.構造一個空類:
這里空類的長度卻是1,是為了用來標識該對象;
2.我們在類中添加成員變量:
這個涉及到了內存對齊問題,之前自己寫過一篇博客說過這個概念。調試看一下:
3.只有虛函數的類:
內存中虛函數表占了4個字節,而構建的虛函數表在我的這一篇博客中也講到了。
4.有成員變量與虛函數的類
就是將情況2、3加起來就行了。
單一繼承(含成員變量+虛函數+虛函數覆蓋)
繼承關系:
通過代碼查看的虛函數表是這樣的:
構建的虛函數表是這樣的:
多繼承(含成員函數+虛函數+虛函數覆蓋)
繼承關系:
三個int型,2個虛函數表,所以長度為20;虛函數表是這個樣子:
內存布局是這樣:
深度為2的繼承(成員變量+虛函數+虛函數覆蓋)
繼承關系:
4個int型,2個虛函數表;代碼顯示的類的布局是這樣:
內存布局:
如果自己手動計算一下繼承的內容,會發現對兩張虛函數表的內容感到奇怪,比如順着CGrandChildren
的CParent1
的虛函數表應該有:f0,g0,h0,g1,h1,h2,f2,f3
,但是我們發現剩下的卻只有f0,g0,h0,h2,f2,f3
,g1,h1
都在CParent2
這個表里。所以,如果在第二個基類中有的虛函數,在深度為2的繼承的第一個基類的虛函數表中需要排除這些虛函數。簡單的一個記憶方法就是按照當前方法計算出虛函數,然后再檢查其他基類中有沒有這個虛函數,如果有的話就刪掉;如果深度為1的派生類里有新的虛函數的話(不是重構基類的虛函數),會在第一張表里生成。當然這也只是大學期間自己做題的小技巧,其原理是這樣的:重構的話必須找到相對應的基類虛函數,而在第二個基類中的虛函數只能在第二個虛函數表才能找到;此外,虛函數表會優先生成新的虛函數在第一次遇見的時候。下面寫一段代碼驗證下:
class A {
public:
virtual void f1() { cout << "A:f1" << endl; };
virtual void f2() { cout << "A:f2" << endl; };
virtual void f3() { cout << "A:f3" << endl; };
};
class B {
public:
virtual void g1() { cout << "B:g1" << endl; };
virtual void g2() { cout << "B:g2" << endl; };
virtual void f2() { cout << "B:f2" << endl; };
};
class C :public A, public B {
virtual void f1() { cout << "C:f1" << endl; };
virtual void g1() { cout << "C:g1" << endl; };
};
class D :public C {
virtual void f1() { cout << "D:f1" << endl; };
virtual void g2() { cout << "D:g2" << endl; };
};
顯示的內存分布是這樣的:
重復繼承(含成員變量+虛函數+虛函數覆蓋)
繼承關系:
這樣的繼承關系在內存分布中是這樣的:
由於基類中的m_nAge在內存分布中出現了兩次,所以最后的結果是5個int類型和2個虛函數表,共計28字節。
內存布局是這樣的:
單一虛繼承(含成員變量+虛函數+虛函數覆蓋)
繼承關系如下:
所謂的虛繼承就是把繼承語法前加上virtual
關鍵字,例如class B:virtual public A{..};
虛擬繼承的出現就是為了解決重復繼承中多個間接父類的問題的 。內存分布是這樣的:
這里需要解釋下,因為出現了vfptr
與vbptr
,前面的我們已經經常看到了,但是vbptr
卻是第一次見,它是CChildren
對應的虛表指針,它指向CChildren
的虛表vtable,另一個vfptr
位於0地址偏移處,它指向vftable。從截圖中也可以看出有兩個表vftable
與vbtable
。第二張vbtable
中的8表示vbptr
與基類的vfptr
之間的偏移。
內存布局為:
另外提及一下,如果CChildren
里全部是重載基類中的虛函數的話,或者說沒有新的虛函數的話,vftptr
指向的虛函數表就是空的,所以計算大小的時候可以不用算進去,因為實際上並沒有創建相應的表格:
舉個例子:
class A {
public:
virtual void f1() { cout << "A:f1" << endl; };
virtual void f2() { cout << "A:f2" << endl; };
virtual void f3() { cout << "A:f3" << endl; };
};
class B:virtual A {
public:
//virtual void g1() { cout << "B:g1" << endl; };
virtual void f2() { cout << "B:f2" << endl; };
virtual void f3() { cout << "B:f3" << endl; };
};
內存分布為:
多虛繼承(含成員變量+虛函數+虛函數覆蓋)
(1)繼承關系如下:
其中CParent1是虛繼承,CParent2是一般繼承。
內存分布為:
內存布局:
(2)再看另一種繼承關系:
其中CParent2是虛繼承,CParent1是一般繼承。
內存分布為:
內存布局為:
(3)繼承關系:
內存分布為:
從這里可以看出vbtable確實是存儲了指向相應的基類的虛函數表指針。
內存布局為:
鑽石型的虛擬多重繼承(含成員變量+虛函數+虛函數覆蓋)
繼承關系:
內存分布為:
內存布局為: