C++独孤九剑第一式——拨云见日(对象内存模型)
归妹趋无妄,无妄趋同人,同人趋大有。甲转丙,丙转庚,庚转癸。子丑之交,辰巳之交,午未之交。风雷是一变,山泽是一变,水火是一变。乾坤相激,震兑相激,离巽相激······好了,要背心法的同学请课后自行背诵。
特别说明:本系列并非严格按照《独孤九剑》的剑式进行命名,来学剑招的请自备传送卷轴,恕不报销往返机票。
之所以称之为“拨云见日”,就是要在这招中和大家一起揭开C++对象内存的迷雾,让她在众位少侠面前一览无余(想想心里还真有点小激动呢o(* ̄︶ ̄*)o)。
我们从最基础的开始,一步步直到出任CEO,迎娶白富美(额,有点扯远了)。
环境说明:本机是windows32位机,实验环境为VC6.0
1. 当我们定义了一个空类的时候
Class base{};
Base b = Base();
Base *pb = &b;
cout<<sizeof(Base)<<endl; //输出为1
cout<<*((char *)pb)<<endl; //输出一个空字符
也就是说,当一个类为空类的时候,它的大小其实是1个字节(编译器悄悄地给了那个可怜的类一个字节)。为什么要插入这一个字节,个人认为是这样的:类是对象的模板,如果类的大小为0,那么它构造出的对象就相当于无物(大小为0的对象!)。这个时候究竟是有对象还是没有对象恐怕就要上升到哲学的高度了。用区区一个字节的内存,谈笑间就解决了这一难题,岂不妙哉。
2. 非空无继承的时候
class Base
{
Public: //方便后面的验证static int s; //静态变量不会占用类中的存储空间,它将存在全局区char a;int i;char b;
<span style="white-space:pre"> </span>virtual void sum(); //测试时先实现该函数,否则将报错
};
此时sizeof(Base)操作将得到的答案是16,是的并不是6(1+4+1)。这是由内存对齐(为了更快的与cup进行数据交换)和存储虚函数表指针所引起的(虚函数本身虽然不保存在类中,但是指向虚函数表的指针将被保存,在具体实现多态时会用到)。
其中,nonstatic datamembers在class object中的排列顺序将和其被声明的顺序一样,members的排列只需符合“较晚出现的members在class object中有较高的地址”这一条即可。也就是说各个members并不一定连续,例如编译器在内存对齐时填补的bytes就会出现在两个连续的members之间。而虚函数表指针被放在对象首部(不同的编译器会有所不同)。关于虚表指针的位置,可用以下语句进行验证:
Base b;
cout<<((long)&(b.a) - (long)&b)<<endl;//该表达式的结果为4,说明成员a前还有不知名的生物存在,毫无疑问,在这种情况下肯定是虚函数表指针无疑了。
此时的内存布局如下左图示:
注:padding表示编译器的填充部分。
将i和b的声明顺序交换后,该类的对象内存如上右图所示,此时大小为12。
3. 单一继承的时候
class Base
{
Public:int i;char c;
<span style="white-space:pre"> </span>virtual void sum(); //测试时先实现该函数,否则将报错
};
class Little : public Base
{
Public:int i;virtual void out(); //测试时先实现该函数,否则将报错
};
此时sizeof(Little)得到的值为16。这里说明两点:首先,虽然父类和子类都有成员变量i,但他们在不同的内存中,互不干扰;其次,子类也有虚函数,但是子类的对象中并不会新添加一个虚表指针(该虚函数将被添加到父类虚函数表中,由父类部分的虚函数指针统一指向)。可用下式证明:
Little l;
cout<<((long)&(l.i)- (long)&(l.c))<<endl;//值为4,说明他们之间并没有其它成员。
关于第二点,我还将在独孤九剑第三式中详加说明。
下面左图是当前Little对象的内存情况:
该内存布局可由以下语句佐证:
Little l;
cout<<((long)&(l.Base::i) - (long) &l)<<endl; //值为4
cout<<((long)&(l.i) - (long)&(l.Base::i))<<endl; //值为8
若把Base中的sum虚函数去掉,则Little对象的内存布局如上右图。此时Little对象没有继承虚表指针,而是由自己新建了一个。
4. 多继承的时候
class Base1
{
public:char *pc;virtual void sum1();
};class Base2
{
public:int ** pi;virtual void sum2();
};class Little : public Base1,public Base2
{
public:double *** pd;virtual void out();
};
这次把数据成员都换成指针,为各位看官换换口味。
此时,sizeof(Base1) 和 sizeof(Base2) 都为 8,sizeof(Little)为20。Little的对象内存如下图所示:
说明的主要是三点:1.不管是几级指针,在32位环境下都是占4个字节;2.基类成员在子类对象中的顺序取决于继承时的声明顺序;3.子类对象依然公用父类部分的虚函数指针,没有自己新建一个。
以下语句可用于验证内存分布:
Little l;
cout<<((long)&(l.pc) - (long)&(l))<<endl;//4
cout<<((long)&(l.pi) - (long)&(l))<<endl;//12
cout<<((long)&(l.pd) - (long)&(l))<<endl;//16
cout<<((long)&(l.pi) - (long)&(l.pc))<<endl;//8
5. 多继承 + 虚继承
class VirtualBase
{
public:int vbi;
};class Base1 : public virtual VirtualBase
{
public:char *pc;virtual void sum1();
};class Base2 : public virtual VirtualBase
{
public:int **pi; virtual void sum2();
};class Little : public Base1, public Base2
{
public:double ***pd;virtual void out();
};
此时,sizeof(Base1) 和 sizeof(Base2) 都为 16,sizeof(Little)为32。假设Base1有实例对象b1,Base2有实例对象b2,则它们的内存如下图所示:
其中__vptr_Base1和__vptr_Base2是指向虚函数表的指针;而__vbptr_Base1和__vbptr_Base2是指向虚基类表的指针。
而此时,Little对象的内存如下图所示:
该布局可用如下语句佐证:
Little l;
cout<<((long)&(l.vbi) - (long)&(l))<<endl;//28 该成员被放在了最后
cout<<((long)&(l.pc) - (long)&(l))<<endl;// 8
cout<<((long)&(l.pi) - (long)&(l))<<endl;// 20
cout<<((long)&(l.pd) - (long)&(l))<<endl;// 24
cout<<((long)&(l.pi) - (long)&(l.pc))<<endl;// 12
说明:在VC++中,对每个继承自虚基类的类实例,将增加一个隐藏的“虚基类表指针”(vbptr)成员变量,从而达到间接计算虚基类位置的目的。该变量指向一个全类共享的偏移量表,表中项目记录了对于该类而言,“虚基类表指针”与虚基类之间的偏移量”。
下面进一步说明虚基类指针指向的虚基类表的内容。
上面Little类实例的__vbptr_Base1和__vbptr_Base2指针所指向的虚基类表的内存分布如下左图所示:
说明:虚基类表中的第一个索引是自己的内存地址相对于自己存放指向虚基类表的地址的偏移量(为-4,因为中间隔了一个虚函数表指针);第二个索引开始是基类内存相对于它自己指向虚基类表地址的偏移量;最后以0标志虚基类表的结束。
右图是VC6.0的实验结果,__vbptr_Base1所指向的内容紧挨着__vbptr_Base2所指向的内容。很容易验证,__vbptr_Base1的值与__vbptr_Base2的值差了12字节,且有:
*((int *)*((int*)(&l) + 1) - 2) == *((int *)*((int*)(&l) + 4) + 1) == 12
从而证明了右图的正确性(并不一定适用于其它编译器)。
到此第一式就练完了,各位少侠是否觉得神清气爽、格外精神(*^-^*)
C++独孤九剑第一式——拨云见日(对象内存模型)相关推荐
- C++对象模型4——多重继承的对象内存模型、vptr与vtbl的创建与重置的时机、不要在含有虚函数的类的构造函数中调用memset
一.多重继承的对象内存模型 class Base1 { public:virtual void f() {cout << "base1::f()" << e ...
- Swift 对象内存模型探究(一)
MemoryLayout 基本使用方法 HandyJSON 是 Swift 处理 JSON 数据的开源库之一,类似 JOSNModel,它可以直接将 JSON 数据转化为类实例在代码中使用. 由于 S ...
- python对象内存模型
原文:Python的垃圾回收机制(二)之内存模型 1. Python对象内存模型 首先介绍一下Python对象的内存模型,如下图1所示: 图1. PyObject对象内存模型 上图可以看到,一个PyO ...
- java对象内存模型_Java对象的内存模型
众所周知,函数调用在内存中是通过压栈,退栈实现的,而Java的方法调用则是在JVM栈中通过栈帧实现的,且所有的Java对象都只在堆上分配内存.那么一个Java对象在堆内存里到底长啥样呢?实际上,当一个 ...
- C++对象内存模型学习
本文原文出处为MSDN.如果你安装了MSDN,可以搜索到C++ Under the Hood.否则也可在网站上找到http://msdn.microsoft.com/archive/default.a ...
- C++多继承时的对象内存模型
#include <cstdio> #include <iostream> using namespace std; //基类A class A{ public:A(int a ...
- C++虚继承下的内存模型(二)
对于虚继承,恰恰和普通继承相反,大部分编译器会把基类成员变量放在派生类成员变量的后面,这样随着继承层级的增加,基类成员变量的偏移就会改变,就得通过其他方案来计算偏移量. 下面我们来一步一步地分析虚继承 ...
- Java对象的内存模型
众所周知,函数调用在内存中是通过压栈,退栈实现的,而Java的方法调用则是在JVM栈中通过栈帧实现的,且所有的Java对象都只在堆上分配内存.那么一个Java对象在堆内存里到底长啥样呢?实际上,当一个 ...
- java内存模型 创建类_JVM内存模型及String对象内存分配
昨天看了一篇关于<Java后端程序员1年工作经验总结>的文章,其中有一段关于String和StringBuffer的描述,对于执行结果仍然把握不准,趁此机会也总结了下JVM内存模型. 1. ...
最新文章
- 说经年——感悟我接触的求职简历
- ASP.NET Web Services Tutorial
- 洛谷 2820 局域网
- PHP字符串offset取值特性
- 策略设计模式_设计模式之策略模式总结
- Atitit 字符串转换数组main参数解析 args splitByWholeSeparator 	 		String string= -host 101.1 8*124 -db 1
- php 三大魔术方法,PHP魔术方法
- JSP九大内置对象以及作用
- 【原创】1985-2021年《中国城市统计年鉴》900+全变量地级市面板数据 Python编程整理直接可用的城市面板数据
- 天正自定义填充图案怎么添加_cad里怎么增加自定义填充图案
- 一个美国人到中国当产品经理的心得:中国App设计真好!
- java方法的重写和重载_Java方法重载和重写原理区别解析
- arXiv镜像加载慢的解决办法
- 两个例子讲清Thread的setDaemon的作用
- WinAPI 对话框DialogBox、EndDialog、MAKEINTRESOURCE(资源转化宏)、窗口消息处理函数(返回值为FALSE)
- 展望十二五:“核高基”突破核心技术走向产业化
- 西北乱跑娃 -- python发送html邮件
- 网校程序源码:网校频频倒闭的背后原因
- Eclipse背景主题设置
- 《Java EE企业级应用开发教程(SSM)》练习题答案---第五章Spring的事务管理(仅供参考)
热门文章
- 项目上线流程-离线SQL
- adb logcat 日志格式
- 【C语言编程学习】当鸣人放了一个螺旋丸,我突然发觉这个事情不简单......
- SIMIO三维智能对象仿真模拟软件版本12 功能介绍 和试用下载
- Hadoop HDFS数据仓库技术
- 通过软件的方式,控制芯片引脚的高低电平
- c++入门 求圆周长(PTA)
- Kubernetes context 上下文配置
- 交互设计[2]--情感化设计
- 关于Proteus安装后找不到Proteus 7 Professional文件夹或者打不开ISIS 7 Professional解决方案