MSVC++
對象內(nèi)存模型深入解析與具體應(yīng)用
前言:本文之所以強調(diào)MSVC, 旨在提醒讀者在不同平臺和解釋器下內(nèi)存布局和實現(xiàn)上存在差異,但編程思想通用,文中內(nèi)容大多來自筆者實際工作經(jīng)驗和網(wǎng)上搜集,力求正確,但水平有限,如有不當之處,敬請指出
面向?qū)ο螅罕疚拿嫦蛴幸欢?/span>C/C++基礎(chǔ),并且可以讀懂部分匯編的讀者
版權(quán):歡迎轉(zhuǎn)載,但請注明出處http://www.shnenglu.com/dawnbreak/, 保留對本文的一切權(quán)力
目錄
1. C++基本類型與結(jié)構(gòu)體內(nèi)存布局
Key words: class, struct, memory alignment
2.虛表, 多態(tài)與動態(tài)綁定
Key words: Virtual Table, polymiorphism
3.對象池
Key words: object pool , reload, new ,delete
4.內(nèi)存泄漏檢測
Key words: memory leak detect
5.智能指針
Key words: smart pointer
6. 編譯期類型約束
Key words: compile-time ,type-constraint
Appendix 1: C++堆棧祥解
第二章 虛表, 多態(tài)與動態(tài)綁定
Key words: Virtual Table, polymiorphism
1.虛表-Virtual Table 多態(tài)-polymiorphism
虛函數(shù)表由虛函數(shù)的地址組成,表中函數(shù)地址的順序和它們第一次出現(xiàn)的順序(即在類定義的順序)一致。若有重載的函數(shù),則替換掉基類函數(shù)的地址,事實上你可以簡單的將虛表定義理解如下:
Int* virtualTable[size]//普通的指針數(shù)組而已
多數(shù)情形下,MSVC的類按如下格局分布:
? 指向虛函數(shù)表的指針(_vtable_或_vftable_),不過它只在類包括虛函數(shù),以及不能從基類復(fù)用合適的函數(shù)表時才會被添加。
? 基類。
? 函數(shù)成員。
請看如下例子:
#include "stdafx.h"
#include "assert.h"
#include "iostream"
using namespace std;
class A


{
public:
int b1;
static int b3;
int b2;
public:

virtual void A_virt1()

{
std::cout<<" call of first A's vf"<<std::endl;
}

virtual void A_virt2()

{
std::cout<<" call of second A's vf"<<std::endl;
}

};
int A::b3=100;
//#pragma pack(1)
class B:public A


{
int a1;
char b1;
float c1;
virtual void A_virt2()

{
std::cout<<" call of second B's vf"<<std::endl;
}
virtual void B_virt1()

{
std::cout<<" call of second B's vf1"<<std::endl;
}
virtual void B_virt2()

{
std::cout<<" call of second B's vf2"<<std::endl;
}
void getsome();
};
void B::getsome()


{
int a=1;
}

class D:public A


{
};

class C:public B,public D


{
virtual void B_virt1()

{
std::cout<<" call of first C's vf"<<std::endl;
}
};
int _tmain(int argc, _TCHAR* argv[])


{
typedef void(*pfunc)();
cout<<"test the class memory layout-virtual table"<<endl;
C cc;
(pfunc(((int**)(&cc))[0][0]))();
(pfunc(((int**)(&cc))[0][1]))();
(pfunc(((int**)(&cc))[0][2]))();
(pfunc(((int**)(&cc))[0][3]))();

system("pause");
return 0;
}
程序輸出結(jié)果:

以下是各個類在內(nèi)存中的布局圖
class A size(12):
+---
0 | {vfptr}
4 | b1
8 | b2
+---
A::$vftable@:
| &A_meta
| 0
0 | &A::A_virt1
1 | &A::A_virt2
A::A_virt1 this adjustor: 0
A::A_virt2 this adjustor: 0
class B size(24):
+---
| +--- (base class A)
0 | | {vfptr}
4 | | b1
8 | | b2
| +---
12 | a1
16 | b1
| <alignment member> (size=3)
20 | c1
+---
B::$vftable@:
| &B_meta
| 0
0 | &A::A_virt1
1 | &B::A_virt2
2 | &B::B_virt1
3 | &B::B_virt2
B::A_virt2 this adjustor: 0
B::B_virt1 this adjustor: 0
B::B_virt2 this adjustor: 0
class D size(12):
+---
| +--- (base class A)
0 | | {vfptr}
4 | | b1
8 | | b2
| +---
+---
D::$vftable@:
| &D_meta
| 0
0 | &A::A_virt1
1 | &A::A_virt2
class C size(36):
+---
| +--- (base class B)
| | +--- (base class A)
0 | | | {vfptr}
4 | | | b1
8 | | | b2
| | +---
12 | | a1
16 | | b1
| | <alignment member> (size=3)
20 | | c1
| +---
| +--- (base class D)
| | +--- (base class A)
24 | | | {vfptr}
28 | | | b1
32 | | | b2
| | +---
| +---
+---
C::$vftable@B@:
| &C_meta
| 0
0 | &A::A_virt1
1 | &B::A_virt2
2 | &C::B_virt1
3 | &B::B_virt2
C::$vftable@D@:
| -24
0 | &A::A_virt1
1 | &A::A_virt2
C::B_virt1 this adjustor: 0
為了調(diào)用虛函數(shù),編譯器首先需要從_vftable_取得函數(shù)地址,然后就像調(diào)用簡單方法一樣(例如,傳入_this_指針作為隱含參數(shù))。例如:
cc.A_virt2()
;esi = ptr [cc]
mov eax, [esi] ;fetch virtual table pointer
mov ecx, esi
call [eax+4] ;call second virtual method
;cc->B_virt1()
;edi = pC
lea edi, [esi+8] ;adjust this pointer
mov eax, [edi] ;fetch virtual table pointer
mov ecx, edi
call [eax] ;call first virtual method
注意到上面class A的內(nèi)存布局圖,首先是VT指針,然后是成員變量b1,b2, 而對于靜態(tài)成員b3并沒有體現(xiàn),事實上b是存儲在程序的全局靜態(tài)數(shù)據(jù)區(qū),供該類的所有實例共享,這里請注意在classA中虛表中虛函數(shù)出現(xiàn)的順序和位置,這一點很重要,接著再看classB中虛函數(shù)出現(xiàn)的順序和位置,注意到A_virt1,A_virt2在classA和classB中出現(xiàn)的順序和位置一致,而所不同的是在classB的虛表中A_virt2已經(jīng)被替換,這就是多態(tài)的關(guān)鍵所在,每一個虛函數(shù)本身其實不過是一個固定的偏移量,而真正實現(xiàn)多態(tài)的其實是在編譯器的虛函數(shù)表的替換動作.
而對于多繼承情況要復(fù)雜一些,例如在ClassC中每一個繼承路徑中都存在一個虛表,如果在沒函數(shù)里再加入如下調(diào)用:
(pfunc(((int**)(&cc))[6][0]))();
(pfunc(((int**)(&cc))[6][1]))();
會輸出:
call of first A's vf
call of second A' vf
這樣一個類中同時存在兩個一抹一樣的函數(shù),那么當你用
C cc;
cc.A_virt2()時會怎么樣呢?
你會得到以下錯誤:
error C2385: ambiguous access of 'A_virt2'
解決辦法有兩種:
1. 調(diào)用時加入域操作符,例如:
cc.A::A_virt2();
cc.B::A_virt2( );
這種辦法最穩(wěn)妥也最清晰
2. 使用虛基類
代碼改動如下:
。。。。。。。
class B:virtual public A
。。。。。。。
class D:virtual public A
。。。。。。。
int _tmain(int argc, _TCHAR* argv[])
{
。。。。。。。
(pfunc(((int**)(&cc))[0][0]))();
(pfunc(((int**)(&cc))[0][1]))();
//(pfunc(((int**)(&cc))[0][2]))();
//(pfunc(((int**)(&cc))[0][3]))();
//(pfunc(((int**)(&cc))[6][0]))();
//(pfunc(((int**)(&cc))[6][1]))();
cc.A::A_virt2();
cc.A_virt2();
。。。。。。。。
}
內(nèi)存布局變?yōu)椋?br>
class C size(36):
+---
| +--- (base class B)
0 | | {vfptr}
4 | | {vbptr}
8 | | a1
12 | | b1
| | <alignment member> (size=3)
16 | | c1
| +---
| +--- (base class D)
20 | | {vbptr}
| +---
+---
+--- (virtual base A)
24 | {vfptr}
28 | b1
32 | b2
+---
C::$vftable@B@:
| &C_meta
| 0
0 | &C::B_virt1
1 | &B::B_virt2
C::$vbtable@B@:
0 | -4
1 | 20 (Cd(B+4)A)
C::$vbtable@D@:
0 | 0
1 | 4 (Cd(D+0)A)
C::$vftable@A@:
| -24
0 | &A::A_virt1
1 | &thunk: this-=4; goto B::A_virt2
多了一個vbtable存儲偏移量,第一個元素存儲vbtable與該類的偏移量,第二個元素存儲vbtable與公共基類的偏移量,而且注意到,vftable@A 的第二個虛函數(shù)被定向到B:A_virt2
這樣問題解決了,但是你會得到一個警告:
Warning 1 warning C4250: 'C' : inherits 'B::B::A_virt2' via dominance
顯示繼承了 'B::B::A_virt2‘ ,也就是說你在調(diào)用
cc.A_virt2時,默認直接去調(diào)用B::A_virt2,這可能并不是你所期望的,所以使用時需要慎重
2 . 動態(tài)邦定與靜態(tài)邦定邦定是指一個計算機程序自身彼此關(guān)聯(lián)的過程。按照邦定所進行的階段不同,可分為兩種不同的邦定方法:靜態(tài)邦定和動態(tài)邦定。
靜態(tài)邦定
靜態(tài)邦定是指邦定工作出現(xiàn)在編譯連接階段,這種邦定又稱早期邦定,因為這種邦定過程是在程序開始運行之前完成的。
在編譯時所進行的這種邦定又稱靜態(tài)束定。在編譯時就解決了程序中的操作調(diào)用與執(zhí)行該操作代碼間的關(guān)系,確定這種關(guān)系又稱為束定,在編譯時束定又稱靜態(tài)束定。
1
class AA
2

{
3
public:
4
void test()
5
{
6
cout<<"I am class AA!"<<endl;
7
}
8
};
9
class BB
10

{
11
public:
12
void test()
13
{
14
cout<<"I am class BB!"<<endl;
15
}
16
};
17
int _tmain(int argc, _TCHAR* argv[])
18

{
19
AA *A=(AA *)(new BB);
20
A->test();
21
}
讀者可以想一下以上例子的結(jié)果,如果說是I am class BB!
C++沒有你想得那么職能,C++調(diào)用函數(shù)不過是指針偏移,而一般成員函數(shù)代碼是在數(shù)據(jù)存儲區(qū)的共享代碼段,聲明了AA類的指針 A 就已經(jīng)指定了偏移的起點是類型AA的代碼段起點,這一步就是所謂的動態(tài)邦定,而調(diào)用->test();只能得到I am class AA!
也許你要說我并沒有實例化AA怎么會有那一段代碼呢,請注意代碼生成和實例化是完全不同的兩個階段,編譯在編譯時發(fā)現(xiàn)你調(diào)用了AA::test(); 那么就會載入相應(yīng)的symbol,程序啟動時就會載入相應(yīng)代碼段。
也許你還要說沒有用繼承的關(guān)系,那么你可以自己試驗一下使BB繼承自AA, 結(jié)果還是一樣的
想要實現(xiàn)想要的結(jié)果唯一的方法就是使用虛函數(shù)來實現(xiàn)動態(tài)邦定;