11. 联合体入门:联合体的定义、特点与结构体对比

好,咱们今天聊聊联合体。说实话,联合体在C语言里是个挺有意思的存在。很多人学完结构体就觉得万事大吉了,结果一碰到联合体就懵了——这玩意儿跟结构体长得差不多,怎么用起来完全不是一回事?

我当年刚入行时也踩过这个坑。有一次调试一个通信协议栈,数据包解析死活不对,查了两天才发现——我把联合体当结构体用了。嗯,从那以后,我对联合体就格外上心。

11.1 联合体的定义

联合体,关键字是 union。定义方式跟结构体几乎一模一样:

union Data {
    int i;
    float f;
    char str[4];
};

你看,语法上跟结构体唯一的区别就是 struct 换成了 union。但别被这表象骗了——它们的内存布局完全是两码事。

声明变量也一样:

union Data data;          // 普通变量
union Data *pData;        // 指针
union Data arr[10];       // 数组

我个人习惯在定义联合体时加上 typedef,用起来更顺手:

typedef union {
    int i;
    float f;
    char str[4];
} Data;

11.2 联合体的核心特点:所有成员共享内存

这才是联合体的灵魂所在。说白了,联合体里所有成员都从同一个内存地址开始存放。你想想看,这意味着什么?

整个联合体的大小,等于它最大成员的大小。

来,看个例子:

#include <stdio.h>

union Test {
    char c;       // 1字节
    int i;        // 4字节
    double d;     // 8字节
};

int main() {
    union Test t;
    printf("union size = %zu\n", sizeof(t));  // 输出 8
    
    t.d = 3.14;
    printf("d = %f\n", t.d);   // 3.140000
    
    t.i = 100;
    printf("i = %d\n", t.i);   // 100
    printf("d = %f\n", t.d);   // 乱码!因为内存被 i 覆盖了
    
    return 0;
}

为什么会这样?因为当你给 t.i 赋值时,它就把 t.d 占用的那8个字节的前4个给覆盖了。double 的剩余4个字节还是原来的值,但整个数据已经面目全非了。

⚠️ 重要提醒: 联合体同一时刻只能保存一个成员的值。你给一个成员赋值,其他成员的值就失效了。这不是bug,是设计如此。

11.3 联合体 vs 结构体:一张图看懂

咱们用SVG画个对比图,一目了然:

联合体 vs 结构体 内存布局对比 结构体 struct char c int i double d 每个成员有独立的内存空间,总大小 = 各成员之和(考虑对齐) 联合体 union 所有成员共享同一块内存 大小 = 最大成员的大小(double → 8字节) 对比 关键区别 ✅ 结构体:成员独立,可同时使用 ⚠️ 联合体:成员互斥,同一时刻只能用1个 ✅ 结构体:大小 ≥ 各成员大小之和 ⚠️ 联合体:大小 = 最大成员大小 ✅ 结构体:适合描述"同时具有"的关系 ⚠️ 联合体:适合描述"互斥选择"的关系 内存布局示意图(非实际比例,仅示意逻辑)

11.4 联合体的典型应用场景

你可能会问:这玩意儿到底有什么用?我项目中遇到过几个典型场景:

场景一:数据解析(协议栈)

比如你从串口收到一个数据包,前1个字节是类型,后面4个字节可能是整数也可能是浮点数。用联合体就特别方便:

typedef union {
    uint32_t u32;
    int32_t  s32;
    float    f32;
    uint8_t  bytes[4];
} PacketData;

typedef struct {
    uint8_t     type;
    PacketData  data;
} Packet;

收到数据后,根据 type 决定用哪个成员。内存不用重新分配,直接解析就行。

场景二:寄存器位操作

嵌入式里经常要操作硬件寄存器。用联合体+位域,读写都方便:

typedef union {
    uint32_t value;           // 整体读写
    struct {
        uint32_t enable : 1;  // bit0
        uint32_t mode   : 2;  // bit1-2
        uint32_t rate   : 3;  // bit3-5
        uint32_t        : 26; // 保留位
    } bits;
} ControlRegister;

想整体赋值就写 reg.value = 0x12345678,想单独改某个位就写 reg.bits.enable = 1。我在做STM32驱动时经常这么用。

场景三:节省内存

有些场景下,多个变量不会同时用到。比如一个学生管理系统,学生要么是在校生(需要年级信息),要么是毕业生(需要毕业年份)。用联合体就能省内存:

typedef struct {
    char name[32];
    int  id;
    union {
        int  grade;        // 在校生:年级
        int  gradYear;     // 毕业生:毕业年份
    } status;
} Student;
💡 我的建议: 联合体最适合用在「互斥数据」的场景。如果你不确定成员之间是否互斥,那就用结构体,别硬上联合体。省那点内存,不值得你熬夜debug。

11.5 避坑指南

我曾经在项目里犯过一个低级错误,差点导致产品延期。分享出来,大家引以为戒:

🚨 坑1:从联合体读未初始化的成员
联合体不会自动初始化所有成员。你给 t.i = 10,然后读 t.f,得到的是垃圾值。这不是随机数,是未定义行为!
🚨 坑2:联合体中的指针成员
如果联合体里有指针,赋值时要格外小心。指针指向的内存不会因为联合体切换成员而自动释放。我曾经因此造成内存泄漏,查了两天才发现。
🚨 坑3:联合体与结构体嵌套时的对齐
联合体嵌套在结构体里时,对齐规则可能会让你意外。比如结构体里有个联合体,联合体最大成员是double,那整个结构体的对齐可能就被拉到了8字节。我建议你用 #pragma pack__attribute__((packed)) 显式控制。

11.6 联合体 vs 结构体:对比总结

对比维度 结构体 struct 联合体 union
内存布局 成员顺序排列,各自独立 所有成员从同一地址开始
大小计算 各成员大小之和(考虑对齐) 最大成员的大小
成员使用 可同时使用所有成员 同一时刻只能使用一个成员
初始化 可初始化所有成员 只能初始化第一个成员
典型用途 描述"同时具有"的复合数据 描述"互斥选择"的变体数据
内存效率 较低(每个成员都占空间) 较高(共享空间)
使用风险 较低(成员独立,不易出错) 较高(误读未赋值成员会出问题)

嗯,说到这,联合体的核心内容基本就这些了。记住一句话:结构体是「和」的关系,联合体是「或」的关系。 搞清楚了这一点,你就掌握了联合体的精髓。

下次写代码时,遇到那种「要么是A,要么是B,不会同时出现」的数据,不妨试试联合体。用对了地方,它能让你的代码既省内存又清晰。

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