简介:这份资源是面向计算机相关专业学生的数据结构与算法课程设计参考文档,聚焦学生成绩管理系统的完整设计与实现,适合正在完成课设、需要参考系统架构与代码实现的学习者。文档围绕数组、链表、栈、队列、树、图等数据结构,以及冒泡排序、选择排序、插入排序、顺序搜索、二分搜索、DFS、BFS等算法展开,涵盖用户登录、成绩录入、成绩统计、成绩分析等模块,并涉及需求分析、设计、实现、测试与维护的完整项目管理流程。压缩包内共1个doc文件,约1.14MB,内容包含系统架构说明、数据库表设计、用户界面设计、核心功能代码实现及单元测试、集成测试、系统测试等环节的整理。目前已有674人学习下载,可帮助读者快速理解成绩管理系统的整体设计思路,对照完成课程设计报告与代码实现,适合作为课设参考与排错思路的补充材料。
1. 从一份 C 语言课设文档说起:它到底能跑出什么
如果你手头正躺着一份名为“数据结构与算法课程设计学生成绩管理系统”的文档,打开后满屏是struct stud、malloc、free和一堆菜单函数,那你大概率正卡在课程设计验收的前一周。这份资源不是那种“讲完概念就结束”的 PPT,而是一份能直接编译、能录入、能查询、能删除、能改分的 C 语言控制台程序源码,外加一份配套的设计说明文档。它解决的核心问题很具体:用最原始的单链表把学生成绩管起来,让你在答辩时能指着代码说清楚“为什么这里用链表而不是数组”“插入时怎么保持学号有序”“删除节点时指针怎么接”。
适合谁?第一类,正在做数据结构课设、需要一份能跑通的参考实现的人;第二类,C 语言指针和结构体学得半懂、想通过一个完整项目把struct、typedef、malloc、链表操作串起来的人;第三类,需要一份带文档、带源码、能改造成自己版本的人。它不涉及图形界面,不依赖数据库,就是纯控制台加单链表,Windows 下用常见 C 编译器就能编。下面我按“先看懂结构、再动手跑、再改、再避坑”的顺序拆一遍。
2. 单链表成绩管理系统的骨架:结构体、菜单与七个核心函数
2.1 数据模型为什么是struct stud套struct stucode
源码里最底层的定义只有两段:
struct stud { long num; // 学号,用 long 存,方便比较大小 char name[20]; // 姓名,定长数组,不涉及动态字符串 double score1, score2; // 两门课成绩,double 保证小数精度 }; typedef struct stucode { struct stud student; // 数据域:一个完整的学生记录 struct stucode *next; // 指针域:指向下一个节点 } L;这里的设计意图很明确:把“学生数据”和“链表节点”拆成两层。struct stud只关心业务字段,struct stucode只关心链式关系。这样做的好处是,后面写插入、删除、查找时,操作的是L类型的节点,而取数据时用p->student.num这种路径,逻辑边界清晰。num用long而不是int,是为了避免学号位数多时溢出;name用char[20]定长数组,是因为课设场景下不需要动态内存管理字符串,减少free出错的机会。score1、score2用double,输出时用%.2lf控制两位小数,这是成绩类程序的常见做法。
2.2 菜单驱动的主循环:switch加getchar的经典组合
主函数main()的结构是一个while(flag)死循环,里面先system("cls")清屏,再调menu()打印选项,然后用choose=getchar()读一个字符,进switch分发。七个功能对应七个分支:1 创建链表、2 按学号查找、3 按姓名查找、4 删除、5 插入、6 显示全部、7 修改,0 退出。
void menu() { printf("\n 1 创建链表\n"); printf("\n 2 按学号查找\n"); printf("\n 3 按姓名查找\n"); printf("\n 4 删除学生\n"); printf("\n 5 插入学生\n"); printf("\n 6 显示全部\n"); printf("\n 7 修改信息\n"); printf("\n 0 退出\n"); printf("\n 请选择:\n"); }这里有个容易被忽略的细节:每个case执行完后都有getchar(); getchar();两次调用。第一次吃掉scanf留下的换行符,第二次等待用户按回车,起到“暂停”效果。如果你把这两行删掉,菜单会瞬间刷屏,根本看不清输出。这是控制台程序里非常典型的“翻车点”,后面避坑章节会展开。
2.3 创建链表createlist:头插还是尾插,这里选了尾插
createlist接收struct stucode **r,也就是二级指针,目的是在函数内部修改外部头指针。逻辑是:先读一条记录,如果学号为 0 就返回;否则malloc一个节点,填数据,next=NULL,让*r指向它;然后继续读,每读一条就挂到当前尾节点的next上。
void createlist(struct stucode **r) { struct stucode *p, *t; long n; char a[20]; double s1, s2; if (*r) *r = NULL; // 如果已有链表,先丢弃(注意:这里没 free,是内存泄漏点) scanf("%ld%s%lf%lf", &n, a, &s1, &s2); if (n == 0) return; p = (L *)malloc(sizeof(L)); p->student.num = n; strcpy(p->student.name, a); p->student.score1 = s1; p->student.score2 = s2; p->next = NULL; *r = p; scanf("%ld%s%lf%lf", &n, a, &s1, &s2); while (n) { t = p; p = (L *)malloc(sizeof(L)); // ... 填数据 ... t->next = p; scanf("%ld%s%lf%lf", &n, a, &s1, &s2); } }输入格式是“学号 姓名 成绩1 成绩2”,以学号 0 作为结束标记。尾插的好处是链表顺序和输入顺序一致,方便后面按输入顺序显示。但注意if (*r) *r = NULL;这一句:如果链表已经存在,直接丢头指针,之前malloc的节点全部泄漏。课设场景下程序跑完就退出,问题不大,但如果你要改造成长期运行的系统,这里必须补free循环。
2.4 查找、删除、插入、修改:指针操作的四个典型场景
search1按学号查找,search2按姓名查找,都是遍历链表,用while(p && p->student.num != x) p = p->next;这种模式。找到就打印,找不到就输出Error! No such student !。
del删除节点分两种情况:如果头节点就是要删的,直接*r = (*r)->next; free(p);;否则用p找待删节点的前驱,找到后t = p->next; p->next = p->next->next; free(t);。这里的关键是删除前必须拿到前驱指针,否则单链表断不开。
insert插入时保持学号有序:如果链表为空,直接作为头节点;如果新学号小于头节点学号,新节点变成新头;否则遍历找到第一个next学号大于新学号的位置,插进去。源码里用while(t->next && t->next->student.num <= p->student.num) t = t->next;来定位。
change修改是先按学号找到节点,打印原信息,然后让用户重新输入学号、姓名、两门成绩,直接覆盖原节点的数据域。注意它改的是p->student.num,如果新学号和原学号不同,链表的排序可能被破坏,这是后面要提的坑。
3. 把源码跑起来:编译、录入、查询、删除的完整操作链
3.1 编译环境与最小验证流程
这份源码是标准 C,没有依赖第三方库,头文件只有stdio.h、stdlib.h、string.h。在 Windows 下,常见做法是用 Dev-C++ 新建控制台项目,把代码粘进去,按 F11 编译运行;或者用 MinGW 的gcc命令行:
gcc student_manager.c -o student_manager.exe ./student_manager.exe编译时如果报system未声明,检查是否包含了stdlib.h;如果报strcpy不安全,那是 MSVC 的警告,不是错误,可以忽略或在文件顶部加#define _CRT_SECURE_NO_WARNINGS。跑起来后先选 1,按“1001 张三 85 90”格式输入几条,最后输入“0 结束”回到菜单,再选 6 看是否全部显示。这一步验证的是链表创建和遍历是否正常。
3.2 录入与显示:输入格式和结束标记
录入时最容易出错的是scanf的格式匹配。源码里用的是scanf("%ld%s%lf%lf", &n, a, &s1, &s2),意味着输入必须是“整数 字符串 小数 小数”,中间用空格或换行隔开。如果你输入“1001,张三,85,90”用逗号分隔,scanf会读失败,n保持原值,程序可能陷入死循环。正确输入示例:
1001 张三 85.5 90.0 1002 李四 78.0 82.5 1003 王五 92.0 88.5 0显示全部选 6,输出格式是printf("%ld%s%.2lf%.2lf\n", ...),学号、姓名、两门成绩连在一起,没有分隔符。如果姓名是中文,在某些控制台编码下可能显示乱码,这是终端编码问题,不是链表逻辑问题。
3.3 查找、删除、插入的交互细节
按学号查找选 2,输入学号,找到就打印一行,找不到就报错。按姓名查找选 3,输入姓名,用strcmp比较。删除选 4,输入学号,如果删的是头节点,头指针会后移;如果删的是中间节点,前驱的next会跳过被删节点。插入选 5,输入新记录,程序会自动按学号顺序插入,插入后选 6 能看到新记录出现在正确位置。修改选 7,先按学号定位,再输入新数据覆盖。
这里有一个操作顺序上的建议:先创建、再插入、再删除、再修改,最后显示。因为插入会改变链表顺序,删除会减少节点,修改可能改变学号,每一步之后都用 6 确认状态,能快速定位是哪一步出的问题。
3.4 用一张表看清七个函数的输入输出
| 菜单项 | 函数 | 输入 | 输出 | 是否修改链表 |
|---|---|---|---|---|
| 1 | createlist | 学号 姓名 成绩1 成绩2,0 结束 | 无(随后可显示) | 是,重建链表 |
| 2 | search1 | 学号 | 匹配记录或错误 | 否 |
| 3 | search2 | 姓名 | 匹配记录或错误 | 否 |
| 4 | del | 学号 | 无(随后可显示) | 是,删除节点 |
| 5 | insert | 学号 姓名 成绩1 成绩2 | 无(随后可显示) | 是,有序插入 |
| 6 | out | 无 | 全部记录 | 否 |
| 7 | change | 先学号,再新数据 | 原记录和新记录 | 是,覆盖数据 |
这张表在答辩时很有用:老师问“你这个系统有哪些功能”,你可以直接按表说清楚每个功能的输入、输出和对链表的影响。
4. 避坑与排查:五个让课设当场翻车的细节
4.1 现象:菜单一闪而过,根本来不及选
原因:scanf读数字后缓冲区里残留换行符,getchar直接读到换行符,导致choose拿到\n,进入default分支,然后getchar(); getchar();又消耗掉后续输入,菜单循环加速。解决:在scanf后加getchar()吃掉换行,或者统一用scanf(" %c", &choose)前面的空格跳过空白符。源码里每个case后面的两次getchar就是补丁,但如果你自己改代码,很容易漏掉。
4.2 现象:删除头节点后,显示全部还是原来的第一条
原因:del函数里判断头节点时用了if(p->student.num == k) *r = (*r)->next, free(p);,但如果p是局部变量且没有重新指向*r,或者调用时传的是一级指针,头指针不会更新。解决:删除头节点必须用二级指针struct stucode **r,并且*r = (*r)->next之后要free旧头。源码里del的签名是void del(struct stucode **r),调用时传&r,这是对的。如果你改成一级指针,头删就会失效。
4.3 现象:插入后顺序乱了,或者插入重复学号
原因:insert里的比较条件是t->next->student.num <= p->student.num,用了<=,意味着相同学号会插到已有节点后面。如果你希望学号唯一,应该改成<,并在插入前先查找是否已存在。另外,如果链表本身不是按学号有序的(比如先用了createlist但输入顺序乱),插入后的“有序”只是局部有序,整体还是乱的。解决:要么在创建时就按学号排序,要么插入前先遍历检查。
4.4 现象:修改学号后,再按学号查找找不到
原因:change函数允许修改p->student.num,但链表的位置没有调整。比如原来学号 1002 在 1001 后面,你把 1002 改成 1005,它还在 1001 后面,但按学号查找时,遍历到 1001 后下一个是 1005,逻辑上没问题;但如果你把 1002 改成 0999,它应该排到 1001 前面,实际还在后面,有序性被破坏。解决:修改学号后,要么重新排序,要么先删除再插入。课设场景下,常见做法是修改时不改学号,只改姓名和成绩。
4.5 现象:程序运行几次后内存占用越来越大
原因:createlist里if (*r) *r = NULL;直接丢弃旧链表,没有free每个节点。每次选 1 重新创建,旧节点全部泄漏。解决:在置空之前,用while(*r) { t = *r; *r = (*r)->next; free(t); }释放。虽然课设程序跑完就退出,操作系统会回收,但如果你要演示“多次创建”,内存泄漏会让程序越来越慢,甚至崩溃。
5. 从能跑到能改:把单链表课设扩展成带排序和统计的版本
5.1 加一个按总分排序的函数
原始链表只保证按学号有序,但成绩管理经常需要按总分排名。常见做法是写一个sort_by_total,用冒泡或选择排序交换节点数据,而不是交换节点指针。交换数据更简单,因为struct stud可以直接赋值:
void sort_by_total(struct stucode *r) { struct stucode *p, *q; struct stud temp; for (p = r; p != NULL; p = p->next) { for (q = p->next; q != NULL; q = q->next) { double total_p = p->student.score1 + p->student.score2; double total_q = q->student.score1 + q->student.score2; if (total_p < total_q) { // 降序:总分高的在前 temp = p->student; p->student = q->student; q->student = temp; } } } }参数说明:传入头指针r,函数内部只交换student结构体的内容,不改变next指针,所以链表长度和节点位置不变,但数据顺序变了。调用后选 6 显示,就是按总分从高到低。注意:排序后学号不再有序,如果还要按学号查找,需要重新按学号排序,或者改用其他查找策略。
5.2 加一个统计功能:平均分、最高分、不及格人数
统计不需要修改链表,只需要遍历累加。常见做法是写一个statistics函数,输出总分平均、单科平均、最高分、最低分、不及格人数。这里用double累加,最后除以节点数。注意节点数为 0 时要直接返回,避免除零。
void statistics(struct stucode *r) { int count = 0, fail1 = 0, fail2 = 0; double sum1 = 0, sum2 = 0; double max1 = -1, max2 = -1; struct stucode *p = r; if (!r) { printf("链表为空\n"); return; } while (p) { sum1 += p->student.score1; sum2 += p->student.score2; if (p->student.score1 < 60) fail1++; if (p->student.score2 < 60) fail2++; if (p->student.score1 > max1) max1 = p->student.score1; if (p->student.score2 > max2) max2 = p->student.score2; count++; p = p->next; } printf("人数:%d 平均1:%.2lf 平均2:%.2lf 最高1:%.2lf 最高2:%.2lf 不及格1:%d 不及格2:%d\n", count, sum1/count, sum2/count, max1, max2, fail1, fail2); }这个函数可以直接挂在菜单里,比如加一个选项 8。参数就是头指针,不需要二级指针,因为不修改链表结构。
5.3 把数据存到文件:退出前保存,启动时加载
课设答辩时,老师常问“你的数据能不能保存”。原始源码没有文件操作,每次运行都要重新录入。加文件读写不难:用fopen、fprintf、fscanf。保存时遍历链表,把学号、姓名、成绩1、成绩2写进文本文件;加载时按同样格式读,用createlist类似的逻辑重建链表。注意文件路径用相对路径,比如"students.txt",避免绝对路径在不同机器上失效。
void save_to_file(struct stucode *r) { FILE *fp = fopen("students.txt", "w"); if (!fp) { printf("无法保存\n"); return; } while (r) { fprintf(fp, "%ld %s %.2lf %.2lf\n", r->student.num, r->student.name, r->student.score1, r->student.score2); r = r->next; } fclose(fp); }加载函数用fscanf循环读,读到文件尾停止,每读一条就malloc一个节点挂到链表尾部。这样程序退出前选保存,下次启动选加载,数据就持久化了。
5.4 一个我反复用的验证习惯
每次改完链表操作函数,我不会直接跑完整流程,而是先造三条数据:一条头、一条中间、一条尾。然后依次测:删头、删中间、删尾、插入到头、插入到中间、插入到尾、修改头、修改尾。每一步之后按 6 看输出,确认指针没断、数据没丢。这个习惯帮我省了很多“明明逻辑对但结果不对”的排查时间。从那以后我每次改指针操作都强制走一遍这个最小用例集,希望帮到你。
本文还有配套的精品资源,点击获取