无用的知识之:C语言中的 `struct` 的内存大小讨论

无用的知识之:C语言中的 struct 的内存大小讨论

和DeepSeek网页端对话:对话链接,发现她错的很厉害。为了不让自己出错,我们也需要来梳理一下:在 C 中的 struct,内存对齐,标准是怎样的。

结论:struct 的大小,对齐方式取决于语言标准 -> ABI

先说结论(感觉好像AI文 QAQ):C 语言中,struct 的最终大小,对齐规则取决于两个要求的交集:(1) C 语言标准;(2) 平台 ABI 定义。这里为了讨论方便,我们只举例常用的编译器/ABI定义。同时不考虑人为对齐方式,仅考虑在没有 align 标识符下的默认对齐场景。

  • 编译器:GCC/Clang/MSVC
  • ABI平台:x86-64的System-V/MSVC-abi

引入

我们首先看以下的例子:

#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来查看。

C23 标准中关于 memory alignment 与 bit-field 的相关表述

在 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: 特殊情况下,具有多个命名成员的结构体的最后一个成员可能具有不完整的数组类型;这被称为灵活数组成员。
在大多数情况下,灵活数组成员会被忽略。具体来说,结构体的大小与省略灵活数组成员时的大小相同,只是它可能比省略灵活数组成员后多一些尾随填充。

说的非常繁杂(反正我是看了很久),总结起来如下:

  • 按照声明顺序来为结构体成员分配地址。
  • 结构体的中间和结尾可以有填充,开头不可以有填充
  • 结构体的大小会忽略变长数组成员
  • 结构体中的内存对齐,应该遵循其成员中最大的类型进行对齐。对齐方式由实现决定
  • 位域的大小取决于存储单元(由实现决定),相邻的位域在大小足够时可以放置在同一单元中,大小不够时处理方式由实现决定
  • 存储单元的对齐在标准中不做定义。
  • 宽度为 0 的位域作为分隔,后面的位域不再放置在和前面位域一样的存储单元中

这样在C语言标准中是没有规定内存是如何对齐的!任何脱离了具体ABI实现的讨论都是片面的。

ABI 的实现

System-V 的相关实现

在 system-V(也就是常见的 AMD64,也就是 x86-64)架构下,我们具有如下的定义:

structunion假定与其最严格对齐的组成部分保持一致。每个成员都被分配到具有相应对齐方式的最小可用偏移量。任何对象的大小始终是其对齐方式的倍数。
结构体和联合体对象可能需要填充以满足大小和对齐约束。填充的内容未定义。
位域既非有符号也非无符号的位域总是具有非负值。(真的神奇)
尽管这些位域的类型可能是 char、short、int 或 long(其中 long 可以包含负值),但它们与相同大小的对应无符号类型的位域具有相同的范围。
位域遵循与其他结构体和联合体成员相同的大小和对齐规则。
位域必须包含在与其声明类型相符的存储单元中。
位域可以与其他结构体/联合体成员共享存储单元。
未命名的位域的类型不影响结构体或联合体的对齐方式。

这样我们就可以来分析

struct A {
    char a;
    int b : 5, c : 11, : 0, d : 8;
    struct {
        int ee : 8;
    } e;
}

结构体了。

  • 首先分析ABI要求的最严格对齐要求:char 要求 1 byte;int 要求 4 bytes;ABI要求位域遵循与其他结构体和联合体成员相同的大小和对齐规则。
    • 因此对齐 4 bytes。一个存储单元为 4 bytes 大小。
  • 对于 a,b,c: ABI要求位域可以与其他结构体/联合体成员共享存储单元。因此 a (8 bits) + b (5 bits) + c (11 bits) = 24 bits < 32 bits(4 bytes)。位于一个存储空间中。所以 a: 0~7 bits, b: 8~12 bits, c: 13~23 bits。
  • 对于 0 位宽:分隔。因此 d 需要放在新的存储空间中。因此 24~31 bits 随机填充。
  • 对于 e:e 是一个结构体,其中要求的对齐是 int (4 byte)。ABI 要求成员被分配到具有相应对齐方式的最小可用偏移量。因此 e 需要从 64 bit 开始。所以 d 将占用 32 ~ 39 bit, 40 ~ 63 bit 将被随机填充。随后 e 将占用 64 ~ 95 bits.
  • 这样,我们的 sizeof(struct A) 在 x86/64 上的执行结果是 12,不是20,也不是16

img

img

中间的例子很简单:C标准规定了结构体的大小一般会忽略变长数组成员。因此为 sizeof(int) * 4 = 16

对于最后一个例子,我们知道:

  • 最大对齐要求是4 byte。
  • 对于 a: 需要填充 3 bytes。
  • 对于 b: 占领整个 4 bytes 单元。
  • 对于 c 和 d: 每个成员都被分配到具有相应对齐方式的最小可用偏移量。这里一个单元 4 bytes 内,前 2 bytes 放置 c, 然后 1 个 byte 放置 d。最后再填充一个 byte。

因此大小为 sizeof(c) == 3

MSVC-ABI 的相关实现

但是但是!在MSVC的实现中,位域变得大不相同。sizeof(struct A) 真的变成了16!但是DeepSeek就是吃白饭的蓝色大肥鱼

我们需要看看MSVC中的ABI实现

未命名的位域它可以用作“虚拟”字段,用于对齐。
宽度指定为 0 的未命名位域保证了结构体声明列表中其后成员的存储空间从 int 边界开始。
位域中的位数必须小于或等于底层类型的大小。
位域与整数类型具有相同的语义。
在表达式中使用位域的方式与使用相同基类型的变量完全相同。
位域的位数无关紧要。
定义为 int 类型的位域被视为有符号类型。
微软对 ANSI C 标准的扩展允许位域使用 char 和 long 类型(包括有符号和无符号类型)。
未命名的位域,如果其基本类型为 long、short 或 char(有符号或无符号),则会强制其对齐到与其基本类型相对应的边界。
位域从LSB填充到MSB。
对于结构体、联合体和数组,其对齐要求是其成员中最大的对齐要求。
如果整数类型大小相同,并且下一个位域适合放入当前分配单元而不跨越位域的共同对齐要求所施加的边界,则相邻的位域将被打包到同一个 1、2 或 4 字节的分配单元中。ref

因此我们发现:

  • 在MSVC中,位域相当于一个整数类型
  • 位域会被强制对齐到基本类型的边界。

这样我们分析我们的结构体:

  • 首先分析ABI要求的最严格对齐要求:char 要求 1 byte;int 要求 4 bytes;ABI要求位域遵循与其他结构体和联合体成员相同的大小和对齐规则。
    • 因此对齐 4 bytes。一个存储单元为 4 bytes 大小。
  • 对于 a:需要填充 0~7 bits。
  • 对于 b, c: 需要强制对齐到最大要求的边界,视为一个整数类型。因此 8~31 bits 将被填充。相邻的位域可以放在一个单元内。因此 32~47bits 放置 b, c。
  • 0 位域截断当前单元。48~64 bit 用于填充。
  • 对于 e:e 是一个结构体,其中要求的对齐是 int (4 byte)。成员被对齐到与其基本类型相对应的边界。因此 e 需要从 64 bit 开始。所以 d 将占用 64 ~ 71 bit, 72 ~ 95 bit 将被随机填充。随后 e 将占用 96 ~ 128 bits.

这样我们的A的大小就应该是 sizeof(struct A) = 16!

而其他的是一样的。因为两者的规定相同;只有位域此处的定义是不同的。

引申阅读

MaskRayMe 的 blog 中有对位域的布局的更仔细的探究与深挖,甚至还讨论了编译器对于位域的访问。此外,位域的 存储单元(Storage Unit)访问单元(Memory location) 是不一样的,这一点也需要注意。

posted @ 2026-09-16 19:13  木木ちゃん  阅读(5)  评论(0)    收藏  举报