類對象實例究竟包含哪些東西
我們的例子代碼非常簡單:
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;
}
輸入WinDbg命令?? sizeof(*p)讓他打印A對象的大小,輸出如下:
unsigned int 0xc
接下來輸入WinDbg命令dt p讓他打印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
最後一個靜態變量s_nCount的地址是0041c3c0, 我們可以通過命令!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
結論: C++中類實例對象由虛表指針和成員變量組成(一般最開始的4個字節是虛表指針),而類靜態變量分佈在PE文件的.data節中,與類實例對象無關。
虛表位置和內容
根據+0x000 __VFN_table : 0x004161d8 繼續上面的調試,我們看到虛表地址是在0x004161d8, 輸入!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
接下來我們看下虛表中有哪些內容, 輸入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
另外我們也可以多new幾個A的實例試下,我們可以看到他們的虛表地址都是 0x004161d8。
我們可以通過__declspec(novtable)來告訴編譯器不要生成虛表,ATL中大量應用這種技術來減小虛表的內存開銷,我們原來的代碼改成
{
public:
void fun1(){ cout << "fun1"; }
virtual void fun2() { cout << "fun2"; }
virtual ~A() {}
char m_cA;
int m_nA;
static int s_nCount;
};
用原來的?? sizeof(*p)命令,可以看到對象大小依然是12 字節, 輸入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
00030328 00030328
0003032c 00030328
00030330 00030330
單繼承對象內存模型
下面我們簡單的將上面的代碼改下下,讓B繼承A,並且重寫原來的虛函數fun2:
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;
}
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
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
結論: 單繼承時父類和子類共用同一虛表指針,而子類的數據被添加在父類數據之後,父類和子類的對象指針在相互轉化時值不變。
多繼承對象內存模型
我們把上面的代碼改成多繼承的方式, class A, class B, 然後C繼承A和B:
查看第一個虛表內容:
再看第二個虛表內容:
我們再看基類對象B的佈局情況:
另外我們上面要特別留意第二個虛表的第一個函數:004161e8 00402850 ConsoleTest![thunk]:C::fun`adjustor{12}'
我們發現這個函數不是我們真正的class C的fun函數:004161f4 004010f0 ConsoleTest!C::fun [f:\test\consoletest\consoletest\consoletest.cpp @ 33]
該函數地址是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
爲什麼會這樣呢? 因爲class C的fun 內部在實現時假設的this指針都是它本身實例的起始地址,但是B指針並不符合這個要求,所以B的指針需要調整後才能去調用真正C的方法。
結論: 多重繼承時派生類和第一個基類公用一個虛表指針,他們的對象指針相互轉化時值不變;而其他基類(非第一個)和派生類的對象指針在相互轉化時有一定的偏移,他們內部虛表保存的函數指針並不一定是最終的實現的虛函數(可能是類似上面的一個代理函數)。
如何用虛表實現多態?
有了上面這些分析,這個咱們就不證明了,直接下結論吧。
恩,有了前面的基礎,這個就當思考題吧...
總之,拿着一把刀,庖丁解牛般的剖析語言背後的實現細節,看起來不是那麼實用,但是它能讓你對語言的理解更深刻。實際上ATL中大量應用上面的技術,如果沒有對C++ 對象模型有比較深刻的理解,是很難深入下去的。