引言

结合网上的一些资料,通过自己的一番摸索,得出了一点个人见解。现在写下来,希望与各位同学共同探讨,共同进步。 
以下所有代码均是在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?很遗憾,我目前也不知道原因。

总结

通过此篇,我们可以了解到在一个无继承关系的普通的类中,其虚函数和数据成员的内存布局,它们之间的内在物理上和逻辑上的关系是怎么样的。此外,还有一个问题:为什么通过地址调用虚函数,其输出的值为什么不是预期的呢?

由于个人水平有限,写的不是很详尽正确。如果同学们发现了错误,烦请指正。也请有能力答复上述疑问的同学,不吝笔墨。

参考资料

  1. 韩宏.老码识图.北京:电子工业出版社,2012
  2. 陈皓博客:http://blog.csdn.net/haoel/article/details/1948051/
 
 
分类: C++

c++对象内存布局的理解的更多相关文章

  1. 我对c++对象内存布局的理解

    引言 结合网上的一些资料,通过自己的一番摸索,得出了一点个人见解.现在写下来,希望与各位同学共同探讨,共同进步. 以下所有代码均是在VS2012下测试. 一个普通的基类 1: #include < ...

  2. 图说C++对象模型:对象内存布局详解

    0.前言 文章较长,而且内容相对来说比较枯燥,希望对C++对象的内存布局.虚表指针.虚基类指针等有深入了解的朋友可以慢慢看. 本文的结论都在VS2013上得到验证.不同的编译器在内存布局的细节上可能有 ...

  3. c++ 对象内存布局详解

    今天看了的,感觉需要了解对象内存的问题.参考:http://blog.jobbole.com/101583/ 1.何为C++对象模型? 引用<深度探索C++对象模型>这本书中的话: 有两个 ...

  4. c++对象内存布局

    这篇文章我要简单地讲解下c++对象的内存布局,虽然已经有很多很好的文章,不过通过实现发现有些地方不同的编译器还是会有差别的,希望和大家交流. 在没有用到虚函数的时候,C++的对象内存布局和c语言的st ...

  5. 好文章系列C/C++——图说C++对象模型:对象内存布局详解

    注:收藏好文章,得出自己的笔记,以查漏补缺!     ------>原文链接:http://blog.jobbole.com/101583/ 前言 本文可加深对C++对象的内存布局.虚表指针.虚 ...

  6. 使用sos查看.NET对象内存布局

    前面我们图解了.NET里各种对象的内存布局,我们再来从调试器和clr源码的角度来看一下对象的内存布局.我写了一个测试程序来加深对.net对象内存布局的了解: using System; using S ...

  7. 【转载】图说C++对象模型:对象内存布局详解

    原文: 图说C++对象模型:对象内存布局详解 正文 回到顶部 0.前言 文章较长,而且内容相对来说比较枯燥,希望对C++对象的内存布局.虚表指针.虚基类指针等有深入了解的朋友可以慢慢看.本文的结论都在 ...

  8. 浅析GCC下C++多重继承 & 虚拟继承的对象内存布局

    继承是C++作为OOD程序设计语言的三大特征(封装,继承,多态)之一,单一非多态继承是比较好理解的,本文主要讲解GCC环境下的多重继承和虚拟继承的对象内存布局. 一.多重继承 先看几个类的定义: 01 ...

  9. C++对象内存布局测试总结

    C++对象内存布局测试总结 http://hi.baidu.com/����/blog/item/826d38ff13c32e3a5d6008e8.html 上文是半年前对虚函数.虚拟继承的理解.可能 ...

随机推荐

  1. java 突击队注意事项:在路上

    情绪: 灵活:让标准成为价格值.为了给你一个想法和标准,你可以有一个不同的使用.不是死扣定理.决这个问题. 看书:分两类,一类依据知识点进行罗列.并且结构清晰,能够看完一章有选择进行总结(不是笔记,总 ...

  2. MessageDigest简要

    本文博客原 參考文章:http://blog.sina.com.cn/s/blog_4f36423201000c1e.html 一.概述 java.security.MessageDigest类用于为 ...

  3. android屏蔽home键的实现

    Android中,网上很多屏蔽Home键都智能在4.0以下运行,在4.0以及以上运行直接崩溃. 需要这样更改(来源:http://androidmaster.iteye.com/): @Overrid ...

  4. 将程序添加到右键菜单和图标(以记事本、UltraEdit为例)

    原文:将程序添加到右键菜单(以记事本.UltraEdit为例) 如何将程序加入右键菜单,这里分别以记事本.UltraEdit为例! 以记事本程序为例: 1. 在运行中输入regedit,打开注册表,找 ...

  5. EF结合SqlBulkCopy

    EF结合SqlBulkCopy在项目中的使用 这是我第一次写博客,由于水平有限,写不出什么好东西,还望见谅. 我现在参与的这个项目采用的是EF框架,方便了数据库的访问.但在实际中,发现项目中导入市县E ...

  6. Javascript规范

    本文地址: http://www.hicss.net/evolve-your-javascript-code/ 方才在程序里看到一段JS代码,写法极为高明,私心想着若是其按照规范来写,定可培养对这门语 ...

  7. ASP.NET 5是如何通过XRE实现跨平台的

    挡不住的好奇心:ASP.NET 5是如何通过XRE实现跨平台的   .NET程序员也有自己的幸福,.NET的跨平台是一种幸福,.NET的开源也是一种幸福,而更幸福的是可以通过开源的.NET了解.NET ...

  8. (C语言)共用体union的使用方法举例

    曾经在学校学习C语言的时候一直搞不懂那个共用体union有什么用的.工作之后才发现它的一些妙用,现举比例如以下: 1. 为了方便看懂代码. 比方说想写一个3 * 3的矩阵,能够这样写: [ 注:以下用 ...

  9. 微信应用号开发知识贮备之altjs官方实例初探

    天地会珠海分舵注:随着微信应用号的呼之欲出,相信新一轮的APP变革即将发生.从获得微信应用号邀请的业内人士发出来的一张开发工具源码截图可以看到,reacjs及其相应的FLUX框架altjs很有可能会成 ...

  10. javascript日历插件

    原文:javascript日历插件 javascript日历插件 最近在尝试着写javascript日历插件,所以也到github上看国外人日历源码,或者国内人写的好点的,也在研究点,虽然看到网上有一 ...