类对象实例究竟包含哪些东西
我们的例子代码非常简单:
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++ 对象模型有比较深刻的理解,是很难深入下去的。