C/C++结构体对齐方式详解,从内存地址进行解析

(转载请注明作者和出处: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

一、结构体的对齐方式满足一下几个原则:

  • 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个字节。

为了更加充分说明,现在列举如下例子补充说明,如下图所示。

图2

 

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所示。

图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个字节对齐,而不是以一结构体成员中,所占字节最大的成员的字节数对齐。

图4

图5是手动指定按照2个字节对齐

标题

图6是按照1个字节对齐,一个字节相当于内部所有成员所占字节数相加。

标题

感兴趣的同学额可以根据上面分析的方法推一下。

以上分析有错误或者不准确的地方,还请各位大佬在下方留言指正,谢谢!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值