简单数据类型和复合数据类型都讲过了,简单类型是单一类型的数据,复合数据类型在C语言里也要求必须都一样,那如果要把身高体重姓名学号都放在一起存,得怎么办呢?这次咱们就讨论一下这些。
如果说数组是整整齐齐的储物柜,里面放的东西都得一样(全是整数或全是字符),那么今天我们要学的结构体就像是一个个人档案袋。在这个袋子里,你可以放照片(图像数据)、身份证(字符串)、体检表(浮点数)等等各种不同类型的东西。
为了解决更复杂的现实问题,C 语言提供了三种强大的构造类型:枚举(给数字起名字)、结构体(打包不同类型的数据)和共用体(节省内存)。
一、枚举(Enum):给数字起个好听的名字
在写代码的时候,我们经常遇到一些选项类的数据。比如交通灯有红、黄、绿三种状态,或者一周有周一到周日七天。
如果直接用数字 0, 1, 2 来表示红黄绿,代码写多了容易晕:if (light == 0) 到底是什么意思?是红灯还是绿灯?
这时候,枚举就派上用场了。它允许我们给这些枯燥的数字起一个有意义的名字,让代码像人话一样易读。
示例 1:交通灯与工作日
看看 ex1_enum.c:
// [引用: code_examples/struct_enum_union/ex1_enum.c]
#include <stdio.h>
// 1. 定义枚举类型
// 就像给 "0, 1, 2" 这些数字起了有意义的名字
// 默认从 0 开始编号:RED=0, YELLOW=1, GREEN=2
enum TrafficLight {
RED,
YELLOW,
GREEN
};
// 也可以手动指定数值
enum Weekday {
MONDAY = 1, // 从1开始
TUESDAY, // 自动变成2
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY,
SUNDAY
};
int main() {
printf("=== 枚举演示:交通灯 ===\n");
// 使用枚举变量
enum TrafficLight current_light = RED;
// 枚举本质上就是整数,所以可以用 %d 打印
printf("红灯的数值是: %d\n", current_light);
// 配合 switch 使用非常清晰
switch (current_light) {
case RED:
printf("信号灯指示:停车!\n");
break;
case YELLOW:
printf("信号灯指示:等一等!\n");
break;
case GREEN:
printf("信号灯指示:通行!\n");
break;
}
printf("\n=== 枚举演示:工作日 ===\n");
enum Weekday today = FRIDAY;
printf("今天是星期 %d\n", today);
if (today == SATURDAY || today == SUNDAY) {
printf("状态:休息日,睡懒觉。\n");
} else {
printf("状态:工作日,去搬砖。\n");
}
return 0;
}
运行结果
=== 枚举演示:交通灯 ===
红灯的数值是: 0
信号灯指示:停车!
=== 枚举演示:工作日 ===
今天是星期 5
状态:工作日,去搬砖。
代码解析
- 可读性提升:
case RED:比case 0:清楚太多了。以后维护代码的人(包括未来的你自己)会感谢现在的你。 - 本质是整数:枚举在 C 语言底层就是
int。你可以直接把它当数字用,但为了代码规范,最好只当枚举用。
二、结构体(Struct):打包不同类型的数据
这是 C 语言里最常用的工具之一。
想象你要在程序里描述一个学生。他有姓名(字符串)、年龄(整数)和成绩(浮点数)。
如果不用结构体,你得定义三个独立的变量:char name[], int age, float score。如果有 50 个学生,你就得维护 3 个大数组,很容易搞乱。
结构体允许你把这些相关联的数据,打包成一个整体,就像填写一张学生登记表。
示例 2:定义和使用结构体
看看 ex2_struct_basic.c:
// [引用: code_examples/struct_enum_union/ex2_struct_basic.c]
#include <stdio.h>
#include <string.h>
// 1. 定义结构体
// 就像设计一张 "学生信息登记表"
struct Student {
char name[50]; // 姓名 (字符串)
int age; // 年龄 (整数)
float score; // 分数 (浮点数)
};
int main() {
printf("=== 结构体基础演示 ===\n");
// 2. 创建并初始化结构体变量
// 就像填写一张具体的表格
struct Student stu1 = {"张三", 18, 95.5};
// 3. 访问结构体成员
// 使用 . 运算符 (点号)
printf("姓名: %s\n", stu1.name);
printf("年龄: %d\n", stu1.age);
printf("成绩: %.1f\n", stu1.score);
printf("\n--- 修改数据后 ---\n");
// 修改成员的值
stu1.score = 98.0;
// 注意:字符串数组不能直接用 = 赋值,要用 strcpy
strcpy(stu1.name, "张三丰");
printf("姓名: %s\n", stu1.name);
printf("成绩: %.1f\n", stu1.score);
// 结构体的大小
// 注意:可能会有 "内存对齐" 现象,所以大小可能比成员加起来略大
printf("\n结构体总大小: %lu 字节\n", sizeof(stu1));
return 0;
}
运行结果
=== 结构体基础演示 ===
姓名: 张三
年龄: 18
成绩: 95.5
--- 修改数据后 ---
姓名: 张三丰
成绩: 98.0
结构体总大小: 60 字节
2.1 结构体指针与箭头运算符 ->
我们在讲函数时说过,C 语言函数参数默认是值传递(复印件)。
结构体通常比较大(比如上面的 Student 有 60 字节)。如果每次传参都复印一份,不仅浪费内存,还很慢。
所以,我们通常传递结构体的指针(地址)。
但是,拿到指针后,怎么访问里面的成员呢?
* 普通方式:(*p).name (先解引用,再访问,写起来麻烦)
* 快捷方式:p->name (箭头运算符,专门给指针用的,帅气又快捷)
看看 ex3_struct_pointer.c:
// [引用: code_examples/struct_enum_union/ex3_struct_pointer.c]
#include <stdio.h>
struct Point {
int x;
int y;
};
// 函数参数传递结构体指针
// 这样做的好处:
// 1. 避免复制整个结构体(如果结构体很大,复制很慢)
// 2. 可以在函数内部修改外面的结构体
void move_point(struct Point *p, int dx, int dy) {
// 方式一:解引用后用点号 (*p).x
// 方式二:使用箭头运算符 p->x (推荐,更简洁)
printf(" [函数内] 移动前: (%d, %d)\n", p->x, p->y);
p->x += dx;
p->y += dy;
printf(" [函数内] 移动后: (%d, %d)\n", p->x, p->y);
}
int main() {
printf("=== 结构体指针演示 ===\n");
struct Point my_point = {10, 20};
printf("原始坐标: (%d, %d)\n", my_point.x, my_point.y);
printf("\n正在调用 move_point 函数...\n");
// 传递地址 (&my_point)
move_point(&my_point, 5, -3);
printf("\n回到 main 函数,坐标变为: (%d, %d)\n", my_point.x, my_point.y);
printf("结论:通过指针,函数成功修改了外部的结构体!\n");
return 0;
}
运行结果
=== 结构体指针演示 ===
原始坐标: (10, 20)
正在调用 move_point 函数...
[函数内] 移动前: (10, 20)
[函数内] 移动后: (15, 17)
回到 main 函数,坐标变为: (15, 17)
结论:通过指针,函数成功修改了外部的结构体!
三、共用体(Union):省吃俭用的变色龙
共用体(也叫联合体)是一种非常特殊的类型。它的所有成员共用同一块内存空间。 这就像是一个多功能插座,虽然有三孔也有两孔,但同一时间你只能插一个插头,不能同时用。
- 结构体:每个人都有自己的房间(内存叠加)。
- 共用体:大家轮流使用同一个房间(内存重叠,大小取决于最大的那个成员)。
它通常用于节省内存,或者在底层开发中处理二进制数据转换。
示例 3:共用体演示
看看 ex4_union.c:
// [引用: code_examples/struct_enum_union/ex4_union.c]
#include <stdio.h>
// 定义共用体
// 就像一个 "多功能插座",同一时间只能插一种插头
union Data {
int i;
float f;
char str[20];
};
int main() {
printf("=== 共用体 (Union) 演示 ===\n");
union Data data;
printf("共用体占用内存大小: %lu 字节\n", sizeof(data));
printf("(它的大小等于最大成员的大小,这里是 str[20] 的 20 字节)\n\n");
// 1. 存入整数
data.i = 10;
printf("存入整数 10 后:\n");
printf("data.i = %d\n", data.i);
// 2. 存入浮点数
// 注意:这会覆盖掉刚才存的整数!
data.f = 220.5;
printf("\n存入浮点数 220.5 后:\n");
printf("data.f = %.1f\n", data.f);
printf("data.i = %d (乱码了!因为内存被改写了)\n", data.i);
// 3. 存入字符串
// 这会覆盖掉刚才的浮点数
sprintf(data.str, "Hello");
printf("\n存入字符串 'Hello' 后:\n");
printf("data.str = %s\n", data.str);
printf("data.f = %.1f (乱码了!)\n", data.f);
return 0;
}
运行结果
=== 共用体 (Union) 演示 ===
共用体占用内存大小: 20 字节
(它的大小等于最大成员的大小,这里是 str[20] 的 20 字节)
存入整数 10 后:
data.i = 10
存入浮点数 220.5 后:
data.f = 220.5
data.i = 1130135552 (乱码了!因为内存被改写了)
存入字符串 'Hello' 后:
data.str = Hello
data.f = 1143139122437582505939828736.0 (乱码了!)
四、结构体 vs 共用体
这两个挺像(都是用 {} 包起来),所以得注意区分异同。
| 特性 | 结构体 (Structure) | 共用体 (Union) |
|---|---|---|
| 内存分配 | 每个成员都有自己独立的房间 | 所有成员挤在同一个房间里 |
| 大小计算 | 至少是所有成员大小之和(可能有填充) | 等于最大那个成员的大小 |
| 同时存值 | 可以同时存储所有成员的值 | 同一时间只能存一个成员的值 |
| 互相影响 | 改了 A 成员,B 成员不受影响 | 改了 A 成员,B 成员的数据会被覆盖/破坏 |
| 生活比喻 | 公寓:你住你的,我住我的 | 试衣间:一次只能进一个人,你进去了我就得出来 |
什么时候用谁? * 绝大多数情况(99%)你需要的都是结构体。 * 只有在你极度缺内存(比如嵌入式开发),或者需要对同一段二进制数据进行不同方式的解读(比如网络协议解析)时,才会用到共用体。
总结
今天我们学习了 C 语言中用来处理复杂数据的三个法宝:
- 枚举 (Enum):让代码里的数字变成有意义的单词,提高可读性(红灯、绿灯)。
- 结构体 (Struct):把不同类型的数据打包在一起,描述一个完整的对象(学生档案)。记住用
->来操作结构体指针。 - 共用体 (Union):一块内存轮流用,省空间,但要注意同一时间只能存一个有效值。
掌握了结构体,你现在可以把之前学的零散变量组织起来,去描述更真实、更复杂的世界了。比如写一个贪吃蛇游戏,蛇身就是一个结构体数组;或者写一个通讯录,每个联系人也是一个结构体。
CycleUser