结构体、联合体内存对齐(例子说明) 您所在的位置:网站首页 u1内存 结构体、联合体内存对齐(例子说明)

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

2024-06-03 17:42| 来源: 网络整理| 查看: 265

目录1. 内存对齐1.1 为什么需要内存对齐1.2 内存对齐规则2. 结构体大小2.1 结构体大小计算准则2.2 结构体及结构体数组2.3 结构体内嵌结构体3. 联合体大小3.1 联合体及联合体数组3.2 联合体内嵌联合体4. 结构体 & 联合体4.1 结构体内嵌联合体4.2 联合体内嵌结构体4.3 复杂的内嵌5. 指定字节对齐5.1 指定字节对齐后的大小5.2 取消指定字节对齐的大小5.3 指定字节对嵌套结构体的影响5.4 取消字节对嵌套结构体的影响

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 #include 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语言结构体大小、联合体大小计算



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有