分享

C语言结构体对齐

 汉无为 2023-06-24 发布于湖北

摘要:最近有粉丝说在笔试的时候,经常遇到求结构体字节数的问题,做完后不知道自己写对了没。这篇文章就来介绍一下结构体对齐的计算方法。不知道你们笔试的时候有没有遇到这种题目呢?

一、字节对齐的基本概念

1.1 什么是字节对齐

在C语言中,结构是一种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以是一些复合数据类型(如数组、结构、联合等)的数据单元。在结构中,编译器为结构的每个成员按其自然边界(alignment)分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。

为了使CPU能够对变量进行快速的访问,变量的起始地址应该具有某些特性,即所谓的”对齐”。比如4字节的int型,其起始地址应该位于4字节的边界上,即起始地址能够被4整除。

1.2 为什么需要字节对齐

当我们在C语言中定义结构体时,编译器会对结构体的成员进行内存对齐,以提高访问效率和节约内存。如果没有对齐的话,CPU在取数的时候,会花更多的指令周期。

一个32位系统,假设有个整型变量的地址不是自然对齐,比如为0x00000002,则CPU取它的值需要访问两次内存,第一次取从0x00000002-0x00000003的一个short,第二次取从0x00000004-0x00000005的一个short,然后组合得到所要的数据;如果变量在0x00000003地址上的话则要访问三次内存,第一次为char,第二次为short,第三次为char,然后组合得到整型数据。而如果变量在自然对齐位置上,则只要访问一次就可以取出数据

1.3 结构体对齐的规则

1,结构体的第一个成员永远放在结构体起始位置偏移为0的地址

2,结构体从第二个成员,总是放在一个对齐数的整数倍数

   对齐数 = 编译器默认的对齐数和变量自身大小的较小值

3,结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。

4,如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

二、结构体对齐的计算

2.1 例子一

先来看一下这个结构体占了几个字节,说明一下,下面的例子都是在32bit系统上运行的

#include <stdio.h>#include <stddef.h>

int main(void){ typedef struct { char c1; int i; char c2; }MyStruct; MyStruct st; printf('%d\r\n', sizeof(MyStruct)); printf('offset c1:%d, i:%d, c2:%d\r\n', offsetof(MyStruct, c1), offsetof(MyStruct, i), offsetof(MyStruct, c2)); printf('addr c1:%x, i:%x, c2:%x\r\n', &st.c1, &st.i, &st.c2);

return 0; }

运行结果,这个结构体占12个字节

先来介绍一下offsetof()这个宏,如果想知道结构体的某个成员相对于结构体的首地址的偏移量,可通过这个宏获取,这个宏在头文件stddef.h中。我们看到结构体中的成员c1,i,c2分别相对于结构体的首地址偏移0,4,8

图片

解释:c1按1字节对齐,但i为int类型,按4字节对齐,所以不能紧跟其后,i的地址要为4的整数倍,所以在c1后空出了3字节开始存放,c2为1字节对齐,紧跟在i后面即可,这样算的话,总字节数为9,但结构体的总大小要为最大对齐数的整数倍,这个结构体的最大对齐数就是4,所以得在c2的后面再补3个字节,所以这个结构体就占用了12字节。

假设下图左边那一列是变量存放的地址,右边是存放的变量

图片

如果将上面例子的结构体成员换一下位置,结果又是怎样的呢?

#include <stdio.h>#include <stddef.h>
int main(void){ typedef struct { char c1; char c2; int i; }MyStruct; MyStruct st; printf('%d\r\n', sizeof(MyStruct)); printf('offset c1:%d, c2:%d, i:%d\r\n', offsetof(MyStruct, c1), offsetof(MyStruct, c2), offsetof(MyStruct, i)); printf('addr c1:%x, c2:%x, i:%x\r\n', &st.c1, &st.c2, &st.i); return 0; }

运行结果:

图片

解释:c1和c2分别按1字节对齐,所以c2紧跟c1后面,i按4字节对齐,所以空2个字节,再存放i。那么整个结构体大小为8字节,也满足是最大对齐数的整数倍。

实际上,这两个例子不管是32bit还是64bit的系统,结果都是一样的,因为char类型和int类型在32bit和64bit系统中,占用的空间是一样的。当然了,最好是在使用前先用sizeof测一下每种类型在当前环境中占用的大小。

因此,在实际项目开发中,如果定义的结构体有很多成员,尽可能地把同类型的成员放在一起,这样可以节省一些空间。

图片

2.2 例子二

#include <stdio.h>#include <stddef.h>
int main(void){ typedef struct { char c1; short s1; char c2; int i; char c3; }MyStruct; MyStruct st; printf('%d\r\n', sizeof(MyStruct)); printf('offset c1:%d, s1:%d, c2:%d, i:%d, c3:%d\r\n', offsetof(MyStruct, c1), offsetof(MyStruct, s1), offsetof(MyStruct, c2), offsetof(MyStruct, i), offsetof(MyStruct, c3)); printf('addr c1:%x, s1:%x, c2:%x, i:%x, c3:%x\r\n', &st.c1, &st.s1, &st.c2, &st.i, &st.c3); return 0; }

运行结果:

图片

解释:c1为1字节对齐,s1为2字节对齐,地址要为2的整数倍,所以不能紧跟c1后面,得从2开始,c2是1字节对齐,i为4字节对齐,不能从地址5开始存放,否则CPU访问的时候需要很多次,甚至可能会出错。所以要在c2后空出3字节,i从地址8开始,c3为1字节对齐,紧跟其后即可,累加起来为13个字节,结构体总大小又要为最大对齐数整数倍,所以该结构体大小为16

图片

那么,将同种类型的成员都放在一起,又占用了多少空间呢?

实际结果:

图片

2.3 例子三

看一下带结构体嵌套的如何计算:

#include <stdio.h>#include <stddef.h>
int main(void){ typedef struct { int j; char c; }MyS1; typedef struct { char c1; MyS1 my_s1; short s1; int i; }MyStruct; MyStruct st; printf('%d\r\n', sizeof(MyStruct)); printf('offset c1:%d, my_s1:%d, s1:%d, i:%d\r\n', offsetof(MyStruct, c1), offsetof(MyStruct, my_s1), offsetof(MyStruct, s1), offsetof(MyStruct, i)); printf('addr c1:%x, my_s1:%x, s1:%x, i:%x\r\n', &st.c1, &st.my_s1, &st.s1, &st.i); return 0; }

解释:看了前面几个例子的分析,相信这个结构体嵌套的大家也会,原理是一样的。c1为1字节对齐,嵌套的结构体my_s1中的 j 为4字节对齐,地址要为4的整数倍,所以c1后要空出3个字节,c为1个字节,紧跟 j 后,s1为2字节,在c后面空出2个字节,i 是4个字节,s1后面再空2个字节保持对齐,这样的话,就是 4+4+2+2+4=16,最大对齐数是4,16也是4的整数倍。因此,这个结构体大小为16字节。有没有很多同学会犯这个错误呢?

不要忽略了嵌套结构体的自身的对齐,嵌套的结构体my_s1的最大对齐数为4,因此嵌套的结构体my_s1的结构体大小要为4的整数倍,所以my_s1的结构体大小为8字节,所以,这个结构体的大小为20字节

图片

运行结果:

图片

那么将嵌套结构体中的int类型改成double类型,又是多少呢?

#include <stdio.h>#include <stddef.h>
int main(void){ typedef struct { double j; char c; }MyS1; typedef struct { char c1; MyS1 my_s1; short s1; int i; }MyStruct; MyStruct st; printf('%d\r\n', sizeof(MyStruct)); printf('offset c1:%d, my_s1:%d, s1:%d, i:%d\r\n', offsetof(MyStruct, c1), offsetof(MyStruct, my_s1), offsetof(MyStruct, s1), offsetof(MyStruct, i)); printf('addr c1:%x, my_s1:%x, s1:%x, i:%x\r\n', &st.c1, &st.my_s1, &st.s1, &st.i); return 0; }

运行结果:

这个我就不带着大家一步一步算了,自己算一下,看下你学会了没

图片

2.4 例子四

看一下带联合体嵌套的如何计算

#include <stdio.h>#include <stddef.h>
int main(void){ typedef struct { char c1; union { int j; char c[8]; }MyUnion; short s1; int i; }MyStruct; MyStruct st; printf('%d\r\n', sizeof(MyStruct)); printf('offset c1:%d, MyUnion:%d, s1:%d, i:%d\r\n', offsetof(MyStruct, c1), offsetof(MyStruct, MyUnion), offsetof(MyStruct, s1), offsetof(MyStruct, i)); printf('addr c1:%x, MyUnion:%x, s1:%x, i:%x\r\n', &st.c1, &st.MyUnion, &st.s1, &st.i); return 0; }

解释:这里要注意一点就是,联合体的各成员共用一块内存空间,并且同时只有一个成员可以得到这块内存的使用权(对该内存的读写),各变量共用一个内存首地址。

图片

运行结果:

图片

个人觉得也不用刻意去记这些规则吧,重在理解,记规则的话,很久没用了估计就忘了。多看看,多笔算算就清楚了。看了前面的分析,其实就两点,第一个就是结构体成员变量存放的地址是该变量类型的整数倍;第二点就是结构体的大小为最大对齐数的整数倍。

三、修改对齐方式

3.1 使用伪指令#pragma pack (n)

如果你不想使用编译器的默认对齐方式,可通过以下方式修改结构体的字节对齐方式

在定义的结构体前后加上这两条指令,n表示你想让这个结构体按照几字节对齐。

· 使用伪指令#pragma pack (n),C编译器将按照n个字节对齐。
·
使用伪指令#pragma pack (),取消自定义字节对齐方式。

#include <stdio.h>#include <stddef.h>
int main(void){ #pragma pack (1) typedef struct { char c1; short s1; int i; }MyStruct; #pragma pack () MyStruct st; printf('%d\r\n', sizeof(MyStruct)); printf('offset c1:%d, s1:%d, i:%d\r\n', offsetof(MyStruct, c1), offsetof(MyStruct, s1), offsetof(MyStruct, i)); printf('addr c1:%x, s1:%x, i:%x\r\n', &st.c1, &st.s1, &st.i); return 0; }

运行结果:

因为已经设定了按1字节对齐,所以就是,c1为1字节,s1为2字节,i 为2字节,加起来就是 7 字节。

图片

改成按2字节对齐又是多少呢?

实际就是c1后面再空了一个字节

图片

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多