当前位置:编程学习 > 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;

 &nb

补充:软件开发 , C++ ,
CopyRight © 2022 站长资源库 编程知识问答 zzzyk.com All Rights Reserved
部分文章来自网络,