什么是数据对齐?
一般情况下,当我们定义一个变量,编译器会按照默认的地址对齐方式,来给该变量分配一个存储空间地址。如果该变量是一个 int 型数据,那么编译器就会按4字节或4字节的整数倍对齐;如果该变量是一个 short 型数据,那么编译器就会按2字节或2字节的整数倍边界对齐;如果是一个 char 类型的变量,那么编译器就会按照1字节对齐。
#include <stdio.h>
int a = 1;
int b = 2;
char c1 = 3;
char c2 = 4;
int main(void) {
printf("a: %p\n",&a);
printf("b: %p\n",&b);
printf("c1:%p\n",&c1);
printf("c2:%p\n",&c2);
return 0;
}
在上面的程序中,我们分别定义2个 int 型变量,2个 char 型变量,然后分别打印它们的地址,运行结果如下。
a: 00402000
b: 00402004
c1: 00402008
c2: 00402009
通过运行结果我们可以看到,对于 int 型数据,其在内存中的地址都是以4字节或4字节整数倍对齐的。而 char 类型的数据,其在内存中是以1字节对齐的。变量 c2 就直接分配到了 c1 变量的下一个存储单元,不用像 int 数据那样考虑4字节对齐。接下来,我们修改一下程序,指定变量 c2 按4字节对齐。
#include <stdio.h>
int a = 1;
int b = 2;
char c1 = 3;
char c2 __attribute__((aligned(4))) = 4;
int main(void) {
printf("a: %p\n",&a);
printf("b: %p\n",&b);
printf("c1:%p\n",&c1);
printf("c2:%p\n",&c2);
return 0;
}
运行结果如下。
a: 00402000
b: 00402004
c1: 00402008
c2: 0040200C
通过运行结果可以看到,字符变量 c2 由于使用 aligned 属性声明按照4字节边界对齐,所以编译器不可能再给其分配 0x00402009 这个地址,因为这个地址不是4字节对齐的。编译器空出3个字节单元,直接从 0x0040200C 这个地址上给变量 c2 分配存储空间。
为什么要数据对齐?
- 通过 aligned 这个属性声明,我们虽然可以显式指定变量的地址对齐方式,但是也会因边界对齐造成一定的内存空洞,浪费一定的内存空间。比如在上面这个程序中,0x00402009~0x0040200b 这三个地址空间的存储单元就没有被使用。
- 既然地址对齐会造成一定的内存空洞,那我们为什么还要按照这种对齐方式去存储数据呢?
一个主要原因就是,这种对齐设置可以简化 CPU 和内存 RAM 之间的接口和硬件设计。比如一个32位的计算机系统,CPU 读取内存时,硬件设计上可能只支持4字节或4字节倍数对齐的地址访问,CPU 每次往内存 RAM 读写数据时,一个周期可以读写4个字节。如果我们把一个数据放在4字节对齐的地址上,那么CPU一次就可以把数据读写完毕;如果我们把一个 int 型数据放在一个非4字节对齐的地址上,那 CPU 就要分2次才能把这个4字节大小的数据读写完毕。 - 为了配合计算机的硬件设计,编译器在编译程序时,对于一些基本数据类型,比如 int、char、short、float 等,会按照其数据类型的大小进行地址对齐,按照这种地址对齐方式分配的存储地址,CPU
一次
就可以读写完毕。虽然边界对齐会造成一些内存空洞,浪费一些内存单元,但是在硬件上的设计却大大简化了。这也是编译器给我们定义的变量分配地址时,不同类型变量按不同字节数地址对齐的原因。除了 int、char、short、float 这些基本类型数据,对于一些复合类型数据,也要满足地址对齐要求。