概述

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

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. jasp报错_jetty启动访问jsp页面报错
  2. 下载python后怎样打开-下载python后如何启动
  3. 由浅入深:自己动手开发模板引擎——置换型模板引擎(四)
  4. 多个应用SD-WAN实现业务连续性的方法——微云网络
  5. Cadence allegro PCB 设计中,出零件位置图时,如何将丝印自动放在器件中心
  6. 字符流与字节流转换输出
  7. Ink on paper 最小生成树-Prim-二分答案并查集
  8. ghelper怎么在手机上用_当长时间不用手机玩《崩坏3》、《战双》
  9. python有类似mybatis的框架_为什么感觉国内比较流行的 mybatis 在国外好像没人用的样子?...
  10. 关于推送系统设计的一些总结与思考(三)
  11. 机器学习基石(1)--The Learning Problem
  12. 打造炫酷通用的ViewPager指示器 玩转字体变色
  13. 计算机应用oas,办公自动化系统(OAS)
  14. Kibana:更有效地构建 Kibana 仪表板 - 7.12 发布
  15. Android流星雨效果---史上最炫,浪漫,值得陪你女朋友一起看~ [捂脸]
  16. Scope及其子类介绍
  17. 马斯洛“需求层次理论” 在《植物大战僵尸》中的运用
  18. 如何对技术视频转换文章投稿进行二次创作
  19. Udacity机器人软件工程师课程笔记(十八)-机械臂仿真控制实例(其三)-KR210机械臂反向运动学
  20. Sothink SWF Decompiler v4.2

热门文章

  1. 如何解决tmux下anaconda激活虚拟环境,python版本不对
  2. R语言与数据分析实战11-数据的删除
  3. java计算机毕业设计医院门诊管理系统源码+数据库+系统+lw文档+mybatis+运行部署
  4. i9 13900Hx性能怎么样 i913900HX参数 相当于什么水平
  5. 【整理】离散数学在计算机学科中的应用
  6. Linux 运维基础(七):用户管理
  7. 聚观早报 | iPadOS 16 推迟发布;罗辑思维创业板 IPO 终止
  8. Spring指南之使用Spring缓存数据(Spring Framework官方文档之缓存抽象详解)
  9. 基于CLT13实现的证据加密代码部署
  10. 怎么把WPS文字自动替换直引号为弯引号?