30、综合实战项目:学生成绩管理系统(链表+文件+排序+查找的综合应用)
终于到了这门课的压轴项目。
说实话,每次我带新人或者给学生讲到这里,都会多说几句。因为前面学的那些零散知识点——链表、文件操作、排序、查找——就像一堆散落的零件。而这个项目,就是让你亲手把它们组装成一台能跑起来的机器。
我个人习惯把这种综合项目叫做「毕业设计」。不是学校那种毕业设计,而是你作为C语言学习者的一次「技术成年礼」。做完它,你才算真正把C语言用起来了。
项目需求:我们要做什么?
先别急着写代码。我见过太多人上来就敲键盘,结果写到一半发现结构乱了,又删掉重来。嗯,咱们先花5分钟把需求理清楚。
这个学生成绩管理系统,说白了就是管理一堆学生的成绩数据。它需要支持以下功能:
- 添加学生记录:输入学号、姓名、各科成绩
- 删除学生记录:按学号删除
- 修改学生记录:按学号修改成绩
- 查询学生记录:按学号或姓名查找
- 显示所有记录:以表格形式输出
- 排序:按总分或单科成绩排序
- 文件持久化:程序退出时保存,启动时加载
你想想看,这些功能是不是把咱们前面学的知识点全串起来了?链表负责动态存储,文件负责数据不丢失,排序和查找负责让数据好用。
系统架构设计
动手之前,我建议你先画一张图。哪怕画在草稿纸上都行。下面这张SVG图是我自己常用的结构,你可以参考:
看到没?整个系统分四层。最上层是菜单,中间是功能模块,核心是链表,底层是文件。每一层只跟相邻层打交道,这就是模块化设计的思想。
数据结构定义
咱们先定义学生结构体。这里我有个小建议:把成绩设计成数组,而不是单独定义语文、数学、英语三个变量。为什么?因为数组配合循环,代码会简洁很多。
#define MAX_NAME 20
#define SUBJECT_NUM 3
typedef struct Student {
int id; // 学号
char name[MAX_NAME]; // 姓名
float scores[SUBJECT_NUM]; // 三门课成绩
float total; // 总分
float average; // 平均分
struct Student *next; // 指向下一个节点
} Student;
核心功能实现
1. 链表的创建与插入
链表操作是基本功。我见过很多新手在插入节点时忘记更新指针,结果链表断了。记住一个原则:先接后断。
Student* insertStudent(Student *head, Student *newNode) {
if (head == NULL) {
newNode->next = NULL;
return newNode;
}
// 按学号升序插入
Student *prev = NULL, *curr = head;
while (curr != NULL && curr->id < newNode->id) {
prev = curr;
curr = curr->next;
}
newNode->next = curr;
if (prev == NULL) {
head = newNode; // 插在头部
} else {
prev->next = newNode; // 插在中间或尾部
}
return head;
}
2. 文件读写
文件操作是让数据「活」起来的关键。我个人偏爱二进制文件,因为读写快,而且不会出现文本编码问题。
// 保存链表到文件
int saveToFile(Student *head, const char *filename) {
FILE *fp = fopen(filename, "wb");
if (fp == NULL) return -1;
Student *curr = head;
while (curr != NULL) {
fwrite(curr, sizeof(Student), 1, fp);
curr = curr->next;
}
fclose(fp);
return 0;
}
// 从文件加载链表
Student* loadFromFile(const char *filename) {
FILE *fp = fopen(filename, "rb");
if (fp == NULL) return NULL;
Student *head = NULL;
Student temp;
while (fread(&temp, sizeof(Student), 1, fp) == 1) {
Student *node = (Student*)malloc(sizeof(Student));
memcpy(node, &temp, sizeof(Student));
node->next = NULL;
head = insertStudent(head, node);
}
fclose(fp);
return head;
}
3. 排序功能
排序我推荐用冒泡排序,虽然效率不是最高,但胜在简单、稳定、好理解。对于学生成绩这种数据量(一般几百条),冒泡完全够用。
void sortByTotal(Student *head) {
if (head == NULL || head->next == NULL) return;
int swapped;
Student *ptr;
Student *last = NULL;
do {
swapped = 0;
ptr = head;
while (ptr->next != last) {
if (ptr->total < ptr->next->total) {
// 交换数据,不交换节点
Student temp = *ptr;
*ptr = *(ptr->next);
*(ptr->next) = temp;
// 修复指针
Student *tmpNext = ptr->next;
ptr->next = tmpNext->next;
tmpNext->next = ptr;
if (ptr == head) head = tmpNext;
swapped = 1;
}
ptr = ptr->next;
}
last = ptr;
} while (swapped);
}
4. 查找功能
查找分两种:按学号(精确查找)和按姓名(模糊查找)。学号是唯一的,所以找到第一个就返回。姓名可能有重名,所以需要返回所有匹配项。
// 按学号查找(精确)
Student* findById(Student *head, int id) {
Student *curr = head;
while (curr != NULL) {
if (curr->id == id) return curr;
curr = curr->next;
}
return NULL;
}
// 按姓名查找(模糊)
void findByName(Student *head, const char *name) {
Student *curr = head;
int found = 0;
while (curr != NULL) {
if (strstr(curr->name, name) != NULL) {
printStudent(curr);
found = 1;
}
curr = curr->next;
}
if (!found) printf("未找到匹配的学生\n");
}
菜单与主循环
最后是用户交互部分。我习惯用do-while循环,因为菜单至少显示一次。
void showMenu() {
printf("\n===== 学生成绩管理系统 =====\n");
printf("1. 添加学生\n");
printf("2. 删除学生\n");
printf("3. 修改成绩\n");
printf("4. 查找学生\n");
printf("5. 显示全部\n");
printf("6. 排序(按总分)\n");
printf("7. 保存并退出\n");
printf("请选择: ");
}
int main() {
Student *head = loadFromFile("students.dat");
int choice;
do {
showMenu();
scanf("%d", &choice);
switch(choice) {
case 1: head = addStudent(head); break;
case 2: head = deleteStudent(head); break;
case 3: head = modifyStudent(head); break;
case 4: searchStudent(head); break;
case 5: displayAll(head); break;
case 6: sortByTotal(head); displayAll(head); break;
case 7: saveToFile(head, "students.dat"); break;
default: printf("无效选项\n");
}
} while (choice != 7);
// 释放链表
Student *curr = head;
while (curr != NULL) {
Student *temp = curr;
curr = curr->next;
free(temp);
}
return 0;
}
项目总结
这个项目看起来代码量不小,但拆开来看,每个模块其实都不难。你想想看:
- 链表操作:就是增删改查,跟咱们前面练的一样
- 文件操作:就是fread/fwrite,没什么新花样
- 排序:冒泡排序,老朋友了
- 查找:顺序查找,简单直接
说白了,这个项目就是把以前学过的零件拼在一起。真正考验你的不是某个知识点有多难,而是你能不能把它们组织成一个完整的系统。
我个人建议你亲手敲一遍代码,不要复制粘贴。哪怕照着上面的代码一个字一个字打,收获都比看十遍大。遇到bug也别慌——我写这个项目的时候也调了整整一个下午。调试的过程,才是真正长本事的过程。
好了,动手试试吧。