一:结构体内存对齐
1.引例
- 开幕雷击,请给出下列四个结构体的大小
- 我们可能第一时间都会有个朴实的想法,把每个变量的大小加一起就是结构体的大小,那上面这四个题的答案应该是6,6,13,22。
- 我在没有学习结构体内存对齐的时候,也是这么计算的,不过编译器会告诉我们,我们的答案是错的。
- 答案是12,8,16,32,这是为什么?为了能够正确计算结构体的大小,我们需要知道结构体的内存对齐。
2.结构体内存对齐的规则
- 结构体的第一个成员,放在结构体变量在内存中存储位置的偏移量为0的地址处。
- 从第2个成员往后的所有成员,都放在一个对齐数的整数倍地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的值为8,Linux中没有默认对齐数(但按照默认对齐数为4计算通常能得到正确的结果) - 结构体总大小为所有成员中最大对齐数的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
- 掌握了以上规则,我们就能正确计算出一个结构体的大小。
-
3.练习1解答
- 现在我们运用上面的四个规则分别计算上文中4个结构体的大小,首先是第一题。
- 首先我们根据规则1,结构体第一个成员放在结构体变量在内存中存储位置的0偏移处的地址处,所以第一个变量c1放在了0偏移处。
- 而c1的类型为char,我们知道char类型的大小为1字节,所以 c1变量占据了一个字节的位置
- 图中一个矩形代表一个字节大小,右边的数字代表相对于结构体在内存中存储位置的偏移大小
- 好了,现在第一个变量 c1放置完了,我们开始放第二个变量i,根据规则2
- 从第二个成员往后的所有成员,都放在一个对齐数的整数倍的地址处
- 而对齐数是默认对齐数和成员大小中的较小值
- int 类型的大小为4字节,默认对齐数为8,所以变量i的对齐数是4,我们需要将变量i放在它对齐数的整数倍处,1,2,3都不是4的整数倍,所以这些位置什么也不放,直到偏移量为4时,我们开始放变量i
- 而int大小为4个字节,所以4,5,6,7这四个位置放我们的变量i
- 最后放变量c2,c2的类型为char,大小为1字节,1<8,故c2的对齐数为1,任何数都是1的倍数,所以我们直接将c2放在偏移量为8的地址处
- 这样我们所有的变量都放置完毕了,从地址0到8,一共9个字节,那这个结构体的大小就是9个字节吗?
- 不是,根据规则3,结构体总大小为所有成员中最大对齐数的整数倍。
- 三个成员的对齐数分别为1,4,1。最大为4。
- 所以结构体的大小应该是4的整数倍,离9最近的4的倍数为12,故这个结构体的大小为12。
4.练习2解答
- 这个结构体S2的成员和结构体S1一模一样,但是创建时c2在i前面,那这个结构体S2的大小和S1一样大吗?
- 首先根据规则1,我们将变量c1放在偏移量为0处,占一个字节
- 然后放c2,根据规则2,我们要把c2放在他对齐数的整数倍的的地址处,c2的大小为1,默认对齐数为8,故c2的对齐数为1,1是1的倍数,可以放c2,c2大小为1个字节,只占据偏移量为1的地址处。
- 最后放变量i,大小为4字节,默认对齐数为8,4<8,对齐数为4,从偏移量为4的地址处开始存放,放4个字节,占据4,5,6,7
- 从0到7,总共8个字节,所有成员变量的对齐数分别为1,1,4,最大为4,8是4的倍数。根据规则3,结构体S2的大小就为8个字节
- 可以看到,结构体S1和S2成员变量均相同,但是创建的顺序不同,也会导致结构体的大小的不同
- 那在设计结构体的时候,我们既要满足对齐,又要节省空间,我们应该让占用空间小的成员尽量集中在一起
5.练习3解答
- 首先根据规则1,我们将变量d放在偏移量为0处,变量d类型为double,占8个字节
- 占据0~7这8个地址
- 然后放c,根据规则2,我们要把c放在他对齐数的整数倍的的地址处,c的大小为1,默认对齐数为8,故c的对齐数为1,8是1的倍数,可以放c,c大小为1个字节,只占据偏移量为8的地址处。
- 最后放变量i,大小为4字节,默认对齐数为8,4<8,对齐数为4,9,10,11都不是4的倍数,从偏移量为12的地址处开始存放,放4个字节,占据12,13,14,15
- 从0到15,总共16个字节,所有成员变量的对齐数分别为8,1,4,最大为8,16是8的倍数。根据规则3,结构体S2的大小就为16个字节
6.练习4讲解
- 可以看到结构体S4中包含了结构体S3,这与我们之前计算的结构体有所不同。
- 我们还是按照老套路来计算,首先根据规则1,我们将变量c1放在偏移量为0处,变量c1类型为char,占1个字节,占据偏移量为0的地址
- 然后放s3,根据规则2,我们要把s3放在他对齐数的整数倍的的地址处,s3的类型为struct S3,根据规则4
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,之前我们计算过结构体S3的最大对齐数为8,故变量s3的对齐数为8,1-7 均不是8的倍数,从偏移量为8的地址处开始存放s3,结构体S3的大小为16字节,占据8~23偏移量的地址
- 最后放变量d,大小为8字节,默认对齐数为8,对齐数为8, 24是8的倍数,从此存放变量d,存放8个字节,占据24~31偏移量的地址。
- 从0到31,总共32个字节,根据规则4,如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
- 所有成员变量的对齐数分别为1,8,8,最大为8, 32是8的倍数。结构体S2的大小就为32个字节。
- 相信通过上文的4个练习讲解,你已经学会如何计算结构体的大小了。
二:为什么存在结构体的内存对齐
- 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。 - 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说:
结构体的内存对齐是拿空间来换取时间的做法。
三:修改默认对齐数
- 我们有时候可能觉得根据编译器设计的默认对齐数不合我们心意,我们可以通过预处理指令#pragma来修改我们的默认对齐数
- 可以看到我们将默认对齐数改成1,这样其实就不用对齐了,我们得出的结果就和我们刚开始想的一样,结构体大小就是各变量大小相加
- 我们可以在一个结构体创建前使用#pragma pack(想要修改的数值)来改变默认对齐数,然后再这个结构体创建完之后#pragma pack()括号中为空,来取消我们之前设置的对齐数,还原为默认对齐数
- 如图,只有S1,S2的默认对齐数被修改为1,S3和S4的不变
- 最后附上代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
//练习1
struct S1
{
char c1;
int i;
char c2;
};
//练习2
struct S2
{
char c1;
char c2;
int i;
};
//练习3
struct S3
{
double d;
char c;
int i;
};
//练习4-结构体嵌套问题
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
printf("%d\n", sizeof(struct S3));
printf("%d\n", sizeof(struct S4));
return 0;
}