5. 结构体字节对齐
这节课我们来学习 C 语言中的结构体字节对齐。
结构体字节对齐 是 C 语言编译器为了适用计算机硬件系统而做出的一种优化策略,目的是在有可能浪费部分内存空间的情况下来换取程序尽可能快的运行速度。
字节对齐方式通常是 1字节对齐、2字节对齐、4字节对齐、8字节对齐。对齐方式都是 2 的 n 次方的方式。
结构体字节对齐 根据编译器和硬件平台不同而不同。
先来说一下什么是字节对齐。我们先看下面这个结构体占用几个字节?
struct mydata {
char c;
short int s;
};
我来写代码测试一下:
// filename: struct_align.c
#include <stdio.h>
struct mydata {
char c;
short int s;
};
int main(int argc, char * argv[]) {
struct mydata a;
printf("sizeof(a.c): %ld\n", sizeof(a.c));
printf("sizeof(a.s): %ld\n", sizeof(a.s));
printf("sizeof(struct mydata): %ld\n", sizeof(struct mydata));
printf("sizeof(a): %ld\n", sizeof(a));
printf("&a: %p\n", &a);
printf("&a.c: %p\n", &a.c);
printf("&a.s: %p\n", &a.s);
return 0;
}
运行结果如下:
sizeof(a.c): 1
sizeof(a.s): 2
sizeof(struct mydata): 4
sizeof(a): 4
&a: 0x7ffe8c2d93a4
&a.c: 0x7ffe8c2d93a4
&a.s: 0x7ffe8c2d93a6
根据打印结果我们绘制其结构体的内存结构如下:
struct mydata a;
这个字节空着
|
V
+-----+-----+-----+-----+
| a.c | | a.s |
+-----+-----+-----+-----+
^ ^
| |
&a.c &a.s
&a 0x7ffe8c2d93a6
0x7ffe8c2d93a4
可见结构体 struct mydata 一共有两个成员,其中 c 占 1 个字节,s 占 2 个字节,两个成员组成一个结构体后的变量 a 却是占用 4 个字节。其中有一个字节没有使用。这就是由编译器字节对齐引起的结果。
字节对齐
要理解字节对齐这个问题,我们要从计算机内存的结构说起。在早期,由于制造工艺的限制。我们在制作内存时规定一个字节都是由 8 个位组成的。构成内存的存储单元都是用一个字节(8 个位)构成。这时我们一个 short int 类型变量要放在两个存储单元中,每次读取这个 short int 类型的数据需要读取两次内存单元,然后组合在一起形成 16 位的 short int 数据。随着计算机技术的发展,内存的每个存储单元使用 8个 位就太小了。于是后来出现了 16 位内存、 32 位内存、 甚至现在有些内存是 64 位内存。就是说在计算中,每一个存储单元用 64 个位构成,一个机器周期就可以一次性的读取这 64 个位的数据。将内存存储单元做大可以节省制造成本,又可以一次访问更多的数据位数,提高了效率。现代计算机很多都是 64 位 CPU,就是说它一次可以处理 64 位的内存数据。
下面用示意图的方式来说明内存位数和地址的关系:
1、8 位内存每个存储单元存储一个字节。存储单元的地址从 0 开始,每个地址 递增 1
+-----+-----+-----+-----+-----+-----+-----+-----+----
| | | | | | | | | ...
+-----+-----+-----+-----+-----+-----+-----+-----+----
0 1 2 3 4 5 6 7 8
2、16 位内存每个存储单元存储2个字节。存储单元的地址从 0 开始,每个地址 递增 2。
16 位内存每次存取的最小单位是 16 位,每次存取的地址都是 2 的 1 次方的整数倍
+-----+-----+-----+-----+-----+-----+-----+-----+----
| | | | | ...
+-----+-----+-----+-----+-----+-----+-----+-----+----
0 2 4 6 8
3、同理,32 位内存每个存储单元存储4个字节。存储单元的地址从 0 开始,每个地址 递增 4。
32 位内存每次存取的最小单位是 32 位,每次存取的地址都是 2 的 2 次方的整数倍
+-----+-----+-----+-----+-----+-----+-----+-----+----
| | | ...
+-----+-----+-----+-----+-----+-----+-----+-----+----
0 4 8
如何理解内存的储存单元呢?我们用建造车库做比喻。你是一个建筑设计师。计划建造能够停放 1000 辆小轿车的车库。于是你从村东头向村西头一共建造了 1000 间小房子,每个房间存放一辆小汽车。这样建造房子会有一个问题,如果我们车库内存放车辆长度比较长的小货车,它要占用两间房,这时候我们就要将小货车的车头和货箱分开后分别放入两间房子中,等取车时在组合在一起。虽然可行,但效率较低。有了这个经验。下一个项目还是要计划建造能够停放 1000 辆小轿车的车库,同样也可能存放小货车。聪明的你这一次建造了 500 间原来两倍大的房间,每个房间可以存放两辆小汽车或一辆小货车。房间号由原来的编号0、1、2、3、... 改成了 0、2、4、6、...。这样你的建造房间的数量少了,成本降低且达到了同样的效果。那有一个问题出现了。我们在第一个房间停放了一辆小汽车,这个房间还能再停放另外一辆小汽车,但此时我要停放一辆小货车。聪明的你不再拆车,而是让或小货车停在隔壁车库里。你让当前这个房间的一个车位空着就行了。
这就是字节对齐的原理。字节对齐可以提高数据存储的效率。
字节对齐的规则
每个变量的起始地址都是这个变量自身长度的整数倍,我们把这个数值称为该类型的对齐字节数。
- 如:
char为 1,short为 2,int为 4,double为 8。
结构体整体对齐规则
- 编译器默认的对齐字节数 通常是 4 或 8。这个规则不同的编译器是不一样的。
- 结构体对齐先保证结构体内所有成员变量都对齐。
- 整个结构体的对齐字节数必须是 最大成员对齐字节数 和 编译器默认对齐字节数 取最小值后的整数倍。
- 编译器可能在结构体的末尾添加填充字节来满足此要求。
- 结构体的起始地址也必须是此结构体字节对齐字节数的整数倍。
改写上述结构体如下:
struct mydata {
char c;
short int s;
int i;
};
我们使用 sizeof(struct myadata) 来计算此结构体占用的字节数为 8,其对齐方式为 4 字节对齐。
我们打印各个变量的地址,然后来分析它的内存结构如下:
// filename: struct_align.c
#include <stdio.h>
struct mydata {
char c;
short int s;
int i;
};
int main(int argc, char * argv[]) {
struct mydata d;
printf("&d: %p\n", &d);
printf("&d.c: %p\n", &d.c);
printf("&d.s: %p\n", &d.s);
printf("&d.i: %p\n", &d.i);
printf("sizeof(struct mydata): %ld\n", sizeof(struct mydata));
return 0;
}
运行结果如下:
&d: 0x7ffdeaf78a00
&d.c: 0x7ffdeaf78a00
&d.s: 0x7ffdeaf78a02
&d.i: 0x7ffdeaf78a04
sizeof(struct mydata): 8
分析内存结构如下:
struct mydata da;
这个字节空着
|
V
+-----+-----+-----+-----+-----+-----+-----+-----+
| d.c | | d.s | d.i |
+-----+-----+-----+-----+-----+-----+-----+-----+
^ ^ ^
| | |
&d.c &d.s &d.i
&d 0x7ffdeaf78a04
0x7ffdeaf78a00