(转载请注明作者和出处:https://blog.csdn.net/wxq_888 未经允许请勿用于商业用途)
注意:童鞋们如果仔仔细细看完这篇博客,肯定能明白结构体的对齐方式。
最近在做一个项目的时候,客户给的激光点云文件是二进制形式,因此需要根据客户定义的结构体,将点云文件保存为文本文件方便在第三方软件如cloudCompare中查看。但是发现客户的结构体所占内存空间跟我的不一样,后来发现是对齐方式导致的问题。
-
编译器结构体(联合体)的对齐方式
// StructAlignType.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <iostream>
typedef struct _Header {
unsigned int packFlag; //4个字节
unsigned short versions; //2个字节
unsigned int serialNum; //4个字节
unsigned char devStaFlag; //1个字节
unsigned int angleRes; //4个字节
unsigned long long pulseNum; //8个字节
unsigned int pointsCount; //4个字节
_Header()
{
packFlag=0; //4个字
versions = 0; //2个字
serialNum = 0; //4个字
devStaFlag = '\0'; //1个字
angleRes = 0; //4个字
pulseNum = 0;
pointsCount = 0; //4个
}
}MY_FILE_HEADER;
int main()
{
MY_FILE_HEADER head;
//通过输出发现结构体MY_FILE_HEADER占64个字节,
std::cout <<"结构体一共占用字节数:"<< sizeof(MY_FILE_HEADER) << std::endl;
std::cout <<"packFlag; //4个字节 " << &head.packFlag << std::endl;
std::cout << "versions; //2个字节 " << &head.versions << std::endl;
std::cout << "serialNum; //4个字节 " << &head.serialNum << std::endl;
std::cout << "devStaFlag; //1个字节 " << (void*)&head.devStaFlag << std::endl;
std::cout << "angleRes; //4个字节 " <<&head.angleRes << std::endl;
std::cout <<"pulseNum; //8个字节 " <<&head.pulseNum << std::endl;
std::cout <<"pointsCount; //4个字节 " <<&head.pointsCount << std::endl;
std::cout <<std::endl;
return 0;
}
上面的输出结果发现是40个字节,为什么是这样呢?按照各个成员所占字节数想加应该是27个字节为什么是40个字节呢?多出来的13个字节是怎么多出来的呢?
不明白对齐规则的同学是不是傻了呢?不相信?有图有真相的。
一、结构体的对齐方式满足一下几个原则:
-
1、在没有#pragma pack()宏的情况下,编译器默认按照结构体中成员的最大长度进行对齐。
-
2、假设结构体内部单个成员最大的对齐大小是4个字节,那么结构体的起始地址应该是4字节对齐出,对齐后的大小也应该为4的倍数。
-
3、结构体内部成员也应该对齐。
-
4、编译器根据以上原则,以最小内存来开辟空间。
分析:
1、packFlag是4个字节,packFlag的第一个字节的地址就是结构体的起始地址(即内存地址000000284A4FF8A8处)。packFlag进行4字节对齐。
但是packFlag的结束地址由下一个成员决定。即结束位置与开始位置相差大于等于4个字节。
2、versions占用2个字节,应该按照2个字节对齐,起始地址应该是2的倍数进行对齐。
由于packFlag是4字节对齐,对齐后结束的内存地址为000000284A4FF8AC,该地址为2字节的倍数,可以作为versions的起始地址,因此versions的起始地址就是000000284A4FF8AC。
-
*************************至此为止该结构体占用4个字节****************************
但是versions的结束地址同样由下一个成员决定。即结束位置与开始位置相差大于等于2个字节。
3、serialNum占用4个字节,在内存的起始位置应该是4的倍数。
由于,versions占用2个字节后,假设versions结束地址为000000284A4FF8AE。那么serialNum起始地址位于000000284A4FF8AE的位置处,显然十六进制的000000284A4FF8AE转换为十进制为173045446830不是4的倍数,所以serialNum不能用该地址作为起始地址。
因此,对versions成员占用2个字节后的内存再进行2个字节的填充,使得内存的结束地址为000000284A4FF8B0,该地址是4的倍数。(即versions在内存中实际上是占用了4个内存的地址,而不是2个,验证了上面黄色标注的语句所说)因此serialNum的起始地址为000000284A4FF8B0。
-
*************************至此为止该结构体占用8个字节****************************
但是serialNum的结束地址由下一个成员决定。即结束位置与开始位置相差大于等于4个字节。
为了更加充分说明,现在列举如下例子补充说明,如下图所示。
4、devStaFlag占用1个字节
由于serialNum是4字节对齐,对齐后结束的内存地址为000000284A4FF8B4,该地址可以作为devStaFlag的起始地址,因此devStaFlag的起始地址就是000000284A4FF8B4。
-
************************至此为止该结构体占用12个字节********************************
同样devStaFlag结束的内存地址由下一个成员决定,即devStaFlag结束的地址位置与起始位置相差应该大于1个字节
5、angleRes占用4个字节,起始地址应该是4的倍数
由于,devStaFlag占用1个字节后,假设devStaFlag结束地址为000000284A4FF8B5。那么serialNum起始地址位于000000284A4FF8B5的位置处,显然十六进制的000000284A4FF8B5转换为十进制173045446837不是4的倍数,所以angleRes不能用该地址作为起始地址。
因此,对devStaFlag成员占用1个字节后的内存再进行3个字节的填充,使得内存的结束地址为000000284A4FF8B8,该地址是4的倍数。(即devStaFlag在内存中实际上是占用了4个内存的地址,而不是1个,验证了上面粉红色标注的语句所说)因此angleRes额起始地址为000000284A4FF8B8。
-
************************至此为止该结构体占用16个字节********************************
同样angleRes结束的内存地址由下一个成员决定,即angleRes结束的地址位置与起始位置相差应该大于等于4个字节
6、pulseNum占用8个字节,起始地址应该是8的倍数。
假设angleRes的结束地址是初始地址000000284A4FF8B8加4个字节后的000000284A4FF8BC。000000284A4FF8BC转换为十进制为173045446844不是8的倍数。因此需要对angleRes再填充4个字节。结束地址为000000284A4FF8C0。也为pulseNum的起始地址,如图3所示。
。(即angleRes在内存中实际上是占用了8个内存的地址,而不是4个,验证了上面蓝色标注的语句所说)
-
************************至此为止该结构体占用24个字节********************************
同样pulseNum结束的内存地址由下一个成员决定,即pulseNum结束的地址位置与起始位置相差应该大于等于8个字节
7、pointsCount占用4个字节,起始地址应该是4的倍数。
假设pulseNum的结束地址为初始地址加上占用的8个字节的内存后的地址000000284A4FF8C8。该地址能够被4整除,可以作为pointsCount的起始地址,如图3所示。
因此pulseNum实际上在内存中占用8个字节。
-
************************至此为止该结构体占用32个字节********************************
8、由于pointsCount占用4个字节,所以结构体结束时应该是32+4=36个字节,但为什么该结构体实际上占用40个呢?
因为上述对齐原则的第一条和第二条:
- 在没有#pragma pack()宏的情况下,编译器默认按照结构体中成员的最大长度进行对齐。
- 假设结构体内部单个成员所占内存的最大大小为8个字节,那么结构体的起始地址应该是8字节对齐,对齐后的大小也应该为8的倍数。
显然36不是8的倍数,所以应该填充4个字节,所以该结构体最终所占内存大小为40个字节
-
************************至此为止该结构体占用40个字节********************************
- 至此结构体_Header为什么占用40个字节的内存空间而不是27个字节大家清楚了吧。
二、为什么结构体需要对齐:
-
因为对齐后,访问该结构体的内存能够提高访问效率,这就是用空间来换取时间。
三、#pragma pack()的使用:
-
#pragma pack()能够屏掉默认的对齐方式,让程序员手动指定对齐字节大小。
如图4所示,规定结构体按照4个字节对齐,而不是以一结构体成员中,所占字节最大的成员的字节数对齐。
图5是手动指定按照2个字节对齐
图6是按照1个字节对齐,一个字节相当于内部所有成员所占字节数相加。
感兴趣的同学额可以根据上面分析的方法推一下。