










struct 的内存大小讨论和DeepSeek网页端对话:对话链接,发现她错的很厉害。为了不让自己出错,我们也需要来梳理一下:在 C 中的 struct,内存对齐,标准是怎样的。
struct 的大小,对齐方式取决于语言标准 -> ABI先说结论(感觉好像AI文 QAQ):C 语言中,struct 的最终大小,对齐规则取决于两个要求的交集:(1) C 语言标准;(2) 平台 ABI 定义。这里为了讨论方便,我们只举例常用的编译器/ABI定义。同时不考虑人为对齐方式,仅考虑在没有 align 标识符下的默认对齐场景。
我们首先看以下的例子:
#include <stdlib.h>
#include <stddef.h>
#include <stdio.h>
int main() {
struct A {
char a;
int b : 5, c : 11, : 0, d : 8;
struct {
int ee : 8;
} e;
} a;
printf("sizeof a: %zu\n", sizeof(a));
printf("sizeof A: %zu\n", sizeof(a));
printf("%zu\n", offsetof(struct A, a));
printf("%zu\n", sizeof(a.e));
printf("%zu\n", offsetof(struct A, e));
struct B {
int a[4];
int b[];
} b;
printf("sizeof b: %zu\n", sizeof(b));
struct C {
char a; // 1 byte
int b; // 4 bytes
short c; // 2 bytes
char d; // 1 byte
} c;
printf("sizeof c: %zu\n", sizeof(c));
return 0;
}
大家可以看一看 Compiler Explorer 上的结果:
我们发现:大部分的结果是:
sizeof a: 12
sizeof A: 12
0
4
8
sizeof b: 16
sizeof c: 12
MSVC 这个离经叛道的是:
sizeof a: 16
sizeof A: 16
0
4
12
sizeof b: 16
sizeof c: 12
为什么会是这样的呢?我们需要一步步从标准开始深入到ABI来查看。
在 6.2.8 中定义了对齐要求:
Para 2: 基本对齐是指小于或等于
alignof(max_align_t)的有效对齐。实现应支持所有存储持续时间的对象的基本对齐。所有结构或联合类型,其元素具有基本对齐要求的类型,并且其元素均没有指定非基本对齐的对齐说明符时,是一个基本对齐。Para 5: 对齐方式按从弱到强或从严格到严格的顺序排列。越严格的对齐方式具有更大的对齐值。满足某种对齐要求的地址也满足任何较弱的有效对齐要求。
在 6.7.3.2 中有相关的描述,我们在这里摘录。
Para 5: 位域的类型必须是限定或非限定的布尔类型、有符号整数、无符号整数、位精确整数类型或其他实现定义的类型。是否允许原子类型由实现定义。
Para 11: 成员可以声明为由指定数量的位组成(包括符号位,如果有)。这样的成员称为位域;其宽度以冒号开头。
Para 12: 位域被解释为具有指定位数的有符号或无符号整数类型。(133处批注:位域是有符号的还是无符号的由实现定义。)如果将值 0 或 1 存储到非零宽度的 bool 类型位域中,则位域的值应与存储的值相等;bool 位域具有 bool 的语义。
Para 13: 实现过程中可以分配任何足够大的可寻址存储单元来容纳一个位域。
如果剩余空间足够,则结构体中紧随另一个位域之后的位域应打包到同一单元的相邻位中。
如果剩余空间不足,是否将不适合的位域放入下一个单元或与相邻单元重叠,由实现方式决定。
单元内位域的分配顺序(高位到低位或低位到高位)由具体实现定义。可寻址存储单元的对齐方式未作规定。Para 14: 作为一种特殊情况,宽度为零的位域结构成员表示,不应再将任何位域打包到先前放置位域的单元中(如果有的话)。
Para 16: 结构体或联合体对象的每个非位域成员都按照与其类型相适应的实现定义方式进行对齐。
Para 17: 在结构体对象中,非位域成员和位域所在的单元的地址按照声明顺序递增。结构体对象内部可能存在未命名的填充,但其开头不能有。
Para 19: 结构体或联合体的末端可能存在未命名的填充。
Para 20: 特殊情况下,具有多个命名成员的结构体的最后一个成员可能具有不完整的数组类型;这被称为灵活数组成员。
在大多数情况下,灵活数组成员会被忽略。具体来说,结构体的大小与省略灵活数组成员时的大小相同,只是它可能比省略灵活数组成员后多一些尾随填充。
说的非常繁杂(反正我是看了很久),总结起来如下:
这样在C语言标准中是没有规定内存是如何对齐的!任何脱离了具体ABI实现的讨论都是片面的。
在 system-V(也就是常见的 AMD64,也就是 x86-64)架构下,我们具有如下的定义:
struct和union假定与其最严格对齐的组成部分保持一致。每个成员都被分配到具有相应对齐方式的最小可用偏移量。任何对象的大小始终是其对齐方式的倍数。
结构体和联合体对象可能需要填充以满足大小和对齐约束。填充的内容未定义。
位域既非有符号也非无符号的位域总是具有非负值。(真的神奇)
尽管这些位域的类型可能是 char、short、int 或 long(其中 long 可以包含负值),但它们与相同大小的对应无符号类型的位域具有相同的范围。
位域遵循与其他结构体和联合体成员相同的大小和对齐规则。
位域必须包含在与其声明类型相符的存储单元中。
位域可以与其他结构体/联合体成员共享存储单元。
未命名的位域的类型不影响结构体或联合体的对齐方式。
这样我们就可以来分析
struct A {
char a;
int b : 5, c : 11, : 0, d : 8;
struct {
int ee : 8;
} e;
}
结构体了。
sizeof(struct A) 在 x86/64 上的执行结果是 12,不是20,也不是16。

中间的例子很简单:C标准规定了结构体的大小一般会忽略变长数组成员。因此为 sizeof(int) * 4 = 16。
对于最后一个例子,我们知道:
因此大小为 sizeof(c) == 3。
但是但是!在MSVC的实现中,位域变得大不相同。sizeof(struct A) 真的变成了16!但是DeepSeek就是吃白饭的蓝色大肥鱼
我们需要看看MSVC中的ABI实现。
未命名的位域它可以用作“虚拟”字段,用于对齐。
宽度指定为 0 的未命名位域保证了结构体声明列表中其后成员的存储空间从 int 边界开始。
位域中的位数必须小于或等于底层类型的大小。
位域与整数类型具有相同的语义。
在表达式中使用位域的方式与使用相同基类型的变量完全相同。
位域的位数无关紧要。
定义为 int 类型的位域被视为有符号类型。
微软对 ANSI C 标准的扩展允许位域使用 char 和 long 类型(包括有符号和无符号类型)。
未命名的位域,如果其基本类型为 long、short 或 char(有符号或无符号),则会强制其对齐到与其基本类型相对应的边界。
位域从LSB填充到MSB。
对于结构体、联合体和数组,其对齐要求是其成员中最大的对齐要求。
如果整数类型大小相同,并且下一个位域适合放入当前分配单元而不跨越位域的共同对齐要求所施加的边界,则相邻的位域将被打包到同一个 1、2 或 4 字节的分配单元中。ref
因此我们发现:
这样我们分析我们的结构体:
这样我们的A的大小就应该是 sizeof(struct A) = 16!
而其他的是一样的。因为两者的规定相同;只有位域此处的定义是不同的。
MaskRayMe 的 blog 中有对位域的布局的更仔细的探究与深挖,甚至还讨论了编译器对于位域的访问。此外,位域的 存储单元(Storage Unit) 和 访问单元(Memory location) 是不一样的,这一点也需要注意。
𝒲𝑒 𝓌𝒽𝑜 𝒸𝓊𝓉 𝓂𝑒𝓇𝑒 𝓈𝓉𝑜𝓃𝑒𝓈 𝓂𝓊𝓈𝓉 𝒶𝓁𝓌𝒶𝓎𝓈 𝒷𝑒 𝑒𝓃𝓋𝒾𝓈𝒾𝑜𝓃𝒾𝓃𝑔 𝒸𝒶𝓉𝒽𝑒𝒹𝓇𝒶𝓁𝓈.
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。