4. 联合体(union)深度解析:联合体的定义、内存共享特性、大小端检测实战

联合体这个东西,刚学C语言的时候我其实挺困惑的。结构体好歹每个成员都有自己的窝,联合体倒好,所有成员挤在一个房间里。说白了,联合体就是一种所有成员共享同一块内存的数据类型。今天咱们就把它的底裤扒干净。

4.1 联合体的定义与基本语法

联合体的定义和结构体长得像亲兄弟,但内核完全不同。看个例子:

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

这里定义了一个叫 Data 的联合体类型。它有三个成员,但这三个成员共用同一段内存空间。你想想看,如果这是个结构体,那大小会是 sizeof(int) + sizeof(float) + sizeof(char[4]),差不多12个字节。但联合体呢?它只取最大成员的大小,也就是4个字节。

核心要点:联合体的大小等于其最大成员的大小。所有成员共享同一块内存的起始地址。

我在项目中遇到过有人把联合体和结构体搞混,结果数据解析全乱套了。嗯,这里要记住:联合体同一时刻只能保存一个成员的值。你给 i 赋值,再去读 f,拿到的就是一堆垃圾。

4.2 内存共享特性——联合体的灵魂

联合体最迷人的地方就是内存共享。咱们用代码说话:

#include <stdio.h>

union Test {
    int a;
    char b[4];
};

int main() {
    union Test t;
    t.a = 0x12345678;
    
    printf("a = 0x%x\n", t.a);
    printf("b[0] = 0x%x\n", (unsigned char)t.b[0]);
    printf("b[1] = 0x%x\n", (unsigned char)t.b[1]);
    printf("b[2] = 0x%x\n", (unsigned char)t.b[2]);
    printf("b[3] = 0x%x\n", (unsigned char)t.b[3]);
    
    return 0;
}

这段代码在小端机器上跑,输出会是:

a = 0x12345678
b[0] = 0x78
b[1] = 0x56
b[2] = 0x34
b[3] = 0x12

为什么会这样?因为 ab 共享同一块4字节内存。你写 a 的时候,实际上就是在写那4个字节。然后你通过 b 数组去读,读到的就是内存里的原始字节。

个人习惯:我经常用联合体来做数据类型的"强制转换",比指针强转安全得多。尤其是在处理通信协议的时候,一个联合体就能搞定数据包解析。

4.3 大小端检测实战

大小端问题,说白了就是数据在内存里的摆放顺序。大端模式是高位字节在低地址,小端模式是低位字节在低地址。咱们用联合体一行代码就能检测出来:

#include <stdio.h>

int check_endian() {
    union {
        int i;
        char c;
    } u = { .i = 1 };
    return u.c;  // 小端返回1,大端返回0
}

int main() {
    if (check_endian()) {
        printf("小端模式\n");
    } else {
        printf("大端模式\n");
    }
    return 0;
}

这段代码的原理很简单:int i = 1 在内存中,小端模式下低地址存的是 0x01,大端模式下低地址存的是 0x00。联合体让 char cint i 共享低地址字节,所以读 c 就能知道大小端。

我曾经踩过的坑:有一次做嵌入式网络协议栈,没考虑大小端问题,直接把收到的字节流当整数用。结果在x86开发板上跑得好好的,部署到ARM板子上全乱套了。后来用联合体写了个大小端转换函数,一劳永逸。

4.4 联合体的高级用法——协议解析

在实际项目中,联合体最常见的用途就是解析通信协议。比如一个简单的数据包:

#include <stdio.h>
#include <stdint.h>

// 数据包结构
typedef struct {
    uint8_t header;   // 帧头
    uint8_t len;      // 数据长度
    uint16_t crc;     // 校验
    uint8_t data[8];  // 数据
} Packet;

// 联合体:方便按字节访问
union PacketView {
    Packet pkt;
    uint8_t raw[sizeof(Packet)];
};

int main() {
    union PacketView pv;
    
    // 模拟接收到的原始数据
    pv.raw[0] = 0xAA;  // 帧头
    pv.raw[1] = 0x04;  // 长度
    pv.raw[2] = 0x12;  // CRC低字节
    pv.raw[3] = 0x34;  // CRC高字节
    pv.raw[4] = 0x01;  // 数据
    pv.raw[5] = 0x02;
    pv.raw[6] = 0x03;
    pv.raw[7] = 0x04;
    
    printf("帧头: 0x%02X\n", pv.pkt.header);
    printf("长度: %d\n", pv.pkt.len);
    printf("CRC: 0x%04X\n", pv.pkt.crc);
    
    return 0;
}

你看,通过联合体,我们可以把原始字节流和结构体无缝对接。这在嵌入式开发里太常见了——串口收到一堆字节,用联合体一映射,直接按字段访问,省去了手动解析的麻烦。

4.5 联合体与结构体的嵌套

联合体和结构体可以嵌套使用,这在处理复杂协议时特别有用。比如一个数据包可能有多种类型:

#include <stdio.h>

// 不同类型的消息体
struct MsgA {
    int x;
    int y;
};

struct MsgB {
    float value;
    char name[8];
};

// 消息联合体
union MessageBody {
    struct MsgA a;
    struct MsgB b;
};

// 完整消息结构
struct Message {
    int type;               // 消息类型
    union MessageBody body; // 消息体
};

int main() {
    struct Message msg;
    msg.type = 1;  // 类型A
    
    // 访问类型A的数据
    msg.body.a.x = 100;
    msg.body.a.y = 200;
    
    printf("Type A: x=%d, y=%d\n", msg.body.a.x, msg.body.a.y);
    
    // 切换类型
    msg.type = 2;
    msg.body.b.value = 3.14f;
    
    printf("Type B: value=%.2f\n", msg.body.b.value);
    
    return 0;
}

我建议:用联合体做协议解析时,一定要加一个类型字段来标记当前联合体里存的是什么。不然你读出来的数据可能完全是另一回事。这就像你打开一个箱子,得先知道里面装的是衣服还是工具,不然瞎摸会出问题。

4.6 联合体的注意事项

注意事项 说明
初始化 C99支持指定初始化器,如 union U u = { .member = value };
对齐问题 联合体的对齐方式由其最严格对齐的成员决定
匿名联合体 C11支持匿名联合体,成员可以直接访问
位域联合体 联合体成员可以是位域,用于精确控制比特位

警告:不要用联合体去访问同一个成员的不同类型,除非你清楚知道自己在做什么。比如你写了一个 float,然后去读 int,这种行为是未定义的。联合体只保证存储共享,不保证解释正确。

4.7 联合体知识体系总览

下面这张图把联合体的核心知识点串起来了,你可以对照着复习:

联合体(union) 定义与基本语法 内存共享特性 大小端检测实战 高级用法:协议解析 联合体与结构体嵌套 所有成员共享内存 大小 = 最大成员 一行代码检测 字节流 ↔ 结构体 复杂协议解析 节省内存 跨平台必备

联合体这东西,说白了就是C语言给开发者的一把瑞士军刀。用好了,代码简洁高效;用不好,bug藏得你找不到。我个人建议你在做底层开发、协议解析、数据转换的时候多想想联合体,它往往能给你一个优雅的解决方案。


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