12. 联合体的大小:联合体大小计算规则、成员对齐对联合体大小的影响

联合体(union)在C语言里是个很有意思的存在。它和结构体长得像,但内存布局完全是两码事。结构体是“大家都有房间住”,联合体是“大家挤一个房间”。

那问题来了——这个“房间”到底该有多大?这就是我们今天要聊的核心:联合体的大小计算规则,以及成员对齐对它造成的影响。

12.1 联合体大小的基本规则

联合体的所有成员共享同一块内存。所以它的大小,至少得能装下最大的那个成员。这是最朴素的直觉,对吧?

但事情没这么简单。我当年刚学C语言时,写过一个联合体:

union Data {
    char c;
    int i;
    double d;
};

我当时想:char占1字节,int占4字节,double占8字节。那这个联合体应该是8字节吧?

嗯,结果是对的。但为什么?因为double是8字节,它最大。联合体的大小就是最大成员的大小——这是第一条规则。

核心规则1:联合体的大小至少等于其最大成员的大小。

但等等,这只是“至少”。实际大小往往比最大成员还要大一点。为什么?因为对齐。

12.2 对齐对联合体大小的影响

联合体也有对齐要求。它的对齐方式,是所有成员中对齐要求最严格的那个。说白了,联合体的对齐值等于所有成员对齐值的最大值。

我举个例子你就明白了:

union Example {
    char arr[5];   // 对齐值1,大小5
    int i;         // 对齐值4,大小4
};

最大成员是arr,5字节。但联合体的对齐要求是4(因为int的对齐值是4)。所以联合体的大小必须是4的倍数。5不是4的倍数,向上取整到8。

所以这个联合体的大小是8字节,而不是5字节。

个人经验:我在做嵌入式通信协议解析时,经常用联合体来解析数据包。有一次我定义了一个联合体来解析一个5字节的报文,结果sizeof返回8,导致我缓冲区分配多了3字节。虽然没出bug,但浪费了宝贵的RAM。从那以后,我每次定义联合体都会手动算一下对齐后的实际大小。

12.3 联合体大小计算三步法

我总结了一个三步法,你照着做就不会错:

  1. 找最大成员:找出所有成员中占用字节数最大的那个。
  2. 找对齐值:找出所有成员中对齐要求最严格的那个值。
  3. 向上取整:将最大成员的大小向上取整到对齐值的整数倍。

咱们用代码验证一下:

#include <stdio.h>
#include <stddef.h>

union U1 {
    char a[5];
    int b;
};

union U2 {
    short c[3];
    double d;
};

union U3 {
    char e;
    int f[2];
    long long g;
};

int main() {
    printf("U1: %zu (预期8)\n", sizeof(union U1));
    printf("U2: %zu (预期16)\n", sizeof(union U2));
    printf("U3: %zu (预期8)\n", sizeof(union U3));
    return 0;
}

运行结果:

U1: 8 (预期8)
U2: 16 (预期16)
U3: 8 (预期8)

为什么会这样?

  • U1:最大成员是char[5](5字节),对齐值是int的4。5向上取整到4的倍数 → 8。
  • U2:最大成员是short[3](6字节),对齐值是double的8。6向上取整到8的倍数 → 16。
  • U3:最大成员是int[2](8字节)和long long(8字节),都是8。对齐值是long long的8。8已经是8的倍数 → 8。

注意:不同编译器、不同平台的对齐规则可能略有差异。比如有些嵌入式编译器默认使用1字节对齐(packed),那联合体的大小就是最大成员的大小,没有填充字节。我在做ARM Cortex-M项目时就遇到过这种情况,当时排查了好久才发现是编译器对齐设置的问题。

12.4 联合体中的结构体成员

联合体里可以嵌套结构体。这时候计算大小就要把结构体当成一个整体来看。

struct Packed {
    char a;
    int b;
    short c;
};

union Container {
    struct Packed p;
    double d;
};

先算struct Packed的大小。假设在默认对齐下:

  • char a 在偏移0
  • int b 对齐值4,从偏移4开始,占4字节
  • short c 对齐值2,从偏移8开始,占2字节
  • 结构体总大小10字节,对齐值4,向上取整到12

所以struct Packed的大小是12字节。联合体Container的最大成员是struct Packed(12字节)和double(8字节),取12。对齐值是double的8和struct Packed的4中的最大值,也就是8。12向上取整到8的倍数 → 16。

所以union Container的大小是16字节。

避坑指南:我曾经在定义网络协议报文时,把一个结构体和一个uint64_t放在联合体里。结构体里有个uint32_t字段,我以为联合体大小就是结构体的大小(假设12字节)。结果sizeof返回16,因为uint64_t的对齐要求是8。后来我加了个__attribute__((packed))才把大小压下来。嗯,嵌入式开发里这种细节特别容易踩坑。

12.5 联合体大小与位域

联合体里也可以有位域成员。位域的大小计算稍微特殊一点。

union BitFieldUnion {
    struct {
        unsigned int a : 3;
        unsigned int b : 5;
        unsigned int c : 8;
    } bits;
    unsigned int value;
};

这个联合体的大小是多少?

位域结构体bits的总位数是3+5+8=16位,也就是2字节。但unsigned int的对齐值是4,所以位域结构体的大小是4字节(向上取整到4的倍数)。联合体的最大成员是bits(4字节)和value(4字节),都是4。对齐值也是4。所以联合体大小是4字节。

你看,位域虽然只用了2字节,但联合体还是占了4字节。这就是对齐在作怪。

12.6 知识体系总结

我把联合体大小的核心逻辑画了张图,方便你理解:

联合体大小计算逻辑 联合体定义 步骤1:找出最大成员的大小 步骤2:找出最严格的对齐值 步骤3:向上取整到对齐值的倍数 联合体最终大小

这张图把计算流程串起来了。你写代码的时候,脑子里过一遍这三步,基本不会出错。

12.7 实用建议

最后给你几个我在项目中积累的经验:

  • 明确对齐要求:跨平台代码里,用sizeof而不是硬编码大小。不同平台的对齐规则可能不同。
  • 考虑使用packed:在嵌入式或网络协议场景下,如果内存是稀缺资源,可以考虑用__attribute__((packed))#pragma pack(1)来取消对齐填充。
  • 注意位域的对齐:位域成员的对齐行为在不同编译器下可能有差异,写可移植代码时要小心。
  • 用sizeof验证:我习惯在代码里加个static_assert来验证联合体的大小是否符合预期。比如:static_assert(sizeof(union MyUnion) == 8, "Unexpected union size");

一句话总结:联合体的大小 = 向上取整(最大成员大小,最严格对齐值)。记住这个公式,联合体大小计算就不再是难题。

公众号:蓝海资料掘金营,微信 deep3321