最近有学生朋友问我,C++学完基础语法之后,能做个什么项目练手,既不能太玩具,又不能在课程设计阶段烂尾。我第一个想到的,就是做一个 c++ 单项选择题标准化考试系统。这个题目听起来有点像教务系统的某个模块,但实际拆开看,它把C++最常用也最核心的几块能力全部串起来了:文件读写、字符串解析、容器操作、随机算法、类设计、交互流程控制。而且做完之后,是真的可以拿来用的,不是那种只能交作业的演示壳子。所以我把从零做一个最小可用版本的完整过程、设计取舍、判分细节和踩过的坑,全部写在这里。
我默认的读者是有C++基础语法、会用 vector 和 string、知道类和结构体区别,但还没独立做过一个完整项目的同学。如果你只是刚学完 if/for,建议先把基础补齐,再看这篇文章。这篇文章里所有代码都可以直接跑,但更重要的是理解为什么这样设计,这样才能自己复现和改造。
1. 拆解需求:这个考试系统真正要管的事
1.1 一句需求清单
需求看起来一句话就能说清:教师录入单选题,考生登录答题,系统自动判分。但这句话背后藏着层层细节。录入的题存在哪?怎么保证选项和答案是标准化的?考试时题目顺序是固定的还是随机的?判分时用户大小写不一致怎么办?这些都直接影响代码结构。
我建议先写一份需求清单,把问题显式化。最小可用版本至少要有这几件事:
- 题库管理:能添加、查看、删除题目,数据重启后不丢失。
- 组卷规则:从题库中随机抽取指定数量的单选题。
- 考试流程:逐题显示题干和选项,接收考生答案,记录状态。
- 自动判分:将考生答案和标准答案比对,输出得分、正确题数和错题信息。
- 数据持久化:题库、成绩都能写入文件,方便下次加载。
这个清单里,最容易被轻视的是最后一项。很多人写完代码,关掉程序,再打开发现题目全没了,于是重新录入,非常崩溃。所以我在写代码时,第一件事就是先把文件格式和持久化方案定下来,再写界面和业务逻辑。
1.2 为什么单选题是这个场景的合适载体
不是所有考试系统都适合作为练手项目。多选题有漏选、错选、全对的中间态,判断题答案只有两个选项,判分逻辑都不如单选题直观。单选题有一个很理想的特性:一道题目,四个选项,一个标准答案,非对即错。这个“非对即错”让判分逻辑变得异常清晰,适合学习阶段的人把注意力集中在程序结构上,而不是被业务规则的复杂性淹没。
同时,单选题在高职、本科的课程设计中属于高频题目类型。很多学校每学期都有类似“标准化考试系统”的题目,但大部分实现要么过于粗糙,要么依赖图形界面库把问题复杂化了。实际上,控制台程序加上精心设计的文件格式,就能把核心功能讲清楚。
还有一点,选题真实重要。如果给一个完全不熟悉的领域做系统,光理解业务规则就要花掉一半时间。单选考试系统是每个人从小到大都经历过的场景,需求不需要额外解释,理解门槛低,这样你才能专注在C++本身上。
1.3 功能边界:先做减法,再做加法
很多同学一开始就想着要做登录注册、数据库、多用户管理、界面美化,最后全部卡在某个环节。我的建议非常明确:第一个版本只做单机、单管理员、单考生的命令行程序。
“单管理员”的意思是题库管理不设密码,打开就是管理入口。如果后续想加登录,再单独加一层函数调用。“单考生”的意思是考试时不需要输入考生信息,答题完直接显示成绩。这样砍掉之后,核心流程就只剩输入、组卷、答题、判分、存盘。
砍掉功能不是偷懒,而是为了让核心代码更稳。我自己做这个东西的时候就踩过类似的坑:一开始想加一个难度等级筛选,结果组卷逻辑越写越复杂,最后发现考试流程本身还没跑通。所以先做最小闭环,跑通了再扩展,这是项目开发的一条通用经验。
2. 题库文件格式:一条题从存储到读取的完整规则
2.1 为什么先定文件格式,而不是先写代码
代码里最怕的不是逻辑复杂,而是数据格式不确定。你今天用一个结构体存题,明天觉得要加难度字段,后天又想把选项从四个变成五个,改来改去,所有相关代码全要跟着动。所以项目开始的第一步,应该把“题目长什么样”定义下来。
我选择用纯文本文件存题库。为什么不选 JSON?因为标准库没有 JSON 解析,引第三方库会引入额外的学习成本和配置问题。为什么不选数据库?因为 SQLite 的接入对课程设计来说过于沉重。自定义纯文本格式虽然“土”,但能让所有人看清楚字段的对应关系,也最容易调试。
文件命名我用bank.txt,和程序放在同一目录下。题目用一组关键字标记,我定的格式是这样的:
#ID: 1 #Q: 下面哪个是合法的变量名? #A: 1. int #B: 2. return #C: 3. num_1 #D: 4. var-name #ANS: C #LEVEL: 2一道题由题干、四个选项、答案、难度等级组成。#ID是题号,#Q表示题目文本,#A到#D对应四个选项,#ANS是标准答案,#LEVEL是难度系数,范围 1-3。这样格式的优点是每一行都有明确标识,程序读起来清楚,人也能直接打开文本修改,不需要专门的可视化编辑器。
2.2 字段设计的细节与取舍
字段命名尽量简短、固定、无歧义。#A到#D和选项的对应关系是硬性的,解析时根据标识符把内容放入对应索引的选项容器里。答案字段只存一个字符,比如C,不存整个选项文本。这样判分时只需比较两个字符,代价最小。
难度等级字段是给组卷用的。如果没有难度筛选需求,这一个字段可以先不设计。但加上之后,后续做随机抽题时就可以支持“只抽简单题”或“按难度比例抽题”,扩展起来非常方便。
还有一个值得注意的地方:题库里题目的#ID可以重复吗?我建议允许重复,但不作为主键使用。程序内部真正的主键是题目在vector中的下标。如果做了删除功能,ID 可能出现空洞,这没关系,删题的语义就是“不再使用这个题”,而不是“补一个同号新题”。
2.3 解析文件时最容易写错的点
文件解析是所有环节里最考验细节的地方。第一个坑是 Windows 下的换行符。Windows 文本文件的行尾是\r\n,用std::getline读取时,行尾会残留一个\r,如果不处理,题目文本末尾会多出一个不可见字符。我在读取时统一做一次判断,去掉行尾的\r。
第二个坑是空行。有人喜欢用空行把题与题隔开,但我建议解析时直接跳过空行,用#ID标志来划分题目边界。这样即使文件末尾有多个换行,也不会解析出空的题目对象。
第三个坑是字段顺序。如果文件里一行是#Q: ...,另一行是#ANS: C,到底先出现哪个?我建议解析时不要依赖字段顺序,而是读到一个字段就填写到当前题目对象的对应成员里;当读到#ID时,认为上一道题已经结束,把上一道题压入题库,并开始组装新题。
下面是我最常用的一段解析逻辑,抽掉细节后的主干:
bool loadBankFromFile(const std::string& filename, std::vector<Question>& bank) { std::ifstream in(filename); if (!in.is_open()) { return false; } std::string line; Question q; bool hasQuestion = false; while (std::getline(in, line)) { if (!line.empty() && line.back() == '\r') { line.pop_back(); } if (line.empty()) { continue; } if (line.rfind("#ID:", 0) == 0) { if (hasQuestion) { bank.push_back(q); } q = Question(); q.id = std::stoi(line.substr(4)); hasQuestion = true; } else if (line.rfind("#Q:", 0) == 0) { q.text = line.substr(3); } else if (line.rfind("#A:", 0) == 0) { q.options.push_back(line.substr(3)); } else if (line.rfind("#B:", 0) == 0) { q.options.push_back(line.substr(3)); } else if (line.rfind("#C:", 0) == 0) { q.options.push_back(line.substr(3)); } else if (line.rfind("#D:", 0) == 0) { q.options.push_back(line.substr(3)); } else if (line.rfind("#ANS:", 0) == 0) { q.answer = toupper(line.substr(5)[0]); } else if (line.rfind("#LEVEL:", 0) == 0) { q.level = std::stoi(line.substr(7)); } } if (hasQuestion) { bank.push_back(q); } return true; }注意,std::stoi和std::toupper都有边界情况要处理。stoi遇到非数字字符串会抛异常,所以文件不能乱写。toupper只对单个字符起作用,如果读到的#ANS后面不是一个字母,就要有一套防御逻辑。我的习惯是,文件格式先让自己信任,但代码里仍然要做基本检查。
3. 数据模型设计:从结构体到类,拒绝大杂烩
3.1 题目对象的正确结构
题目对象是整个系统的数据基础。用结构体还是类?我的建议是:需要被外部直接访问的纯数据用结构体,有行为逻辑的对象用类。所以Question用结构体,QuestionBank用类,职责划分清晰。
struct Question { int id = 0; std::string text; std::vector<std::string> options; char answer = ' '; int level = 1; };options用vector<string>存放,而不是四个独立的string成员。原因有两个:第一,后续遍历选项时用下标访问,写起来清爽;第二,如果想扩展成五个选项,结构体都不用改,解析时顶多多加一个分支。
答案字段用char存,同时统一转成大写。这样用户不管输入c还是C,都能正确匹配。代码里处理这个问题的地方越少越好,统一在载入时转换是最佳时机。
3.2 题库类的接口设计
题库类需要对外提供哪些方法?我建议只暴露最核心的几个:
bool loadFromFile(const std::string& filename):从文件载入题库。bool saveToFile(const std::string& filename):题库写回文件。void addQuestion(const Question& q):添加题目。bool removeQuestionByIndex(int index):按下标删除题目。std::vector<Question> selectQuestions(int count, int maxLevel = 3):按条件组卷。
类内部维护一个std::vector<Question> questions_。外部不能直接操作内部数组,必须通过方法访问。这一步看似多写几个包装函数,但能避免很多误操作。比如外部如果有办法直接questions_.clear(),那考试还没开始题库就空了。
组卷函数里最难的是“随机选出 N 道不重复的题”。简单做法是循环随机找一个下标,如果已经被选过就重新随机;但题库越大,后半段重复选中概率越高,效率越差。所以我用洗牌思路,先把候选池打乱,再取前 N 个,后面第四部分会细讲。
3.3 考试会话对象:保存考到一半的状态
很多入门者最大的问题,是不知道考试进行中的“当前状态”存在哪里。答到一半用户按了回车,计算完一题,下一题怎么知道从哪里继续?答案是把状态保存在一个会话对象里。
struct ExamSession { std::vector<int> questionIndexes; std::vector<char> userAnswers; int currentIndex = 0; time_t startTime = 0; bool submitted = false; };questionIndexes保存本次考试抽到的题在题库数组里的下标,顺序就是出题顺序。userAnswers和questionIndexes一一对应,存储每道题用户输入的答案。currentIndex标记当前做到第几题,submitted表示是否已经交卷。
为什么所有状态都放一个结构体里?因为考试的逻辑相对独立,把状态集中管理,写“下一题”“上一题”“交卷”这些操作时,只需操作一个对象。如果你能把状态和逻辑拆开,这个项目的代码质量就已经超过一半的课程设计作品了。
3.4 主菜单:用枚举代替魔法数字
入口处的主程序可以选择用 switch 加数字分支,但很容易写着写着就分不清1表示管理题库还是开始考试。我建议定义菜单操作的枚举:
enum class MenuAction { Admin = 1, StartExam = 2, ShowResult = 3, Exit = 4 };配合while循环,程序可以持续运行直到用户选择退出。菜单循环是整个系统的“外壳”,里面调用管理员模块、考试模块、成绩模块。这个外壳不涉及具体业务逻辑,只负责把用户的选择分发出去。
4. 随机组卷与判分逻辑:考试系统的灵魂
4.1 洗牌抽题而不是反复随机
假设题库有 50 道题,要抽 10 道。最简单的方法是写一个while循环,不断生成随机下标,然后判断是否已经被选过。题量少时问题不大,但抽到后半段,重复率会急剧上升,白白浪费时间。
更稳定的方案是 Fisher-Yates 洗牌。把候选题目数组自己内部打乱,然后取前 N 道。这个算法的时间复杂度是 O(n),每个元素最多被交换一次,抽到的题目天然不重复。实现也不复杂:
#include <random> #include <algorithm> void shuffleQuestions(std::vector<Question>& pool) { std::random_device rd; std::mt19937 gen(rd()); for (size_t i = pool.size(); i > 1; --i) { std::uniform_int_distribution<size_t> dist(0, i - 1); std::swap(pool[i - 1], pool[dist(gen)]); } }注意,这里我用std::mt19937而不是rand()。rand()在多数平台使用的线性同余算法周期短、分布质量一般,而且如果没有播种好,每次运行都会得到相同的序列。C++11 引入的<random>库虽然初学觉得绕,但确实是现在推荐的做法。
组卷函数里,不需要真的修改题库里题目的顺序,只需要对候选池的副本做洗牌:
std::vector<Question> selectQuestions(const std::vector<Question>& bank, int count) { std::vector<Question> pool = bank; shuffleQuestions(pool); count = std::min(count, static_cast<int>(pool.size())); return std::vector<Question>(pool.begin(), pool.begin() + count); }这里还有一个防御性检查:如果题库总数小于要抽的题数,std::min保证不会越界。否则直接pool.begin() + count会导致未定义行为,程序崩溃。
4.2 判分规则:别把简单问题复杂化
单选题判分就是逐题比较,用户答案与标准答案一致则得分,否则不得分。这里有一堆细节要注意。
第一,用户输入答案前要去空格、转大写。如果用户在控制台输入了一个带空格的大写字母,比如输入C,直接比较会判错。我在存用户答案时,会做一次统一的规范化处理,只保留字母并转大写。
第二,用户对不确定的题可以跳过。跳过的含义是这题不能得分,但不能导致数组错位。我的做法是,每道题先默认置为空格符' ',用户输入时如果回车直接跳过,就维持空格。判分时空格不算匹配。
第三,要处理“用户输入的字符是 E”这种情况。选择题只有 A-D,如果收到E,说明输入非法,这一题按错误处理,并提示用户重新检查。同时要允许用户对当前这一题重新输入,不然错一个字母就直接进入下一题,体验太差。
下面是判分循环的代码骨架:
int scoreExam(const ExamSession& session, const std::vector<Question>& bank, std::vector<int>& wrongIndexes) { int correct = 0; for (size_t i = 0; i < session.questionIndexes.size(); ++i) { int qIndex = session.questionIndexes[i]; char userAns = session.userAnswers[i]; char standardAns = bank[qIndex].answer; if (userAns != ' ' && toupper(userAns) == standardAns) { ++correct; } else { wrongIndexes.push_back(static_cast<int>(i)); } } return correct; }wrongIndexes里存的是第 i 道考试题的序号,不是题库下标。这样回看错题时,通过session.questionIndexes[wrongIndexes[j]]找到题库里的题目,展示题干和正确选项。
4.3 成绩统计与回看错题
成绩展示不要只输出一个分数。我写了一个printResult函数,输出本次考试的总题数、答对题数、答错题数、正确率,并遍历错题列表,显示用户当时选了什么、正确选项是什么。这样考试结束后,考生不用去翻题库也能知道自己错在哪。
正确率的计算容易出现除零异常。如果考试抽了 0 道题,分母就是 0。我在组卷时如果发现题库为空,就直接提示“题库为空,请先录入题目”,然后返回主菜单,阻断后续流程。
成绩落盘也建议做一下。我在成绩文件result.txt里每次追加一行,格式为时间, 总题数, 正确数, 正确率。这样多次考试的数据都能保留,后续还可以做简单的历史分析,也算是在不增加复杂度的前提下提升了实用性。
5. 考试流程状态管理:从登录到交卷的一步步
5.1 控制台交互的取舍
很多初学者纠结要不要上图形界面。我的观点很明确:控制台程序足够完成课程设计的核心目标。Qt 或 EasyX 界面会让项目看起来“高大上”,但界面的引入会把大量精力消耗在控件布局、事件响应、绘制刷新上,核心的考试逻辑反而变成配角。
所以这个项目首先做成控制台交互,但是交互方式要尽量做到“稳”。我不建议用cin >>一个接着一个读,因为一旦用户输入了超出预期的内容,缓冲区就会残留垃圾字符,后续的getline就会读到空行。所以我采用统一原则:凡是读取一行内容的输入,全部用std::getline,然后再解析里面的字符串;凡是读取单个字符的输入,也用getline取首字符,而不是用cin >> char。
这个习惯能帮你规避掉大量输入缓冲问题。后面第六部分我会专门展开讲。
5.2 用状态机思想组织考试过程
考试过程的状态可以抽象成几个阶段:待开始、答题中、已交卷。我用一个枚举变量保存状态,每当用户执行操作时,先判断当前状态允不允许,然后再做动作。
例如在答题界面,用户输入的选项是S(提交),这时要检查状态是否在答题中。如果已经交卷,再次输入S就不应该触发判分,而是提示“本场考试已结束”。用这种状态判断,可以有效避免重复交卷、交卷后继续答题等诡异问题。
考试进入时的流程大概是:
- 选择考试模式,系统调用组卷函数,得到本场考试的题目序列。
- 初始化
ExamSession,记录开始时间。 - 进入答题循环,逐题显示题干和选项,等待输入。
- 用户输入答案后,保存到
userAnswers,currentIndex加一,继续下一题。 - 最后一题答完,或用户主动输入
S提交,进入判分阶段。 - 判分后展示成绩,并保留错题信息。
这个流程里最需要小心的是“最后一题答完自动交卷”和“用户提前交卷”两个分支。如果最后一题答完还要用户再按一次提交,体验会很怪;如果提前交卷没做确认,又很容易误触。我的处理是:最后一题答完后,提示用户输入C表示确认交卷,输入N可回到上一题修改。
5.3 计时功能的轻量实现
计时是标准化考试系统的加分项。控制台程序的计时不需要复杂的线程或定时器,一个time_t变量就够了。进入考试时记录startTime,交卷时取当前时间相减,得到考试耗时。
time_t startTime = time(nullptr); // ... 答题过程 ... int elapsedSeconds = static_cast<int>(time(nullptr) - startTime);但如果想实现“倒计时强制交卷”,那就必须用线程或者异步机制,难度会跳一大截。我的建议是第一个版本只记录耗时,不做倒计时强制交卷。把强制交卷放到扩展方向里,后面有精力了再深挖。
给用户显示剩余时间可以用difftime加格式化输出,但控制台环境下,实时刷新倒计时需要清屏,频繁清屏在老旧终端里会闪屏。所以我更推荐在交卷后统一显示“本次用时 xx 分 xx 秒”。这样做既简单又不会干扰答题过程。
5.4 文件保存成绩的并发问题
成绩写入文件看起来很简单的ofstream out("result.txt", std::ios::app),但如果程序在短时间内多次交卷,后一次写入的内容不会覆盖前一次,因为追加模式会写在文件末尾。这里唯一要注意的是编码一致性,不要一次用 UTF-8,一次用 GBK 写入,否则用记事本打开时会出现乱码。我在程序里统一规定所有文本文件使用 UTF-8,并用带 BOM 的方式保存题库和成绩文件。这样用 Windows 记事本打开也能正常显示中文。
6. 实测踩过的坑:输入残留、编码乱码、边界崩溃
6.1 输入缓冲区的残留问题
这是所有入门者碰到最多的问题,没有之一。std::cin >> x在读入一个变量后,遇到空格或换行会停止,但换行符仍然留在缓冲区。这时候如果你调用std::getline(std::cin, str),它读到的第一个字符就是那个残留的换行,于是str直接变成空字符串。
我的解决方案是:在整个项目中,不要用cin >>读取用户选项。统一用std::getline读整行,然后从字符串里取第一个字符作为选项。读取数字也先读字符串,再用stoi转换。这样可以彻底绕开混用>>和getline带来的问题。
std::string input; std::getline(std::cin, input); if (input.empty()) { // 用户直接回车,视为跳过本题 continue; } char choice = toupper(input[0]);这段代码是考试交互的基础。它保证了即使用户误输入一整行文本,程序也只会取第一个字符,不崩溃、不串行。
6.2 中文乱码问题
C++ 控制台程序在 Windows 上经常出现中文乱码,根源在于源文件编码、运行时代码页和终端显示代码页三方不一致。最稳妥的跨平台做法是:源代码统一存为 UTF-8 带 BOM,在程序入口处调用系统 API 设置控制台输出代码页为 UTF-8。
在 Windows 下,可以这样写:
#if defined(_WIN32) SetConsoleOutputCP(CP_UTF8); #endif注意SetConsoleOutputCP是 Windows API,需要包含<windows.h>。在 Linux 或 macOS 下一般不需要这行,因为终端默认 UTF-8。用#if defined(_WIN32)包住,不会影响其他平台编译。
课程设计项目通常只在 Windows 上演示,所以不要纠结“为什么代码到了 Linux 输出乱码”这类问题,明确目标平台,把当前平台完全跑通更重要。
6.3 题库为空和选项数量异常
有一次我测试的时候,所有功能都正常,但一不小心把bank.txt内容清空了,程序启动后直接显示“下一题”时崩溃。原因就是我按下标访问了空数组。后来我在每一层都加防御性检查:进入考试前判断题库是否为空;加载文件后判断题目数量是否大于零;解析时如果某题选项少于 4 个,直接跳过该题并输出警告。
不要觉得防御性检查是啰嗦。题目文件被用户手动编辑后,内容可能任何样子都出现。少一个选项、多一个答案、题干写成两行……这些都应该被程序识别出来,而不是崩溃给用户看。
if (q.options.size() != 4 || q.answer < 'A' || q.answer > 'D') { // 记录错误,跳过该题 continue; }正因为异形数据不可避免,解析时只把合法题目压入题库才是一种负责任的行为。这个检查也帮我发现过自己手误写错了#ANS字段的问题。
6.4 vector 越界与引用失效
C++ 的std::vector虽然安全,但它只在你用at()时抛异常,用operator[]越界是未定义行为,程序可能正常、可能崩溃、可能输出垃圾数据。我建议在调试阶段统一使用at()访问关键下标,跑通后再决定要不要换回[]。
另外要特别警惕“引用失效”。如果代码里保存了一个对vector元素的引用,然后又往这个vector里push_back新元素,之前的引用可能失效,因为它指向的内存被重新分配了。我在做删除题目功能时就犯过这个错:先拿到questionIndexes里的下标,再调用removeQuestionByIndex修改题库数组,结果后续循环里复用旧下标访问,数据全乱了。
解决办法是删除操作只按下标号单独进行,涉及批量删除时,先把要删的下标收集起来,从大到小逐个删除,避免下标错位。
7. 做完基础版之后,这些方向值得继续扩展
7.1 选项乱序与答案联动
一个很容易加的扩展是“同一道题每次考试选项顺序不同”。实现的本质是:抽题后,对每道题的四个选项做一次小范围洗牌,同时把标准答案从原来的A/B/C/D改成洗牌后正确选项所在的位置。这道题是一个很好的练习,它要求你深入理解“题目的内容”和“答案的物理位置”之间的区别。很多同学会在这里绕晕,但一旦绕通,对数据结构的理解会上一个台阶。
7.2 难度比例抽题
如果题库给每道题都打了难度等级,那么组卷时就可以设定“简单 60%、中等 30%、困难 10%”的抽取比例。实现方案是先按难度分组,再每组分别洗牌取题,最后合并打乱顺序,避免试卷全是前面简单后面困难。这个功能我建议放在掌握了基础抽题之后再做,因为比例计算涉及浮点数取整的边界情况,比单纯取 N 道复杂。
7.3 成绩历史与错题本
把每次交卷的时间、得分、错题题号写入文件后,还可以增加一个“查看历史成绩”的菜单。更进一步,可以从历史成绩统计出某道题被答错的次数,这就是最简单的“班级错题本”。这个功能不需要改核心逻辑,只需要多写一个读文件、统计、排序的模块,但对整个项目的完整度提升非常明显。
7.4 迁移到图形界面的思路
如果想把控制台程序变成带界面的工具,不建议用 GUI 代码把现有逻辑包裹起来。最合理的做法是把题库加载、组卷、判分这些不依赖终端的功能全部提取成独立模块,界面层只负责调用这些模块的公开接口。也就是说,你现在写QuestionBank::selectQuestions的时候,就已经在为未来的 GUI 版本铺路了。把界面和逻辑分离,不是玄学,是每一个上规模项目的必经之路。
我个人实际使用中发现,这套代码即使后续改成 Qt 界面,核心类也能原封不动地复用。一个结构清晰的类设计,带来的收益会在项目后期成倍放大。这也是为什么我一开始就强调不要省掉类封装这一步。
最后,关于这个项目的一个小建议:写完代码后,一定找一位同学帮你测试一把,让他故意输入各种乱七八糟的内容,比如空答案、数字、超长字符串。你才会发现自己的程序在用户乱输入时到底稳不稳。我第二次重构时,就是因为测试者在题目答案那里输入了一长串英文,结果程序直接跳过了所有后续题目。这种问题只有真实场景才能暴露出来。