第十五章:结构体与联合体——让数据“物以类聚”

各位同学,欢迎来到第十五章。从这一章开始,我们正式进入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);
我曾经踩过的坑: 用malloc分配结构体后,一定要检查返回值是否为NULL。另外,如果结构体中有指针成员(比如char *name),记得单独为它分配内存,不能直接strcpy。否则程序会崩溃给你看。

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_REDERR_TIMEOUT。这样在代码里看到就知道是枚举常量,不会和普通变量混淆。

知识体系总览

下面这张图帮你理清本章的知识脉络:

结构体与联合体 结构体 struct 定义与使用 结构体数组 结构体指针(->) typedef 类型别名 简化代码 联合体 union 共享内存 大小 = 最大成员 枚举 enum 命名整数常量

这张图把本章的四个核心概念串起来了。结构体是基础,typedef是语法糖,联合体是结构体的“变种”,枚举是另一种常量管理方式。它们之间没有绝对的先后顺序,但建议先掌握结构体,再学其他的。


好了,第十五章的内容就到这里。结构体和联合体是C语言中“面向对象”思想的雏形——把数据和操作数据的函数(后面会讲)组织在一起。你想想看,如果没有结构体,你要怎么描述一个“学生”?五个数组?太原始了。有了结构体,一切变得井井有条。

下一章我们会继续深入,看看结构体怎么和函数、指针玩出更多花样。到时候你会感叹:原来C语言也可以这么“高级”。

本章要点回顾:
  • 结构体:用 struct 定义,成员用 . 访问,指针用 -> 访问
  • 结构体数组:批量管理同类型数据
  • 结构体指针:传参高效,支持动态分配
  • typedef:给类型起别名,简化代码
  • 联合体:所有成员共享内存,同一时刻只能用一个
  • 枚举:用名字代替魔法数字,提高可读性
避坑指南:
  • 结构体定义末尾别忘了分号
  • 结构体指针用 -> 访问成员,不要写成 .
  • 联合体赋值后,只能读取最后赋值的成员
  • 枚举本质是 int,不要假设它的值范围
公众号:蓝海资料掘金营,微信 deep3321