资讯详情

C语言单链表学生成绩管理系统课设源码与避坑指南

📅 2026/10/9 11:10:01 | 华诺云谱 👁 阅读
C语言单链表学生成绩管理系统课设源码与避坑指南
简介这份资源是面向计算机相关专业学生的数据结构与算法课程设计参考文档聚焦学生成绩管理系统的完整设计与实现适合正在完成课设、需要参考系统架构与代码实现的学习者。文档围绕数组、链表、栈、队列、树、图等数据结构以及冒泡排序、选择排序、插入排序、顺序搜索、二分搜索、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()打印选项然后用choosegetchar()读一个字符进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一个节点填数据nextNULL让*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 用一张表看清七个函数的输入输出菜单项函数输入输出是否修改链表1createlist学号 姓名 成绩1 成绩20 结束无随后可显示是重建链表2search1学号匹配记录或错误否3search2姓名匹配记录或错误否4del学号无随后可显示是删除节点5insert学号 姓名 成绩1 成绩2无随后可显示是有序插入6out无全部记录否7change先学号再新数据原记录和新记录是覆盖数据这张表在答辩时很有用老师问“你这个系统有哪些功能”你可以直接按表说清楚每个功能的输入、输出和对链表的影响。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 看输出确认指针没断、数据没丢。这个习惯帮我省了很多“明明逻辑对但结果不对”的排查时间。从那以后我每次改指针操作都强制走一遍这个最小用例集希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑