ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C++数据结构实训:基于链表的作业管理系统实现与避坑指南

C++数据结构实训:基于链表的作业管理系统实现与避坑指南 简介一套用于数据结构C实训的作业完成情况管理程序资源适合正在学习数据结构与C面向对象编程的高校学生旨在通过实现作业添加、更新与查询功能帮助学习者掌握数组、链表、栈与队列等数据结构的实际应用。压缩包内共十一个文件整体大小约为三点二兆字节主要包含文档格式的实习论文与实施计划书、幻灯片答辩汇报、源码文件以及项目配置文件与作业数据文件等便于对照学习完整的开发脉络。目前已有六百五十五人学习下载属于轻量实用的课程设计参考。通过其中的程序源码、实训论文、实施计划书和汇报幻灯片既可理解从需求分析到代码实现再到成果展示的全程也能学习如何用数组或链表等结构高效组织作业信息适合正在完成类似实训或复习C与数据结构的学生。1. 实训项目拆解作业完成情况管理到底在考什么拿到“数据结构C实训作业完成情况管理程序.zip”这个压缩包时你多半正对着老师发下来的实训选题发愁。别被这个略显平淡的名字骗了——作业完成情况管理听起来像是个“数组加几个if”的CRUD程序实际上它把数据结构课里最核心的几个考点全串起来了链表节点的动态内存管理、按关键字查找与删除、遍历统计、以及程序退出后数据怎么不丢。真正拉开差距的不是你会不会写链表而是指针操作是否稳健、文件读写是否周全。这个项目适合两类人一是数据结构课开了C实验、需要在有限时间内交出一份能跑通全部验收点的学生二是刚学完类与对象、想用一个小而完整的项目把“封装 指针 文件流”练扎实的C初学者。本文按我平时带实训的套路从选型、骨架设计、功能实现到文件持久化一步步把这个程序写完并把那些不看血泪经验根本发现不了的坑提前指给你。2. 数据结构选型与骨架设计为什么是单链表而不是顺序表2.1 三种候选结构的对比数组、链表、STL容器作业完成情况管理最朴素的做法是开一个固定数组里面放结构体。常见做法是定义一个struct Student { string id; string name; int status; int score; };然后Student arr[100]。数组的优点是随机访问快、代码直观但三个问题在实训验收时会被放大第一删除中间某条记录要把后面所有元素前移时间复杂度O(n)且容易漏移第二数组长度写死实训要求里“不限人数”就不好交代第三动态添加超出最大容量时得手写扩容逻辑很容易翻车。STL容器像vector或list当然能用但多数实训评分标准里明确写了“要求手动实现链表或其他数据结构”直接用库会扣分。而且说实话用vector你根本练不到new和delete的配合实训就失去意义了。所以核心存储结构选单向链表不选双向——这个业务的遍历方向是“从头到尾”添加记录走尾插删除和查找都是线性扫描双向链表多出来的prev指针只有删除中间节点时有一点优势却要多管理一块内存对于新手来说把next一个指针维护好就已经很不容易了。2.2 程序骨架三个类各管一段我一般会把程序拆成三个层次HomeworkNode只管一条作业记录的数据HomeworkList管所有节点的组织App管用户交互和文件读写。这样拆的好处是每个类单测起来很干净你改文件格式不用动链表改链表结构不用动主菜单。// main.h #ifndef MAIN_H #define MAIN_H #include iostream #include fstream #include string using namespace std; // 作业完成状态 enum HomeworkStatus { STATUS_UNSUBMITTED 0, // 未提交 STATUS_SUBMITTED 1, // 已提交未批改 STATUS_GRADED 2 // 已批改 }; // 单条作业记录 struct HomeworkNode { string studentId; // 学号作为查找主键 string studentName; // 姓名 string assignId; // 作业编号 string assignName; // 作业名称 int status; // 完成状态取 HomeworkStatus 枚举值 int score; // 成绩未批改时用 -1 表示 string submitDate; // 提交日期格式 yyyy-mm-dd HomeworkNode* next; // 指向下一条记录链表的精髓所在 HomeworkNode() : status(STATUS_UNSUBMITTED), score(-1), next(nullptr) {} }; // 单向链表管理所有作业记录 class HomeworkList { private: HomeworkNode* head; // 头指针始终指向第一个节点 int nodeCount; // 节点数量添加/删除时同步维护 public: HomeworkList(); ~HomeworkList(); bool insertNode(const HomeworkNode node); // 尾插 bool deleteNode(const string studentId, const string assignId); HomeworkNode* findNode(const string studentId, const string assignId); int getCount() const { return nodeCount; } void clearAll(); // 释放所有节点 HomeworkNode* getHead() const { return head; } }; #endif这里的核心设计是把“作业编号 学号”作为唯一主键而不是只靠学号。原因很实际一个人要交多份作业一份作业要收全班几十个人只用学号删数据会把该同学的所有记录全删掉实训验收时演示“删除一条记录”就没法收敛。nodeCount这个字段是很多人会漏的每次插入和删除都维护它统计功能里就不用遍历两遍链表。2.3 构造函数与析构函数内存管理的两个闸门链表最容易出事的两个位置就是开头和结尾——初始化时头指针没置空、程序退出时节点没释放。析构函数里的清理一定要单独抽一个clearAll()出来因为除了程序退出时要调用用户在菜单里选“清空所有记录”时也要复用两处都用同一段逻辑才不会出现“退出时没内存泄漏、清空时却把链表弄坏”的尴尬。// main.cpp 中链表的构造与析构 HomeworkList::HomeworkList() : head(nullptr), nodeCount(0) {} HomeworkList::~HomeworkList() { clearAll(); } void HomeworkList::clearAll() { HomeworkNode* cur head; while (cur ! nullptr) { HomeworkNode* temp cur; // 先保存当前节点地址 cur cur-next; // 再往后移动 delete temp; // 最后释放当前节点 } head nullptr; nodeCount 0; }析构里释放链表的顺序有讲究先把cur-next存到cur的下一个位置再delete当前节点。顺序反过来的话你删了当前节点它的next指针所在的这块内存已经归还给系统了再去读cur-next就是读野指针这在实训跑起来时往往不是必现但每次退出程序都有可能崩一次属于典型的“看运气翻车”。释放完后记得把head和nodeCount归零不然析构之后还有人调用getCount()或findNode()拿到的就是一个挂着空指针的链表对象。2.4 为什么业务字段用 string 而不是 char 数组C语言版数据结构教材里习惯用char name[20]因为C语言只有字符数组。到了C实训我强烈建议直接用std::string省去strcpy、strcmp、strlen三件套不说关键是不用操心数组越界——你永远不知道用户在控制台里会输入多长的名字string虽然底层也在堆上动态分配但对使用者来说是透明的。这一点在你做文件读写时要格外留意后面持久化那一章会讲到字符串解析的坑。3. 增删改查落地链表的插入、删除与查找怎么写才不丢节点3.1 尾插先画图再写指针链表的每个操作我都建议先用纸笔画一下节点图再落代码。尾插的场景是链表可能为空也可能已经有节点。空链表时直接把head指向新节点非空时找到最后一个next nullptr的节点把它的next接到新节点上。这个操作本身不难但它要求“找到尾节点”和“接到尾节点”分两步很多同学写成一个循环把p p-next一路走到底退出循环后p已经是nullptr再写p-next newNode就直接写到了空地址上——这是新手写链表最常见的段错误来源。// 尾插把一条新的作业记录加到链表尾部 bool HomeworkList::insertNode(const HomeworkNode node) { HomeworkNode* newNode new HomeworkNode(node); // 调用拷贝构造 if (newNode nullptr) { return false; // 内存分配失败实训环境少见但要做防御 } if (head nullptr) { head newNode; // 空链表新节点即头节点 } else { HomeworkNode* cur head; while (cur-next ! nullptr) { cur cur-next; // 一路走到尾 } cur-next newNode; // 此刻 cur 是尾节点接上 } nodeCount; return true; }new HomeworkNode(node)依赖编译器生成的默认拷贝构造函数把node的所有成员逐字节拷到新对象里。这里有一个隐藏陷阱如果HomeworkNode里定义了指针成员且你没有自己写拷贝构造默认拷贝构造会把指针值也复制一份导致两个节点指向同一块内存析构时被delete两次直接崩溃。这个程序里节点没有指针指向外部资源所以默认拷贝安全往后你要是给节点加一个char*成员记得回来补拷贝构造函数。参数说明studentId和assignId是复合主键两个都相同才认为是同一条记录所以插入前最好先调用findNode查重避免同一份作业被录两次。3.2 按学号 作业编号删除三步操作缺一不可删除是链表操作里最容易出错的因为要同时维护两个指针当前指针cur和前驱指针prev。单向链表的删除逻辑是先一直向前找找到目标节点后让prev-next跳过cur直接指向cur-next再把cur释放。这里的“跳过”动作一旦写成cur cur-next链表就断了后面的所有节点全部丢失。// 按学号 作业编号删除记录 bool HomeworkList::deleteNode(const string studentId, const string assignId) { if (head nullptr) { return false; // 空链表没什么可删的 } HomeworkNode* cur head; HomeworkNode* prev nullptr; while (cur ! nullptr) { if (cur-studentId studentId cur-assignId assignId) { // 找到目标节点分两种情况处理 if (prev nullptr) { head cur-next; // 删除的是头节点head 要后移 } else { prev-next cur-next; // 前驱直接跳过当前节点 } delete cur; nodeCount--; return true; } prev cur; // 保存前驱 cur cur-next; // 继续走 } return false; // 走完全链表都没找到 }这段代码里最容易被忽略的是prev nullptr的判断。如果目标节点刚好是头节点prev是空的此时直接写prev-next cur-next就是对空指针解引用程序必崩。实训里演示删除头节点是老师最常做的操作之一这个分支不写你就当场翻车。另一个细节删除成功要立刻return true不能继续往下走否则cur已经delete再访问就是野指针。nodeCount--别忘了不然你的统计模块算出的总人数永远比实际多。3.3 查找与修改返回指针还是返回引用查找功能在实训中一般用来支持“按学号查询该生所有作业”和“按作业编号查班级提交情况”。findNode返回HomeworkNode*调用方拿到指针后可以直接修改成绩、状态等字段。这里有个释放归属问题查找返回的指针是链表内真实节点的地址调用方绝不能对它delete否则链表自身就缺了一截之后任何遍历都会崩溃。我见过有同学把“查找”和“删除”混在一起找到后顺手delete导致程序运行到第二次查找时读到一个已释放的内存地址报错信息还特别诡异一会儿崩一会儿不崩。// 查找一条记录学号 作业编号精确匹配 HomeworkNode* HomeworkList::findNode(const string studentId, const string assignId) { HomeworkNode* cur head; while (cur ! nullptr) { if (cur-studentId studentId cur-assignId assignId) { return cur; // 返回的是链表内部节点的地址 } cur cur-next; } return nullptr; // 没找到 }查找的时间复杂度是O(n)因为单向链表不支持随机访问。实训里数据规模就几百条这个复杂度完全够用如果将来数据量上万再去改索引结构也不迟。修改成绩的流程是先用findNode拿到节点指针然后直接ptr-score 95; ptr-status STATUS_GRADED;即可不需要额外的更新函数。把“查找”和“修改”分开的好处是职责清晰后续你想加“按姓名模糊查询”或“按分数区间筛选”都复用这段遍历逻辑。3.4 排序冒泡排序在链表上的别扭与妥协实训要求里通常有“按成绩排序输出”这一项。最常见的做法是把链表节点里的数据搬运到vectorHomeworkNode用现成的sort排序再从数组重建链表。有些老师严格要求“原地排序”那就只能在链表上写冒泡排序——每次比较相邻两个节点的评分字段不合序就交换节点里的业务数据。这里要慎重交换next指针本身因为那会牵动链表的拓扑结构调试难度陡增。// 链表上的冒泡排序按成绩降序只交换数据不交换指针 void HomeworkList::sortByScoreDesc() { if (head nullptr || head-next nullptr) { return; // 空链表或只有一个节点无需排序 } HomeworkNode* end nullptr; while (end ! head) { HomeworkNode* cur head; bool swapped false; while (cur-next ! end) { if (cur-score cur-next-score) { // 交换两个节点的业务数据除了 next 指针 swap(cur-studentId, cur-next-studentId); swap(cur-studentName, cur-next-studentName); swap(cur-assignId, cur-next-assignId); swap(cur-assignName, cur-next-assignName); swap(cur-status, cur-next-status); swap(cur-score, cur-next-score); swap(cur-submitDate, cur-next-submitDate); swapped true; } cur cur-next; } end cur; if (!swapped) { break; // 本轮没有交换说明已有序提前退出 } } }这个实现里有两个设计决定值得说明。第一只交换业务数据、不交换next指针这样链表的结构从头到尾没变过你调试时打印一遍链表地址会发现每个节点的next关系始终一致只是节点里的内容变了逻辑上清晰很多。第二加了swapped提前退出的优化避免已经有序的链表白白多走几轮。代价是排序一趟要多交换好几次字符串数据量大时性能不好看——但实训数据量下完全无感。真正要紧的是如果你只交换score那学号、姓名、作业编号全和成绩错位输出结果会让人看不懂所以交换字段要一次全换。4. 文件持久化与统计模块程序关了数据还在才算完成实训4.1 文件格式选型文本格式优先别急着上二进制实训验收时老师大概率会做“添加记录 → 保存 → 关闭程序 → 重新打开 → 加载数据 → 确认记录还在”这条闭环。实现持久化有两个方向二进制文件和文本文件。文本文件的优势是可以用记事本打开肉眼检查每一行格式是否正确验收现场即便数据不对也能当场排查二进制文件省空间、读写快但一旦格式设计错了数据就是一团乱码调试体验非常差。我建议实训项目一律用文本格式字段之间用竖线|分隔而不是逗号——因为姓名和作业名称里可能出现英文字符串中含逗号竖线的出现概率几乎为零。// 保存所有记录到文本文件每行一条 bool saveToFile(HomeworkList list, const string filename) { ofstream fout(filename.c_str(), ios::out); if (!fout.is_open()) { cout 无法创建文件: filename endl; return false; } HomeworkNode* cur list.getHead(); while (cur ! nullptr) { fout cur-studentId | cur-studentName | cur-assignId | cur-assignName | cur-status | cur-score | cur-submitDate \n; cur cur-next; } fout.close(); return true; }ofstream的默认打开模式就会截断原文件重写所以这里不需要额外设ios::trunc。每一行结束都要写\n最后一个节点后面也要有换行不然读取时用getline处理最后一行会少一个分隔符容易在空行判断上出错。注意status和score直接写整数不需要转换成字符串operator会自动处理。读取时因为status是int直接用流运算符读也可以但我的习惯是每行都用getline读成字符串再手动解析这样对“某一行缺字段”“某一行多了空格”的异常情况控制力更强。4.2 读取与解析一行一个记录字段个数要对得上读取文件是持久化里最容易翻车的环节因为文件可能是旧的、空的、被手动改坏的。写解析代码时要抱着“文件内容不可信”的心态每个字段解析完都检查是否成功行格式不对就跳过而不是直接崩溃。解析的常用做法是两次getline外层用getline(fin, line)读一行内层用一个stringstream配合getline(ss, token, |)切分字段。// 从文本文件加载记录返回成功加载的条数 bool loadFromFile(HomeworkList list, const string filename) { ifstream fin(filename.c_str(), ios::in); if (!fin.is_open()) { cout 文件不存在按空数据启动: filename endl; return true; // 首次运行这不是错误按空表处理 } string line; int loadCount 0; while (getline(fin, line)) { if (line.empty()) { continue; // 空行直接跳过不处理 } stringstream ss(line); string studentId, studentName, assignId, assignName; string statusStr, scoreStr, submitDate; getline(ss, studentId, |); getline(ss, studentName, |); getline(ss, assignId, |); getline(ss, assignName, |); getline(ss, statusStr, |); getline(ss, scoreStr, |); getline(ss, submitDate, |); // 字段数量校验任何一段为空都视为无效行 if (studentId.empty() || studentName.empty() || assignId.empty() || assignName.empty() || statusStr.empty() || scoreStr.empty() || submitDate.empty()) { cerr 警告: 跳过格式错误的行: line endl; continue; } HomeworkNode node; node.studentId studentId; node.studentName studentName; node.assignId assignId; node.assignName assignName; node.status atoi(statusStr.c_str()); node.score atoi(scoreStr.c_str()); node.submitDate submitDate; list.insertNode(node); loadCount; } fin.close(); cout 已加载 loadCount 条记录 endl; return true; }这里有两个刻意设计。第一个是文件不存在时返回true而不是false首次运行程序文件本来就不存在这应该是正常启动而不是报错。如果返回false主程序可能会直接退出或提示用户体验很差。第二个是atoi转换前先检查字符串非空否则atoi对空字符串返回 0一条本来成绩是 0 分的记录和一条缺失成绩字段的记录就无法区分。注意getline(ss, submitDate, |)后面没有内容了所以最后一个字段的定界符是行尾而不是|这在stringstream中能正常读到但如果你保存文件时最后一行漏了换行某些编译器下最后一个字段可能读不到所以保存时统一在每行末尾加\n很重要。4.3 统计模块一次遍历拿到所有指标统计功能是数据结构实训的“送分题”但送分题做得好也能体现对遍历的理解。常见的统计需求有总作业记录数、已提交/未提交/已批改各占多少、平均分、某位同学的作业完成率。写统计函数时不要每个小功能都全表扫描一遍那样虽然正确但显得你对复杂度没概念。正确做法是遍历一次链表在循环体里同时累加各个计数器的值。// 一次遍历完成多维度统计 void generateStatistics(HomeworkList list) { int total list.getCount(); int submitted 0; // 已提交含已批改 int graded 0; // 已批改 int unsubmitted 0; // 未提交 int scoreSum 0; int scoreCount 0; // 有成绩的记录数 double avgScore 0.0; HomeworkNode* cur list.getHead(); while (cur ! nullptr) { if (cur-status STATUS_UNSUBMITTED) { unsubmitted; } else if (cur-status STATUS_SUBMITTED) { submitted; } else if (cur-status STATUS_GRADED) { graded; scoreSum cur-score; scoreCount; } cur cur-next; } if (scoreCount 0) { avgScore scoreSum * 1.0 / scoreCount; } cout 作业完成情况统计 endl; cout 总记录数 : total endl; cout 未提交 : unsubmitted endl; cout 已提交未批改: submitted endl; cout 已批改 : graded endl; cout 平均分(只统计已批改): avgScore endl; }注意统计里的“已提交未批改”和“已批改”互不重叠STATUS_GRADED的记录在业务逻辑上已经提交过了但在状态枚举里它是独立的一档所以统计时要么把“已提交”定义为“STATUS_SUBMITTED或STATUS_GRADED”要么像我这样分三档列出来避免重复计数。平均分只用已批改的记录做分母未批改的score是 -1直接累加会把平均值拉成负数这是个逻辑上的隐蔽坑。5. 避坑指针悬挂、中文乱码与文件读写的翻车现场5.1 现象删除后遍历程序直接崩溃原因删除节点后某个全局指针或者被findNode返回过的局部指针仍然指着那块已释放的内存后续遍历一旦经过这个地址读取的操作就是访问野指针轻则拿到脏数据重则段错误。这个问题的痛点是它不必然每次复现跟内存分配器的行为有关。解决删除操作完成后所有指向该节点的外部指针都要手动置空。实训场景里最容易踩的是先findNode拿到ptr记录了一个地址然后调用deleteNode删掉了同一个节点接着又用ptr去打印信息。处理办法是不要在外部长时间保存节点指针用完之后立刻用完即丢如果确需跨函数持有删除后把指针变量置nullptr并在使用前统一加空指针判断。5.2 现象控制台里中文正常写入文件后变成乱码原因Windows 控制台默认使用本地代码页GBK显示字符而ofstream默认以系统 ANSI 编码写文本文件在简体中文系统上恰好也是 GBK所以“写文件 → 记事本打开”通常是正常的。乱码往往出在把文件拷到 macOS 或 Linux 上或者用某些编辑器强制按 UTF-8 打开。另一种情况是你在代码里用了setlocale(LC_ALL, chs)之后某些库函数对编码的处理发生变化。解决实训项目别纠结跨平台编码统一按 Windows GBK 来这是老师验收最可能的环境。如果你在 VSCode 里编译运行注意 VSCode 的终端默认 UTF-8和 Windows 控制台的 GBK 不一致建议在main开头加一句setlocale(LC_ALL, zh_CN.UTF-8);或者在系统设置里把“使用 Unicode UTF-8 提供全球语言支持”打开否则你在 VSCode 终端里看到的中文控制台输出就是乱码但这个乱码不影响写入文件的内容。写入文件时如果想明确指定编码可以改用wofstreamwstring但那会让字符串处理和getline变复杂实训阶段不必上。5.3 现象修改了一条记录的成绩发现其他记录的成绩也跟着变原因这多半是浅拷贝的锅。如果你在插入时手写了HomeworkNode newNode; newNode node;而node内部有指针成员或者你在读取文件时把HomeworkNode node;定义成了某个临时对象的引用多个节点共享了同一块字符串内存改一个等于改所有。另一个常见场景是insertNode的形参写成HomeworkNode node且直接head node;把栈上临时对象的地址塞进了链表函数返回后临时对象销毁链表里存的就是悬空地址。解决插入时一定用new HomeworkNode(node)在堆上申请独立内存不要用栈对象的地址。HomeworkNode内部成员全是string和int默认拷贝构造是安全的如果你后续把string换成char*就必须手写拷贝构造函数和赋值运算符。排查这类问题的方法很土但有效打印每个节点的地址和每个节点的studentName内存地址看看是不是有节点共享了同一块内存。5.4 现象程序启动时报缺少 “VCRUNTIME140.dll” 或 “MSVCP140.dll”原因C 程序在 Windows 上依赖 Microsoft Visual C Redistributable 运行库。你本机能跑是因为装过 Visual Studio 或运行库但老师验收机器或同学电脑上不一定有。这不是你代码的问题但会在演示现场让你非常尴尬。解决发布时把程序编成 Release x64 模式或根据老师环境选 x86然后在项目属性里把“C 运行时”选为“静态链接/MT”这样 exe 就不依赖动态 DLL。具体路径是项目属性 → 配置属性 → C/C → 代码生成 → 运行库 → 多线程/MT。用 VSCode MinGW 的话则要确认编译命令里没有漏掉-static-libgcc -static-libstdc。做完这一步后在干净虚拟机里测一次是最稳的做法。5.5 现象尾插的节点打印时少了一条查了半天发现是覆盖了头节点原因插入逻辑里cur走到nullptr就直接newNode而不是找到真正的尾节点再接上。常见写法错误是while (cur ! nullptr) { cur cur-next; }退出循环后 cur 已经是nullptr此时cur-next newNode是非法的或者更隐蔽的错误是循环条件写成了while (cur-next ! head)导致链表成环后死循环。解决尾插时用while (cur-next ! nullptr)作为循环条件退出循环时cur一定是一个非空节点且它的next是nullptr这时候cur-next newNode才安全。凡是涉及链表的循环都先检查循环边界是对cur还是cur-next做判断这是链表代码最常见的两类 bug 之一。6. 性能与验证进阶从链表走向哈希索引以及一套让老师信服的测试流程6.1 查找链表升级为 STL unordered_map 索引如果实训要求里有“按学号查询该生所有作业”这种高频操作链表 O(n) 的查找在几千条记录时勉强能忍几万条时就会卡顿。升级做法是在HomeworkList内部增加一个unordered_mapstring, HomeworkNode*索引键是studentId | assignId值是节点地址。插入链表时同时往 map 写一条删除时先查 map 拿到节点指针再执行删除同时从 map 里擦除。这样按主键查找退化为 O(1)而且不影响链表的遍历顺序和文件输出顺序。但要注意map 里存的裸指针在节点被delete后必须同步移除否则会变成悬挂指针这是比链表本身更隐蔽的地雷。实训作品里加这段索引能明显加分前提是你把删除和清空两个入口都同步维护好 map。6.2 召回测试每次改完代码都跑一遍五步闭环我自己的习惯是每完成一个功能模块就执行一遍固定的回归流程避免“加了排序功能结果把文件读取出卖了”这种局部修好全局崩掉的事故。具体分五步启动程序并加载一个已知内容的测试文件添加三条记录包含一条学号重复但作业编号不同的记录删除中间那条排序并打印保存退出后重新启动确认所有字段和顺序仍然正确。这五步基本覆盖了链表的新增、删除、遍历、排序、持久化五个高风险动作任何一步出问题都能立刻定位到是哪个模块被改坏了。6.3 畸形输入测试让老师故意“刁难”你实训验收时老师经常尝试一些边界操作比如空链表里删除、文件里手动删掉一行导致缺字段、把成绩改成负数、输入学号时多打一个空格。应对这些刁难最好的办法是你在交付前自己先做一轮畸形输入测试。代码里已有的防御点包括deleteNode对空链表的空指针检查、loadFromFile对格式错误行的跳过、generateStatistics对scoreCount 0时除数为零的保护。补充一个容易被忽略的成绩输入的合法性校验score应该限定在 0 到 100用户输入 150 时应该提示重新输入而不是直接存进链表否则后续排序和统计的逻辑都会被污染。把这几类输入全部试一遍程序表现稳定验收时你心里就有底了。这套从链表到持久化再到索引的写法我带过好几届实训说句实在话能把文件读写和指针释放做干净的人比把菜单写得花里胡哨的人得分高得多——因为前者证明你真的理解了内存后者只证明你愿意花时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表