概述

我们都知道类中包含着数据成员,但是数据成员在内存中是怎样分布的呢?继承之后数据又是怎样布局的呢?下面对这些问题进行整理解答。首先说明的是类的空间分布是编译器编译的结果,不同的编译器有可能会不一样,但是原理是一样的。

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、增加虚函数

首先看一下在增加虚函数之后,编译器做了什么(详情请看虚函数原理):

  1. 导入一个virtual table(虚函数表),用来存放它所声明的每一个虚函数地址。这个table的元素个数一般而言是虚函数的个数,再加上一个或两个(用以支持runtime type identification)。
  2. 在每一个类对象中导入一个vptr,提供执行期的链接,使每个对象能够找到相应对的virtual table
  3. 加强构造函数,使它能够为vptr设定初值,让它指向类所对应的virtual table
  4. 加强析构函数,使它能够抹消指向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++ 类中数据成员分布详解相关推荐

  1. python 函数参数self_Python类中self参数用法详解

    Python编写类的时候,每个函数参数第一个参数都是self,一开始我不管它到底是干嘛的,只知道必须要写上.后来对Python渐渐熟悉了一点,再回头看self的概念,似乎有点弄明白了. 首先明确的是s ...

  2. [YTU]_2618 ( B 求类中数据成员的最大值-类模板)

    题目描述 声明一个类模板,类模板中有三个相同类型的数据成员,有一函数来获取这三个数据成员的最大值. 类模板声明如下: template<class numtype> class Max { ...

  3. C++ : 类的成员函数修改类中数据成员值

    遇到一个问题是:在类中有一个数据成员,是public的,在类的成员函数中进行修改,这个类的成员函数可能是要调用多次,想知道是不是每一次调用都有效 写了一个测试函数: #include <iost ...

  4. java类中数据成员

    一.数据成员特点 --表示java类的状态 --声明数据成员必须指定变量名以及所属类型,同时还可以指定其他属性 --数据成员的类型可以是基本数据类型,byte,short,char,int,long, ...

  5. python类中数据成员_Python 入门 之 类成员

    1.类的私有成员 私有: 只能自己拥有 以 __ 开头就是私有内容 对于每一个类的成员而言都有两种形式: - 公有成员,在任何地方都能访问 - 私有成员,只有在类的内部才能使用 私有成员和公有成员的访 ...

  6. String类中的intern()方法详解

    来源地址:https://blog.csdn.net/soonfly/article/details/70147205 在翻<深入理解Java虚拟机>的书时,又看到了2-7的 String ...

  7. Java中Arrays类中的数组操作方法详解

  8. 中yeti不能加载_第二十章_类的加载过程详解

    类的加载过程详解 概述 在 Java 中数据类型分为基本数据类型和引用数据类型.基本数据类型由虚拟机预先定义,引用数据类型则需要进行类的加载 按照 Java 虚拟机规范,从 Class 文件到加载到内 ...

  9. C#中WPF ListView绑定数据的实例详解

    C#中WPF ListView绑定数据的实例详解 发布时间: 2019-03-09 19:29:46 来源: 互联网 作者: 晨曦888 栏目: C#教程 点击: 298 这篇文章主要介绍了C#中WP ...

最新文章

  1. [你必须知道的css系列]第一回:丰富的利器2:CSS选择符之子选择符、相邻选择符...
  2. 数组中的逆序对,为什么要在第一个小于等于的时候计数?
  3. 二、【SAP-PM模块】PM模块(含服务采购)组织架构
  4. js控制table中tr位置互换
  5. C ++中带有示例的llabs()函数
  6. Java知识点汇总1
  7. (转)linux下vi编辑器编写C语言的配置
  8. MYSQL——表操作
  9. 手机APP和微信小程序能否取代域名?
  10. vb.net的UI设计
  11. Axure实战002:APP原型设计思路
  12. C++求解一元二次方程
  13. 237. 删除链表中的节点
  14. 记录一下iter()的用法
  15. 163vip邮箱账号登录入口在哪儿?163邮箱登录不了怎么办?
  16. apollo自动驾驶进阶学习之:如何实现施工路段限速绕行及其参数调试
  17. java root权限_Android应用获取Root权限
  18. c语言代码查错软件,Ubuntu下面的C语言代码检查工具 Splint
  19. 向量空间模型原理(VSM)
  20. 阿里大鱼发送短信(工具类)

热门文章

  1. C高级-Makefile
  2. 不现实的“机器化软件人假设” v2.0与“容许自由的温和家长制”助推
  3. input输入框 禁止输入中文
  4. 数学建模算法之优化模型【线性规划问题、非线性规划问题、整数规划问题、二次规划问题】
  5. linux 符号连接文件,Linux 硬链接和软链接(符号链接)
  6. linux下操作svn,实现根据时间段查看某个指定用户提交的记录
  7. php sql语句过滤,PHP过滤用户提交信息(防SQL注入)
  8. 从A到Z, 这份区块链术语词典据说80%的人都认不全 | 科普
  9. 记MFC俄罗斯方块制作过程
  10. 超级账本项目由linux基金会发起并管理,百度金融加入Hyperledger超级账本项目,成为核心成员...