引言
结合网上的一些资料,通过自己的一番摸索,得出了一点个人见解。现在写下来,希望与各位同学共同探讨,共同进步。
以下所有代码均是在VS2012下测试。
一个普通的基类
1: #include <iostream>
2: using namespace std;
3:
4: class Base
5: {
6: public:
7: Base():
8: i(0)
9: {
10: }
11: void test(){
12: cout << "Base::test" << " i = " << i << endl;
13: }
14: virtual void virtualTest()
15: {
16: cout << "Base::virtualTest" << " i = " << i << endl;
17: }
18: static void staticTest()
19: {
20: cout << "Bae::staticTest" << endl;
21: }
22:
23:
24:
25: private:
26: int i;
27:
28: };
29:
30: int main()
31: {
32: Base b;
33: b.test();
34: b.virtualTest();
35: Base::staticTest();
36:
37: return 0;
38: }
我们定义了一个Base类,其类成员不言自明。在第31行处打断点,调试模式下运行。通过观察b对象,可以得到下图:
此时b还未初始化,我们可以在watch窗口中,双击b,在b前面加上”&”符号,得到b的地址。同理,将i也添加如watch窗口中,然后可得如下图:
我们可以看到,对象b的地址为0x003afc00,变量i的地址为0x003afc04。同时,我们可以通过Type列看到其类型的变化(通过这个,我们很容易写出相应的代码来验证其显示的数据)。按f10运行至第32行。
我们可以看到b的结构里包含了两个成员及其他们的地址:_vfptr 和 i。我们先来聊聊i。
数据成员
我们通过观察比较对象b的地址与i的地址,得出一个结论:i的地址是对象b的地址加上一个int*单位(即4字节)。现在,我们来验证我们的结论。
添加37、38两行代码,将b的地址强制转换为int*,然后将1赋值给b的地址增加一个int*单位后的地址。我们调试运行至39行,可以看到如下图:
i的值确实变了!证明我们的结论没错。数据成员的值存放在对象地址的+4偏移位置上。但是还有些疑问,如果i是个char类型的,内存布局会怎么样? 是不是存放在&b加一个char*单位(即1字节)地址上呢?如果有多个数据成员呢?这些请大家自行验证。通过观察他们的地址,我们得出最后的结论如下:不管数据成员的类型是什么,第一个定义的数据成员的地址总是与对象的地址相差四个字节,如果有多个数据成员,后面的数据成员的地址将根据第一个数据成员的地址加上其自身的类型的指针单位长度(比如char,则+1,float则+4)。
成员函数
在watch窗口中,对象b包含的成员,除了i,就只有一个_vfptr了,它是什么呢?那么多成员函数呢?我们仔细观察_vfptr的结构:
发现_vfptr好像是一个数组一样的结构,但是又不尽然。从图上我们可以看出,它本身是一个void**类型的结构,其“0下标”处存放的是一个void*类型,而且看起来像是virtualTest这个函数。根据图,我们有以下猜想:普通的成员函数和静态成员函数的存放位置,在类的对象中并没有保存。类对象中仅保存了虚函数的信息。又依据35行调用静态成员函数的代码,我们可以得到:在类中定义的静态成员函数,等同于在一个普通的函数上面包了一层命名空间。至于普通的成员函数,在这篇里暂且不表。下面我们来验证_vtptr是否是保存着对象的虚函数信息,以及是如何保存的。
我们通过图,可以看到三个地址信息:&b即对象的地址为0x0046fa10,_vfptr的地址为0x0024dc74,可能是表示虚函数的“[0]”的地址为0x002410d7,直观上看,他们毫无物理上联系,他们的地址相隔很大。为了称呼方便,我们用虚表(Virtual Table)来指称_vfptr,用虚函数virtualTest来指称“[0]”。
首先,我们看一下0x0046fa10地址上到底存的是什么,通过按Alt+6,我们可以呼出memory窗口,该窗口显示了相应内存地址存放的信息。
该窗口大概的内容布局如红色部分所示。通过在address栏输入0x002af8f4(下图b的地址),我们可以看到其中存放的是 74 dc f6 00。现在再结合虚表的地址0x00f6dc74来看,是不是有那么点联系?脑中是否立马浮现了一些大端机、小端机之类的信息?没错,因为我的机器是小端机,所以0x002af8f4中存的数据是0x00f6dc74。即虚表的地址。现在两者的逻辑关系很明显了,只要这样写即可得到虚表的地址:
如37行代码所示,我们将b的地址强制转换成int*,然后解引用,即可得到一个int值。我们将vbAdd添加到watch窗口中查看(注意将value调成16进制显示),得到结果如下:
是的,我们的确得到了虚表的地址!我们按照前面的步骤,看是否能得到虚函数virtualTest的地址。在内存窗口中查询得到如下结果:
果真如此,通过虚表地址存放的值,即可找到虚函数virtualTest的地址。下面我们用代码验证一下。
不出所料。我们得到了虚函数virtualTest的地址。现在三个地址(对象b、虚表、虚函数virtualTest)之间的逻辑关系很清晰了:对象b地址上存放的是指向虚表地址的数据,虚表地址上存放的是指向虚函数地址的数据。
既然我们得到了虚函数的地址,那么我们是不是可以手工来调用他们?如同前面通过地址来给类的私有数据成员赋值一样。让我们来写代码验证一下
我们可以看到,确实能够通过函数指针调用虚函数virtualTest,但是他的输出却出乎我们的意料。why?很遗憾,我目前也不知道原因。
总结
通过此篇,我们可以了解到在一个无继承关系的普通的类中,其虚函数和数据成员的内存布局,它们之间的内在物理上和逻辑上的关系是怎么样的。此外,还有一个问题:为什么通过地址调用虚函数,其输出的值为什么不是预期的呢?
由于个人水平有限,写的不是很详尽正确。如果同学们发现了错误,烦请指正。也请有能力答复上述疑问的同学,不吝笔墨。
参考资料
- 韩宏.老码识图.北京:电子工业出版社,2012
- 陈皓博客:http://blog.csdn.net/haoel/article/details/1948051/