C++类虚函数内存分布(这个 你必须懂)
转自: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++类虚函数内存分布(这个 你必须懂)的更多相关文章
- [GeekBand] C++继承关系下虚函数内存分布
本文参考文献:GeekBand课堂内容,授课老师:侯捷 :深度探索C++对象模型(侯捷译) :网络资料,如:http://blog.csdn.net/sanfengshou/article/detai ...
- c++类对象的内存分布
要想知道c++类对象的内存布局, 可以有多种方式,比如: 1)输出成员变量的偏移, 通过offsetof宏来得到 2)通过调试器查看, 比如常用的VS 1.没有数据成员的对象 class A{ }; ...
- MFC浅析(7) CWnd类虚函数的调用时机、缺省实现
CWnd类虚函数的调用时机.缺省实现 FMD(http://www.fmdstudio.net) 1. Create 2. PreCreateWindow 3. PreSubclassWindow 4 ...
- 如何使用C#调用C++类虚函数(即动态内存调用)
本文讲解如何使用C#调用只有.h头文件的c++类的虚函数(非实例函数,因为非虚函数不存在于虚函数表,无法通过类对象偏移计算地址,除非用export导出,而gcc默认是全部导出实例函数,这也是为什么 ...
- CWnd类虚函数的调用时机、缺省实现
MFC(VC6.0)的CWnd及其子类中,有如下三个函数: class CWnd : public CCmdTarget{ public: virtual BOOL PreCrea ...
- c++内存分布之虚函数(单一继承)
系列 c++内存分布之虚函数(单一继承) [本文] c++内存分布之虚函数(多继承) 结论 1.虚函数表指针 和 虚函数表 1.1 影响虚函数表指针个数的因素只和派生类的父类个数有关.多一个父类,派生 ...
- c++ 对象内存分配和虚函数
1. c++类对象(不含虚函数)在内存中的分布 c++类中有四种成员:静态数据.非静态数据.静态函数.非静态函数. 1. 非静态数据成员放在每个对象内部,作为对象专有的数据成员 2. 静态数据成员被抽 ...
- 【转】C++类-内存分布
C++类内存分布 - 转载自Jerry19880126 - 博客园 的文章 在上面这篇文章的基础上做了些整理. 主要讨论了C++类对象的内存分布结构. 来看看编译器是怎么处理类成员内存分布的,特别是在 ...
- c++内存分布之虚析构函数
关于 本文代码演示环境: VS2017+32程序 虚析构函数是一种特殊的虚函数,可以知道,虚函数影响的内存分布规律应该也适用虚析构函数.看看实际结果. Note,一个类中,虚析构函数只能有一个. 本文 ...
随机推荐
- docker run 的基本用法
docker run 命令用来创建并启动一个容器 语法:docker run [options] image [command] [args-] 示例:docker run -dit -v 别名:容器 ...
- Go 里的超时控制
前言 日常开发中我们大概率会遇到超时控制的场景,比如一个批量耗时任务.网络请求等:一个良好的超时控制可以有效的避免一些问题(比如 goroutine 泄露.资源不释放等). Timer 在 go 中实 ...
- 期望dp好题选做
前言: 最近连考两场期望dp的题目,sir说十分板子的题目我竟然一点也不会,而且讲过以后也觉得很不可改.于是开个坑. 1.晚测10 T2 大佬(kat) 明明有\(O(mlog)\)的写法,但是\(m ...
- 2021.9.9考试总结[NOIP模拟50]
T1 第零题 神秘结论:从一个点满体力到另一个点的复活次数与倒过来相同. 于是预处理出每个点向上走第$2^i$个死亡点的位置,具体实现可以倍增或二分. 每次询问先从两个点同时向上倍增,都转到离$LCA ...
- PCB电路板元器件布局的一般原则*(转)
PCB电路板元器件布局的一般原则: 设计人员在PCB电路板布局过程中需要遵循的一般原则如下. (1)元器件最好单面放置.如果需要双面放置元器件,在底层(Bottom Layer)放置插针式元器件, ) ...
- 与 Python 之父聊天:更快的 Python!
Python猫注: 在今年 5 月的 Python 语言峰会上,Guido van Rossum 作了一场<Making CPython Faster>的分享(材料在此),宣告他加入了激动 ...
- cat userlist(课上练习)
问题描述 Linux文件系统的三层抽象是什么? 写出Cat userlist的过程,要详述目录文件,i-node.数据块,要画图示意. 假设块大小为4k, userlist的大小不小于10k,自己假设 ...
- linux tr
转载:tr命令_Linux tr 命令用法详解:将字符进行替换压缩和删除 (linuxde.net) tr命令 文件过滤分割与合并 tr命令可以对来自标准输入的字符进行替换.压缩和删除.它可以将一组字 ...
- AGC036 A-Triangle | 构造
题目链接 题意: 给出一个数$S(1\leqslant S \leqslant 10^{18})$. 要求在平面直角坐标系中找到三个点$(X_1,Y_1),(X_2,Y_2),(X_3,Y_3)$,满 ...
- POJ 3692 Kindergarten(二分图最大独立集)
题意: 有G个女孩,B个男孩.女孩彼此互相认识,男孩也彼此互相认识.有M对男孩和女孩是认识的.分别是(g1,b1),.....(gm,bm). 现在老师要在这G+B个小孩中挑出一些人,条件是这些人都互 ...