综合实战:学生成绩管理系统
说实话,链表学了那么多理论,到底能干啥?
我当年刚学完链表时也有这个疑问。直到有一次,我需要给一个培训班写个简单的成绩管理工具——增删改查、排序、还要能存文件。用数组?学生人数不确定,太死板。用数据库?杀鸡用牛刀。链表,刚刚好。
今天我们就手撸一个完整的学生成绩管理系统。麻雀虽小,五脏俱全。
系统功能概览
先看看我们要实现什么:
- 增加:录入新学生信息(学号、姓名、成绩)
- 删除:按学号删除学生记录
- 修改:按学号修改成绩
- 查询:按学号或姓名查找
- 排序:按成绩从高到低排序
- 文件存储:程序退出时保存,启动时加载
嗯,这就是一个迷你版的学生管理系统。你想想看,很多公司的后台管理,核心逻辑不也就这些吗?
数据结构设计
先定义学生节点。我习惯把数据域和指针域分开想清楚再写代码。
typedef struct Student {
int id; // 学号
char name[20]; // 姓名
float score; // 成绩
} Student;
typedef struct Node {
Student data; // 数据域
struct Node *next; // 指针域
} Node;
小提示:把学生信息单独定义成一个结构体,以后扩展起来方便。比如要加个「班级」字段,改一处就行。我在项目中吃过这个亏,一开始把字段全塞在节点里,后来改需求改到想哭。
核心功能实现
1. 创建节点与插入
Node* createNode(Student stu) {
Node *newNode = (Node*)malloc(sizeof(Node));
if (!newNode) {
printf("内存分配失败!\n");
exit(1);
}
newNode->data = stu;
newNode->next = NULL;
return newNode;
}
void insertNode(Node **head, Student stu) {
Node *newNode = createNode(stu);
newNode->next = *head;
*head = newNode;
}
这里我用了头插法。为什么?简单、高效。如果你需要按学号顺序插入,那就改成尾插或者有序插入。我个人习惯在原型阶段先用头插,后面再优化。
2. 删除节点
int deleteNode(Node **head, int id) {
Node *cur = *head, *prev = NULL;
while (cur) {
if (cur->data.id == id) {
if (prev) prev->next = cur->next;
else *head = cur->next;
free(cur);
return 1; // 删除成功
}
prev = cur;
cur = cur->next;
}
return 0; // 未找到
}
注意:删除节点后一定要 free 掉内存。我曾经在嵌入式项目里忘了 free,结果跑了三天后系统内存耗尽,直接死机。排查了一整天才发现是链表删除没释放内存。
3. 修改与查询
int updateScore(Node *head, int id, float newScore) {
Node *cur = head;
while (cur) {
if (cur->data.id == id) {
cur->data.score = newScore;
return 1;
}
cur = cur->next;
}
return 0;
}
Node* searchById(Node *head, int id) {
Node *cur = head;
while (cur) {
if (cur->data.id == id) return cur;
cur = cur->next;
}
return NULL;
}
查询和修改的逻辑很像,都是遍历链表找节点。说白了,链表的增删改查核心就是「遍历 + 条件判断」。你把这个套路记住,90%的链表操作都能搞定。
4. 排序(冒泡排序)
void sortByScore(Node *head) {
if (!head || !head->next) return;
int swapped;
Node *cur;
Node *last = NULL;
do {
swapped = 0;
cur = head;
while (cur->next != last) {
if (cur->data.score < cur->next->data.score) {
Student tmp = cur->data;
cur->data = cur->next->data;
cur->next->data = tmp;
swapped = 1;
}
cur = cur->next;
}
last = cur;
} while (swapped);
}
这里我交换的是数据域,不是节点本身。为什么?简单。如果你追求性能,可以交换指针,但代码会复杂很多。对于学生成绩这种小规模数据,交换数据完全够用。
文件存储
程序退出时保存数据,启动时加载数据。这是让系统「活」起来的关键一步。
void saveToFile(Node *head, const char *filename) {
FILE *fp = fopen(filename, "wb");
if (!fp) {
printf("无法打开文件 %s\n", filename);
return;
}
Node *cur = head;
while (cur) {
fwrite(&cur->data, sizeof(Student), 1, fp);
cur = cur->next;
}
fclose(fp);
printf("数据保存成功!\n");
}
Node* loadFromFile(const char *filename) {
FILE *fp = fopen(filename, "rb");
if (!fp) return NULL;
Node *head = NULL;
Student stu;
while (fread(&stu, sizeof(Student), 1, fp) == 1) {
insertNode(&head, stu);
}
fclose(fp);
return head;
}
避坑指南:用二进制方式读写结构体,速度快、代码少。但要注意结构体字节对齐问题。如果你要在不同平台间交换文件,建议用文本格式(如CSV)。我曾经在ARM和x86之间传文件,因为字节对齐不一样,读出来全是乱码。
系统主流程
把上面这些功能串起来,就是一个完整的主程序了。我画了个流程图,帮你理清逻辑:
完整代码示例
下面给出主函数的骨架,你可以直接拿去用:
int main() {
Node *head = loadFromFile("students.dat");
int choice, id;
float score;
Student stu;
while (1) {
printf("\n===== 学生成绩管理系统 =====\n");
printf("1. 添加学生 2. 删除学生\n");
printf("3. 修改成绩 4. 查询学生\n");
printf("5. 成绩排序 6. 保存数据\n");
printf("0. 退出系统\n");
printf("请选择: ");
scanf("%d", &choice);
switch (choice) {
case 1:
printf("输入学号 姓名 成绩: ");
scanf("%d %s %f", &stu.id, stu.name, &stu.score);
insertNode(&head, stu);
break;
case 2:
printf("输入要删除的学号: ");
scanf("%d", &id);
if (deleteNode(&head, id))
printf("删除成功\n");
else
printf("未找到该学生\n");
break;
// ... 其他 case 类似
case 0:
saveToFile(head, "students.dat");
// 释放所有节点内存
while (head) {
Node *tmp = head;
head = head->next;
free(tmp);
}
return 0;
}
}
}
核心要点总结:
- 链表的核心操作就是「遍历 + 条件判断」,增删改查都是这个套路
- 文件存储用二进制读写结构体,简单高效
- 排序时交换数据域比交换节点更易实现
- 程序退出前一定要释放所有动态内存,这是好习惯
这个系统虽然简单,但已经涵盖了链表在实际项目中的典型用法。你想想看,从学生管理到员工管理,从商品库存到订单系统,底层不都是这套增删改查的逻辑吗?
把今天这个例子吃透,以后遇到类似需求,你就能直接复用这套代码框架了。