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 ...
最新文章
- jasp报错_jetty启动访问jsp页面报错
- 下载python后怎样打开-下载python后如何启动
- 由浅入深:自己动手开发模板引擎——置换型模板引擎(四)
- 多个应用SD-WAN实现业务连续性的方法——微云网络
- Cadence allegro PCB 设计中,出零件位置图时,如何将丝印自动放在器件中心
- 字符流与字节流转换输出
- Ink on paper 最小生成树-Prim-二分答案并查集
- ghelper怎么在手机上用_当长时间不用手机玩《崩坏3》、《战双》
- python有类似mybatis的框架_为什么感觉国内比较流行的 mybatis 在国外好像没人用的样子?...
- 关于推送系统设计的一些总结与思考(三)
- 机器学习基石(1)--The Learning Problem
- 打造炫酷通用的ViewPager指示器 玩转字体变色
- 计算机应用oas,办公自动化系统(OAS)
- Kibana:更有效地构建 Kibana 仪表板 - 7.12 发布
- Android流星雨效果---史上最炫,浪漫,值得陪你女朋友一起看~ [捂脸]
- Scope及其子类介绍
- 马斯洛“需求层次理论” 在《植物大战僵尸》中的运用
- 如何对技术视频转换文章投稿进行二次创作
- Udacity机器人软件工程师课程笔记(十八)-机械臂仿真控制实例(其三)-KR210机械臂反向运动学
- Sothink SWF Decompiler v4.2
热门文章
- 如何解决tmux下anaconda激活虚拟环境,python版本不对
- R语言与数据分析实战11-数据的删除
- java计算机毕业设计医院门诊管理系统源码+数据库+系统+lw文档+mybatis+运行部署
- i9 13900Hx性能怎么样 i913900HX参数 相当于什么水平
- 【整理】离散数学在计算机学科中的应用
- Linux 运维基础(七):用户管理
- 聚观早报 | iPadOS 16 推迟发布;罗辑思维创业板 IPO 终止
- Spring指南之使用Spring缓存数据(Spring Framework官方文档之缓存抽象详解)
- 基于CLT13实现的证据加密代码部署
- 怎么把WPS文字自动替换直引号为弯引号?