一、结构体内存对齐(重要)
结构体内存对齐是结构体大小的计算规则,是校招笔试和面试过程中一个十分热门的考点,希望大家认真对待。
在学习结构体内存对齐之前,我们先给两组计算结构体大小的题目,看看你能否做对:
//计算结构体大小
#include <stdio.h>
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
int main()
{
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
return 0;
}
我知道你很急,但你先别急,我们接着分析规则探索答案的由来。
结构体内存对齐的规则
关于结构体内存对齐规则,大部分参考资料是这样说的:
第一个成员在与结构体变量偏移量为0的地址处(第一个成员的地址)
其他成员变量对齐到它的对齐数的整数倍的地址处(决定非第一个成员与上一个成员地址的关系)eg: 一个值的对齐数是3,那么在地址图中,它只可以存放在0,3,6,9…
- 对齐数 = 编译器默认的对齐数与该成员变量大小的较小值
- VS的默认对齐数是8
- 只有VS编译器下才有默认对齐数的概念,其他编译器下变量的对齐数 = 变量的大小
结构体总大小为最大对齐数的整数倍。(最大对齐数为所有变量的对齐数的最大值)
如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小为所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
现在我对其进行规则提纯:
a.先算出每一个变量的对齐数(并标注最大)
b.分类讨论不同成员的偏移量
c.计算总结构体的大小
知道了最大对齐数的对齐规则,我们再来看上面的练习题:
struct S1
{
char c1; //变量大小为1,默认对齐数为8 -> 对齐数为1
int i; //变量大小为4,默认对齐数为8 -> 对齐数为4
char c2; //变量大小为1,默认对齐数为8 -> 对齐数为1
//最大对齐数是4
};
分析过程:
我们假设struct S1的起始位置为图中箭头所示位置,则各位置的偏移量如图;由内存对齐的规则:
第一个成员在与结构体变量偏移量为0的地址处:所以c1在偏移量为0处,且c1占一个字节;
其他成员变量要对齐到它的对齐数的整数倍的地址处:由于 i 的对齐数是4,所以 i 只能从偏移量为4的位置开始存储,且 i 占四个字节;
其他成员变量要对齐到它的对齐数的整数倍的地址处:由于 c2 的对齐数是1,所以 c2 紧挨着 i 存储,且 c2 占一个字节;
结构体总大小为最大对齐数的整数倍:由于最大对齐数为4,所以总对齐数要为4的倍数,大于9的最小的4的倍数为12,所以整个结构体的大小为12个字节。
struct S2
{
char c1; //变量大小为1,默认对齐数为8 -> 对齐数为1
char c2; //变量大小为1,默认对齐数为8 -> 对齐数为1
int i; //变量大小为4,默认对齐数为8 -> 对齐数为4
};
分析过程:
我们假设struct S2的起始位置为图中箭头所示位置,则各位置的偏移量如图;由内存对齐的规则:
第一个成员在与结构体变量偏移量为0的地址处:所以c1 从0偏移处开始,占一个字节;
其他成员变量要对齐到它的对齐数的整数倍的地址处:c2 对齐数为1,所以紧挨着 c1 存储,占一个字节;
其他成员变量要对齐到它的对齐数的整数倍的地址处:i 对齐数为4,所以在4的整数倍位置 – 4偏移处开始存储,占4个字节;
存放完毕后0~7一共占8个字节,因为最大对齐数为4,8为4的整数倍,所以不变
二、offsetof 宏(求结构体偏移量)
offsetof 的介绍
offsetof 是C语言中定义的一个用于求结构体成员在结构体中的偏移量的一个宏,其对应的头文件是 <stddef.h>,由于 offsetof 的使用方法与函数一样,所以它经常被错误的认为是一个函数;我们可以在VS中右键单击offsetof转到定义,查看offsetof的在VS中的实现方式。
offsetof 的参数
size_t offsetof( 结构体变量名, 成员变量名 );
offsetof 的使用
#include <stdio.h>
#include <stddef.h> //offsetof对应头文件
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
int main()
{
printf("%d\t", offsetof(struct S1, c1));
printf("%d\t", offsetof(struct S1, i));
printf("%d\n", offsetof(struct S1, c2));
printf("%d\t", offsetof(struct S2, c1));
printf("%d\t", offsetof(struct S2, c2));
printf("%d\n", offsetof(struct S2, i));
return 0;
}
这里offsetof的第一个参数写成S1 和 S2也是对的
offsetof 的模拟实现
我们以上面的 struct S1为例,经过上面的分析我们已经知道了 struct S1的大小为12,并且画出来具体的图示:
我们观察后发现:结构体成员在结构体中的偏移量 = 结构体成员的地址 - 结构体的起始地址,比如 struct S1中 i 的地址 - 结构体的起始地址可以得到结构体成员 i 的偏移量等于4;那么如果结构体的起始地址在0处,那么结构体成员的偏移量 = 结构体成员的地址 - 0 = 结构体成员地址,所以我们可以把0强转为对应结构体指针类型,然后返回结构体成员的地址即可得到结构体成员的偏移量,具体代码如下:
#include <stdio.h>
#define OFFSETOF(type, member) (size_t)&(((type*)0)->member)
struct S1
{
char c1;
int i;
char c2;
};
int main()
{
printf("%d\n", OFFSETOF(struct S1, c1));
printf("%d\n", OFFSETOF(struct S1, i));
printf("%d\n", OFFSETOF(struct S1, c2));
return 0;
}
三、为什么存在内存对齐
从上面的例子我们可以看到,结构体内存对齐会浪费一定的内存空间,但是计算机不是要尽可能的做到不浪费资源吗?那为什么还要存在内存对齐呢?关于内存对齐存在的原因,大部分的参考资料是这样说的:
- 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
- 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于:为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。所以内存对齐能够提高访问效率。
- 总体来说:结构体的内存对齐是拿空间来换取时间的做法。
这里我对原因中的第二点做一下解释:
大家都知道,我们的机器分为32位机器和64位机器,这里的32位和64位其实指的是CPU的位数,而CPU的位数对应着CPU的字长,而字长又决定着CPU读取数据时一次访问多大即空间,即一次读取几个字节,我们以32位机器为例:
如图,32位机器一次访问四个字节的大小,如果不存在内存对齐,那么要取出 i 中的数据需要两次读取,存在内存对齐则只需要读取一次。
设计结构体的技巧
在了解了结构体的对齐规则之后,有没有一种方法能让我们在设计结构体的时候既满足对齐规则,又能尽量的节省空间呢?其实是有的,方法就是:让占用空间小的成员尽量集中在一起。就像的习题,我们把占用空间下的 c1 和 c2 放在一起,从而使得 struct S2 比 struct S1 小了四个字节。
四、修改默认对齐数
我们可以使用 “#pragma pack(num)” 命令来修改VS中的默认对齐数。例如:
#include <stdio.h>
#pragma pack(8)//设置默认对齐数为8
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
#pragma pack(1)//设置默认对齐数为1
struct S2
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
//输出的结果是什么?
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
return 0;
}
在 struct S2 中,我们通过 " #pragma pack(1) " 命令把VS的默认对齐数设置为1(相当于不对齐),使得其大小变为6。
五、结构体大小计算习题
习题1
#include <stdio.h>
struct S3
{
double d;
char c;
int i;
};
int main()
{
printf("%d\n", sizeof(struct S3));
return 0;
}
妈了个巴子,double是8个字节,老糊涂了
d 从0偏移处开始存储,占8个字节,所以0~7;c 紧挨 d 存储,占一个字节,所以8,i 从4的整数倍即12处开始存储,占4个字节,所以12~15;所以0 ~ 15合计16个字节,16为最大对齐数8的倍数,所以不变。
习题2
#include <stdio.h>
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
printf("%d\n", sizeof(struct S4));
return 0;
}
c1 从0偏移位置开始存储,占一个字节,所以0;struct S3 s3 我们上面已经算出占16个字节,又因为嵌套的结构体对齐到自己的最大对齐数的整数倍处,所以从8的整数倍即8偏移处开始存储,所以8~23;d 从8的整数倍即24偏移处开始存储,占8个字节,所以24~31;合计32个字节,且为最大偏移数8的整数倍,所以不变。
习题3
#include <stdio.h>
#pragma pack(4)
struct tagTest1
{
short a;
char d;
long b;
long c;
};
struct tagTest2
{
long b;
short c;
char d;
long a;
};
struct tagTest3
{
short c;
long b;
char d;
long a;
};
#pragma pack()
int main(int argc, char* argv[])
{
struct tagTest1 stT1;
struct tagTest2 stT2;
struct tagTest3 stT3;
printf("%d %d %d", sizeof(stT1), sizeof(stT2), sizeof(stT3));
return 0;
}
这是每个变量存储的地址位数,快看看能不能对上!
stT1:
a: 0~1 d:2 b:4~7 c:8~11 合计:0~11 = 12(4的倍数);
stT2:
b:0~3 c:4~5 d:6 a:8~11 合计:0~11 = 12(4的倍数);
stT3:
c:0~1 b:4~7 d:8 a:12~15 合计:0~15 = 16(4的倍数);
芜湖!! 希望这篇结构体对齐规则可以给你带来帮助!