大家好,我是嵌入式老林,从事嵌入式软件开发多年,今天分享的内容是C语言结构体对齐介绍,希望能对你有所帮助
摘要:最近有粉丝说在笔试的时候,经常遇到求结构体字节数的问题,做完后不知道自己写对了没。这篇文章就来介绍一下结构体对齐的计算方法。不知道你们笔试的时候有没有遇到这种题目呢?
一、字节对齐的基本概念
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
", sizeof(MyStruct));
printf("offset c1:%d, i:%d, c2:%d
", offsetof(MyStruct, c1), offsetof(MyStruct, i), offsetof(MyStruct, c2));
printf("addr c1:%x, i:%x, c2:%x
", &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
", sizeof(MyStruct));
printf("offset c1:%d, c2:%d, i:%d
", offsetof(MyStruct, c1), offsetof(MyStruct, c2), offsetof(MyStruct, i));
printf("addr c1:%x, c2:%x, i:%x
", &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
", sizeof(MyStruct));
printf("offset c1:%d, s1:%d, c2:%d, i:%d, c3:%d
", 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
", &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
", sizeof(MyStruct));
printf("offset c1:%d, my_s1:%d, s1:%d, i:%d
", offsetof(MyStruct, c1), offsetof(MyStruct, my_s1), offsetof(MyStruct, s1), offsetof(MyStruct, i));
printf("addr c1:%x, my_s1:%x, s1:%x, i:%x
", &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
", sizeof(MyStruct));
printf("offset c1:%d, my_s1:%d, s1:%d, i:%d
", offsetof(MyStruct, c1), offsetof(MyStruct, my_s1), offsetof(MyStruct, s1), offsetof(MyStruct, i));
printf("addr c1:%x, my_s1:%x, s1:%x, i:%x
", &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
", sizeof(MyStruct));
printf("offset c1:%d, MyUnion:%d, s1:%d, i:%d
", offsetof(MyStruct, c1), offsetof(MyStruct, MyUnion), offsetof(MyStruct, s1), offsetof(MyStruct, i));
printf("addr c1:%x, MyUnion:%x, s1:%x, i:%x
", &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
", sizeof(MyStruct));
printf("offset c1:%d, s1:%d, i:%d
", offsetof(MyStruct, c1), offsetof(MyStruct, s1), offsetof(MyStruct, i));
printf("addr c1:%x, s1:%x, i:%x
", &st.c1, &st.s1, &st.i);
return 0;
}
运行结果:
因为已经设定了按1字节对齐,所以就是,c1为1字节,s1为2字节,i 为2字节,加起来就是 7 字节。
改成按2字节对齐又是多少呢?
实际就是c1后面再空了一个字节
评论
查看更多