C++內存模型


前言

之前阿里面試的時候有個面試官就問了我會不會"什么什么的內存模型",當時自己還不知道這個名詞(知道概念,但確確實實不知道叫這個名字.....),所以就回了是問關於大小端存儲么?面試官就問下一個問題了.....
后來在《程序員的自我修養》這本書中,看了相關的概念,在這里整理一下:

Visual Studio查看虛函數表

在這里首先插一個話題,講解一下如何查看虛函數表。

我們通過調試去查看變量的分布的時候,會發現只能顯示出來基類的虛函數表,而派生類的虛函數表卻是被隱藏的;我們想查看這個怎么辦?下面是步驟:

image.png

image.png

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

運行程序的話就會自動生成一張虛函數表了:

image.png

這個內存結構圖分成了兩個部分,上面是內存分布,下面是虛表;就可以簡單進行查看了。

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.構造一個空類:

image.png

這里空類的長度卻是1,是為了用來標識該對象;

2.我們在類中添加成員變量:

image.png

這個涉及到了內存對齊問題,之前自己寫過一篇博客說過這個概念。調試看一下:

image.png

3.只有虛函數的類:

image.png

內存中虛函數表占了4個字節,而構建的虛函數表在我的這一篇博客中也講到了。

image.png

4.有成員變量與虛函數的類

image.png

就是將情況2、3加起來就行了。

單一繼承(含成員變量+虛函數+虛函數覆蓋)
繼承關系:
image.png

通過代碼查看的虛函數表是這樣的:

image.png

構建的虛函數表是這樣的:

image.png

多繼承(含成員函數+虛函數+虛函數覆蓋)

繼承關系:

image.png

三個int型,2個虛函數表,所以長度為20;虛函數表是這個樣子:

image.png

內存布局是這樣:

image.png

深度為2的繼承(成員變量+虛函數+虛函數覆蓋)

繼承關系:

image.png

4個int型,2個虛函數表;代碼顯示的類的布局是這樣:

image.png

內存布局:

image.png

如果自己手動計算一下繼承的內容,會發現對兩張虛函數表的內容感到奇怪,比如順着CGrandChildrenCParent1的虛函數表應該有:f0,g0,h0,g1,h1,h2,f2,f3,但是我們發現剩下的卻只有f0,g0,h0,h2,f2,f3g1,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; };
};

顯示的內存分布是這樣的:

image.png

重復繼承(含成員變量+虛函數+虛函數覆蓋)

繼承關系:

image.png

這樣的繼承關系在內存分布中是這樣的:

image.png

由於基類中的m_nAge在內存分布中出現了兩次,所以最后的結果是5個int類型和2個虛函數表,共計28字節。

內存布局是這樣的:

image.png

單一虛繼承(含成員變量+虛函數+虛函數覆蓋)

繼承關系如下:

image.png

所謂的虛繼承就是把繼承語法前加上virtual關鍵字,例如class B:virtual public A{..};

虛擬繼承的出現就是為了解決重復繼承中多個間接父類的問題的 。內存分布是這樣的:

image.png

這里需要解釋下,因為出現了vfptrvbptr,前面的我們已經經常看到了,但是vbptr卻是第一次見,它是CChildren對應的虛表指針,它指向CChildren的虛表vtable,另一個vfptr位於0地址偏移處,它指向vftable。從截圖中也可以看出有兩個表vftablevbtable。第二張vbtable中的8表示vbptr與基類的vfptr之間的偏移。

內存布局為:

image.png

另外提及一下,如果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; };
};

內存分布為:

image.png

多虛繼承(含成員變量+虛函數+虛函數覆蓋)

(1)繼承關系如下:

1540985431636

其中CParent1是虛繼承,CParent2是一般繼承。

內存分布為:

image.png

內存布局:

image.png

(2)再看另一種繼承關系:

image.png

其中CParent2是虛繼承,CParent1是一般繼承。

內存分布為:

image.png

內存布局為:

image.png

(3)繼承關系:

image.png

內存分布為:

image.png

從這里可以看出vbtable確實是存儲了指向相應的基類的虛函數表指針。

內存布局為:

image.png

鑽石型的虛擬多重繼承(含成員變量+虛函數+虛函數覆蓋)

繼承關系:

image.png

內存分布為:

image.png

內存布局為:

image.png


免責聲明!

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



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