结构体与联合体:C语言的数据组织艺术

各位同学,今天我们来聊聊C语言中一个非常实用的特性——结构体与联合体。说实话,我刚开始学C语言时,觉得结构体不就是把几个变量打包在一起吗?有什么了不起的。直到我在一个嵌入式项目中,需要管理几十个传感器的数据,才真正体会到结构体的威力。

结构体的定义与使用

结构体,说白了就是自定义的数据类型。你可以把不同类型的数据组合在一起,形成一个逻辑上的整体。比如你要描述一个学生,有姓名、年龄、成绩,用单独变量太零散,用结构体就清爽多了。

// 定义一个学生结构体
struct Student {
    char name[20];
    int age;
    float score;
};

// 使用结构体
struct Student stu1;
strcpy(stu1.name, "张三");
stu1.age = 18;
stu1.score = 92.5;

我个人习惯在定义结构体时,把相关的成员放在一起,这样阅读代码时一目了然。你想想看,如果成员顺序乱糟糟的,维护起来多痛苦。

小技巧:结构体成员可以包含其他结构体,形成嵌套。我在项目中经常用这种方式来组织复杂的数据,比如一个"设备"结构体里包含"位置"结构体。

结构体数组

一个学生不够?那就来一群。结构体数组就是用来处理批量数据的。我在做智能家居项目时,就用结构体数组管理了32个传感器的状态,每个传感器有ID、类型、数值、报警阈值等字段。

// 定义结构体数组
struct Student class[50];

// 初始化
for (int i = 0; i < 50; i++) {
    class[i].age = 18 + i % 5;
    class[i].score = 60 + rand() % 40;
}

嗯,这里要注意:结构体数组的初始化可以像普通数组一样用花括号,也可以逐个赋值。我建议在数据量不大时用花括号初始化,代码更紧凑。

结构体指针

结构体指针是C语言的精髓之一。为什么要用指针?因为传值拷贝太慢了。你想想看,一个结构体可能几十个字节,每次函数调用都复制一遍,性能损失不小。用指针就快多了,只传4个字节(32位系统)的地址。

struct Student *pStu;
pStu = &stu1;

// 两种访问方式
(*pStu).score = 95.0;  // 传统写法
pStu->score = 95.0;    // 箭头写法,更常用

我曾经在调试一个通信协议时,发现数据总是对不上。查了半天,原来是结构体指针用错了,->写成了.。这种错误编译器不会报错,但运行结果就是不对。所以,用指针访问成员时,一定要用->。

避坑指南:结构体指针必须指向有效的内存地址,否则就是野指针。我曾经在项目中因为忘记给指针分配内存,导致系统随机崩溃,排查了两天才找到原因。

typedef:给类型起个外号

每次写struct Student是不是很烦?用typedef给它起个简洁的名字吧。

typedef struct Student {
    char name[20];
    int age;
    float score;
} Student_t;

// 现在可以这样用
Student_t stu2;
Student_t *pStu2;

我个人习惯在类型名后面加_t后缀,一眼就能看出这是个类型。当然,也有人用_s表示结构体,_u表示联合体。这个没有标准,团队统一就好。

为什么需要typedef?说白了就是为了代码的可移植性和可读性。如果你要修改结构体的定义,只需要改一处,所有使用Student_t的地方自动生效。

联合体union:共享内存的艺术

联合体和结构体长得很像,但有一个本质区别:联合体的所有成员共享同一块内存。也就是说,联合体的大小等于最大成员的大小,而不是所有成员大小之和。

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

union Data data;
data.i = 10;
printf("%d\n", data.i);  // 输出10
data.f = 3.14;
printf("%f\n", data.f);  // 输出3.14
// 注意:此时data.i的值已经被覆盖了

联合体最常见的用途是数据解析。我在做串口通信时,经常收到一个数据包,里面可能是整数、浮点数或者字符串。用联合体就可以灵活地解析,而不需要关心具体是什么类型。

核心区别:
  • 结构体:每个成员有独立的内存空间,可以同时存储所有成员的值
  • 联合体:所有成员共享同一块内存,同一时刻只能存储一个成员的值

还有一个经典用法:用联合体实现字节序转换。比如你要把一个32位整数拆成4个字节发送,或者把4个字节拼成一个整数。用联合体就非常方便:

union {
    uint32_t word;
    uint8_t bytes[4];
} converter;

converter.word = 0x12345678;
// 现在converter.bytes[0]到converter.bytes[3]就是四个字节
// 注意大小端问题!

我曾经在移植一个网络协议栈时,就靠联合体解决了字节序的问题。不同平台的字节序不同,用联合体可以轻松地在大小端之间转换,代码还特别简洁。

知识体系总览

下面这张图总结了本章的核心内容,你可以把它当作一个快速参考:

结构体与联合体知识体系 结构体 struct 联合体 union 定义与使用 结构体数组 结构体指针 typedef 别名 共享内存 数据解析 字节序转换 节省内存 结构体 vs 联合体 对比 结构体 联合体 说明 成员独立存储 成员共享存储 内存布局不同 大小 = 成员之和 大小 = 最大成员 内存占用不同

实战建议

在实际项目中,我总结了几条经验:

  • 结构体适合描述实体:比如学生、设备、配置参数等,这些对象有多个属性需要同时存在
  • 联合体适合数据转换:比如协议解析、字节序处理、类型转换等场景
  • 结构体指针是函数传参的首选:避免大结构体的拷贝开销
  • typedef让代码更清爽:特别是当结构体类型在多个文件中使用时
我的习惯:在嵌入式开发中,我经常用结构体来管理硬件寄存器。把一组相关的寄存器定义成一个结构体,然后用指针映射到具体的地址,操作起来特别方便。比如GPIO的配置寄存器、状态寄存器等,都可以用结构体来组织。

好了,结构体和联合体就讲到这里。记住一句话:结构体是"和"的关系,联合体是"或"的关系。理解了这个本质,你就能在合适的场景用对它们。


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