第十五章:结构体与联合体——让数据“物以类聚”
各位同学,欢迎来到第十五章。从这一章开始,我们正式进入C语言的“高级玩法”阶段。前面我们学了数组、指针、函数,这些都是基本功。但说实话,光靠这些,你很难描述现实世界中的复杂对象。比如一个学生——他有学号、姓名、年龄、成绩。用五个数组分别存?太乱了。这时候,结构体就派上用场了。
我个人习惯把结构体看作“自定义的集装箱”。你可以把不同类型的数据打包在一起,形成一个新类型。这在实际开发中太常用了。我当年做嵌入式项目时,一个传感器节点要上报温度、湿度、时间、设备ID,不用结构体的话,代码简直没法看。
15.1 结构体的定义与使用
结构体的定义语法很简单,但有几个细节容易踩坑。先看一个标准定义:
struct Student {
int id; // 学号
char name[20]; // 姓名
int age; // 年龄
float score; // 成绩
}; // 注意这个分号!
嗯,这里要注意:花括号后面的分号不能丢。我见过不少新手在这里漏掉分号,然后编译器报一堆莫名其妙的错误。定义完结构体类型后,就可以用它来声明变量了:
// 方式一:先定义类型,再声明变量
struct Student stu1;
// 方式二:定义类型的同时声明变量
struct Student {
int id;
char name[20];
int age;
float score;
} stu2, stu3;
// 方式三:匿名结构体(不推荐,因为无法再次使用该类型)
struct {
int id;
char name[20];
} stu4;
访问结构体成员用点号(.):
stu1.id = 1001;
strcpy(stu1.name, "张三");
stu1.age = 20;
stu1.score = 88.5;
struct Student stu1 = {1001, "张三", 20, 88.5};
15.2 结构体数组
一个学生不够?那就来一打。结构体数组就是存储多个同类型结构体变量的数组。说白了,就是“学生花名册”。
struct Student class[30]; // 一个班30个学生
// 初始化
struct Student class[3] = {
{1001, "张三", 20, 88.5},
{1002, "李四", 21, 92.0},
{1003, "王五", 19, 76.5}
};
// 遍历访问
for (int i = 0; i < 3; i++) {
printf("学号:%d,姓名:%s,成绩:%.1f\n",
class[i].id, class[i].name, class[i].score);
}
我在项目中用结构体数组处理过传感器数据采集——每秒钟采集100个数据点,每个点包含时间戳、温度、湿度。用结构体数组存起来,后续分析处理非常方便。
15.3 结构体指针
结构体指针,说白了就是一个指向结构体变量的指针。为什么要用指针?两个原因:一是函数传参时避免拷贝整个结构体(省内存、提速度);二是可以动态分配结构体。
struct Student stu = {1001, "张三", 20, 88.5};
struct Student *p = &stu;
// 通过指针访问成员:两种写法等价
printf("%d\n", (*p).id); // 方式一:先解引用,再点号
printf("%d\n", p->id); // 方式二:箭头运算符(推荐)
箭头运算符(->)是结构体指针的专属语法。我个人觉得它比解引用再点号更直观,一看就知道是在操作指针。
再看一个动态分配的例子:
struct Student *p = (struct Student *)malloc(sizeof(struct Student));
if (p == NULL) {
printf("内存分配失败!\n");
return -1;
}
p->id = 1001;
strcpy(p->name, "张三");
p->age = 20;
p->score = 88.5;
// 使用完毕后释放
free(p);
15.4 typedef——给类型起个“外号”
每次写 struct Student 是不是有点烦?typedef 就是用来解决这个问题的。它给已有类型起一个新名字,让代码更简洁。
typedef struct Student {
int id;
char name[20];
int age;
float score;
} Student; // 现在 Student 就是一个类型名了
// 使用
Student stu1; // 不用写 struct 了
Student *p = &stu1;
更常见的写法是把定义和typedef合在一起:
typedef struct {
int id;
char name[20];
int age;
float score;
} Student;
你想想看,如果不用typedef,每次声明变量都要写 struct Student xxx,代码里到处都是 struct 关键字,看着就累。用了typedef之后,清爽多了。
15.5 联合体(union)——让内存“一物多用”
联合体和结构体长得很像,但有一个本质区别:所有成员共享同一块内存。结构体是每个成员都有自己的内存空间,联合体是所有成员从同一个地址开始存放。
union Data {
int i;
float f;
char str[20];
};
union Data data;
printf("大小:%lu 字节\n", sizeof(data)); // 输出20(取最大成员的大小)
联合体的大小等于最大成员的大小。同一时刻,只能使用其中一个成员。如果你给 data.i 赋值,再读取 data.f,得到的是乱码——因为内存中的字节被按照float的格式解释了。
我在做通信协议解析时用过联合体。一个数据包可能是整数、浮点数或字符串,用联合体来存储,省内存又方便转换。
- 结构体:各成员独立存储,总大小 ≈ 各成员大小之和(考虑对齐)
- 联合体:各成员共享存储,总大小 = 最大成员的大小
- 结构体可以同时使用所有成员,联合体同一时刻只能用一个
15.6 枚举(enum)——让“魔法数字”滚出你的代码
枚举,说白了就是给整数常量起个名字。比如一周七天,用0-6表示,谁记得住?用枚举就一目了然。
enum Weekday {
MON, // 默认0
TUE, // 1
WED, // 2
THU, // 3
FRI, // 4
SAT, // 5
SUN // 6
};
enum Weekday today = WED;
if (today == SAT || today == SUN) {
printf("周末啦!\n");
}
你也可以手动指定值:
enum Color {
RED = 1,
GREEN = 2,
BLUE = 4
};
枚举的本质是整数,所以可以用于switch-case语句。我建议你在所有需要固定选项的地方都用枚举,而不是直接用数字。为什么?因为半年后你回头看代码,看到 if (status == 3) 你会想半天“3是什么鬼”,但看到 if (status == ERROR_TIMEOUT) 就一目了然。
COLOR_RED、ERR_TIMEOUT。这样在代码里看到就知道是枚举常量,不会和普通变量混淆。
知识体系总览
下面这张图帮你理清本章的知识脉络:
这张图把本章的四个核心概念串起来了。结构体是基础,typedef是语法糖,联合体是结构体的“变种”,枚举是另一种常量管理方式。它们之间没有绝对的先后顺序,但建议先掌握结构体,再学其他的。
好了,第十五章的内容就到这里。结构体和联合体是C语言中“面向对象”思想的雏形——把数据和操作数据的函数(后面会讲)组织在一起。你想想看,如果没有结构体,你要怎么描述一个“学生”?五个数组?太原始了。有了结构体,一切变得井井有条。
下一章我们会继续深入,看看结构体怎么和函数、指针玩出更多花样。到时候你会感叹:原来C语言也可以这么“高级”。
- 结构体:用 struct 定义,成员用 . 访问,指针用 -> 访问
- 结构体数组:批量管理同类型数据
- 结构体指针:传参高效,支持动态分配
- typedef:给类型起别名,简化代码
- 联合体:所有成员共享内存,同一时刻只能用一个
- 枚举:用名字代替魔法数字,提高可读性
- 结构体定义末尾别忘了分号
- 结构体指针用 -> 访问成员,不要写成 .
- 联合体赋值后,只能读取最后赋值的成员
- 枚举本质是 int,不要假设它的值范围