一、结构体:把零散数据打包成“一个整体”
写程序久了你会发现,光靠 int、char、float 这些基本类型,根本不够用。
比如你要描述一个学生:有学号、姓名、成绩、年龄。用四个独立变量?
int stu_id;
char stu_name[20];
float stu_score;
int stu_age;
这样写,数据是散的。你传参得传四个,排序得来回倒腾。说白了,数据之间没有“归属感”。
结构体就是干这个的——把相关的数据打包成一个自定义类型。
1.1 结构体的定义
语法很简单,用 struct 关键字:
struct Student {
int id; // 学号
char name[20]; // 姓名
float score; // 成绩
int age; // 年龄
}; // 别忘了分号!
嗯,这里要注意:结构体定义末尾的分号不能丢。我见过不少新手在这里栽跟头,编译器报错半天找不到原因。
1.2 结构体变量的声明与初始化
定义完类型,就可以声明变量了:
// 方式一:先定义类型,再声明变量
struct Student stu1;
// 方式二:定义类型的同时声明变量
struct Student {
int id;
char name[20];
float score;
} stu2, stu3;
// 方式三:初始化
struct Student stu4 = {1001, "张三", 89.5f, 20};
我个人习惯用方式一,代码更清晰。初始化时,成员顺序必须和定义一致。
1.3 访问结构体成员
用点号 . 操作符:
stu1.id = 1002;
strcpy(stu1.name, "李四");
stu1.score = 92.0f;
stu1.age = 21;
printf("姓名:%s,成绩:%.1f\n", stu1.name, stu1.score);
我在项目中遇到过一个问题:结构体里有字符数组成员,直接用 = 赋值是不行的,必须用 strcpy。这点和基本类型不一样,容易踩坑。
stu1.name = "王五";,结果编译没报错,运行直接崩溃。因为 name 是数组名,是常量地址,不能赋值。记住:数组成员用 strcpy,基本类型直接用 =。
二、结构体数组:批量管理同类数据
一个学生用结构体,一百个学生呢?用结构体数组。
struct Student class[100]; // 一个班100个学生
// 初始化
struct Student class[3] = {
{1001, "张三", 89.5f, 20},
{1002, "李四", 92.0f, 21},
{1003, "王五", 78.0f, 19}
};
// 访问
for (int i = 0; i < 3; i++) {
printf("学号:%d,姓名:%s\n", class[i].id, class[i].name);
}
结构体数组的用法和普通数组一模一样,只是每个元素是一个结构体。你想想看,配合循环,批量处理数据是不是很方便?
三、结构体指针:高效传递,避免拷贝
结构体可能很大,如果直接传值给函数,整个结构体都会被复制一遍,浪费内存和时间。
用指针就高效多了——只传一个地址。
struct Student stu = {1001, "张三", 89.5f, 20};
struct Student *p = &stu;
// 两种访问方式
printf("成绩:%f\n", (*p).score); // 先解引用,再访问
printf("成绩:%f\n", p->score); // 直接用 -> 操作符
我个人强烈推荐用 -> 操作符,代码更简洁,可读性更好。
3.1 结构体指针作为函数参数
void printStudent(const struct Student *p) {
printf("学号:%d,姓名:%s,成绩:%.1f\n",
p->id, p->name, p->score);
}
// 调用
printStudent(&stu);
这里加 const 是个好习惯——告诉编译器这个函数不会修改结构体内容。我在代码审查时经常强调这一点。
四、结构体嵌套:描述复杂对象
现实中的对象往往不是一层结构。比如学生还有“出生日期”,日期本身又包含年、月、日。
struct Date {
int year;
int month;
int day;
};
struct Student {
int id;
char name[20];
struct Date birthday; // 嵌套结构体
float score;
};
struct Student stu = {1001, "张三", {2000, 5, 15}, 89.5f};
// 访问嵌套成员
printf("出生年份:%d\n", stu.birthday.year);
嵌套的层次不要太深,我一般控制在两层以内。超过三层,代码可读性会急剧下降。
五、联合体(union):同一内存,不同解读
联合体和结构体长得很像,但有一个本质区别:所有成员共享同一块内存。
union Data {
int i;
float f;
char str[4];
};
union Data data;
data.i = 42;
printf("int: %d\n", data.i);
data.f = 3.14f;
printf("float: %f\n", data.f); // 此时 i 的值已经被覆盖了
为什么会这样?因为联合体的大小等于最大成员的大小。所有成员从同一个地址开始存储。你写了一个成员,其他成员的值就变得不可预测了。
我在嵌入式项目中常用联合体来解析通信协议。比如接收一个数据包,可以用联合体把同一段内存解释为不同的字段:
union Packet {
unsigned char raw[4]; // 原始字节
struct {
unsigned char id;
unsigned char cmd;
unsigned short value;
} fields;
};
这样,你可以用 raw 接收数据,然后用 fields 解析具体内容。说白了,就是同一块内存,不同视角。
六、枚举(enum):让代码自己说话
枚举的本质是给整数起个名字。比如表示一周的七天:
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
};
枚举的好处是让代码自文档化。你看到 RED 就知道是红色,看到 1 还得猜一下。我在状态机里特别喜欢用枚举,每个状态一目了然。
七、知识体系总览
下面这张图总结了本章的核心内容,帮你理清结构体、联合体、枚举之间的关系:
八、总结与避坑
这一章的内容,说白了就是三件事:
- 结构体:把不同类型的数据打包,成员独立存储
- 联合体:所有成员共享内存,同一块内存不同解读
- 枚举:给整数起名字,让代码自己说话
最后分享几个我踩过的坑:
- 结构体赋值别用 = 给数组——用 strcpy
- 联合体写一个成员,别指望另一个成员还保留值——它们共享内存
- 枚举本质是 int——别指望它做类型安全检查
- 结构体传参尽量用指针——避免拷贝大结构体
📌 核心要点: 结构体是“打包”,联合体是“共享”,枚举是“命名”。理解这三者的本质区别,你就能在合适的场景用对工具。
公众号:蓝海资料掘金营,微信deep3321