联合体 (union)

← C语言知识地图


索引

  1. 什么是联合体
  2. 内存示意图
  3. 基本用法
  4. 实际用途1:协议解析
  5. 实际用途2:大小端 / 字节拆分
  6. 实际用途3:节省内存
  7. 常见误区
  8. 总结对比

1. 什么是联合体

联合体和结构体很像,但有一个关键区别:

// 结构体 - 每个成员都有自己的内存
struct MyStruct {
    int   a;    // 4字节
    float b;    // 4字节
    char  c;    // 1字节
};              // 总共 9字节(加对齐可能更多)
 
// 联合体 - 所有成员共享同一块内存
union MyUnion {
    int   a;    // 4字节
    float b;    // 4字节
    char  c;    // 1字节
};              // 总共只有 4字节(最大成员的大小)

核心特点:所有成员从同一个地址开始,同时只有一个成员的值是”有效”的。


2. 内存示意图

结构体(各成员独立占用空间):

┌────┬────┬────┬────┬────┬────┬────┬────┬────┐
│    a (int)    │    b (float)  │  c │...│...│
└────┴────┴────┴────┴────┴────┴────┴────┴────┘

联合体(所有成员共享同一块内存):

┌────┬────┬────┬────┐
│    a (int)    │
│    b (float)  │   ← 同一块内存!
│  c │           │
└────┴────┴────┴────┘

3. 基本用法

#include <stdio.h>
 
union Data {
    int   i;
    float f;
    char  str[4];
};
 
int main(void) {
    union Data d;
 
    d.i = 0x41424344;
    printf("int:   %d\n",   d.i);      // 1094861636
    printf("float: %f\n",   d.f);      // 12.141...(同一块内存解读为float)
    printf("str:   %c%c%c%c\n", d.str[0], d.str[1], d.str[2], d.str[3]);
    //                          // DCBA(小端序)
 
    // 写float会覆盖之前的int!
    d.f = 3.14f;
    printf("int now: %d\n", d.i);      // 值变了!因为内存被覆盖
 
    return 0;
}

4. 实际用途1:协议解析(最常用!)

这正是嵌入式项目中最常见的场景——把字节数组和结构体绑在一起,解析超方便:

// 协议格式:[0x5A][0xA5][Length][SenderID 4-bytes][Command]...
// 用联合体把字节数组和结构体绑在一起
 
typedef union {
    // 原始字节视图
    unsigned char raw[10];
 
    // 结构化视图
    struct {
        unsigned char  head1;       // 0x5A
        unsigned char  head2;       // 0xA5
        unsigned char  length;
        unsigned long  sender_id;   // 4字节ID
        unsigned char  command;
        unsigned char  data;
    } fields;
} Protocol_t;
 
void parse_packet(unsigned char *buf) {
    Protocol_t pkt;
 
    // 直接把收到的字节塞进去
    for (int i = 0; i < 10; i++)
        pkt.raw[i] = buf[i];
 
    // 用结构体成员读取,清晰!
    if (pkt.fields.head1 == 0x5A && pkt.fields.head2 == 0xA5) {
        printf("命令: 0x%02X\n", pkt.fields.command);
        printf("发送方ID: 0x%08lX\n", pkt.fields.sender_id);
    }
}

5. 实际用途2:大小端 / 字节拆分

嵌入式里超常用——把一个 int 拆成4个字节分别发送:

union U32_Bytes {
    unsigned long  value;       // 32位整数
    unsigned char  byte[4];     // 4个独立字节
};
 
void send_id(unsigned long device_id) {
    union U32_Bytes u;
    u.value = device_id;        // 比如 0x12345678
 
    // 直接取每个字节发送,不用位运算!
    UART_Send(u.byte[0]);       // 0x78(小端)
    UART_Send(u.byte[1]);       // 0x56
    UART_Send(u.byte[2]);       // 0x34
    UART_Send(u.byte[3]);       // 0x12
}
 
// 对比传统写法(繁琐):
// UART_Send((id >> 0)  & 0xFF);
// UART_Send((id >> 8)  & 0xFF);
// UART_Send((id >> 16) & 0xFF);
// UART_Send((id >> 24) & 0xFF);

6. 实际用途3:节省内存(8位MCU必备)

RAM 紧张时,不同状态下用不同的数据,但不会同时用到:

union StateData {
    struct {                    // 配对状态时用
        unsigned char target_id[4];
        unsigned char retry_count;
    } pairing;
 
    struct {                    // 工作状态时用
        unsigned char last_cmd;
        unsigned char valve_status;
        unsigned int  timer_ms;
    } working;
};
// 只占 max(5, 4) = 5字节,而不是 5+4 = 9字节

7. 常见误区

union U {
    int   a;
    float b;
};
 
union U u;
u.a = 42;       // 写入 a
u.b = 3.14f;    // 写入 b,此时 a 的值已经无意义!
 
printf("%d\n", u.a);  // ❌ 未定义行为!b覆盖了a的内存
printf("%f\n", u.b);  // ✅ 最后写的是b,读b有效

记住:写哪个成员,就只能读哪个成员(除非故意用于类型转换)。


8. 总结对比

特性structunion
内存各成员独立共享最大成员的空间
同时有效所有成员只有最后写入的
典型用途组合数据协议解析、类型转换、省内存