转自:http://www.cnblogs.com/jerry19880126/p/3616999.html

C++类内存分布

书上类继承相关章节到这里就结束了,这里不妨说下C++内存分布结构,我们来看看编译器是怎么处理类成员内存分布的,特别是在继承、虚函数存在的情况下。

工欲善其事,必先利其器,我们先用好Visual Studio工具,像下面这样一步一步来:

先选择左侧的C/C++->命令行,然后在其他选项这里写上/d1 reportAllClassLayout,它可以看到所有相关类的内存布局,如果写上/d1 reportSingleClassLayoutXXX(XXX为类名),则只会打出指定类XXX的内存布局。近期的VS版本都支持这样配置。

下面可以定义一个类,像下面这样:

1 class Base
2 {
3 int a;
4 int b;
5 public:
6 void CommonFunction();
7 };

然后编译一下,可以看到输出框里面有这样的排布:

这里不想花精力在内存对齐因素上,所以成员变量都设为int型。

从这里可以看到普通类的排布方式,成员变量依据声明的顺序进行排列(类内偏移为0开始),成员函数不占内存空间。

再看下继承,往后面添加如下代码:

1 class DerivedClass: public Base
2 {
3 int c;
4 public:
5 void DerivedCommonFunction();
6 };

编译,然后看到如下的内存分布(父类的内存分布不变,这里只讨论子类成员变量的内存分布):

可以看到子类继承了父类的成员变量,在内存排布上,先是排布了父类的成员变量,接着排布子类的成员变量,同样,成员函数不占字节。

下面给基类加上虚函数,暂时注释掉DerivedClass,看一下这时的内存排布:

1 class Base
2 {
3 int a;
4 int b;
5 public:
6 void CommonFunction();
7 void virtual VirtualFunction();
8 };

这个内存结构图分成了两个部分,上面是内存分布,下面是虚表,我们逐个看。VS所带编译器是把虚表指针放在了内存的开始处(0地址偏移),然后再是成员变量;下面生成了虚表,紧跟在&Base1_meta后面的0表示,这张虚表对应的虚指针在内存中的分布,下面列出了虚函数,左侧的0是这个虚函数的序号,这里只有一个虚函数,所以只有一项,如果有多个虚函数,会有序号为1,为2的虚函数列出来。

编译器是在构造函数创建这个虚表指针以及虚表的。

那么编译器是如何利用虚表指针与虚表来实现多态的呢?是这样的,当创建一个含有虚函数的父类的对象时,编译器在对象构造时将虚表指针指向父类的虚函数;同样,当创建子类的对象时,编译器在构造函数里将虚表指针(子类只有一个虚表指针,它来自父类)指向子类的虚表(这个虚表里面的虚函数入口地址是子类的)。

所以,如果是调用Base *p = new Derived();生成的是子类的对象,在构造时,子类对象的虚指针指向的是子类的虚表,接着由Derived*到Base*的转换并没有改变虚表指针,所以这时候p->VirtualFunction,实际上是p->vfptr->VirtualFunction,它在构造的时候就已经指向了子类的VirtualFunction,所以调用的是子类的虚函数,这就是多态了。

下面加上子类,并在子类中添加虚函数,像下面这样:

1 class DerivedClass: public Base
2 {
3 int c;
4 public:
5 void DerivedCommonFunction();
6 void virtual VirtualFunction();
7 };

可以看到子类内存的排布如下:

上半部是内存分布,可以看到,虚表指针被继承了,且仍位于内存排布的起始处,下面是父类的成员变量a和b,最后是子类的成员变量c,注意虚表指针只有一个,子类并没有再生成虚表指针了;下半部的虚表情况与父类是一样的。

我们把子类换个代码,像这样:

1 class DerivedClass1 : public Base
2 {
3 int c;
4 public:
5 void DerivedCommonFunction();
6 void virtual VirtualFunction2();
7 };

注意到这时我们并没有覆写父类的虚方法,而是重声明了一个新的子类虚方法,内存分布如下:

还是只有一个虚表指针,但是下方虚表的内容变化了,虚表的0号是父类的VirtualFunction,而1号放的是子类的VirtualFunction2。也就是说,如果定义了DerivedClass的对象,那么在构造时,虚表指针就会指向这个虚表,以后如果调用的是VirtualFunction,那么会从父类中寻找对应的虚函数,如果调用的是VirtualFunction2,那么会从子类中寻找对应的虚函数。

我们再改造一下子类,像这样:

1 class DerivedClass1 : public Base
2 {
3 int c;
4 public:
5 void DerivedCommonFunction();
6 void virtual VirtualFunction();
7 void virtual VirtualFunction2();
8 };

我们既覆写父类的虚函数,也有新添的虚函数,那么可以料想的到,是下面的这种内存分布:

下面来讨论多重继承,代码如下:

 1 class Base
2 {
3 int a;
4 int b;
5 public:
6 void CommonFunction();
7 void virtual VirtualFunction();
8 };
9
10
11 class DerivedClass1: public Base
12 {
13 int c;
14 public:
15 void DerivedCommonFunction();
16 void virtual VirtualFunction();
17 };
18
19 class DerivedClass2 : public Base
20 {
21 int d;
22 public:
23 void DerivedCommonFunction();
24 void virtual VirtualFunction();
25 };
26
27 class DerivedDerivedClass : public DerivedClass1, public DerivedClass2
28 {
29 int e;
30 public:
31 void DerivedDerivedCommonFunction();
32 void virtual VirtualFunction();
33 };

内存分布从父类到子类,依次如下:

Base中有一个虚表指针,地址偏移为0

DerivedClass1继承了Base,内存排布是先父类后子类。

DerivedClass2的情况是类似于DerivedClass1的。

下面我们重点看看这个类DerivedDerivedClass,由外向内看,它并列地排布着继承而来的两个父类DerivedClass1与DerivedClass2,还有自身的成员变量e。DerivedClass1包含了它的成员变量c,以及Base,Base有一个0地址偏移的虚表指针,然后是成员变量a和b;DerivedClass2的内存排布类似于DerivedClass1,注意到DerivedClass2里面竟然也有一份Base。

这里有两份虚表了,分别针对DerivedClass1与DerivedClass2,在&DerivedDericedClass_meta下方的数字是首地址偏移量,靠下面的虚表的那个-16表示指向这个虚表的虚指针的内存偏移,这正是DerivedClass2中的{vfptr}在DerivedDerivedClass的内存偏移。

如果采用虚继承,像下面这样:

 1 class DerivedClass1: virtual public Base
2 {
3 int c;
4 public:
5 void DerivedCommonFunction();
6 void virtual VirtualFunction();
7 };
8
9 class DerivedClass2 : virtual public Base
10 {
11 int d;
12 public:
13 void DerivedCommonFunction();
14 void virtual VirtualFunction();
15 };
16
17 class DerivedDerivedClass : public DerivedClass1, public DerivedClass2
18 {
19 int e;
20 public:
21 void DerivedDerivedCommonFunction();
22 void virtual VirtualFunction();
23 };

Base类没有变化,但往下看:

DerivedClass1就已经有变化了,原来是先排虚表指针与Base成员变量,vfptr位于0地址偏移处;但现在有两个虚表指针了,一个是vbptr,另一个是vfptr。vbptr是这个DerivedClass1对应的虚表指针,它指向DerivedClass1的虚表vbtable,另一个vfptr是虚基类表对应的虚指针,它指向vftable。

下面列出了两张虚表,第一张表是vbptr指向的表,8表示{vbptr}与{vfptr}的偏移;第二张表是vfptr指向的表,-8指明了这张表所对应的虚指针位于内存的偏移量。

DerivedClass2的内存分布类似于DerivedClass1,同样会有两个虚指针,分别指向两张虚表(第二张是虚基类表)。

下面来仔细看一下DerivedDerivedClass的内存分布,这里面有三个虚指针了,但base却只有一份。第一张虚表是内含DerivedClass1的,20表示它的虚指针{vbptr}离虚基表指针{vfptr}的距离,第二张虚表是内含DerivedClass2的,12表示它的虚指针{vbptr}离虚基表指针{vfptr}的距离,最后一张表是虚基表,-20指明了它对应的虚指针{vfptr}在内存中的偏移。

虚继承的作用是减少了对基类的重复,代价是增加了虚表指针的负担(更多的虚表指针)。

下面总结一下(当基类有虚函数时):

1. 每个类都有虚指针和虚表;

2. 如果不是虚继承,那么子类将父类的虚指针继承下来,并指向自身的虚表(发生在对象构造时)。有多少个虚函数,虚表里面的项就会有多少。多重继承时,可能存在多个的基类虚表与虚指针;

3. 如果是虚继承,那么子类会有两份虚指针,一份指向自己的虚表,另一份指向虚基表,多重继承时虚基表与虚基表指针有且只有一份。

C++类虚函数内存分布(这个 你必须懂)的更多相关文章

  1. [GeekBand] C++继承关系下虚函数内存分布

    本文参考文献:GeekBand课堂内容,授课老师:侯捷 :深度探索C++对象模型(侯捷译) :网络资料,如:http://blog.csdn.net/sanfengshou/article/detai ...

  2. c++类对象的内存分布

    要想知道c++类对象的内存布局, 可以有多种方式,比如: 1)输出成员变量的偏移, 通过offsetof宏来得到 2)通过调试器查看, 比如常用的VS 1.没有数据成员的对象 class A{ }; ...

  3. MFC浅析(7) CWnd类虚函数的调用时机、缺省实现

    CWnd类虚函数的调用时机.缺省实现 FMD(http://www.fmdstudio.net) 1. Create 2. PreCreateWindow 3. PreSubclassWindow 4 ...

  4. 如何使用C#调用C++类虚函数(即动态内存调用)

      本文讲解如何使用C#调用只有.h头文件的c++类的虚函数(非实例函数,因为非虚函数不存在于虚函数表,无法通过类对象偏移计算地址,除非用export导出,而gcc默认是全部导出实例函数,这也是为什么 ...

  5. CWnd类虚函数的调用时机、缺省实现

    MFC(VC6.0)的CWnd及其子类中,有如下三个函数: class CWnd : public CCmdTarget{    public:        virtual BOOL PreCrea ...

  6. c++内存分布之虚函数(单一继承)

    系列 c++内存分布之虚函数(单一继承) [本文] c++内存分布之虚函数(多继承) 结论 1.虚函数表指针 和 虚函数表 1.1 影响虚函数表指针个数的因素只和派生类的父类个数有关.多一个父类,派生 ...

  7. c++ 对象内存分配和虚函数

    1. c++类对象(不含虚函数)在内存中的分布 c++类中有四种成员:静态数据.非静态数据.静态函数.非静态函数. 1. 非静态数据成员放在每个对象内部,作为对象专有的数据成员 2. 静态数据成员被抽 ...

  8. 【转】C++类-内存分布

    C++类内存分布 - 转载自Jerry19880126 - 博客园 的文章 在上面这篇文章的基础上做了些整理. 主要讨论了C++类对象的内存分布结构. 来看看编译器是怎么处理类成员内存分布的,特别是在 ...

  9. c++内存分布之虚析构函数

    关于 本文代码演示环境: VS2017+32程序 虚析构函数是一种特殊的虚函数,可以知道,虚函数影响的内存分布规律应该也适用虚析构函数.看看实际结果. Note,一个类中,虚析构函数只能有一个. 本文 ...

随机推荐

  1. BUAA2020软工作业——提问回顾与个人总结

    项目 内容 这个作业属于哪个课程 2020春季计算机学院软件工程(罗杰 任健) 这个作业的要求在哪里 提问回顾与个人总结 我在这个课程的目标是 进一步提高自己的编码能力,工程能力 这个作业在哪个具体方 ...

  2. 基于RequestBodyAdvice和ResponseBodyAdvice来实现spring中参数的加密和解密

    在日常开发中,有时候我们经常需要和第三方接口打交道,有时候是我们调用别人的第三方接口,有时候是别人在调用我们的第三方接口,那么为了调用接口的安全性,一般都会对传输的数据进行加密操作,如果每个接口都由我 ...

  3. linux下命令拼接

    前言:我个five,一道特别简单的拼接题没有做出来,我吐了,不过也是涨知识了 直接切入正题了 linux命令是可以拼接的,也就是说在一个system("???")下我们的???可以 ...

  4. objdump--反汇编查看

    转载:objdump命令_Linux objdump 命令用法详解:显示二进制文件信息 (linuxde.net) objdump命令 编程开发 objdump命令是用查看目标文件或者可执行的目标文件 ...

  5. PWN二进制漏洞学习指南

    目录 PWN二进制漏洞学习指南 前言 前置技能 PWN概念 概述 发音 术语 PWN环境搭建 PWN知识学习途径 常见漏洞 安全机制 PWN技巧 PWN相关资源博客 Pwn菜鸡小分队 PWN二进制漏洞 ...

  6. Android Jetpack Compose 引入示例工程

    引入 Jetpack Compose 示例工程 去GitHub上找到Compose的示例工程 https://github.com/android/compose-samples ,clone到本地 ...

  7. robot_framewok自动化测试--(3)测试项目与测试套件的概念

    测试项目与测试套件的概念 如果你查看当前所创建的项目会发现,"test_project"是一个目录: "test_suit"则是一个 txt 文件: " ...

  8. 使用BadBoy录制JMeter脚本

     BadBoy简介 BadBoy是一款免费WEB自动化测试工具,其实就是一个浏览器模拟工具,具有录制和回放功能,支持对录制出来的脚本进行调试.同时支持捕获表单数据的功能,所以能够进行自动化测试.但目前 ...

  9. Kafka面试题总结

    1.Kafka 都有哪些特点? 高吞吐量.低延迟:kafka每秒可以处理几十万条消息,它的延迟最低只有几毫秒,每个topic可以分多个partition, consumer group 对partit ...

  10. 华为C/C++编码规范+《数学之美》感想

    1.排版 1.1 程序块要采用缩进风格编写, 缩进的空格数为4个.(说明: 对于由开发工具自动生成的代码可以有不一致)1.2 相对独立的程序块之间.变量说明之后必须加空行.1.3 循环.判断等语句中若 ...