14. 联合体与结构体的混合使用:结构体中包含联合体、联合体中包含结构体

各位同学,咱们今天聊一个在实际工程中非常常见、但也容易让人绕晕的话题——联合体和结构体怎么混着用。

说白了,就是两种组合方式:结构体里套联合体,以及联合体里套结构体。这两种模式在嵌入式开发、协议解析、内存复用这些场景下,几乎是天天见。我个人习惯把这种用法叫做“数据结构的套娃”,但别怕,套娃也有套娃的规矩。

14.1 为什么需要混合使用?

先问一个问题:你写一个数据包,里面有一个字段是“类型”,根据类型不同,后面的数据格式完全不一样。你会怎么设计?

用纯结构体?那你会定义出好几个不同的结构体,每次都要判断类型再强转,代码又臭又长。用纯联合体?联合体本身没有“类型标识”,你没法知道当前存的是哪种数据。

嗯,这里就是混合使用的用武之地了。用结构体来承载“公共信息”(比如类型标识),用联合体来承载“可变数据”。这样既保留了类型信息,又做到了内存复用。

核心思想:结构体负责“组织”,联合体负责“复用”。两者结合,就能用最小的内存表达最灵活的数据结构。

14.2 结构体中包含联合体

这是最常见的用法。我在项目中遇到过好几次,比如解析不同版本的传感器数据帧,帧头一样,但数据体长度和含义不同。用结构体套联合体,一行代码都不用改,就能兼容多个版本。

// 结构体中包含联合体:根据类型选择不同的数据解释方式
#include <stdio.h>
#include <string.h>

// 定义几种不同的数据格式
typedef struct {
    int x;
    int y;
} Point2D;

typedef struct {
    int x;
    int y;
    int z;
} Point3D;

typedef struct {
    float latitude;
    float longitude;
} GPSData;

// 主结构体:公共头部 + 联合体数据区
typedef struct {
    int dataType;   // 0: 2D点, 1: 3D点, 2: GPS
    union {
        Point2D p2d;
        Point3D p3d;
        GPSData gps;
    } data;         // 联合体成员,根据dataType选择解释方式
} DataPacket;

int main() {
    DataPacket packet;

    // 存储一个2D点
    packet.dataType = 0;
    packet.data.p2d.x = 10;
    packet.data.p2d.y = 20;

    printf("2D Point: (%d, %d)\n", packet.data.p2d.x, packet.data.p2d.y);

    // 存储一个GPS数据(会覆盖之前的2D点数据)
    packet.dataType = 2;
    packet.data.gps.latitude = 39.9042f;
    packet.data.gps.longitude = 116.4074f;

    printf("GPS: (%.4f, %.4f)\n", packet.data.gps.latitude, packet.data.gps.longitude);

    return 0;
}

你看,DataPacket 这个结构体里,dataType 是公共的,data 是联合体。不管存哪种数据,整个结构体的大小只取决于最大的那个联合体成员。我算过,这个结构体的大小是 sizeof(int) + max(sizeof(Point2D), sizeof(Point3D), sizeof(GPSData)),也就是 4 + 12 = 16 字节(假设 int 是 4 字节)。

我的习惯:在结构体里放联合体时,我会把联合体成员放在结构体的最后,这样前面的公共字段可以快速访问,不会因为联合体的大小变化而偏移。

14.3 联合体中包含结构体

这种用法稍微少见一点,但一旦用上,往往是为了解决“同一块内存,多种解读方式”的问题。说白了,就是你想用不同的结构体去解析同一段内存。

举个例子,你从串口收到一个 4 字节的数据,它可能是一个 int32,也可能是四个 uint8 组成的字节数组,还可能是一个 RGB 颜色值(三个字节加一个保留字节)。用联合体套结构体,就能轻松切换视角。

// 联合体中包含结构体:同一块内存,多种解读方式
#include <stdio.h>
#include <stdint.h>

// 定义一个联合体,里面包含多个结构体
typedef union {
    uint32_t rawValue;          // 原始32位整数值

    // 结构体1:拆分成4个字节
    struct {
        uint8_t byte0;
        uint8_t byte1;
        uint8_t byte2;
        uint8_t byte3;
    } bytes;

    // 结构体2:拆分成两个16位值
    struct {
        uint16_t lowWord;
        uint16_t highWord;
    } words;

    // 结构体3:RGB颜色 + 保留字节
    struct {
        uint8_t red;
        uint8_t green;
        uint8_t blue;
        uint8_t alpha;  // 保留或透明度
    } color;
} MultiViewData;

int main() {
    MultiViewData data;

    // 写入一个32位值
    data.rawValue = 0x12345678;

    printf("原始值: 0x%08X\n", data.rawValue);
    printf("字节视角: 0x%02X 0x%02X 0x%02X 0x%02X\n",
           data.bytes.byte0, data.bytes.byte1,
           data.bytes.byte2, data.bytes.byte3);
    printf("16位视角: 低16位=0x%04X, 高16位=0x%04X\n",
           data.words.lowWord, data.words.highWord);
    printf("颜色视角: R=0x%02X, G=0x%02X, B=0x%02X, A=0x%02X\n",
           data.color.red, data.color.green,
           data.color.blue, data.color.alpha);

    return 0;
}

这里要注意一个细节:字节序问题。在小端模式下(x86、ARM 默认),byte0 对应的是最低地址的字节,也就是 0x78。如果你在通信协议里用这种方式,一定要确认双方字节序一致。我曾经因为没注意这个,调试了整整一个下午,最后发现是大小端搞反了。

避坑指南:联合体中的结构体成员,其内存布局是实现定义的。不同编译器、不同优化选项,可能会对结构体进行对齐填充。如果你要跨平台使用,建议用 #pragma pack(1)__attribute__((packed)) 强制紧凑对齐。

14.4 更复杂的嵌套:结构体套联合体再套结构体

有时候,一层嵌套不够用。比如你要解析一个复杂的协议帧,帧头里有多个可变字段,每个字段又可能有多种子格式。这时候就需要多层嵌套。

我画了一张图,帮你理清这种嵌套关系:

外层结构体:ProtocolFrame frameType (int) length (int) 联合体:payload 成员1:struct ControlData command (int) targetId (int) param (float) 成员2:struct SensorData sensorId (int) value (float) timestamp (long) 根据 frameType 的值,选择联合体中的不同结构体来解析 payload

你看,外层是一个结构体,里面有一个联合体,联合体里又包含了两个不同的结构体。这种设计在工业协议、网络协议里非常常见。比如 Modbus 协议、CAN 总线协议,底层都是这种思路。

14.5 实际工程中的注意事项

说了这么多,最后总结几条我踩过的坑,你写代码时多留个心眼:

  • 内存对齐问题:结构体套联合体时,整个结构体的对齐方式由最严格的那个成员决定。如果你不确定,用 sizeof() 打印一下,别靠猜。
  • 初始化要小心:如果你用 = {0} 初始化一个包含联合体的结构体,它只会把联合体的第一个成员清零。如果你第一个成员不是最大的,其他成员可能残留垃圾数据。
  • 不要直接 memcpy 整个结构体:尤其是跨平台传输时,结构体内部的填充字节可能不同。我建议你序列化时,只拷贝有效字段,或者用 #pragma pack(1) 强制紧凑。
  • 联合体里的结构体成员顺序:C 标准保证结构体成员按声明顺序排列,但联合体里的多个结构体共享同一块内存,它们的起始地址相同。这意味着,如果你在联合体里放了两个结构体,它们的第一个成员会重叠。

我的建议:在嵌入式开发中,如果你要定义“结构体套联合体”的数据结构,最好在注释里写明每个字段的字节偏移和大小。这样半年后你回头看代码,不用重新数偏移量。

好了,关于联合体和结构体的混合使用,就聊到这里。这两种组合方式,说白了就是“用结构体搭骨架,用联合体填血肉”。你只要记住:结构体负责组织逻辑,联合体负责复用内存,两者结合就能写出既灵活又高效的数据结构。

下次写协议解析或者数据包处理时,不妨试试这种设计。你会发现,代码量少了一半,可读性却翻了一倍。


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