画成这样你想打我吗?

st1
+---+---+---+---+
| a | b |
+---+---+---+---+
| b | c |
+---+---+---+

st2
+---+---+---+---+
| c | a | b |
+---+---+---+---+
| b |
+---+---+---+

这样画不香吗?

st1
+---+
| a |
+---+---+---+---+
| b |
+---+---+---+---+
| c |
+---+---+

st2
+---+---+---+
| c | a |
+---+---+---+---+
| b |
+---+---+---+---+

然后(各种意义上)把空白处补齐:

st1
+---+---+---+---+
| a | XXXXXXXXX |
+---+---+---+---+
| b |
+---+---+---+---+
| c | XXXXX |
+---+---+-------+

st2
+---+---+---+---+
| c | a | X |
+---+---+---+---+
| b |
+---+---+---+---+

把一个 b 拆到两行,无论人类还是机器读起来都没有不拆来得开心。至于图中为什么是 4 个格子一换行,这是你的机器决定的。

了解更多:维基百科 - 数据结构对齐


C 设计上存在遗憾。

当前只保证顺序不保证 padding 策略(标准甚至没有禁止对齐必要之外的 padding,意味著你拿 alignof 和 max_align 算出来的也是依赖实现定义的)不上不下怪难受的。当你对顺序有要求的时候,难免也得依赖实现定义的对齐和 padding 策略了,说实话成员顺序先后这种保证是现在这样单独放标准里还是都丢给实现定义就现状来说区别不大(除了第一个成员顶头放),反而是导致了我不依赖布局的情况下如果期望紧凑还得人肉精打细算,没有编译器会帮我优化顺序。

当然更优解是 attribute 开关控制。C 标准也有 attribute 了。


为了简化 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 这些基本类型数据,对于一些复合类型数据,也要满足地址对齐要求。

结构体作为一种复合数据类型,编译器在给一个结构体变数分配存储空间时,不仅要考虑结构体内各个基本成员的地址对齐,还要考虑结构体整体的对齐。为了结构体内的成员地址对齐,编译器可能会在结构体内填充一些空间;为了结构体整体对齐,编译器可能会在结构体的末尾填充一些空间。

接下来,我们定义一个结构体,结构体内定义 int、char 和 short 三种成员,并列印结构体的大小和各个成员的地址。

struct data{
char a;
int b ;
short c ;
}
int main(void)
{
struct data s;
printf("size:%d
",sizeof(s));
printf("a:%p
",s.a);
printf("b:%p
",s.b);
printf("c:%p
",s.c);
}

程序运行结果如下。

size: 12
s.a: 0028FF30
s.b: 0028FF34
s.c: 0028FF38

我们可以看到,因为结构体的成员 b 需要4位元组对齐,编译器在给成员 a 分配完空间后,接著会空出3个位元组,在满足4位元组对齐的 0x0028FF34 地址处才给成员 b 分配存储空间。接著是 short 类型的成员 c 占据2位元组的存储空间。三个结构体成员一共占据4+4+2=10位元组的存储空间,根据结构体的对齐规则,结构体的整体对齐要向结构体所有成员中最大对齐位元组数或其整数倍对齐,或者说结构体的整体长度要为其最大成员位元组数的整数倍,如果不是整数倍要补齐。因为结构体最大成员 int 为4个位元组,或者说按4位元组的整数倍对齐,所以结构体的长度要为4的整数倍,要在结构体的末尾补充2个位元组,所以最后结构体的 size 为12个位元组。

结构体成员中,不同的排放顺序,可能也会导致结构体的整体长度不一样,我们修改一下上面的程序。

struct data{
char a;
short b ;
int c ;
};
int main(void)
{
struct data s;
printf("size: %d
",sizeof(s));
printf("s.a: %p
",s.a);
printf("s.b: %p
",s.b);
printf("s.c: %p
",s.c);
}

程序运行结果如下。

size: 8
s.a: 0028FF30
s.b: 0028FF32
s.c: 0028FF34

我们调整了一些成员顺序,你会发现,char 型变数 a 和 short 型变数 b,分配在了结构体的前4个位元组存储空间中,而且都满足各自的地址对齐,整个结构体大小是8位元组,只造成一个位元组的内存空洞。我们继续修改程序,让 short 型的变数 b 按4位元组对齐:

struct data{
char a;
short b __attribute__((aligned(4)));
int c ;
};

程序运行结果如下。

size: 12
s.a: 0028FF30
s.b: 0028FF34
s.c: 0028FF38

你会发现,结构体的大小又重新变为12个位元组。这是因为,我们显式指定 short 变数以4位元组地址对齐,导致变数 a 的后面填充了3个位元组空间。int 型变数 c 也要4位元组对齐,所以变数 b 的后面也填充了2个位元组,导致整个结构体的大小为12位元组。

我们不仅可以显式指定结构体内某个成员的地址对齐,也可以指定整个结构体的对齐方式。

struct data{
char a;
short b;
int c ;
}__attribute__((aligned(16)));

程序运行结果如下。

size: 16
s.a: 0028FF30
s.b: 0028FF32
s.c: 0028FF34

在这个结构体中,各个成员一共占8个位元组。通过前面学习我们知道,整个结构体的对齐只要是最大成员对齐位元组数的整数倍即可。所以这个结构体整体就以8位元组对齐,结构体的整体长度为8位元组。但是我们在这里,显式指定结构体整体以16位元组对齐,所以编译器就会在这个结构体的末尾填充8个位元组以满足16位元组对齐的要求,导致结构体的总长度变为16位元组。

原文地址:

嵌入式C语言自我修养 07:地址对齐那些事儿?

w.url.cn图标

这里引发一个数据结构定义中的位对齐问题,而位对齐又牵涉到内存分页需要,内存是按页管理,每一个页一般是4K,如果发生基本数据的位元组在一个页面不完整,也就是跨页了,会出现系统操作数据异常,虽然不会引起程序崩溃,但是会额外消耗时间让系统存取这样的数据。


因为要位元组对齐


推荐阅读:
相关文章