代码改变世界

结构体、联合体内存对齐(例子说明)

2022-12-29 11:29  书书书书君  阅读(1083)  评论(1编辑  收藏  举报

1. 内存对齐

1.1 为什么需要内存对齐

1、平台原因(移植原因):各个平台对存储空间的处理有很大的不同,且并不是可以访问任意地址上的任意数据。

2、性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐,对齐的内存访问仅需一次,不对齐则处理器需要进行两次访问。

按照成员的声明顺序,依次安排对齐,其偏移量为任何一个成员数据类型大小的整数倍

所以数据内存占据的大小一般都是1、2、4、8的整数倍。

本质:拿空间换时间。

1.2 内存对齐规则

1、一般设置的对齐方式为1,2,4字节对齐方式。结构的首地址必须是结构内最宽类型的整数倍地址;另外,结构体的每一个成员起始地址必须是自身类型大小的整数倍(需要特别注意的是windows下是这样的,但在linux的gcc编译器下最高为4字节对齐),否则在前一类型后补0。

2、结构体的整体大小必须可被对齐值整除,默认4(结构中的类型大小都小于默认的4)。

3、结构体的整体大小必须可被本结构内的最宽类型整除。


2. 结构体大小

2.1 结构体大小计算准则

结构体计算要遵循字节对齐原则。

1、结构体变量的首地址能够被其最宽基本类型成员的大小所整除。

2、结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding)。

3、结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节(trailing padding)。

2.2 结构体及结构体数组

	struct S1{
		char a;
		int b;
		char c;
	}s1; //12

上图中的s1.a大小为1,s1.b大小为4,s1.c大小为1。
s1.a的地址偏移量为0,占据1个内存偏移量。
按照内存对齐规则,s1.b的类型大小为4,因此其地址偏移量需要是4的整数倍,因此应该从偏移量4开始存放,即s1.a的后面需要填充3个0。
s1.b存放完后,偏移量来到了8。
s1.c的类型大小为1,由于偏移量8为1的整数倍,但存放完s1.c后,整体内存大小为9。由于9不能整除s1.b的类型大小(4),因此需要填充到12(12可以整除1和4)。
填充完毕后,整个结构体的内存大小为12。


	struct{
		short a;
		int b[2];
		char c[3];
		long d;
	}s3; //24

32 位系统:
LP32 或 2/4/4(int 为 16-bit,long 和指针为 32 位): Win16 API
ILP32 或 4/4/4(int,long 和指针都为 32 位) : Win32 API 、Unix 和 Unix 类的系统(Linux,Mac OS X)

64 位系统:
LLP64 或 4/4/8(int 和 long 为 32 位,指针为 64 位):Win64 API
LP64 或 4/8/8(int 为 32 位,long 和指针为 64 位): Unix 和 Unix 类的系统(Linux,Mac OS X)

上图中的s3.a大小为2;s3.b为整型数组,类型大小为4,内存占据大小为8;s3.c为字符型数组,内存占据大小为3;s3.d在64位程序中内存大小为8。
s3.a的地址偏移量为0,占据2个内存偏移量。
按照内存对齐规则,s3.b的类型大小为4,因此其地址偏移量需要是4的整数倍,因此应该从偏移量4开始存放,即s3.a的后面需要填充2个0。
s3.b存放完后,偏移量来到了12。
s3.c的类型大小为1,由于偏移量12为1的整数倍,可以继续存放完s3.c后,内存偏移量来到15。
按照内存对齐规则,s3.d的类型大小为8,由于15不能整除s3.d的大小(8),因此需要填充到16(16可以整除2、1、4和8)。
存放完s3.d后,内存偏移量来到了24。由于24可以整除2、1、4、8,因此结构体整体内存大小为24。

2.3 结构体内嵌结构体

	struct S1{
		char a;
		int b;
		char c;
	};

	struct{
		short a;
		struct S1 ss1;
		char c;
	}s2; //20

上图中的s2.a大小为2;s2.ss1为内嵌的结构体,从上述章节 结构体及结构体数组 的第一个案例可知其最大的数据类型大小为4,占据内存大小为12;s2.c的类型大小为1。
s2.a的地址偏移量为0,占据2个内存偏移量。
按照内存对齐规则,s2.ss1的内嵌结构体中最大类型int的大小为4,因此s2.ss1地址偏移量需要是4的整数倍,因此应该从偏移量4开始存放,即s2.a的后面需要填充2个0。
s2.ss1存放完后,偏移量来到了16。
s2.c的类型大小为1,由于偏移量16为1的整数倍,可以继续存放完s2.c后,内存偏移量来到17。
由于17不可以整除2、4、1,因此需要填充到20,填充完毕后,结构体整体内存大小为20。


上述验证的结果可以查看如下折叠代码,其他的验证方法类似,这里就不再补充了。

点击查看代码
#include <stdio.h>
#include <math.h>
int main()
{
	struct S1{
		char a;
		int b;
		char c;
	}s1;

	struct{
		short a;
		int b[2];
		char c[3];
		long d;
	}s3;
	
	struct{
		short a;
		struct S1 ss1;
		char c;
	}s2;
	
    printf("结构体s1的长度 =  %d\n", sizeof(s1));
	printf("s1.a=%d \t s1.b=%d \t s1.c=%d\n",&s1.a,&s1.b,&s1.c);
	printf("s1.a的类型大小:%d\t s1.b的类型大小:%d\t s1.c的类型大小:%d\n",sizeof(s1.a),sizeof(s1.b),sizeof(s1.c));
	printf("s1的 a->b内存间距:%d\t b->c内存间距:%d\t c内存大小:%d\n",abs(abs(&s1.a)-abs(&s1.b)),abs(abs(&s1.b)-abs(&s1.c)),abs(&s1.a)+sizeof(s1)-abs(&s1.c));

    printf("\n数组结构体s3的长度 =  %d\n", sizeof(s3));
	printf("s3.a=%d \t s3.b=%d \t s3.c=%d \t s3.d=%d\n",&s3.a,&s3.b,&s3.c,&s3.d);
	printf("s3.a的类型大小:%d\t s3.b的类型大小:%d\t s3.c的类型大小:%d\t s3.d的类型大小:%d\n",sizeof(s3.a),sizeof(s3.b),sizeof(s3.c),sizeof(s3.d));
	printf("s3的 a->b内存间距:%d\t b->c内存间距:%d\t c->d内存间距:%d\t d内存大小:%d\n",abs(abs(&s3.a)-abs(&s3.b)),abs(abs(&s3.b)-abs(&s3.c)),abs(abs(&s3.c)-abs(&s3.d)),abs(&s3.a)+sizeof(s3)-abs(&s3.d));
	
	printf("\n嵌套结构体s2的长度 =  %d\n", sizeof(s2));
	printf("s2.a=%d \t s2.ss1=%d \t s2.c=%d\n",&s2.a,&s2.ss1,&s2.c);
	printf("s2.a的类型大小:%d\t s2.ss1的类型大小:%d\t s2.c的类型大小:%d\n",sizeof(s2.a),sizeof(s2.ss1),sizeof(s2.c));
	printf("s2的 a->ss1内存间距:%d\t ss1->c内存间距:%d\t c内存大小:%d\n",abs(abs(&s2.a)-abs(&s2.ss1)),abs(abs(&s2.ss1)-abs(&s2.c)),abs(&s2.a)+sizeof(s2)-abs(&s2.c));
}

运行结果如下:

结构体s1的长度 =  12
s1.a=1075126228 	 s1.b=1075126232 	 s1.c=1075126236
s1.a的类型大小:1	 s1.b的类型大小:4	 s1.c的类型大小:1
s1的 a->b内存间距:4	 b->c内存间距:4	 c内存大小:4

数组结构体s3的长度 =  24
s3.a=1075126192 	 s3.b=1075126196 	 s3.c=1075126204 	 s3.d=1075126208
s3.a的类型大小:2	 s3.b的类型大小:8	 s3.c的类型大小:3	 s3.d的类型大小:8
s3的 a->b内存间距:4	 b->c内存间距:8	 c->d内存间距:4	 d内存大小:8

嵌套结构体s2的长度 =  20
s2.a=1075126160 	 s2.ss1=1075126164 	 s2.c=1075126176
s2.a的类型大小:2	 s2.ss1的类型大小:12	 s2.c的类型大小:1
s2的 a->ss1内存间距:4	 ss1->c内存间距:12	 c内存大小:4

3. 联合体大小

1、联合体的大小取决于他所有成员中占用空间最大的一个成员的大小。

2、当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数类型的整数倍

3.1 联合体及联合体数组

	union U1{
		char a[6];
		int b;
		char c;
	}u1; //8

u1.a的类型为字符型数组,占据内存空间大小为6;u1.b内存大小为4;u1.c内存大小为1。
根据联合体大小的规则,可知最大为u1.a的长度6。
但6不是数据类型中最长的int的4的整数倍,因此需要填充对齐到8。
填充完毕后,联合体的整体内存大小为8。


3.2 联合体内嵌联合体

	union U1{
		char a[6];
		int b;
		char c;
	};

	union{
		char a[9];
		int b;
		union U1 uu1;
	}u2; //12

u2.a的类型为字符型数组,占据内存空间大小为9;u2.b内存大小为4;u2.uu1为内嵌的联合体,从上述章节 联合体及联合体数组 可知其内存空间大小为8,即该u2.uu1占据空间为8。
根据联合体大小的规则,成员中占据内存空间最大的为u2.a为9,但9不是2、4、1的整数倍,因此需要填充到12。
填充完毕后,联合体的整体内存大小为12。


	union U1{
		char a[6];
		int b;
		char c;
	};

	union{
		char a[7];
		int b;
		union U1 uu1;
	}u3; //8

同理,此处联合体u3的所有成员中最大的内存为u3.uu1的8,且8是2、4、1的整数倍,因此不需要进行填充了。


4. 结构体 & 联合体

下面介绍结构体和联合体之间内嵌之后的内存大小的计算方式。

4.1 结构体内嵌联合体

	union U1{
		char a[6];
		int b;
		char c;
	};

	struct{
		short a;
		union U1 uu1;
	}s4; //12

s4.a的类型大小为2,地址偏移量为0,占据2个内存偏移量。
根据上述章节 联合体及联合体数组 可以知道s4.uu1的内存大小为8,其内最大类型int的大小为4,因此偏移量需要是4的整数倍,因此应该从偏移量4开始存放,占据8个内存空间。
s4.uu1存放完后,偏移量来到了12。由于12是2、1、4的整数倍,因此该结构体s4的内存大小为12。


4.2 联合体内嵌结构体

	struct S1{
		char a;
		int b;
		char c;
	};

        union U1{
		char a[6];
		int b;
		struct S1 ss1;
	}u5; //12

u5.a的类型为字符型数组,内存大小为6;u5.b占据的内存大小为4;根据上述章节 结构体及结构体数组 可以知道u5.ss1占据的内存大小为12。
根据联合体大小的规则,最大成员u5.ss1占据的内存大小为12。由于12是2、1、4的整数倍,因此该联合体u5的内存大小为12。


4.3 复杂的内嵌

	struct S1{
		char a;
		int b;
		char c;
	};

	union U1{
		char a[6];
		int b;
		char c;
	};

	struct{
		short a;
		struct S1 ss1;
		union U1 uu1;
	}s4;

上图中的s4.a类型为短整型,地址偏移量为0,占据2个内存偏移量。
根据上述章节可知 s4.ss1 内最大类型int的大小为4,占据的内存大小为12。按照结构体内存大小的规则,s4.ss1的偏移量应该为4的整数倍,因此应该从偏移地址4开始存放。
s4.ss1存放完毕后,偏移量来到16。
根据上述章节可知s4.uu1 内最大类型int的大小为4,占据的内存大小为8。按照结构体内存大小的规则,当前地址偏移量16可以满足是4的整数倍,因此直接进行存放s4.uu1。
s4.uu1存放完毕后,偏移量来到24。由于24可以整除2、4、1,因此不需要再进行填充对齐,结构体s4的整体内存大小为24。


	struct S1{
		char a;
		int b;
		char c;
	};

	union U1{
		char a[6];
		int b;
		char c;
	};

	struct{
		char a[2];
		int b;
		short c;		
		struct S1 ss1;
		union U1 uu1;
	}s5;

这里的分析过程同上,可自行试试。


5. 指定字节对齐

除了由操作系统自动按数据结构分配内存外,还支持自己更改默认对齐数来修改内存。

#pragma pack宏声明 可以改变对齐规则。

#pragma pack(1) // 直接紧凑排列,不需要填充

#pragma pack(n) // 指定n为对齐格式的字节数,一般建议设为2^m

#pragma pack()   //取消对齐格式操作,恢复缺省按照8字节对齐

5.1 指定字节对齐后的大小

	#pragma pack(1)

	struct{
		char a[2];
		int b;
		short c;		
	}s6;

由于在结构体s6的前面有#pragma pack(1),因此采取紧凑排列的方式,内存上如上图的方式。整体紧凑排列存放后,结构体的内存大小为8。

由于在结构体s6的前面有#pragma pack(3),因此采取3为对齐字节数的方式。
s6.a的地址偏移量为0,占据2个内存偏移量。此时地址偏移量来到2,由于s6.b类型为int,类型大小为4,且2不是3的整倍数,因此需要补充1个0填充到3。
s6.b存放到地址偏移量为3的位置,存放完毕后,地址偏移量来到了7。由于7不是3的整倍数,因此需要补充2个0填充到9。
s6.c存放到地址偏移量为9的位置,存放完毕后,地址偏移量来到了11。由于11不是3的整数倍,因此需要补充1个0填充到12。
全部填充完毕后,该结构体整体内存大小为12。

注:此处的对齐字节3,只是为了测试。在某些GCC中,会严格要求对齐字节按照\(2^m\)来进行选择,因此可能会出现如下报错信息:

main.c: In function ‘main’:
main.c:56:10: warning: alignment must be a small power of two, not 3 [-Wpragmas]
56 |  #pragma pack(3)
|          ^~~~

由于在结构体s6的前面有#pragma pack(8),因此采取8为对齐字节数的方式。但结构体s6的所有成员中最大类型int的大小为4,而此时指定的对齐字节8已经超过结构体成员类型的最大值,因此继续采取最大类型的大小作为对齐数。
s6.a的地址偏移量为0,占据2个内存偏移量。
s6.b类型大小为4,因此当前地址偏移量2不满足条件,因此需要补充2个0填充到4来存放。
s6.b存放完毕后,地址偏移量来到8。由于s6.c类型大小为2,当前偏移量8满足条件,因此继续存放。
s6.c存放完毕后,地址偏移量来到10。由于10不是2、4的整数倍,因此需要补充2个0来到12。


5.2 取消指定字节对齐的大小

	struct{
		char a[5];
		short b[2];	
		long c;		
	}s6; //24

	#pragma pack(4)

	struct{
		char a[5];
		short b[2];	
		long c;		
	}s7; //20

	#pragma pack()	

	struct{
		char a[5];
		short b[2];	
		long c;		
	}s8; //24

此处按照4字节对齐,s7.a和s7.b的类型大小都是小于4的,按照其自身类型的整数倍来进行对齐;
但s7.c类型大小为8,超过了指定的4,因此指定生效,s7.c按照4的整数倍对齐,即上图中的地址偏移量12处进行存放s7.c。
存放完毕后,整体地址偏移量来到20,刚好满足指定字节的整数倍,因此整体结构体内存大小为20。

此处已经使用了#pragma pack()取消了4字节对齐,因此此处采取各个结构体成员类型的整数倍进行对齐。


5.3 指定字节对嵌套结构体的影响

	#pragma pack(4)	
	
	struct S2{	
		char a[3];
		long b;
		int c;
	};	
	
//	#pragma pack()	

	struct{	
		char a[5];
		short b[2];
		struct S2 ss2;
		long c;
	}s8; //36	

不取消指定对齐、对嵌套的影响:
依旧采取前面指定的字节对齐,且内嵌的结构体也会采取该字节对齐

此处已经设置了#pragma pack(4),按照4字节对齐,因此结构体s8的成员应该均按照4字节进行内存偏移量对齐:
s8.ss2中的long b和s8.c的类型长度为8,超出了指定的4字节对齐,因此需要按照4字节对齐进行存放。
因此如果ss2.a地址偏移量为0时,ss2.b就从偏移量4开始存放。
因此在结构体s8的内存示意图中,s8.b从偏移量8开始存放,s8.ss2从12开始存放,s8.c从28开始存放。
全部存放完毕后,地址偏移量来到36,刚好满足是指定4字节对齐的整数倍,因此不需要再进行填充。


5.4 取消字节对嵌套结构体的影响

	#pragma pack(4)	
	
	struct S2{	
		char a[3];
		long b;
		int c;
	};	
	
	#pragma pack()	

	struct{	
		char a[5];
		short b[2];
		struct S2 ss2;
		long c;
	}s8; //40	

在嵌套结构体前取消指定位数对齐,此时后面的嵌套结构体中嵌套的长度仍然按前面字节对齐进行;
在取消对齐前定义的结构体,在取消对齐之后再调用,仍然按之前对齐的字节来执行。
嵌套的结构体的最大类型长度是包含了嵌套结构体内类型,即全部成员的类型的最大长度来对齐。

此处在结构体s8的前面已经使用了#pragma pack()取消了前面设置的指定4字节对齐的要求,因此此处的s8按照其结构体成员的类型大小来在内存中进行存放。
由于s8内部嵌套的结构体ss2在#pragma pack()之前,因此依旧按照指定4字节对齐的要求来进行存放(图中类型大小的橙色字体的4),即如果ss2.a从地址偏移量0开始存放,ss2.b就从地址偏移量4开始存放。




参考:
结构体及内存对齐
一文轻松理解内存对齐
结构体对齐详解
结构体大小的计算
C语言结构体大小、联合体大小计算