閱讀本文前最好已經(jīng)讀過(guò) 理解程序內(nèi)存 和 理解C++變量存儲(chǔ)模型 相關(guān)的內(nèi)容, C++對(duì)象模型比較經(jīng)典的書(shū)是《深度探索C++對(duì)象模型》, 但是書(shū)本的知識(shí)始終局限在理論上,熟話說(shuō)“紙上得來(lái)終覺(jué)淺”,只有我們自已用工具經(jīng)過(guò)驗(yàn)證,我們才能真正的理解這些知識(shí)。下面我們用WinDbg為工具對(duì)C++對(duì)象模型進(jìn)行探索。類(lèi)對(duì)象實(shí)例究竟包含哪些東西
我們的例子代碼非常簡(jiǎn)單:#include <iostream>
using namespace std;
class A
{
public:
void fun1(){ cout << "fun1"; }
virtual void fun2() { cout << "fun2"; }
virtual ~A() {}
char m_cA;
int m_nA;
static int s_nCount;
};
int A::s_nCount = 0;
int main()
{
A* p = new A;
p->fun2();
system("pause");
return 0;
}
我們?cè)趍ain函數(shù)里 system("pause");的地方設(shè)置斷點(diǎn),然后讓程序運(yùn)行到這里。
輸入WinDbg命令?? sizeof(*p)讓他打印A對(duì)象的大小,輸出如下: 0:000> ?? sizeof(*p)
unsigned int 0xc
可以看到A的實(shí)例對(duì)象大小是 0xc = 12 字節(jié)接下來(lái)輸入WinDbg命令dt p讓他打印p所指下對(duì)象的內(nèi)存布局, 輸出如下:0:000> dt p
Local var @ 0x13ff74 Type A*
0x00034600
+0x000 __VFN_table : 0x004161d8
+0x004 m_cA : 120 'x'
+0x008 m_nA : 0n0
=0041c3c0 A::s_nCount : 0n0
可以看到A的對(duì)象實(shí)例由虛表指針,m_cA, m_nA組成,正好是12字節(jié)(內(nèi)部char作了4字節(jié)對(duì)齊)。最后一個(gè)靜態(tài)變量s_nCount的地址是0041c3c0, 我們可以通過(guò)命令!address 0041c3c0查看它所在地址的屬性, 結(jié)果如下:0:000> !address 0041c3c0
Usage: Image
Allocation Base: 00400000
Base Address: 0041b000
End Address: 0041f000
Region Size: 00004000
Type: 01000000 MEM_IMAGE
State: 00001000 MEM_COMMIT
Protect: 00000004 PAGE_READWRITE
More info: lmv m ConsoleTest
More info: !lmi ConsoleTest
More info: ln 0x41c3c0
可以看到類(lèi)靜態(tài)變量被編譯在consoletest.exe可執(zhí)行文件的 可讀寫(xiě)數(shù)據(jù)節(jié)(.data)結(jié)論: C++中類(lèi)實(shí)例對(duì)象由虛表指針和成員變量組成(一般最開(kāi)始的4個(gè)字節(jié)是虛表指針),而類(lèi)靜態(tài)變量分布在PE文件的.data節(jié)中,與類(lèi)實(shí)例對(duì)象無(wú)關(guān)。虛表位置和內(nèi)容
根據(jù)+0x000 __VFN_table : 0x004161d8 繼續(xù)上面的調(diào)試,我們看到虛表地址是在0x004161d8, 輸入!address 0x004161d8, 查看虛表地址的屬性:0:000> !address 0x004161d8
Usage: Image
Allocation Base: 00400000
Base Address: 00416000
End Address: 0041b000
Region Size: 00005000
Type: 01000000 MEM_IMAGE
State: 00001000 MEM_COMMIT
Protect: 00000002 PAGE_READONLY
More info: lmv m ConsoleTest
More info: !lmi ConsoleTest
More info: ln 0x4161d8
可以看到類(lèi)虛表被編譯在consoletest.exe可執(zhí)行文件的 只讀數(shù)據(jù)節(jié)(.rdata)
接下來(lái)我們看下虛表中有哪些內(nèi)容, 輸入dps 0x004161d8 查看虛表所在地址的符號(hào),輸出如下:0:000> dps 0x004161d8
004161d8 00401080 ConsoleTest!A::fun2 [f:\test\consoletest\consoletest\consoletest.cpp @ 13]
004161dc 004010a0 ConsoleTest!A::`scalar deleting destructor'
004161e0 326e7566
004161e4 00000000
我們可以看到虛表里正好包含了我們的2個(gè)虛函數(shù)fun2()和~A().另外我們也可以多new幾個(gè)A的實(shí)例試下,我們可以看到他們的虛表地址都是 0x004161d8。結(jié)論: C++中類(lèi)的虛表內(nèi)容由虛函數(shù)地址組成,虛表分布在PE文件的.rdata節(jié),并且同一類(lèi)的所有實(shí)例共享同一個(gè)虛表。
禁止生成虛表會(huì)怎樣
我們可以通過(guò)__declspec(novtable)來(lái)告訴編譯器不要生成虛表,ATL中大量應(yīng)用這種技術(shù)來(lái)減小虛表的內(nèi)存開(kāi)銷(xiāo),我們?cè)瓉?lái)的代碼改成class __declspec(novtable) A
{
public:
void fun1(){ cout << "fun1"; }
virtual void fun2() { cout << "fun2"; }
virtual ~A() {}
char m_cA;
int m_nA;
static int s_nCount;
};
繼續(xù)原來(lái)的方法調(diào)試,我們會(huì)看到一運(yùn)行到p->fun2(), 程序就會(huì)Crash, 究竟是什么原因?用原來(lái)的?? sizeof(*p)命令,可以看到對(duì)象大小依然是12 字節(jié), 輸入dt p, 輸出:0:000> dt p
Local var @ 0x13ff74 Type A*
0x00033e58
+0x000 __VFN_table : 0x00030328
+0x004 m_cA : 40 '('
+0x008 m_nA : 0n0
=0040dce0 A::s_nCount : 0n0
從上面可以看到虛表似乎依然存在, 但是再輸入dps 0x00030328 查看虛表內(nèi)容, 你就會(huì)發(fā)現(xiàn)現(xiàn)在虛表內(nèi)容果然已經(jīng)不存在了:0:000> dps 0x00030328
00030328 00030328
0003032c 00030328
00030330 00030330
但是我們的程序還是通過(guò)虛表去調(diào)用虛函數(shù)fun2, 難怪會(huì)Crash了。結(jié)論: 通過(guò)__declspec(novtable),我們只能禁止編譯器生成虛表,但是不能阻止對(duì)象仍包含虛表指針(不能減小對(duì)象的大小),也不能阻止程序?qū)μ摫淼脑L問(wèn)(盡管實(shí)際虛表不存在),所以禁止生成虛表只適用于永遠(yuǎn)不會(huì)實(shí)例化的類(lèi)(基類(lèi))
單繼承對(duì)象內(nèi)存模型
下面我們簡(jiǎn)單的將上面的代碼改下下,讓B繼承A,并且重寫(xiě)原來(lái)的虛函數(shù)fun2:#include <iostream>
using namespace std;
class A
{
public:
void fun1(){ cout << "fun1"; }
virtual void fun2() { cout << "fun2"; }
virtual ~A() {}
char m_cA;
int m_nA;
static int s_nCount;
};
int A::s_nCount = 0;
class B: public A
{
public:
virtual void fun2() { cout << "fun2 in B"; }
virtual void fun3() { cout << "fun3 in B"; }
public:
int m_nB;
};
int main()
{
B* p = new B;
A* p1 = p;
p1->fun2();
system("pause");
return 0;
}
用原來(lái)的方法進(jìn)行調(diào)試,查看B對(duì)象的內(nèi)存布局0:000> dt p
Local var @ 0x13ff74 Type B*
0x00034640
+0x000 __VFN_table : 0x004161d8
+0x004 m_cA : 120 'x'
+0x008 m_nA : 0n0
=0041c3e0 A::s_nCount : 0n0
+0x00c m_nB : 0n0
可以看到B對(duì)象的大小是原來(lái)A對(duì)象的大小加4(m_nB), 總共是16字節(jié),查看B的虛表內(nèi)容如下:0:000> dps 0x004161d8
004161d8 00401080 ConsoleTest!B::fun2 [f:\test\consoletest\consoletest\consoletest.cpp @ 26]
004161dc 004010c0 ConsoleTest!B::`scalar deleting destructor'
004161e0 004010a0 ConsoleTest!B::fun3 [f:\test\consoletest\consoletest\consoletest.cpp @ 27]
004161e4 326e7566
可以看到虛表中保存的都是B的虛函數(shù)地址: fun2(), ~B(), fun3()結(jié)論: 單繼承時(shí)父類(lèi)和子類(lèi)共用同一虛表指針,而子類(lèi)的數(shù)據(jù)被添加在父類(lèi)數(shù)據(jù)之后,父類(lèi)和子類(lèi)的對(duì)象指針在相互轉(zhuǎn)化時(shí)值不變。多繼承對(duì)象內(nèi)存模型
我們把上面的代碼改成多繼承的方式, class A, class B, 然后C繼承A和B:#include <iostream>
using namespace std;
class A
{
public:
virtual void fun() {cout << "fun in A";}
virtual void funA() {cout << "funA";}
virtual ~A() {}
char m_cA;
int m_nA;
static int s_nCount;
};
int A::s_nCount = 0;
class B
{
public:
virtual void fun() {cout << "fun in B";}
virtual void funB() {cout << "funB";}
int m_nB;
};
class C: public A, public B
{
public:
virtual void fun() {cout << "fun in C";};
virtual void funC(){cout << "funC";}
int m_nC;
};
int main()
{
C* p = new C;
B* p1 = p;
p->fun();
system("pause");
return 0;
}
依舊用原來(lái)的方式調(diào)試,查看C的內(nèi)存布局0:000> dt p
Local var @ 0x13ff74 Type C*
0x00034600
+0x000 __VFN_table : 0x004161f4
+0x004 m_cA : 120 'x'
+0x008 m_nA : 0n0
=0041c4a0 A::s_nCount : 0n0
+0x00c __VFN_table : 0x004161e8
+0x010 m_nB : 0n0
+0x014 m_nC : 0n0
可以看到C對(duì)象由0x18 = 24字節(jié)組成,可以看到數(shù)據(jù)依次是虛表指針,A的數(shù)據(jù),虛表指針, B的數(shù)據(jù), C的數(shù)據(jù)。查看第一個(gè)虛表內(nèi)容:
0:000> dps 0x004161f4
004161f4 004010f0 ConsoleTest!C::fun [f:\test\consoletest\consoletest\consoletest.cpp @ 33]
004161f8 004010b0 ConsoleTest!A::funA [f:\test\consoletest\consoletest\consoletest.cpp @ 16]
004161fc 00401130 ConsoleTest!C::`scalar deleting destructor'
00416200 00401110 ConsoleTest!C::funC [f:\test\consoletest\consoletest\consoletest.cpp @ 34]
可以看到前面虛表的前面3個(gè)虛函數(shù)符合A的虛表要求(第一個(gè)A::fun讓C::fun取代了,第三個(gè)A的析構(gòu)函數(shù)~A讓~C取代了),最后加上了C的新增虛函數(shù)funC, 所以該虛表同時(shí)符合A和C的要求,也就是說(shuō)A和C共用同一個(gè)虛表指針。再看第二個(gè)虛表內(nèi)容:0:000> dps 0x004161e8
004161e8 00402850 ConsoleTest![thunk]:C::fun`adjustor{12}'
004161ec 004010d0 ConsoleTest!B::funB [f:\test\consoletest\consoletest\consoletest.cpp @ 27]
004161f0 004187a0 ConsoleTest!C::`RTTI Complete Object Locator'
可以看到第二個(gè)虛表符合B的虛表要求,并且把B的虛函數(shù)fun用C的改寫(xiě)了,所以它是給B用的。 我們?cè)倏椿?lèi)對(duì)象B的布局情況:0:000> dt p1
Local var @ 0x13ff70 Type B*
0x0003460c
+0x000 __VFN_table : 0x004161e8
+0x004 m_nB : 0n0
我們可以看到p1指針本身在堆棧上的地址是0x13ff70,而p1所指向?qū)ο蟮牡刂肥?/span>0x003e460c ,所以將C指針轉(zhuǎn)成B指針后,B的地址和C的地址之差是0x003e460c- 0x00034600 = 0xc = 12字節(jié), 也就是說(shuō)B的指針p1指向我們上面的第二個(gè)虛表指針。
另外我們上面要特別留意第二個(gè)虛表的第一個(gè)函數(shù):004161e8 00402850 ConsoleTest![thunk]:C::fun`adjustor{12}'
我們發(fā)現(xiàn)這個(gè)函數(shù)不是我們真正的class C的fun函數(shù):004161f4 004010f0 ConsoleTest!C::fun [f:\test\consoletest\consoletest\consoletest.cpp @ 33]
該函數(shù)地址是00402850, 我們可以反匯編看下:0:000> u 00402850
ConsoleTest![thunk]:C::fun`adjustor{12}':
00402850 83e90c sub ecx,0Ch
00402853 e998e8ffff jmp ConsoleTest!C::fun (004010f0)
00402858 cc int 3
00402859 cc int 3
0040285a cc int 3
0040285b cc int 3
0040285c cc int 3
0040285d cc int 3
可以看到這個(gè)函數(shù)是編譯器生成的一個(gè)代理函數(shù),它內(nèi)部實(shí)現(xiàn)只是把我們B的this指針(ecx)加上12個(gè)字節(jié)的偏移后,然后再去調(diào)用我們真正的C的fun函數(shù)。
為什么會(huì)這樣呢? 因?yàn)閏lass C的fun 內(nèi)部在實(shí)現(xiàn)時(shí)假設(shè)的this指針都是它本身實(shí)例的起始地址,但是B指針并不符合這個(gè)要求,所以B的指針需要調(diào)整后才能去調(diào)用真正C的方法。
結(jié)論: 多重繼承時(shí)派生類(lèi)和第一個(gè)基類(lèi)公用一個(gè)虛表指針,他們的對(duì)象指針相互轉(zhuǎn)化時(shí)值不變;而其他基類(lèi)(非第一個(gè))和派生類(lèi)的對(duì)象指針在相互轉(zhuǎn)化時(shí)有一定的偏移,他們內(nèi)部虛表保存的函數(shù)指針并不一定是最終的實(shí)現(xiàn)的虛函數(shù)(可能是類(lèi)似上面的一個(gè)代理函數(shù))。
如何用虛表實(shí)現(xiàn)多態(tài)?
有了上面這些分析,這個(gè)咱們就不證明了,直接下結(jié)論吧。
結(jié)論: C++通過(guò)虛表來(lái)實(shí)現(xiàn)多態(tài),派生類(lèi)的虛表和基類(lèi)的虛表根據(jù)索引依次保存相同的函數(shù)類(lèi)型指針,但是這些函數(shù)指針最終指向他們各自最終的實(shí)現(xiàn)函數(shù),調(diào)用虛函數(shù)時(shí),我們只是根據(jù)函數(shù)所在虛表的索引來(lái)調(diào)用,所以他們可以在派生類(lèi)中有各自不同的實(shí)現(xiàn)。
虛擬繼承
恩,有了前面的基礎(chǔ),這個(gè)就當(dāng)思考題吧...
總之,拿著一把刀,庖丁解牛般的剖析語(yǔ)言背后的實(shí)現(xiàn)細(xì)節(jié),看起來(lái)不是那么實(shí)用,但是它能讓你對(duì)語(yǔ)言的理解更深刻。實(shí)際上ATL中大量應(yīng)用上面的技術(shù),如果沒(méi)有對(duì)C++ 對(duì)象模型有比較深刻的理解,是很難深入下去的。
posted on 2012-09-21 23:02
Richard Wei 閱讀(4154)
評(píng)論(2) 編輯 收藏 引用 所屬分類(lèi):
C++