C++ 类中数据成员分布详解
概述
我们都知道类中包含着数据成员,但是数据成员在内存中是怎样分布的呢?继承之后数据又是怎样布局的呢?下面对这些问题进行整理解答。首先说明的是类的空间分布是编译器编译的结果,不同的编译器有可能会不一样,但是原理是一样的。
1、空类
我们定义了一个空类,然后对空类进行sizeof计算,如下:
class myclass
{
};
cout << sizeof myclass << endl; //打印出1
按照正常的逻辑,这里应该为0啊,为什么会为1呢?
因为编译器在我们背后搞鬼,在我们定义空类后,编译器会安插一个char到类中。这样这个类定义出来的两个对象在内存中就是独一无二的。
2、数据成员的分布
C++标准要求,在同一个防控属性(private、protect、public)中,非静态数据变量在对象中的排列顺序和声明顺序一样。静态成员变量会存放在数据模块中,不会放在对象布局中,和类对象无关。
对象的成员变量初始化的顺序是由声明顺序决定就是这样来的。
数据与数据之后并不一定是连续空间,因为有 边界调整(对齐补齐)的原因会填补一些字节。如
class myclass
{char m_char; //占1个字节int m_iNum; //占4个字节short m_iport; //占2个字节
};
cout << sizeof myclass << endl;
输出的结果为12,内存占用如下图所示:
每个方格代表一个字节,黄色的代表填补字节。
编译器为了支持对象模型,有时会合成一些内部使用的数据成员,如vptr(指向虚函数表的指针)。传统的编译器会把这些合成的数据放到显示声明的变量后面,但是也有的会放到前面。
3、继承数据分布
在C++继承模型中,一个派生类所表现出来的东西,是其自己的成员加上其基类成员的总和。至于顺序则没有强制规定。但是大部分的编译器,基类的数据总是先出现。下面我们就按照大部分的情况进行整理。
3.1、只有继承,没有虚函数
我们首先来看下面的例子:
class Concrete1
{int val;char bit1;
};class Concrete2 : public Concrete1
{char bit2;
};class Concrete3 : public Concrete2
{char bit3;
};
类对象在内存中的分布情况下:
你会发现每个类后面都有3个字节个填补字节,这样造成了内存的浪费。但是这样又是必须的,下面让我们来看把填补字节去掉会发生什么。如下图:
如果用Concrete1的对象给Concrete2对象赋值,则bit2的字节赋值时错误的。
所以在设计继承时,要考虑这样设计合不合理。
3.2、增加虚函数
首先看一下在增加虚函数之后,编译器做了什么(详情请看虚函数原理):
- 导入一个virtual table(虚函数表),用来存放它所声明的每一个虚函数地址。这个table的元素个数一般而言是虚函数的个数,再加上一个或两个(用以支持runtime type identification)。
- 在每一个类对象中导入一个vptr,提供执行期的链接,使每个对象能够找到相应对的virtual table
- 加强构造函数,使它能够为vptr设定初值,让它指向类所对应的virtual table
- 加强析构函数,使它能够抹消指向class之相关的virtual table的vptr
对于vptr放到对象布局中哪个地方,一般会放到尾端,因为这样可以兼容C语言代码,如:
struct no_virts
{
int d1,d2;
};class has_virts : public no_virts
{
virtual void foo();
int d3;
};
内存中布局为:
当然也有编译器把vptr放到前面,只要把__vptr__has_virts移动到d1前面。
3.3、多重继承
假设有这样的几个类,声明和继承关系如下:
class Point2d
{
virtual foo();
float _x,_y;
};class Point3d : public Point2d
{
float _z;
};class Vertex
{
virtual foo2();
Vertex* next;
};class Vertex3d: public Point3d, public Vertex
{
float mumble;
};
类的内存分布如下:
一般编译器是根据继承的声明顺序来排列它们,Vertex3d对象,可被视为一个Point2D的子对象加上一个Point3d的数据,在加上Vertex子对象,最后加上vertex3d自己的部分。内存布局如图所示:
当然编译器会进行优化,对于多个虚指针会合并成一个,只是对虚函数表进行修改。
3.3、虚继承
一般的编译器是,如果Class内含一个或多个virtual base class,则对象会被分隔成两部分:一个不变区域和一个共享区域。
对于不变区域中的数据,不管后续如何衍化,总是拥有固定的offset,所以这一部分数据可以被直接存取。对于共享区域,就是virtual base class对象数据,其位置会因为每次的派生操作而有变化,所以他们只能被间接存取。
一般的编译器会在虚函数表中放置虚基类的偏移,和虚函数指针放在混在一起。可通过虚函数表的索引值进行区分。正值,是虚函数,负值,则是虚基类。
这样虚继承的内存分布就和普通继承一样了,但是对于虚基类中的成员做存取操作,需要先去虚函数表中进行索引,然后进行存取操作,时间会稍微慢些。
所以一般而言,虚基类最好是一个抽象类,且没有任何的成员变量。
上面说的是一般做法,有的编译器会在类中增加一个vbptr。类似于vptr,这个是指向虚基类的指针。原意跟虚函数的一样。所以不同的编译器要具体分析。
4、数据存取
对于静态成员变量,存放在静态区,读取和使用没有影响。如果多个类拥有同名的静态变量,编译器会对每一个静态成员进行编码,获得一个独一无二的程序识别代码。
对于基类非静态成员变量,在对象编译的时候,变量相对于类对象的偏移是固定的,所以读取速度和struct成员是一样的。
对于非虚基类非静态成员变量,在对象编译的时候偏移量也是已知的,只是有时需要加减虚指针的大小,但是这原本就是编译器生成的,所以速度和上面的一样。
对于虚基类中成员,在存取时需要导入一层新的间接性,所以存取会稍慢一些。
5、总结
类对象的实际分布情况是编译器决定的,不同的编译器会有不同的结果。上面整理只是一些常用的方法,所以具体的情况需要具体的分析。
在Visual Studio中,右击项目,在属性(Properties)-> C/C++ -> 命令行(Command Line)-> 附加选项(Additional Options)中输入/d1 reportAllClassLayout即可在输出窗口中查看类的内存分布
感谢大家,我是假装很努力的YoungYangD(小羊)
参考资料:
《深度探索 C++对象模型》
C++ 类中数据成员分布详解相关推荐
- python 函数参数self_Python类中self参数用法详解
Python编写类的时候,每个函数参数第一个参数都是self,一开始我不管它到底是干嘛的,只知道必须要写上.后来对Python渐渐熟悉了一点,再回头看self的概念,似乎有点弄明白了. 首先明确的是s ...
- [YTU]_2618 ( B 求类中数据成员的最大值-类模板)
题目描述 声明一个类模板,类模板中有三个相同类型的数据成员,有一函数来获取这三个数据成员的最大值. 类模板声明如下: template<class numtype> class Max { ...
- C++ : 类的成员函数修改类中数据成员值
遇到一个问题是:在类中有一个数据成员,是public的,在类的成员函数中进行修改,这个类的成员函数可能是要调用多次,想知道是不是每一次调用都有效 写了一个测试函数: #include <iost ...
- java类中数据成员
一.数据成员特点 --表示java类的状态 --声明数据成员必须指定变量名以及所属类型,同时还可以指定其他属性 --数据成员的类型可以是基本数据类型,byte,short,char,int,long, ...
- python类中数据成员_Python 入门 之 类成员
1.类的私有成员 私有: 只能自己拥有 以 __ 开头就是私有内容 对于每一个类的成员而言都有两种形式: - 公有成员,在任何地方都能访问 - 私有成员,只有在类的内部才能使用 私有成员和公有成员的访 ...
- String类中的intern()方法详解
来源地址:https://blog.csdn.net/soonfly/article/details/70147205 在翻<深入理解Java虚拟机>的书时,又看到了2-7的 String ...
- Java中Arrays类中的数组操作方法详解
- 中yeti不能加载_第二十章_类的加载过程详解
类的加载过程详解 概述 在 Java 中数据类型分为基本数据类型和引用数据类型.基本数据类型由虚拟机预先定义,引用数据类型则需要进行类的加载 按照 Java 虚拟机规范,从 Class 文件到加载到内 ...
- C#中WPF ListView绑定数据的实例详解
C#中WPF ListView绑定数据的实例详解 发布时间: 2019-03-09 19:29:46 来源: 互联网 作者: 晨曦888 栏目: C#教程 点击: 298 这篇文章主要介绍了C#中WP ...
最新文章
- [你必须知道的css系列]第一回:丰富的利器2:CSS选择符之子选择符、相邻选择符...
- 数组中的逆序对,为什么要在第一个小于等于的时候计数?
- 二、【SAP-PM模块】PM模块(含服务采购)组织架构
- js控制table中tr位置互换
- C ++中带有示例的llabs()函数
- Java知识点汇总1
- (转)linux下vi编辑器编写C语言的配置
- MYSQL——表操作
- 手机APP和微信小程序能否取代域名?
- vb.net的UI设计
- Axure实战002:APP原型设计思路
- C++求解一元二次方程
- 237. 删除链表中的节点
- 记录一下iter()的用法
- 163vip邮箱账号登录入口在哪儿?163邮箱登录不了怎么办?
- apollo自动驾驶进阶学习之:如何实现施工路段限速绕行及其参数调试
- java root权限_Android应用获取Root权限
- c语言代码查错软件,Ubuntu下面的C语言代码检查工具 Splint
- 向量空间模型原理(VSM)
- 阿里大鱼发送短信(工具类)
热门文章
- C高级-Makefile
- 不现实的“机器化软件人假设” v2.0与“容许自由的温和家长制”助推
- input输入框 禁止输入中文
- 数学建模算法之优化模型【线性规划问题、非线性规划问题、整数规划问题、二次规划问题】
- linux 符号连接文件,Linux 硬链接和软链接(符号链接)
- linux下操作svn,实现根据时间段查看某个指定用户提交的记录
- php sql语句过滤,PHP过滤用户提交信息(防SQL注入)
- 从A到Z, 这份区块链术语词典据说80%的人都认不全 | 科普
- 记MFC俄罗斯方块制作过程
- 超级账本项目由linux基金会发起并管理,百度金融加入Hyperledger超级账本项目,成为核心成员...