简介面向计算机相关专业课程设计场景的学生成绩管理系统资料包以C语言链表为核心数据结构配套课设报告与可运行代码适合需要完成同类作业、梳理系统设计与答辩展示的学生。系统包含菜单界面与密码登录支持学生信息录入、增加、删除、修改、显示、保存和读取并可查询单科成绩排名、学生成绩及不及格超过两科的名单代码采用多文件结构与模块化函数划分交互和健壮性较完整。压缩包共27个文件以10个.c源文件、10个.o编译文件和1个.h头文件为主另含docx课设报告、txt测试数据以及dev、win工程与exe可执行文件整体约837KB便于对照编译与运行。报告目录覆盖问题描述、需求分析、概要设计、详细设计、测试和总结等环节已有963人学习可为课程设计撰写、功能扩展和调试排错提供较完整的参考。1. 从数组到链表这套C语言成绩管理课设到底解决什么问题很多C语言课设做到成绩管理第一反应是开一个固定长度的数组学生人数一多就顶到上限插班、退学、改学号还得整体搬数据。这套C语言链表实现的学生成绩管理系统把增删改查压到节点指针上配了一份能直接照着写的课设报告代码核心就是数据结构与算法里最常考的链表。它不是插件也不是某个软件的附属模块而是一个能独立编译的C控制台项目课设报告代码更像一份可改的模板/素材功能覆盖录入、删除、修改、查找、排序、统计和文件读写。适合正在做课设、需要一份能跑能改的底稿的人也适合刚学完指针想找个完整项目练手的开发者。2. 工程骨架与数据模型结构体、菜单和模块目录怎么摆这套资源的工程骨架不复杂常见做法是单文件版和拆文件版各留一套。单文件版把结构体、链表操作、文件读写和菜单全塞进main.c交课设时方便老师直接编译拆文件版按职责分成student.h、student.c、file_io.c、main.c后面加功能不容易把主函数撑爆。我一般建议先看单文件版跑通再照着拆文件版理解模块边界因为课设报告里的“总体设计”和“详细设计”需要把模块调用关系写清楚单文件版反而容易漏掉层次。2.1 学生节点结构体与链表头指针链表版成绩管理的第一步不是写菜单而是把学生节点定稳。学号、姓名、三门课成绩、总分、平均分和next指针放在同一个结构体里后续插入、删除、排序都围绕这个结构体转。这里用固定长度字符数组存学号和姓名课设场景够用也避免动态字符串带来的二次释放问题。#include stdio.h #include stdlib.h #include string.h #define NAME_LEN 32 #define ID_LEN 20 typedef struct Student { char id[ID_LEN]; /* 学号按字符串比较避免前导0丢失 */ char name[NAME_LEN]; /* 姓名 */ float score[3]; /* 0:C语言 1:数学 2:英语 */ float total; /* 总分录入或修改后重算 */ float average; /* 平均分total / 3.0f */ struct Student *next; /* 下一个学生节点 */ } Student; /* 全局头指针在课设里常见拆文件版可以改成在 main 里传二级指针 */ Student *g_head NULL;逻辑说明Student同时承担数据节点和链表节点两个角色next指向下一个学生空链表时g_head NULL。总分和平均分不单独录入而是在创建节点和修改成绩后统一重算避免文件和内存里的统计值对不上。参数说明ID_LEN取 20 能放下常见学号NAME_LEN取 32 对中文姓名留了余量score[3]把三门课固定下来后面统计和排序不用再写三套变量。2.2 函数清单与菜单入口把函数清单先列出来课设报告的“详细设计”就能按表展开。下面这张表是我拆这类项目时常用的划分方式单文件版也能按这些函数名组织代码后面改多课程、加登录都从这张表找落点。函数作用关键参数create_node创建一个学生节点并算总分学号、姓名、三门课成绩append_student尾插到链表顺带查重学号头指针的二级指针、新节点find_by_id按学号查找返回节点指针头指针、学号delete_by_id按学号删除并释放内存头指针的二级指针、学号modify_by_id按学号修改姓名或成绩头指针、学号save_to_file把链表写成文本文件头指针、文件路径load_from_file从文本文件重建链表头指针的二级指针、文件路径sort_by_total按总分从高到低排序头指针print_stat输出人数、总分均分、最高最低头指针菜单入口决定交互体验也决定后面避坑章里输入缓冲问题从哪来。下面这个菜单函数只负责打印不负责读选择读选择放在主循环里统一处理这样后面加“按班级查询”“按课程排序”时不用动菜单结构。static void print_menu(void) { 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(8. 从文件读取\n); printf(0. 退出\n); printf(请选择: ); } /* 读一整行去掉换行并把超长输入剩余字符清掉 */ static void read_line(char *buf, size_t size) { if (fgets(buf, (int)size, stdin) NULL) { buf[0] \0; return; } size_t len strlen(buf); if (len 0 buf[len - 1] \n) { buf[len - 1] \0; } else { int ch; while ((ch getchar()) ! \n ch ! EOF) { } } }逻辑说明print_menu只输出编号主循环用read_line读一行再atoi比直接scanf(%d)更容易处理“输入字母”“多按回车”这类课设演示现场。read_line里先判断末尾是不是换行不是就说明输入超长用getchar把剩余字符吃掉避免下一次读姓名直接读到空串。参数说明buf是调用方给的字符数组size是数组容量函数保证结果以\0结尾fgets的第二个参数要转成int这是标准库签名要求。2.3 主循环怎么串起模块主循环不承担业务只做分发。下面这段代码把菜单、输入、函数调用串起来课设报告里的“主控模块”可以直接按这个画调用图。注意每个分支只做一件事保存和读取都单独成函数后面换二进制存储时只动file_io.c不动主循环。int main(void) { char line[64]; int choice; do { print_menu(); read_line(line, sizeof(line)); choice atoi(line); switch (choice) { case 1: /* 录入学生读学号、姓名、三门课create_node append_student */ break; case 2: /* 删除学生读学号delete_by_id */ break; case 3: /* 修改学生读学号modify_by_id */ break; case 4: /* 查找学生读学号find_by_id 后打印 */ break; case 5: /* 显示全部遍历 g_head */ break; case 6: /* 统计与排序sort_by_total print_stat */ break; case 7: /* 保存save_to_file(g_head, students.txt) */ break; case 8: /* 读取load_from_file(g_head, students.txt) */ break; case 0: printf(退出\n); break; default: printf(无效选择\n); break; } } while (choice ! 0); /* 退出前释放整条链表课设里不写也能跑但内存检查会报泄漏 */ return 0; }逻辑说明choice用atoi从字符串转整数输入非数字时得到 0直接落到退出分支不会卡在scanf的失败状态里。每个case里只写调用具体读写放在对应函数中课设报告写“模块独立性”时有东西可写。参数说明line长度 64 对菜单选择足够read_line会清掉换行g_head是全局头指针拆文件版可以改成Student *head NULL;然后在函数间传head。3. 核心链表操作拆解插入、删除、查找、修改的完整链路链表版成绩管理的重头戏在增删改查课设答辩也最爱问“删除节点后内存有没有释放”“头节点删除怎么处理”。这一章按“创建节点 → 尾插 → 查找 → 删除 → 修改”的顺序拆代码可以直接抄进单文件版拆文件版把函数声明放进student.h即可。每个操作都围绕学号做唯一键插入前查重删除和修改都先查找逻辑不容易乱。3.1 创建节点与尾插把新学生挂到链表末尾创建节点时把总分和平均分一次算好后面排序和统计直接读字段不用每次遍历重算。尾插适合课设演示因为录入顺序就是显示顺序老师看功能时不会觉得数据跳来跳去如果要做“按总分插入”把尾插改成有序插入即可但先跑通尾插更稳。Student *create_node(const char *id, const char *name, float c, float math, float english) { Student *node (Student *)malloc(sizeof(Student)); if (node NULL) { return NULL; /* 内存不足调用方负责提示 */ } strncpy(node-id, id, sizeof(node-id) - 1); node-id[sizeof(node-id) - 1] \0; strncpy(node-name, name, sizeof(node-name) - 1); node-name[sizeof(node-name) - 1] \0; node-score[0] c; node-score[1] math; node-score[2] english; node-total c math english; node-average node-total / 3.0f; node-next NULL; return node; } /* 尾插返回 0 成功-1 参数错误-2 学号重复 */ int append_student(Student **head, Student *node) { if (head NULL || node NULL) { return -1; } if (find_by_id(*head, node-id) ! NULL) { return -2; } if (*head NULL) { *head node; return 0; } Student *p *head; while (p-next ! NULL) { p p-next; } p-next node; return 0; }逻辑说明create_node用strncpy限制拷贝长度再手动补\0防止姓名刚好卡满数组时没有结束符。append_student用二级指针Student **head空链表时直接改*head不需要返回新头指针删除头节点时也能复用这套写法。参数说明id和name是调用方读到的字符串函数内部会拷贝不保存外部指针c、math、english是单科成绩total和average在节点创建时算好head指向头指针的地址调用时写append_student(g_head, node)。3.2 按学号查找与删除头节点、中间节点、尾节点都要覆盖查找是删除和修改的前置操作返回节点指针后修改可以直接改字段删除则要重新遍历找前驱。删除函数必须处理三种情况删除头节点、删除中间节点、删除尾节点。很多课设代码在删除头节点时只改head不释放或者在删除尾节点时把prev-next置空后忘了释放cur答辩时被问一句“内存去哪了”就答不上来。Student *find_by_id(Student *head, const char *id) { Student *p head; while (p ! NULL) { if (strcmp(p-id, id) 0) { return p; } p p-next; } return NULL; } /* 删除成功返回 0空表或未找到返回 -1 */ int delete_by_id(Student **head, const char *id) { if (head NULL || *head NULL) { return -1; } Student *cur *head; Student *prev NULL; while (cur ! NULL) { if (strcmp(cur-id, id) 0) { if (prev NULL) { *head cur-next; /* 删除头节点 */ } else { prev-next cur-next; /* 删除中间或尾节点 */ } free(cur); /* 先摘链再释放 */ return 0; } prev cur; cur cur-next; } return -1; }逻辑说明find_by_id只读遍历找不到返回NULL调用方必须判空。delete_by_id用prev记住前驱找到目标后分两种情况改链prev NULL说明目标是头节点直接让*head指向cur-next否则让prev-next跨过cur。摘链完成后再free(cur)顺序不能反否则cur-next读不到。参数说明head是头指针地址删除头节点时通过*head改主函数里的指针id是待删学号按字符串比较返回-1时调用方提示“学号不存在或表为空”。3.3 修改学生信息只改允许改的字段改完重算总分修改函数先用find_by_id定位再逐项读新值。课设里常见做法是“直接回车表示不改”这样演示时不用重复输入学号。姓名和成绩改完后必须重算total和average否则排序和统计会读到旧值。学号一般不开放修改因为学号是链表里的唯一键改了要重新查重课设阶段没必要给自己加难度。int modify_by_id(Student *head, const char *id) { Student *p find_by_id(head, id); if (p NULL) { return -1; } char buf[64]; printf(新姓名(直接回车不改): ); read_line(buf, sizeof(buf)); if (buf[0] ! \0) { strncpy(p-name, buf, sizeof(p-name) - 1); p-name[sizeof(p-name) - 1] \0; } printf(新C语言成绩(输入-1不改): ); read_line(buf, sizeof(buf)); if (buf[0] ! \0 atof(buf) 0.0f) { p-score[0] (float)atof(buf); } /* 数学、英语按同样方式读取这里省略重复代码 */ p-total p-score[0] p-score[1] p-score[2]; p-average p-total / 3.0f; return 0; }逻辑说明read_line保证读到的是整行空串表示跳过该项成绩用atof转浮点输入-1表示不改所以判断条件写成 0.0f。修改完成后统一重算总分和平均分避免三个成绩改了俩、统计值还是旧的。参数说明head是链表头指针id是定位用的学号buf是临时输入缓冲长度 64 对姓名和成绩都够用函数返回-1表示学号不存在调用方提示后回到菜单。3.4 遍历显示与释放整表退出前把内存还回去显示全部和释放整表都靠遍历但释放时不能在free(p)后继续用p-next必须先用next存下一个节点。课设演示时程序退出就结束不释放也能跑但如果有内存检查环节整表释放是必查项。显示函数可以把表头打出来再用% -10s对齐报告里的“运行截图”会好看很多。void print_all(Student *head) { if (head NULL) { printf(暂无学生记录\n); return; } printf(%-12s %-10s %8s %8s %8s %8s %8s\n, 学号, 姓名, C语言, 数学, 英语, 总分, 平均分); for (Student *p head; p ! NULL; p p-next) { printf(%-12s %-10s %8.1f %8.1f %8.1f %8.1f %8.2f\n, p-id, p-name, p-score[0], p-score[1], p-score[2], p-total, p-average); } } void free_list(Student **head) { if (head NULL) { return; } Student *p *head; while (p ! NULL) { Student *next p-next; /* 先存下一个再释放当前 */ free(p); p next; } *head NULL; }逻辑说明print_all先判空避免空表时表头下面一片空白格式化输出里学号和姓名左对齐成绩右对齐截图更整齐。free_list用next暂存后继节点释放当前节点后还能继续往后走最后把*head置空防止野指针。参数说明print_all只读头指针不修改链表free_list用二级指针是为了把主函数里的头指针一起置空调用时写free_list(g_head)。4. 文件持久化与统计排序成绩数据落盘、排名和报表输出课设如果只做内存链表关掉程序数据就没了答辩时老师一般会要求“下次打开还能看到上次录的学生”。文件持久化不用一上来就写二进制文本文件更适合课设用记事本就能打开检查格式错了也能一眼看出来。这一章按“保存 → 读取 → 统计 → 排序”拆保存和读取的返回值要能区分“文件打不开”和“读到了 0 条”排序则要注意别把next指针交换断。4.1 文本文件保存一行一个学生字段用空格分开保存函数遍历链表把学号、姓名、三门课成绩写到文件里。总分和平均分不存读取时用create_node重算这样文件格式更干净后面改统计规则也不用迁移旧文件。文件路径建议做成宏或常量课设演示时在当前目录生成students.txt老师打开就能看。int save_to_file(Student *head, const char *path) { FILE *fp fopen(path, w); if (fp NULL) { return -1; /* 路径不可写或磁盘满 */ } int count 0; for (Student *p head; p ! NULL; p p-next) { fprintf(fp, %s %s %.1f %.1f %.1f\n, p-id, p-name, p-score[0], p-score[1], p-score[2]); count; } fclose(fp); return count; /* 返回写入条数0 表示空表也写成功了 */ }逻辑说明fopen用w覆盖写每次保存都把当前链表完整落盘不做追加避免重复保存后文件里出现两份数据。fprintf的格式和读取端的fscanf必须严格对应成绩用%.1f保留一位小数课设够用。参数说明head是链表头指针空表时循环不执行仍然会创建空文件并返回 0path是文件路径调用时传students.txt或绝对路径返回-1表示文件打不开调用方要提示用户检查路径权限。4.2 从文件重建链表用 fscanf 返回值控制循环读取文件时最容易翻车的是while (!feof(fp))因为feof要到读失败之后才置位循环里会多处理一条旧数据。正确做法是直接用fscanf的返回值判断有没有读满 5 个字段读满才创建节点。读取前可以先清空旧链表或者把新节点挂到临时头指针上读完再替换避免读一半失败把原数据弄丢。int load_from_file(Student **head, const char *path) { FILE *fp fopen(path, r); if (fp NULL) { return -1; } char id[ID_LEN]; char name[NAME_LEN]; float c, math, english; int count 0; /* 读满 5 个字段才处理fscanf 返回成功赋值个数 */ while (fscanf(fp, %19s %31s %f %f %f, id, name, c, math, english) 5) { Student *node create_node(id, name, c, math, english); if (node NULL) { break; } if (append_student(head, node) ! 0) { free(node); /* 学号重复或参数错误释放新节点 */ continue; } count; } fclose(fp); return count; }逻辑说明fscanf的%19s和%31s限制字符串宽度防止文件里的超长字段撑爆数组返回值为 5 说明学号、姓名、三门成绩都读到了才进入建节点流程。append_student失败时释放node避免重复学号导致内存泄漏。参数说明head是头指针地址函数会把读到的节点追加到现有链表后面path是待读文件路径返回-1表示文件打不开返回 0 表示文件存在但没有有效记录返回正数表示成功读取的条数。4.3 统计总分、平均分、最高最低一次遍历拿全指标统计函数不需要额外存储遍历一遍就能算出人数、总分均分、最高总分和最低总分。课设报告里的“统计报表”可以把单科平均分也加上但先跑通总分统计更稳。空表要单独提示否则除零会得到nan截图里出现nan很尴尬。void print_stat(Student *head) { int count 0; float sum_total 0.0f; float max_total -1.0f; float min_total 1000.0f; for (Student *p head; p ! NULL; p p-next) { count; sum_total p-total; if (p-total max_total) { max_total p-total; } if (p-total min_total) { min_total p-total; } } if (count 0) { printf(暂无学生记录无法统计\n); return; } printf(人数: %d\n, count); printf(总分均分: %.2f\n, sum_total / count); printf(最高总分: %.2f\n, max_total); printf(最低总分: %.2f\n, min_total); }逻辑说明count在循环里累加sum_total累加总分max_total和min_total用比较更新循环结束就能输出四个指标。min_total初始值取 1000 比任何三门课总分都大第一轮就会被真实数据替换。参数说明head是链表头指针只读函数没有返回值统计结果直接打印空表分支必须放在除法之前避免sum_total / count除零。4.4 按总分排序交换数据字段不交换 next 指针链表排序有两种写法一种交换节点位置改next指针一种只交换节点里的数据字段链表结构不动。课设阶段推荐第二种因为改指针容易把链表拆断尤其冒泡排序里同时动i和j的后继调试成本高。下面这个版本用双层循环外层从前往后内层从i-next开始发现后面总分更高就交换业务字段next保持原样。static void swap_student_data(Student *a, Student *b) { char tmp_id[ID_LEN]; char tmp_name[NAME_LEN]; float tmp_score[3]; float tmp_total, tmp_avg; strcpy(tmp_id, a-id); strcpy(tmp_name, a-name); memcpy(tmp_score, a-score, sizeof(tmp_score)); tmp_total a-total; tmp_avg a-average; strcpy(a-id, b-id); strcpy(a-name, b-name); memcpy(a-score, b-score, sizeof(a-score)); a-total b-total; a-average b-average; strcpy(b-id, tmp_id); strcpy(b-name, tmp_name); memcpy(b-score, tmp_score, sizeof(b-score)); b-total tmp_total; b-average tmp_avg; } void sort_by_total(Student *head) { if (head NULL || head-next NULL) { return; } for (Student *i head; i ! NULL; i i-next) { for (Student *j i-next; j ! NULL; j j-next) { if (j-total i-total) { swap_student_data(i, j); } } } }逻辑说明swap_student_data只交换学号、姓名、三门成绩、总分和平均分不碰next所以链表节点顺序不变变的只是节点里的内容。sort_by_total外层每走一步当前节点就固定为从它开始到末尾的最大总分内层继续往后比较。参数说明a和b是待交换的两个节点指针调用前保证非空head是链表头指针空表或只有一个节点直接返回不需要排序。5. 避坑与排查链表课设最容易翻车的五类现场这类课设代码量不大但坑很集中输入缓冲、删除释放、文件读多一条、排序交换断链、重复学号。下面按“现象 → 原因 → 解决”拆五条都是我在看这类C语言链表课设时反复见到的现场。每条都给可抄的修法答辩前照着过一遍能少被问住几次。5.1 指针和内存现场删除后打印乱码、重复插入不报错现象一删除一个学生后显示全部时出现乱码或者程序直接崩。原因删除函数里先free(cur)然后继续用cur-next或者prev-next访问已经释放的内存堆上的内容被回收后就是野指针。另一种写法是删除头节点时只把head往后移没有释放原头节点内存检查会报泄漏。解决严格按“先摘链再释放”的顺序写删除头节点用*head cur-next删除中间和尾节点用prev-next cur-next摘链完成后再free(cur)。如果要在删除后打印被删学生的姓名先在free之前把姓名拷到临时数组或者先打印再释放。现象二录入学号重复的学生程序不提示链表里出现两个相同学号。原因插入函数只负责挂节点没有查重或者查重写在菜单分支里换一个入口调用append_student就漏了。学号是后面删除、修改、查找的唯一键重复学号会让find_by_id永远只返回第一个第二个变成删不掉的“黑匣子”。解决把查重放进append_student插入前统一调find_by_id(*head, node-id)返回非空就拒绝插入并返回-2。菜单分支拿到-2后提示“学号已存在”并释放刚创建的节点避免新节点没人挂链又没释放。现象三程序退出时没有释放链表内存检查工具报一堆泄漏。原因课设演示只看功能退出直接return 0每个malloc出来的节点都还挂在堆上。单次运行看不出问题但如果老师用检查工具跑一遍或者后面改成循环重启菜单泄漏会累积。解决在main返回前调free_list(g_head)函数里先用next暂存后继再free(p)最后把*head NULL。如果菜单支持“清空全部”也复用这个函数不要另写一套遍历释放。5.2 输入、文件和排序现场回车残留、feof 多读、交换断链现象四录入学号后姓名还没输入就直接跳过了或者菜单选择输错一次后一直循环。原因前面用scanf(%d, choice)读菜单回车符留在输入缓冲里后面的fgets读到空行或者scanf(%s)读字符串时遇到空格就停姓名里带空格只读到一半。课设演示时输入节奏一快这种问题必现。解决统一用read_line读整行菜单选择也用字符串接再atoi不要在同一个程序里混用scanf和fgets。如果必须用scanf读数字读完立刻用while (getchar() ! \n)清缓冲。姓名允许空格时用fgets去掉末尾换行后再存节点。现象五从文件读取时最后一条学生重复出现或者文件里明明有 3 条却读了 4 条。原因循环写成while (!feof(fp))feof只在读操作失败后才置位最后一次成功读取后循环还会再进一次fscanf没读到新值旧变量被重复建节点。另一种原因是fscanf返回值没判断文件末尾的半条记录也被当成完整记录。解决用while (fscanf(...) 5)控制循环读满 5 个字段才建节点文件格式里一行一个学生字段之间用空格读取端用%19s %31s %f %f %f限制宽度。读取前可以先打印“开始读取”读完打印实际条数和文件行数对不上时先看最后一行有没有多余空格或空行。现象六按总分排序后显示全部只输出一两个学生后面全丢了。原因排序时直接交换了整个Student结构体把next指针也交换了链表结构被拆断或者冒泡排序里同时修改i和j的后继循环条件失效。课设里最常见的错误是写Student tmp *i; *i *j; *j tmp;看起来交换了数据实际上把两个节点的后继也换了。解决只交换业务字段不交换next用swap_student_data把学号、姓名、成绩、总分、平均分逐个换。排序后先打印一遍总分确认从高到低再用“显示全部”检查人数有没有变少人数变少基本就是链断了回到排序函数检查有没有碰next。现象七保存文件后再次读取学号前面的 0 丢了或者中文姓名变成乱码。原因学号用int存2024001没问题但001这类学号存成整数就变成1中文姓名用char存但文件编码和终端编码不一致记事本打开正常、控制台输出乱码。解决学号统一用字符数组存文件里也按字符串读写比较用strcmp不要转int。中文姓名在课设里用 GBK 或 UTF-8 都行但源码文件、终端和记事本要保持一致如果控制台乱码先把姓名改成英文测试确认是编码问题还是链表问题。6. 二次开发与验证把课设改成能写进简历的小系统这套代码跑通之后最值的改造不是加花哨界面而是把“单机课设”往“能演示的小系统”推一步。我一般先加一个test.c不连菜单直接调链表函数把边界跑一遍空表删除返回-1、重复学号插入返回-2、文件不存在读取返回-1、删除头节点后人数减一。下面这张表是我常改的四个点每个点都带验证方式改完能说清楚“为什么改、怎么验证”。改造点落点验证方式加课程维度Student里把score[3]扩成score[5]统计和排序按总分字段走录入 5 门课检查总分和平均分是否重算加简单登录main进入菜单前读口令口令错不进入主循环错口令连续输 3 次确认不会读到菜单换二进制存储save_to_file/load_from_file改fwrite/fread保存后文件变小读取条数和保存前一致加按班级查询节点加class_id查找函数多一个班级过滤两个班各录 2 人查一个班只出 2 条/* test.c 里先测空链表删除不连菜单也能跑 */ Student *head NULL; assert(delete_by_id(head, 2024001) -1); assert(find_by_id(head, 2024001) NULL);上面这段测试代码不依赖菜单输入编译时把student.c一起链进去就能跑断言失败会直接停在出错行比在菜单里反复点更快定位。参数说明head初始为空delete_by_id传head才能改头指针断言的预期值按函数约定写改函数返回值时测试也要同步改。从那以后我每次接课设类项目都先把输入缓冲、删除释放和文件读写的边界各写一个小测试再动业务代码这套资源我也建议你按这个顺序拆先跑通main再对照报告里的模块图改。希望帮到你。本文还有配套的精品资源点击获取