电子工程师技术服务社区
公告
登录
|
注册
首页
技术问答
厂商活动
正点原子
板卡试用
资源库
下载
文章
社区首页
文章
面试常考,项目易错,长文详解C/C++中的字节对齐
分 享
扫描二维码分享
面试常考,项目易错,长文详解C/C++中的字节对齐
C
C++
李肖遥
关注
发布时间: 2020-10-12
丨
阅读: 1406
## 引入主题,看代码 我们先来看看以下程序 ```cpp //编译器:https://tool.lu/coderunner/ //来源:技术让梦想更伟大 //作者:李肖遥 #include
using namespace std; struct st1 { char a ; int b ; short c ; }; struct st2 { short c ; char a ; int b ; }; ``` 编译的结果如下: ![](https://cf03.ickimg.com/bbsimages/202010/ddb609951cf4065cddd762b5d2bc7b7b.png) 问题来了,两个结构体的内容一样,只是换了个位置,为什么`sizeof(st)`的时候大小不一样呢? 没错,这正是因为内存对齐的影响,导致的结果不同。对于我们大部分程序员来说,都不知道内存是怎么分布的,因为这是编译器该干的活,编译器把程序中的每个数据单元安排在合适的位置上,导致了相同的变量,不同声明顺序的结构体大小的不同。 ## 几种类型数据所占字节数 int,long int,short int的宽度和机器字长及编译器有关,但一般都有以下规则(ANSI/ISO制订的) 1. `sizeof(short int)` <= `sizeof(int)` 2. `sizeof(int)` <= `sizeof(long int) ` 3. `short int`至少应为16位(2字节) 4. `long int`至少应为32位 | 数据类型 | 16位编译器 | 32位编译器 | 64位编译器 | | --- | --- | --- | --- | | char | 1字节 | 1字节 | 1字节 | | char* | 2字节 | 4字节 | 8字节 | | short int | 2字节 | 2字节 | 2字节 | | int | 2字节 | 4字节 | 4字节 | | unsigned int | 2字节 | 4字节 | 4字节 | | float | 4字节 | 4字节 | 4字节 | | double | 8字节 | 8字节 | 8字节 | | long | 4字节 | 4字节 | 8字节 | | long long | 8字节 | 8字节 | 8字节 | | unsigned long | 4字节 | 4字节 | 8字节 | ## 什么是对齐 现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问都可以从任何地址开始,但实际情况是在访问特定变量的时候经常在特定的内存地址访问。 所以这就需要各类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。内存对齐又分为自然对齐和规则对齐。 对于内存对齐问题,主要存在于struct和union等复合结构在内存中的分布情况,许多实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们要求这些数据的首地址的值是某个数M(通常是4或8); 对于内存对齐,主要是为了提高程序的性能,数据结构,特别是栈,应尽可能在自然边界上对齐,经过对齐后,cpu的内存访问速度大大提升。 ### 自然对齐 指的是将对应变量类型存入对应地址值的内存空间,即数据要根据其数据类型存放到以其数据类型为倍数的地址处。 例如char类型占1个字节空间,1的倍数是所有数,因此可以放置在任何允许地址处,而int类型占4个字节空间,以4为倍数的地址就有0,4,8等。编译器会优先按照自然对齐进行数据地址分配。 ### 规则对齐 以结构体为例就是在自然对齐后,编译器将对自然对齐产生的空隙内存填充无效数据,且填充后结构体占内存空间为结构体内占内存空间最大的数据类型成员变量的整数倍 ## 实验对比 ### 首先看这个结构体 ``` typedef struct test_32 { char a; short b; short c; char d; }test_32; ``` 首先按照自然对齐,得到如下图的内存分布位置(第一个格子地址为0,后面递增,下面测试的同样) ![](https://cf03.ickimg.com/bbsimages/202010/4e234d0918a8be89b9e2044af1f10477.png) 编译器将对空白处进行无效数据填充,最后将得到此结构体占内存空间为8字节,这个数值也是最大的数据类型short的2个字节的整数倍,将程序编译,得到也是8字节的结果 ### 如果稍微调换一下位置的结构体 ``` typedef struct test_32 { char a; char b; short c; short d; }test_32; ``` 同样按照自然对齐如下图分布 ![](https://cf03.ickimg.com/bbsimages/202010/22afee87ac8180651e341708af9909c1.png) 可以看到按照自然对齐,变量之间没有出现间隙,所以规则对齐也不用进行填充,而这里有颜色的方格有6个,也就是6个字节 按照规则对齐,6字节是此结构体中最大数据类型short的整数倍,因此此结构体为6字节,后面的空白不需理会,可以实际编译。一下运行,结果和分析一致为6个字节。 ### double的情况 我们知道32位处理器一次只能处理32位也就是4个字节的数据,而double是8字节数据类型,这要怎么处理呢? 如果是64位处理器,8字节数据可以一次处理完毕,而在32位处理器下,为了也能处理double8字节数据,在处理的时候将会把double拆分成两个4字节数进行处理,从这里就会出现一种情况如下: ``` typedef struct test_32 { char a; char b; double c; }test_32; ``` 这个结构体在32位下所占内存空间为12字节,而在64位环境下所占内存空间为16字节,原因就是上述的处理方式不同导致的,32位下只能拆分成两个4字节进行处理,所以这里规则对齐将判定该结构体最大数据类型长度为4字节,因此总长度为4字节的整数倍,也就是12字节。 而64位判定最大为8字节,所以结果也是8字节的整数倍:16字节。这里的结构体中的double没有按照自然对齐放置到理论上的8字节倍数地址处,我认为这里编译器也有根据规则对齐做出相应的优化,节省了4个多余字节。 这部分各位可以按照上述规则自行分析测试。 ### 数组 对齐值为:min(数组元素类型,指定对齐长度).但数组中的元素是连续存放,存放时还是按照数组实际的长度. 如char t[9],对齐长度为1,实际占用连续的9byte.然后根据下一个元素的对齐长度决定在下一个元素之前填补多少byte. ### 嵌套的结构体 假设 ``` struct A { ...... struct B b; ...... }; ``` 对于B结构体在A中的对齐长度为:min(B结构体的对齐长度,指定的对齐长度). B结构体的对齐长度为:上述2中结构整体对齐规则中的对齐长度. **举个例子** ``` //编译器:https://tool.lu/coderunner/ //来源:技术让梦想更伟大 //作者:李肖遥 #include
#include
using namespace std; #pragma pack(8) struct Args { char ch; double d; short st; char rs[9]; int i; } args; struct Argsa { char ch; Args test; char jd[10]; int i; }arga; ``` **输出结果:** ![](https://cf03.ickimg.com/bbsimages/202010/951cbad3d8e54648ebb9b74aaecf26aa.png) 改成#pragma pack (16)结果一样. 这个例子证明了三点: - 对齐长度长于struct中的类型长度最长的值时,设置的对齐长度等于无用. - 数组对齐的长度是按照数组成员类型长度来比对的. - 嵌套的结构体中,所包含的结构体的对齐长度是结构体的对齐长度. ### 指针 主要是因为32位和64位机寻址上,来看看例子 ``` //编译器:https://tool.lu/coderunner/ //来源:技术让梦想更伟大 //作者:李肖遥 #include
#include
using namespace std; #pragma pack(4) struct Args { int i; double d; char *p; char ch; int *pi; }args; ``` 结果如下 | pack | 4 | 8 | | --- | --- | --- | | length | 32 | 40 | args1 | 8 | 8 | | args2 | 4 | 8 | ## 内存对齐的规则 1. 数据成员对齐规则 结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。(例如struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储.)。 2. 结构体作为成员 如果一个结构里有某些结构体成员,则结构体成员要从其内部"最宽基本类型成员"的整数倍地址开始存储.(例如struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储.)。 在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。 3. 1&2 当#pragma pack的n值等于或超过所有数据成员长度的时候,这个n值的大小将不产生任何效果。 ### 例子 ``` #include
#include
using namespace std; #pragma pack(4) struct m { int a; short b; int c; }; int main() { cout <<"结构体m的大小:"<< sizeof(m) << endl; cout << endl; // 获得成员a相对于m储存地址的偏移量 int offset_b = offsetof(struct m, a); cout <<"a相对于m储存地址的偏移量:"<< offset_b << endl; system("pause"); return 0; } ``` ![](https://cf03.ickimg.com/bbsimages/202010/2981ad296134e74f10cbfd21a8193467.png) 从运行结果来看我们可以证实上面内存对齐规则的第一条:第一个数据成员放在offset为0的地方; 现在咱来看看上面结构体是如何内存对齐的;先用代码打印它们每个数据成员的存储地址的偏移量 ``` //编译器:https://tool.lu/coderunner/ //来源:技术让梦想更伟大 //作者:李肖遥 #include
#include
using namespace std; #pragma pack(4) struct m { int a; short b; int c; }; int main() { cout <<"结构体m的大小:"<< sizeof(m) << endl; cout << endl; int offset_b = offsetof(struct m, a);// 获得成员a相对于m储存地址的偏移量 int offset_b1 = offsetof(struct m, b);// 获得成员a相对于m储存地址的偏移量 int offset_b2 = offsetof(struct m, c);// 获得成员a相对于m储存地址的偏移量 cout <<"a相对于m储存地址的偏移量:"<< offset_b << endl; cout << "b相对于m储存地址的偏移量:" << offset_b1 << endl; cout << "c相对于m储存地址的偏移量:" << offset_b2 << endl; //system("pause"); return 0; } ``` ![](https://cf03.ickimg.com/bbsimages/202010/a6ba770c02e8c29d26714576344f0ba6.png) 在此c在结构体中偏移量为8加上它自身(int)4个字节,刚好是12(c的开始位置为8,所以要加它的4个字节) 上面内存结束为11,因为0-11,12是最大对齐数的整数倍,故取其临近的倍数,所以就取4的整数倍即12; 上图中我用连续的数组来模仿内存,如图是它们的内存对齐图; 如果将最大内存对齐数改为8,他将验证内存对齐规则中的第3条。 如果将其改为2,会发生什么:我们来看看: ``` //编译器:https://tool.lu/coderunner/ //来源:技术让梦想更伟大 //作者:李肖遥 #include
#include
using namespace std; #pragma pack(2) struct m { int a; short b; int c; }; int main() { cout <<"结构体m的大小:"<< sizeof(m) << endl; cout << endl; int offset_b = offsetof(struct m, a);// 获得成员a相对于m储存地址的偏移量 int offset_b1 = offsetof(struct m, b);// 获得成员a相对于m储存地址的偏移量 int offset_b2 = offsetof(struct m, c);// 获得成员a相对于m储存地址的偏移量 cout <<"a相对于m储存地址的偏移量:"<< offset_b << endl; cout << "b相对于m储存地址的偏移量:" << offset_b1 << endl; cout << "c相对于m储存地址的偏移量:" << offset_b2 << endl; //system("pause"); return 0; } ``` ![](https://cf03.ickimg.com/bbsimages/202010/295be62c1a10e528e2b3bd6684e6b797.png) 对于这个结果,我们按刚才第一个例子我所分析的过程来分析这段代码,得到的是10; 故当我们将#pragma pack的n值小于所有数据成员长度的时候,结果将改变。 ## 对齐的作用和原因 各个硬件平台对存储空间的处理上有很大的不同。如果不按照适合其平台要求对数据存放进行对齐,可能会在存取效率上带来损失。 比如有些平台每次读都是从偶地址开始,如果一个int型在32位地址存放在偶地址开始的地方,那么一个读周期就可以读出; 而如果存放在奇地址开始的地方,就可能会需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该int数据。那么在读取效率上下降很多,这也是空间和时间的博弈。 CPU每次从内存中取出数据或者指令时,并非想象中的一个一个字节取出拼接的,而是根据自己的字长,也就是CPU一次能够处理的数据长度取出内存块。总之,CPU会以它“最舒服的”数据长度来读取内存数据 ### 举个例子 如果有一个4字节长度的指令准备被读取进CPU处理,就会有两种情况出现: 1、4个字节起始地址刚好就在CPU读取的地址处,这种情况下,CPU可以一次就把这个指令读出,并执行,内存情况如下 ![](https://cf03.ickimg.com/bbsimages/202010/233a8988171e1807b21fd4a03ac6f985.png) 2、而当4个字节按照如下图所示分布时 ![](https://cf03.ickimg.com/bbsimages/202010/35fced8ce19d7746b25d34506950f239.png) 假设CPU还在同一个地址取数据,则取到第一个4字节单元得到了1、2字节的数据,但是这个数据不符合需要的数啊,所以CPU就要在后续的内存中继续取值,这才取到后面的4字节单元得到3、4字节数据,从而和前面取到的1、2字节拼接成一个完整数据。 而本次操作进行了两次内存读取,考虑到CPU做大量的数据运算和操作,如果遇到这种情况很多的话,将会严重影响CPU的处理速度。 因此,系统需要进行内存对齐,而这项任务就交给编译器进行相应的地址分配和优化,编译器会根据提供参数或者目标环境进行相应的内存对齐。 ## 什么时候需要进行内存对齐. 一般情况下都不需要对编译器进行的内存对齐规则进行修改,因为这样会降低程序的性能,除非在以下两种情况下: 1. 这个结构需要直接被写入文件; 2. 这个结构需通过网络传给其他程序; ## 对齐的实现 可以通知给编译器传递预编译指令,从而改变对指定数据的对齐方法。 ``` unsigned int calc_align(unsigned int n,unsigned align) { if ( n / align * align == n) return n; return (n / align + 1) * align; } ``` 不过这种算法的效率很低,下面介绍一种高效率的数据对齐算法: ``` unsigned int calc_align(unsigned int n,unsigned align) { return ((n + align - 1) & (~(align - 1))); } ``` 这种算法的原理是: `(align-1)` :对齐所需的对齐位,如:2字节对齐为1,4字节为11,8字节为111,16字节为1111... `(&~(align-1))` :将对齐位数据置位为0,其位为1 `(n+(align-1)) & ~(align-1)` :对齐后的数据
原创作品,未经权利人授权禁止转载。详情见
转载须知
。
举报文章
点赞
(
0
)
李肖遥
关注
评论
(0)
登录后可评论,请
登录
或
注册
相关文章推荐
MK-米客方德推出工业级存储卡
Beetle ESP32 C3 蓝牙数据收发
Beetle ESP32 C3 wifi联网获取实时天气信息
开箱测评Beetle ESP32-C3 (RISC-V芯片)模块
正点原子数控电源DP100测评
DP100试用评测-----开箱+初体验
Beetle ESP32 C3环境搭建
【花雕体验】16 使用Beetle ESP32 C3控制8X32位WS2812硬屏之二
X
你的打赏是对原创作者最大的认可
请选择打赏IC币的数量,一经提交无法退回 !
100IC币
500IC币
1000IC币
自定义
IC币
确定
X
提交成功 ! 谢谢您的支持
返回
我要举报该内容理由
×
广告及垃圾信息
抄袭或未经授权
其它举报理由
请输入您举报的理由(50字以内)
取消
提交