程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> 探索C++對象模型

探索C++對象模型

編輯:C++入門知識


類對象實例究竟包含哪些東西

我們的例子代碼非常簡單:
#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;
}
我們在main函數裡 system("pause");的地方設置斷點,然後讓程序運行到這裡。

輸入WinDbg命令?? sizeof(*p)讓他打印A對象的大小,輸出如下:
 0:000> ?? sizeof(*p)
unsigned int 0xc
可以看到A的實例對象大小是 0xc = 12 字節

接下來輸入WinDbg命令dt p讓他打印p所指下對象的內存布局, 輸出如下:
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的對象實例由虛表指針,m_cA, m_nA組成,正好是12字節(內部char作了4字節對齊)。

最後一個靜態變量s_nCount的地址是0041c3c0, 我們可以通過命令!address 0041c3c0查看它所在地址的屬性, 結果如下:
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
可以看到類靜態變量被編譯在consoletest.exe可執行文件的 可讀寫數據節(.data)

結論: C++中類實例對象由虛表指針和成員變量組成(一般最開始的4個字節是虛表指針),而類靜態變量分布在PE文件的.data節中,與類實例對象無關。


虛表位置和內容

根據+0x000 __VFN_table : 0x004161d8  繼續上面的調試,我們看到虛表地址是在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
可以看到類虛表被編譯在consoletest.exe可執行文件的 只讀數據節(.rdata)

接下來我們看下虛表中有哪些內容, 輸入dps 0x004161d8 查看虛表所在地址的符號,輸出如下:
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個虛函數fun2()和~A().

另外我們也可以多new幾個A的實例試下,我們可以看到他們的虛表地址都是 0x004161d8。

結論: C++中類的虛表內容由虛函數地址組成,虛表分布在PE文件的.rdata節,並且同一類的所有實例共享同一個虛表。


禁止生成虛表會怎樣

我們可以通過__declspec(novtable)來告訴編譯器不要生成虛表,ATL中大量應用這種技術來減小虛表的內存開銷,我們原來的代碼改成
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;
};
繼續原來的方法調試,我們會看到一運行到p->fun2(), 程序就會Crash, 究竟是什麼原因?
用原來的?? sizeof(*p)命令,可以看到對象大小依然是12 字節, 輸入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 查看虛表內容, 你就會發現現在虛表內容果然已經不存在了:
0:000> dps 0x00030328
00030328  00030328
0003032c  00030328
00030330  00030330
但是我們的程序還是通過虛表去調用虛函數fun2, 難怪會Crash了。

結論: 通過__declspec(novtable),我們只能禁止編譯器生成虛表,但是不能阻止對象仍包含虛表指針(不能減小對象的大小),也不能阻止程序對虛表的訪問(盡管實際虛表不存在),所以禁止生成虛表只適用於永遠不會實例化的類(基類)


單繼承對象內存模型

下面我們簡單的將上面的代碼改下下,讓B繼承A,並且重寫原來的虛函數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;
}
用原來的方法進行調試,查看B對象的內存布局
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對象的大小是原來A對象的大小加4(m_nB), 總共是16字節,查看B的虛表內容如下:
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的虛函數地址: fun2(), ~B(), fun3()

結論: 單繼承時父類和子類共用同一虛表指針,而子類的數據被添加在父類數據之後,父類和子類的對象指針在相互轉化時值不變。


多繼承對象內存模型

我們把上面的代碼改成多繼承的方式, 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;
}
依舊用原來的方式調試,查看C的內存布局
0:000> dt p
Local var @ 0x13ff74 Type C*
0x00034600
   +0x000 __VFN_table : 0x004161e4
   +0x004 m_cA             : 120 'x'
   +0x008 m_nA             : 0n0
   =0041c3e0 A::s_nCount      : 0n0
   +0x00c __VFN_table : 0x004161d8
   +0x010 m_nB             : 0n0
   +0x014 m_nC             : 0n0
可以看到C對象由0x18 = 24字節組成,可以看到數據依次是虛表指針,A的數據,虛表指針, B的數據, C的數據。

查看第一個虛表內容:
0:000> dps 0x004161e4
004161e4  004010f0 ConsoleTest!C::fun [f:\test\consoletest\consoletest\consoletest.cpp @ 36]
004161e8  004010b0 ConsoleTest!A::funA [f:\test\consoletest\consoletest\consoletest.cpp @ 13]
004161ec  00401130 ConsoleTest!C::`scalar deleting destructor'
004161f0  00401110 ConsoleTest!C::funC [f:\test\consoletest\consoletest\consoletest.cpp @ 37]
004161f4  416e7566
可以看到前面虛表的前面3個虛函數符合A的虛表要求,最後加上了C的新增虛函數funC, 所以該虛表同時符合A和C的要求,也就是說A和C共用同一個虛表指針。

再看第二個虛表內容:
0:000> dps 0x004161d8
004161d8  00401560 ConsoleTest![thunk]:C::fun`adjustor{12}'
004161dc  004010d0 ConsoleTest!B::funB [f:\test\consoletest\consoletest\consoletest.cpp @ 28]
可以看到第二個虛表符合B的虛表要求,並且把B的虛函數fun用C的改寫了,所以它是給B用的。

我們再看基類對象B的布局情況:
0:000> dt p1
Local var @ 0x13ff70 Type B*
0x003e460c
   +0x000 __VFN_table : 0x004161d8
   +0x004 m_nB             : 0n0
我們可以看到p1指針本身在堆棧上的地址是0x13ff70,而p1所指向對象的地址是0x003e460c ,所以將C指針轉成B指針後,B的地址和C的地址之差是0x003e460c- 0x00034600  = 0xc = 12字節, 也就是說B的指針p1指向我們上面的第二個虛表指針。

結論: 多重繼承時派生類和第一個基類公用一個虛表指針,他們的對象指針相互轉化時值不變;而其他基類(非第一個)和派生類的對象指針在相互轉化時有一定的偏移。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved