第30章:结构体与联合体综合大作业——学生信息管理系统
终于到了最后一章。说实话,我每次带新人时,都会让他们写一个类似的小系统。不是为了炫技,而是因为——只有把结构体、联合体、链表、文件操作串起来用一遍,你才算真正掌握了C语言的数据管理能力。
这一章,我们直接动手。做一个学生信息管理系统,支持增、删、改、查。我会带着你一步步拆解,把前面29章的知识点全部揉进去。
30.1 系统需求与设计思路
先明确我们要做什么:
- 数据存储:每个学生有学号、姓名、性别、年龄、成绩(可存多科)。
- 操作功能:增加学生、删除学生、修改信息、按学号或姓名查找。
- 持久化:程序退出后数据不丢,下次启动能加载回来。
- 数据结构:用链表管理动态数据,用联合体处理成绩类型的灵活存储。
我个人习惯先把数据模型画出来。你想想看,如果连数据长什么样都没想清楚,代码写一半肯定要重构。
核心设计原则:链表负责动态增删,联合体负责节省内存,文件操作负责数据持久化。三者各司其职。
30.2 数据结构定义
先定义学生信息。这里我用了联合体来处理成绩——有的科目是整数分,有的可能是等级制(A/B/C)。
// 成绩联合体:支持整数分数或等级制
typedef union {
int score; // 百分制分数
char grade; // 等级制:'A','B','C','D','F'
} Score;
// 学生结构体
typedef struct Student {
int id; // 学号
char name[32]; // 姓名
char gender; // 'M' 或 'F'
int age; // 年龄
Score scores[3]; // 三门课的成绩
int useGrade; // 0=用分数,1=用等级
struct Student *next; // 链表指针
} Student;
我的经验:联合体在这里最大的好处是——如果某个学生只记录了等级制成绩,那分数字段就不占额外内存。虽然省的不多,但养成这种习惯,以后做大项目时内存管理会从容很多。
30.3 核心功能实现
30.3.1 增加学生
增加学生就是往链表尾部插入一个新节点。注意要检查学号是否重复——我曾经在一个项目中忘了做这个检查,结果系统里出现了两个学号相同的学生,查成绩时数据全乱了。
Student* addStudent(Student *head, int id, const char *name,
char gender, int age, int *scores, char *grades, int useGrade) {
// 检查学号是否已存在
Student *cur = head;
while (cur) {
if (cur->id == id) {
printf("学号 %d 已存在!\n", id);
return head;
}
cur = cur->next;
}
// 创建新节点
Student *newNode = (Student*)malloc(sizeof(Student));
if (!newNode) {
printf("内存分配失败!\n");
return head;
}
newNode->id = id;
strcpy(newNode->name, name);
newNode->gender = gender;
newNode->age = age;
newNode->useGrade = useGrade;
for (int i = 0; i < 3; i++) {
if (useGrade)
newNode->scores[i].grade = grades[i];
else
newNode->scores[i].score = scores[i];
}
newNode->next = NULL;
// 插入链表尾部
if (head == NULL) {
return newNode;
}
cur = head;
while (cur->next) cur = cur->next;
cur->next = newNode;
return head;
}
30.3.2 删除学生
删除操作要处理三种情况:删头节点、删中间节点、删尾节点。说白了就是指针重连,但千万别忘了 free 掉内存。
Student* deleteStudent(Student *head, int id) {
if (head == NULL) return NULL;
Student *temp;
// 如果要删除的是头节点
if (head->id == id) {
temp = head->next;
free(head);
printf("已删除学号 %d\n", id);
return temp;
}
// 查找要删除的节点
Student *cur = head;
while (cur->next && cur->next->id != id) {
cur = cur->next;
}
if (cur->next == NULL) {
printf("未找到学号 %d\n", id);
return head;
}
temp = cur->next->next;
free(cur->next);
cur->next = temp;
printf("已删除学号 %d\n", id);
return head;
}
注意:删除节点后,一定要把被删节点指向的 next 指针先保存下来,否则链表就断了。我见过有人直接 free 掉节点,然后才想起来 next 还没取——嗯,那叫一个酸爽。
30.3.3 修改与查找
查找很简单,遍历链表比对学号或姓名。修改就是找到节点后,直接改字段值。
Student* findById(Student *head, int id) {
Student *cur = head;
while (cur) {
if (cur->id == id) return cur;
cur = cur->next;
}
return NULL;
}
void updateStudent(Student *stu, const char *newName, int newAge) {
if (stu == NULL) return;
if (newName) strcpy(stu->name, newName);
if (newAge > 0) stu->age = newAge;
printf("信息已更新\n");
}
30.4 文件操作:让数据活下来
没有文件操作的系统,程序一关数据就没了。那还叫系统吗?
保存时,我建议用二进制方式写文件。速度快,而且结构体可以直接整体写入,省去逐字段格式化的麻烦。
void saveToFile(Student *head, const char *filename) {
FILE *fp = fopen(filename, "wb");
if (!fp) {
printf("无法打开文件 %s\n", filename);
return;
}
Student *cur = head;
while (cur) {
fwrite(cur, sizeof(Student), 1, fp);
cur = cur->next;
}
fclose(fp);
printf("数据已保存到 %s\n", filename);
}
Student* loadFromFile(const char *filename) {
FILE *fp = fopen(filename, "rb");
if (!fp) return NULL;
Student *head = NULL;
Student *tail = NULL;
Student temp;
while (fread(&temp, sizeof(Student), 1, fp) == 1) {
Student *newNode = (Student*)malloc(sizeof(Student));
if (!newNode) break;
*newNode = temp;
newNode->next = NULL;
if (head == NULL) {
head = newNode;
tail = newNode;
} else {
tail->next = newNode;
tail = newNode;
}
}
fclose(fp);
return head;
}
避坑指南:用 fwrite 直接写结构体虽然方便,但要注意——如果结构体里有指针(比如 char *name 而不是 char name[32]),那保存的只是指针地址,下次加载时那个地址早就失效了。所以这里我用了定长数组,就是为了能直接序列化。
30.5 主控逻辑与菜单
最后,写一个简单的菜单循环,把上面所有功能串起来。
int main() {
Student *head = loadFromFile("students.dat");
int choice, id, age, scores[3];
char name[32], gender, grades[3], useGrade;
while (1) {
printf("\n===== 学生信息管理系统 =====\n");
printf("1. 增加学生\n");
printf("2. 删除学生\n");
printf("3. 修改学生\n");
printf("4. 查找学生\n");
printf("5. 显示所有学生\n");
printf("6. 保存并退出\n");
printf("请选择: ");
scanf("%d", &choice);
switch (choice) {
case 1:
// 输入学生信息,调用 addStudent
break;
case 2:
printf("输入要删除的学号: ");
scanf("%d", &id);
head = deleteStudent(head, id);
break;
case 3:
// 查找后修改
break;
case 4:
// 按学号或姓名查找
break;
case 5:
// 遍历打印链表
break;
case 6:
saveToFile(head, "students.dat");
// 释放链表内存
return 0;
default:
printf("无效选项\n");
}
}
return 0;
}
30.6 总结与思考
这个系统虽然简单,但五脏俱全。你仔细看:
- 结构体:定义了学生的数据模型
- 联合体:灵活处理成绩的两种表示方式
- 链表:实现了动态增删,不浪费内存
- 文件操作:让数据在程序重启后依然存在
说实话,很多嵌入式项目里的数据管理模块,本质上就是这套东西的变体。只不过把学生换成了传感器节点,把成绩换成了采集数据。
如果你能把这个系统完整写出来,并且跑通所有功能——恭喜你,C语言的结构体与联合体,你已经真正掌握了。
最后送你一句话:学编程,动手永远比看书重要。代码写出来,跑起来,出错了再改——这个过程本身,就是最好的老师。
公众号:蓝海资料掘金营,微信deep3321