【C/C++】结构体内存对齐 ----- 面试必考(超详细解析,小白一看就懂!!!)

目录

一、前言

 二、引出 ---- 结构体内存对齐

 🍎结构体偏移量计算:offsetof

 🥝结构体内存对齐详解

 💦规则介绍(重点!!)

 💦例题解析

 三、习题演练

 🍍练习①

🍐练习②

 四、为什么存在内存对齐?

 1、平台原因(移植原因)

2、性能原因 

五、如何修改默认对齐数 

 六、实战演练

 七、共勉


一、前言

结构体 大家都应该了解过,可是大家是否会去深究结构体中的---结构体内存问题呢?由于最近在找实习的过程中,每次都会被问到结构体内存大小的问题,每次都是以回答错误而结束面试。

所以现在现在才醒悟过来,才知道这些知识点有多么的重要,所以咬紧牙,把这个内容的知识点记录下来!

 二、引出 ---- 结构体内存对齐

         在结构体章节,我们掌握了结构体的基本使用,但是现在我要你去计算一个结构体的大小,你会怎么做呢?

  • 现在我定义了两个结构体,通过观察可以发现它们内部的成员变量都是一样的,均有c1c2i三个成员变量,那此时分别去计算它们两个结构体的大小, 最后的结果会是多少呢?会是一样的吗
struct S1 {char c1;int i;char c2;
};struct S2 {char c1;char c2;int i;
};int main(void)
{printf("%d\n", sizeof(struct S1));printf("%d\n", sizeof(struct S2));return 0;
}
  •  通过运行可以发现两者是不一样的,这是为什么呢?如果你没有结构体内存对齐的相关知识,那相信你一定会这么去计算:
  • 在结构体S1中,c1的类型为【char】,是1个字节;
  • i的类型是【int】,是4个字节
  • c2的类型为【char】,是1个字节;
  • 那么最后的结果就是1 + 4 + 1 = 6B,可事实呢,原不止这些。

         大家仔细的想一下,为什么会出现这样的结果呢?这与结构体内存有什么关系呢?下面我用结构体偏移量计算:offsetof 来给大家详细的解释一下!!

 🍎结构体偏移量计算:offsetof

         就上面的内容,大家会产生很大的困惑?下面给大家介绍一个宏叫做offsetof,它可以用来计算结构体成员相对于起始位置的偏移量

 它的第一个参数是结构体类型,第二个参数是结构体成员

printf("%d\n", offsetof(struct S1, c1));
printf("%d\n", offsetof(struct S1, i));
printf("%d\n", offsetof(struct S1, c2));

  • 最后,计算出来的结果分别是【0】【4】【8】,那我们可以通过画内存图来看看结构体中的三个成员变量在内存中究竟是如何分布的
  •  可以看出,因为总的结构体大小为12B,可是在放完这3个成员后中间空出了三个位置,并且对于最后在c放完之后还没有到达12B,所以还得再浪费3个空间的废位置

为什么会出现上面这样的现象呢?对于结构体内存对齐的规则是怎样,让我们继续看下去👇 

 🥝结构体内存对齐详解

 💦规则介绍(重点!!)

  1. 第一个成员在与结构体变量偏移量为0的地址处
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
    对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
    VS中默认的值为8、Linux环境默认不设对齐数(对齐数是结构体成员自身的大小)】
  3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍

 💦例题解析

 知晓了上面这些规则后,大部分老铁呢肯定还是会有点懵逼,接下来我们通过例题来回顾上面的规则,这样大家能更好的理解 ----- 结构体的大小该如何计算

  • 假设我这里创建一个结构体变量叫做ss,它的起始地址就从0开始,所以根据第一条规则,第一个成员变量在与结构体变量偏移量为0的地址处,而且它的类型还是char,所以只占1个内存单元

  • 接下去看第二个成员变量 i,其为整型所以在内存中就需要存储4个字节的大小,此时便要拿其和VS下默认对齐数8去进行比较,取较小的值4
  • 算出来【4】之后便要对齐到4整数倍的地址处,那就是4这块空间,往下一直占用4个字节,这就是成员变量i在这个结构体中的内存占用分布
  • 那既然这个 i是从4的位置开始放的,中间空出来的位置就不会再放置其他成员变量了,那么这个3个空间也就浪费了

  • 接下去放置第三个成员变量c2,char类型的变量为1个字节,和8比较取小就是1,那就要将其放到1整数倍的地址处,那其实任何空间都是可以的,直接放到这个【8】的位置就行
  •  那截止目前为止这个结构体中的所有成员变量都放置完了,此时去计算一个所占的内存空间就可以发现只有9个字节。但是在一开始我们计算的这个结构体的大小为12个字节,可是现在还差3个字节,所以最后就要去进行一个填充。但是,为什么呢?

 

  • 这就要用到第三条规则了:结构体总大小为最大对齐数的整数倍
  • 那在这么计算下来之后,就可以知道结构体中的最大对齐数为4,那么【9】、【10】、【11】都不是它的整数倍,只有【12】是它的整数倍的地址处(注意这里是地址处!),因此我们需要填充3个字节,此时从0 ~ 11就有12个字节了,便为4的整数倍 👉这就是【12】如何被计算出来的全过程,你听懂了吗?


看完了,这个结构体后,还记得结构体S2吗,我再来讲一道,当然你也可以试着自己写写画画看👈

  •  首先还是一样,c1放在这个与结构体变量偏移量为0的地址处,而且它的类型还是char,所以只占1个内存单元

  • 接下去还是一样,在放置第二个成员变量开始就要考虑【对齐数】了,char所占的字节为1B,与8去进行比较一下就可以知道1来得小,那我们直接放在偏移处为1的地方就可以了,此时在内存中也只占了1个字节

  • 接下去放置第三个成员变量【i】,大小为4个字节小于8因此选择在4的整数倍的地址处开始放置这个变量,整型占4个字节,所以一直占用到偏移量为7的地方
  • 接下去就是计算整个结构体的大小,最大对齐数为4,所以要为4的整数倍,此时去计算一下得知从0 ~ 7偏移了7个字节,占用了8个空间,刚好为4的整数倍,所以结构体S2的大小为【8】是这么算出来的,你明白了吗?

 三、习题演练

通过上面两道例题的讲解,相信你对如何去计算结构体大小一定有了一个自己的认识,接下去就让我们趁热打铁🔥来做两道题目再练一练,看看自己是否真的掌握了 

 🍍练习①

 你可以先试着自己做一做,然后和我对一下是否正确

struct S3
{double d;char c;int i;
};
printf("%d\n", sizeof(struct S3));

 【分析】:

  • 首先看到第一个成员变量,从偏移量为0的地址处开始放起,因为double类型的数据在内存中占8个字节,所以一直占用偏移处为7的地方

  • 对于第二个成员变量【c】,类型为char,所以在内存中占用1个字节,那直接放在偏移量为8的地址处即可 

  • 接下去来安排第三个成员变量【i】,整型占用4个字节,比VS下默认对齐数8来得小所以【对齐数为4】,去寻找4整数倍的地址处,【9】、【10】、【11】都不是,【12】是4的整数所偏移的地址处,从此处开始往下数4个字节的空间,刚好放满15
  • 最后我们便去计算整个结构体的大小,为最大对齐数的整数倍,最大对齐数是8,计算一下放置三个成员变量占了16个空间,刚好是8的整数倍,因此16即为结构体的大小

 运行结果如下:

🍐练习②

接下去再来做一道练习,涉及结构体嵌套的问题,对应的需要使用到规则4,忘记了可以翻上去看看👈

struct S3
{double d;char c;int i;
};struct S4
{char c1;struct S3 s3;	//成员变量为另一个结构体double d;
};

 因为本题的结构体比较大,所以就标出4的整数倍所在的地址

  • 首先还是一样,来看到第一个成员变量【c1】,放到与结构体变量偏移量为0的地址处,又因为类型为char,所以只占一个字节的空间

  • 接下去,就是嵌套的结构体s3,此时我们要对齐到s3这个结构体中最大对齐数的整数倍处,那么最大对齐数就是【8】,所以要从8的地址处开始往下放置,那要占用多少空间呢?这就是s3这个结构体的大小【16】,所以一直往下数16个空间即可,一直到23这个地址处
  • 那么中间的这7个位置就算是浪费了👈

  • 最后就是这个【d】,与VS中的默认对齐数一致,所以为【8】,下一个24刚好为8整数倍的地址处,所以从这开始放,double类型的数据在内存中占8个字节,所以一直到31的地址处
  •  然后来算整个结构体s4的大小,为所有最大对齐数(含嵌套结构体的对齐数)的整数倍,也就是取s3和s4中的最大对齐数,那也就是【8】,计算一下结构体s4所占的内存空间为32,刚好为8的整数倍,所以整个结构体的大小即为32

 运行结果如下:

 四、为什么存在内存对齐?

         经过了两道例题和两道练习题的训练,相信你对如何计算结构体的大小一定是心中有数了,但在阅读的过程中你是否有疑惑为什么会存在这个【结构体内存对齐】呢?有什么实际意义吗?

 1、平台原因(移植原因)

       不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常 

2、性能原因 

  • 假设下面有一个结构体,内部有两个成员变量ci,然后要在内存中存储它们,我分为了两种,一个是【无内存对齐】,呈现的是紧密存放;一个是【内存对齐】,需要考虑到最大对齐数
  • 然后在32位平台下去分别访问结构体中的成员,假设现在读取数据的时候一次性读四个字节。
  • 首先看到的是【无内存对齐】的结构体内存分布,读一次就能读到 c,但是若要全部读取完 i,就还需要再读取一次,那访问到所有的成员变量就需要两次;
  • 接下去看到的是【内存对齐】的结构体内存分布,因为内存对齐的缘故,所有两个成员变量 c和i互不干扰,此时再看到成员变量 i,从它的初始地址处开始读取,一次读4个字节,那么读1次就刚刚好可以读完这个变量了,而不是像上面那样还需要再读一次

  •  所以原因就在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

 总体来说:

结构体的内存对齐是拿空间来换取时间的做法 


struct S1 {char c1;int i;char c2;
};struct S2 {char c1;char c2;int i;
};

了解了为什么会存在内存对齐之后,我们再回到一开始的这两个结构体,你是否有想过为什么两个结构体的成员变量都一模一样但是大小却是一个【12】,一个【8】呢?

  • 没错,就是你想到的它们所存放的位置不一样罢了。因为要存在内存对齐,所以若两个对齐数大的成员变量定义在一起的话为了满足规则就可能会浪费很多空间的内存。
  • 但若是两个对齐数较小甚至相同规定的变量定义在一块的话,可能它们就是挨着放的,占用的空间少了↓,那最后结构体的大小就变小了

所以,那在设计结构体的时候,我们既要满足对齐,又要节省空间,就要让占用空间小的成员尽量集中在一起 

五、如何修改默认对齐数 

之前我们见过了 #pragma 这个预处理指令#pragma comment,用来链接函数的静态库。这里我们再次使用,可以改变我们的默认对齐数 

  • 用法很简单#pragma pack(1)就可以设置默认对齐数为1,#pragma pack()就可以取消设置的默认对齐数,还原为默认。到它为止的默认对齐数还是被修改后的对齐数
  • 接下去就来看下面这个修改完默认对齐数后的结构体,它的大小会是多少呢?
#pragma pack(1)//设置默认对齐数为1
struct S1
{char c1;int i;char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认int main()
{//输出的结果是什么?printf("%d\n", sizeof(struct S1));return 0;
}
  • 可以看到,若是默认的对齐数设置为1的话,那其实可以看出每个成员变量的对齐数就都是1了,那么也就不存在浪费的现象,因为任何数都是1的整数倍,所以3个成员变量的内存分布如下,大小即为【6】

运行结果如下: 

 

结论:

 结构在对齐方式不合适的时候,我么可以自己更改默认对齐数 

 六、实战演练

 💬两道高频面试题

💦结构体怎么对齐? 为什么要进行内存对齐? 

  1. 第一个成员在与结构体变量偏移量为0的地址处
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
    对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
    VS中默认的值为8、Linux环境默认不设对齐数(对齐数是结构体成员自身的大小)】
  3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍

为什么要进行内存对齐呢?原因有两个,一个是平台本身的原因,任意地址上的任意数据是不能随意访问的,如果不正确访问可能会造成硬件异常。第二个就是性能原因,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问 


💦如何让结构体按照指定的对齐参数进行对齐?能否按照3、4、5即任意字节对齐? 

可以的,只需要使用一个预处理指令#pragma pack(3)便可以将默认对齐数修改为3,其他的也是同理,因为结构体默认对齐数发生了变化,此时就会导致结构体大小发生变化 

 七、共勉

 以下就是我对结构体内存对齐的理解,如果有不懂和发现问题的小伙伴,请在评论区说出来哦,同时我还会继续更新对【C/C++】的理解,请持续关注我哦!!!!! 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/723919.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Spring Cloud 面试题及答案整理,最新面试题

Spring Cloud中断路器的原理及其作用是什么? Spring Cloud断路器的原理和作用基于以下几个关键点: 1、故障隔离机制: 在微服务架构中,断路器作为一种故障隔离机制,当某个服务实例出现问题时,断路器会“断…

Docker知识点总结二

四、 Docker 架构 Docker使用客户端-服务器(C/S)架构模式,使用远程API来管理和创建Docker容器。 介绍: 1、Docker的客户端client,我们在命令行发送一些信息(命令)给Docker服务端。2、中间这个就是Docker的服务端,在这个服务端里面…

漫步者、南卡、Cleer开放式耳机怎么样?硬核对比测评性能强者!

​在当今市场上,开放式耳机的型号层出不穷,作为一名专业的测评博主,我对这类产品有着深入的了解和丰富的经验。最近,我的粉丝们通过私信向我咨询如何选择适合自己的开放式耳机,面对众多品牌的选择,他们感到…

【Unity】使用ScriptableObject存储数据

1.为什么要用ScriptableObject? 在游戏开发中,有大量的配置数据需要存储,这个时候就需要ScriptableObject来存储数据了。 很多人会说我可以用json、xml、txt,excel等等 但是你们有没有想过,假设你使用的是json&#x…

【Linux基础(一)】设备和文件IO

学习分享 1、Linux中的设备管理1.1、设备管理的特点1.2、设备分类1.3、设备工作原理1.4、Linux设备操作1.5、系统调用和系统API等区别 2、文件IO2.1、C库的文件操作2.2、文件描述符2.3、特殊文件描述符2.4、系统调用2.4.1、open系统调用4-12.4.2、open系统调用4-22.4.3、write系…

爬虫实战——伯克利新闻【内附超详细教程,你上你也行】

文章目录 发现宝藏一、 目标二、简单分析网页1. 寻找所有新闻2. 分析模块、版面和文章 三、爬取新闻1. 爬取模块2. 爬取版面3. 爬取文章 四、完整代码五、效果展示 发现宝藏 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不…

2023年扫地机器人行业分析报告(2024年扫地机器人行业未来趋势分析)

当前,随着人们生活水平的提高,扫地机器人因为操作简单、使用方便而越来越多地走入了人们的生活,成为清洁电器中的重要一员,深受消费者欢迎。 伴随科技水平的进步,当前的扫地机器人已经具备了高度智能化的功能&#xf…

C++_位图

目录 1、位图的使用 2、位图实现 3、位图与哈希表的区别 4、位图的应用 结语 前言: 位图采用的是哈希表的思想,哈希表的映射层面是在字节上,而位图的映射层面就是在bit位上。由于bit位所能展现的信息无非只有‘1’和‘0’,所…

【Apple Vision Pro应用源码】Vision Pro吸尘器项目源代码

超级有趣Vision Pro 应用 吸尘器项目 这是一个非常有趣的 Vision Pro项目,会让孩子们爱上打扫卫生。 在这里我展示了如何使用 ARKit:头部跟踪、手部跟踪、场景理解加载和播放声音进程冲突使用 MTLBuffers 处理底层网格数据 项目源代码:Git…

动态规划(算法竞赛、蓝桥杯)--状态压缩DP蒙德里安的梦想

1、B站视频链接&#xff1a;E31 状态压缩DP 蒙德里安的梦想_哔哩哔哩_bilibili #include <bits/stdc.h> using namespace std; const int N12,M1<<N; bool st[N];//st[i]存储合并列的状态i是否合法 long long f[N][M];//f[i][j]表示摆放第i列&#xff0c;状态为…

java-ssm-jsp-大学社团管理系统

java-ssm-jsp-大学社团管理系统 获取源码——》公主号&#xff1a;计算机专业毕设大全

PSINS工具箱笔记——函数定义

绘图函数&#xff1a; 时间进度条&#xff1a; timebar&#xff08;用起来简单&#xff09; 姿态转换&#xff1a; 欧拉角、姿态矩阵、等效旋转矩阵、姿态四元数、运载火箭使用的欧拉角之间的转换。 轨迹生成&#xff1a; seg trjsegment(seg, segtype, lasting, w, a, var…

centos上部署k8s

环境准备 四台Linux服务器 主机名 IP 角色 k8s-master-94 192.168.0.94 master k8s-node1-95 192.168.0.95 node1 k8s-node2-96 192.168.0.96 node2 habor 192.168.0.77 镜像仓库 三台机器均执行以下命令&#xff1a; 查看centos版本 [rootlocalhost Work]# cat /…

武汉灰京文化:游戏市场推广与用户增长的成功典范

作为游戏行业的明星企业&#xff0c;武汉灰京文化在市场推广和用户增长方面的成功经验备受瞩目。他们以创造性和独特性的市场营销策略&#xff0c;成功吸引了大量用户。这不仅提高了其游戏的知名度&#xff0c;还为公司带来了持续的增长。这一成功模式不仅对公司自身有益&#…

PaddlePaddle----基于paddlehub的OCR识别

Paddlehub介绍 PaddleHub是一个基于PaddlePaddle深度学习框架开发的预训练模型库和工具集&#xff0c;提供了丰富的功能和模型&#xff0c;包括但不限于以下几种&#xff1a; 1.文本相关功能&#xff1a;包括文本分类、情感分析、文本生成、文本相似度计算等预训练模型和工具。…

政安晨【示例演绎虚拟世界开发】(六):从制作一个对战小游戏开始(Cocos Creator 《击败老大》)(第三段)

在上一篇文章中&#xff0c;我们已经将游戏的场景基本搭建完毕&#xff0c;接下来我们就可以为游戏编写代码并实现相关的核心逻辑了。 政安晨的个人主页&#xff1a;政安晨 欢迎 &#x1f44d;点赞✍评论⭐收藏 收录专栏: AI虚拟世界大讲堂 希望政安晨的博客能够对您有所裨益&a…

基于NB-IoT的西红柿基地温湿度监测系统

总体硬件架构 在西红柿种植园内&#xff0c;我们为每株作物分配RFID标签&#xff0c;以便在每次照顾作物后记录其生长状况、施肥和灌溉等信息。这些数据将上传至云端&#xff0c;便于用户在线实时监控作物生长情况。 为了确保温湿度的精确控制&#xff0c;我们在作物棚内每隔3米…

appium2的一些配置

appium-desktop不再维护之后&#xff0c;需要使用appium2。 1、安装appium2 命令行输入npm i -g appium。安装之后输入appium或者appium-server即可启动appium 2、安装安卓/ios的驱动 安卓&#xff1a;appium driver install uiautomator2 iOS&#xff1a;appium driver i…

算法沉淀——贪心算法一(leetcode真题剖析)

算法沉淀——贪心算法一 01.柠檬水找零02.将数组和减半的最少操作次数03.最大数04.摆动序列 贪心算法&#xff08;Greedy Algorithm&#xff09;是一种基于贪心策略的优化算法&#xff0c;它通常用于求解最优化问题&#xff0c;每一步都选择当前状态下的最优解&#xff0c;以期…

鸿蒙Harmony应用开发—ArkTS声明式开发(基础手势:TapGesture)

支持单击、双击和多次点击事件的识别。 说明&#xff1a; 从API Version 7开始支持。后续版本如有新增内容&#xff0c;则采用上角标单独标记该内容的起始版本。 接口 TapGesture(value?: { count?: number, fingers?: number }) 参数&#xff1a; 参数名称参数类型必填参…