引言

结合网上的一些资料,通过自己的一番摸索,得出了一点个人见解。现在写下来,希望与各位同学共同探讨,共同进步。
以下所有代码均是在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++对象内存布局的理解的更多相关文章

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

    我对c++对象内存布局的理解   引言 结合网上的一些资料,通过自己的一番摸索,得出了一点个人见解.现在写下来,希望与各位同学共同探讨,共同进步. 以下所有代码均是在VS2012下测试. 一个普通的基 ...

  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. Hanoi Tower问题分析

    前言 回家休息第3天了,状态一直不是太好,主要是要补牙,检查身体,见同学见亲戚,心里又着急校招,难得能腾出时间来好好思考,这里也是看<cracking the coding interview& ...

  2. w3cmark前端精彩博文周报 10.20-10.27

    w3cmark 官方Q群 145423956 | 官方微博 @w3cmark 自从最近微博屏蔽了我的站点域名,就很懒了.毕竟和不爽,一个纯技术站点还被认为不安全链接,还申诉无门,那些所谓的客服都是自动 ...

  3. C++ C++ 控制台程序 设置图标

    . 实现过程 创建1个控制台程序. 新建1个 Resource Script文件 #include "stdio.h" #include <windows.h> #in ...

  4. delphi 使用进度条查看浏览器状态

    使用进度条查看浏览器状态     procedure TForm1.WebBrowser1ProgressChange(ASender: TObject; Progress,  ProgressMax ...

  5. epoll使用具体解释(精髓)

    epoll - I/O event notification facility 在linux的网络编程中,非常长的时间都在使用select来做事件触发.在linux新的内核中,有了一种替换它的机制,就 ...

  6. SAP BW 重新初始化

  7. Linux内核--网络栈实现分析(二)--数据包的传递过程--转

    转载地址http://blog.csdn.net/yming0221/article/details/7492423 作者:闫明 本文分析基于Linux Kernel 1.2.13 注:标题中的”(上 ...

  8. nandsim ubi nand nor

    nandsim模拟mtd测试UBI模块 利用nandsim挂载ubi文件系统 MTD设备及JFFS2, UBIFS文件系统的使用简介 首先需要安装mtd_utils工具: sudo apt-get i ...

  9. 小白日记15:kali渗透测试之弱点扫描-漏扫三招、漏洞管理、CVE、CVSS、NVD

    发现漏洞 弱点发现方法: 1.基于端口服务扫描结果版本信息,比对其是否为最新版本,若不是则去其 官网查看其补丁列表,然后去逐个尝试,但是此法弊端很大,因为各种端口应用比较多,造成耗时大. 2.搜索已公 ...

  10. [Javascript,JSON] JQuery处理json与ajax返回JSON实例

    转自:http://www.php100.com/html/program/jquery/2013/0905/5912.html [导读] json数据是一种经型的实时数据交互的数据存储方法,使用到最 ...