结对编程:在线答题系统 —— 金铲铲知识测验
第一部分:学号2452829
一、分工与负责模块
- 提出核心模块划分,将系统拆分为:题库数据层、答题引擎层、持久化层、交互菜单层
- 编写题库数据结构、主菜单框架、文件读写函数
- 实现错题本查看功能viewWrongNotebook(),及重做错题功能 redoWrongNotebook()
- 负责整体流程测试、边界情况测试
二、算法设计思路
- 数据结构设计 —— Question 结构体
点击查看代码
struct Question {
std::string stem; // 题干
std::array<std::string, 4> choices;// 四个选项
int correct; // 0-3 表示正确选项索引,-1 表示不计对错(娱乐题目)
};
这样无论是主卷还是娱乐题都可以使用同一套逻辑处理
- 错题本持久化设计
为了实现“错题解析”与“重做错题”功能,需要将主卷做错的题目标记下来并保存到文件。选择纯文本文件 wrong_notebook.txt,格式如下:
错题本(主卷题号 0–9),可删除本文件清空
MAIN|2
MAIN|5
设计思路:
- 读取时,按行解析以 MAIN| 开头的行,提取数字索引存入 std::set
(自动去重且有序) - 更新时,遍历主卷作答结果:答对的从集合中移除,答错的插入集合,最后覆盖写回文件
- 错题本查看时,直接根据索引从题库常量数组中取出题目信息并打印
- 错题重做算法流程(保证错题本始终反映用户当前掌握薄弱的题目)
- 从文件中读取错题索引集合 book
- 若 book 为空,提示用户并返回
- 根据 book 构建子题库 subset(只包含错题)
- 调用通用答题引擎 runQuizSession 进行作答
- 提交后,根据作答结果更新 book:
- 答对的题目从 book 中移除
- 依然答错的题目保留
将更新后的 book 写回文件
- 主菜单循环设计
使用了一个无限循环来显示主菜单并处理用户选择:
点击查看代码
while (true) {
printMainMenu();
std::getline(std::cin, line);
char c = std::tolower(line[0]);
if (c == 'q') break;
if (c == '1') runFullExam();
else if (c == '2') viewWrongNotebook();
else if (c == '3') redoWrongNotebook();
else std::cout << "无效输入" << std::endl;
}
三、关键代码展示
- 题库定义(部分主卷题目)
点击查看代码
static const std::vector<Question> kMainQuestions = {
{"下面选项中哪一件装备的合成材料与心之钢相同( )",
{"斯塔缇克电刃", "卢安娜的飓风", "救赎", "静止法衣"},
3},
{"一局游戏中同一种二费牌一共有多少张( )",
{"30", "27", "25", "18"},
2},
// ... 其余题目略
};
- 错题本加载函数
点击查看代码
static bool loadWrongNotebook(std::set<int> &out) {
out.clear();
std::ifstream in(kNotebookFile);
if (!in) return true; // 文件不存在,视为空
std::string line;
while (std::getline(in, line)) {
if (line.rfind("MAIN|", 0) != 0) continue;
if (line.size() <= 5) continue;
try {
int idx = std::stoi(line.substr(5));
if (idx >= 0 && idx < (int)kMainQuestions.size())
out.insert(idx);
} catch (...) {}
}
return true;
}
- 错题本查看函数
点击查看代码
static void viewWrongNotebook() {
std::set<int> book;
loadWrongNotebook(book);
if (book.empty()) {
std::cout << "\n错题本为空(暂无主卷错题)。\n";
return;
}
std::cout << "\n========== 错题本(主卷) ==========\n";
for (int id : book) {
const auto &q = kMainQuestions[id];
std::cout << "\n【原卷第 " << (id + 1) << " 题】\n" << q.stem << "\n";
for (int i = 0; i < 4; ++i)
std::cout << " " << choiceLetter(i) << ". " << q.choices[i] << "\n";
std::cout << "正确答案: " << choiceLetter(q.correct) << "\n";
}
}
四、运行结果
-
主菜单界面
![image]()
-
错题本查看功能测试
当主卷答错题目后,选择菜单 [2] 可看到如下输出:
![image]()
-
重做错题功能测试
选择菜单 [3] 后,进入答题界面,只包含错题。答对后错题本自动移除该题:
![image]()
五、结对编程作业体会
通过本次结对编程,我收获颇丰。
- 在代码设计方面,我意识到提前规划数据结构的重要性。例如一开始我试图用多个数组分别存储题干、选项和答案,导致代码难以维护。经讨论后,我们重构为Question 结构体,整个逻辑瞬间清晰。
- 在协作模式上,我编写错题本功能时,另一位同学及时发现了我未考虑文件不存在情况的漏洞,并建议我使用std::set来避免重复记录同一错题。这些实时审查极大提升了代码的健壮性。
- 测试环节也让我学到很多。我原本认为重做错题逻辑简单,但实际运行时发现错题索引与子题库下标映射容易搞混。经过反复调试,我们采用了indices向量来桥接原始索引与子题库位置,问题迎刃而解。
总的来说,结对编程虽然需要不断交流,但确实能减少低级错误,产出更高质量的代码。以后在团队项目中我会更积极地推行这种方式。
第二部分:学号2452609
一、分工与负责模块
- 补充交互细节,设计了命令行指令集(n/p/a-d/s/q/h),提出UTF‑8控制台兼容方案
- 审查另一同学的题库定义和文件读写函数,提出改进建议
- 编写通用答题引擎函数runQuizSession()、题目打印函数printQuestion()、答案选择与提交逻辑
- 实现主卷考试流程 runFullExam()及娱乐题衔接逻辑>
二、算法设计思路
- 通用答题引擎 ——runQuizSession设计
为了同时支持主卷、附加题、以及错题重做,我需要一个与具体题库内容无关的答题循环。因此我设计了如下状态机:
初始化:cur = 0,显示第 cur 题
循环读取用户输入:
├── 'n' → cur 向后移动(边界检查)
├── 'p' → cur 向前移动(边界检查)
├── a/b/c/d → 将当前题答案存入 answers[cur]
├── 's' → 检查是否所有答案均已填写,是则提交并返回;否则提示未完成
├── 'h' → 显示帮助
└── 'q' → 退出当前答题会话
每次翻题或选答案后,刷新当前题目显示及已选答案。
关键点:
- 使用 std::vector
&answers 作为输入输出参数,既可保留之前的作答,又可在提交后供外部判分 - 通过 bool objective 参数区分主卷(需判对错)和娱乐题(不计对错)
- 提交时统一打印每道题的作答结果,主卷计算总分
-
题目打印与答案状态显示
编写了printQuestion函数,它除了显示题目和选项外,还会显示当前题目已选答案。如果未选则显示“(未选择)”。这为用户提供了清晰的实时反馈,符合“实时保存答案”的要求 -
主卷与附加题衔接逻辑
在 runFullExam 中,设计了如下流程:
- 运行主卷答题会话 runQuizSession(主卷题库)
- 根据作答更新错题本
- 询问用户是否挑战附加题
- 若选是:运行附加题答题会话,完成后奖励10分
- 若选否:跳过
- 输出最终合计得分
这样既保证了核心功能,又增加了趣味性拓展
- 跨平台中文显示处理
考虑到 Windows 控制台默认编码为 GBK,直接输出中文会乱码,我在程序入口添加了 UTF‑8 设置:
点击查看代码
#ifdef _WIN32
SetConsoleOutputCP(65001);
SetConsoleCP(65001);
#endif
三、关键代码展示
- 通用答题引擎函数
点击查看代码
static bool runQuizSession(const std::vector<Question> &questions,
std::vector<int> &answers,
bool objective,
const char *submitTitle) {
if (answers.size() != questions.size())
answers.assign(questions.size(), -1);
size_t cur = 0;
printHelp(objective);
printQuestion(questions[cur], cur, questions.size(), answers[cur]);
std::string line;
while (std::cout << "\n> " && std::getline(std::cin, line)) {
if (line.empty()) continue;
char cmd = std::tolower(line[0]);
if (cmd == 'q') {
std::cout << "已中止本套题目。\n";
return false;
}
if (cmd == 'n') {
if (cur + 1 < questions.size()) ++cur;
else std::cout << "已在最后一题。\n";
printQuestion(questions[cur], cur, questions.size(), answers[cur]);
continue;
}
if (cmd == 'p') {
if (cur > 0) --cur;
else std::cout << "已在第一题。\n";
printQuestion(questions[cur], cur, questions.size(), answers[cur]);
continue;
}
if (cmd == 's') {
if (!allAnswered(answers)) {
std::cout << "还有题目未选择答案,请答完后再提交。\n";
continue;
}
// 提交判分并显示结果
// ... (判分代码略)
return true;
}
int pick = letterToIndex(cmd);
if (pick >= 0) {
answers[cur] = pick;
std::cout << "已记录第 " << (cur + 1) << " 题答案为 " << choiceLetter(pick) << "。\n";
printQuestion(questions[cur], cur, questions.size(), answers[cur]);
continue;
}
std::cout << "未知指令,输入 h 查看帮助。\n";
}
return false;
}
- 主卷考试与附加题衔接
点击查看代码
static void runFullExam() {
std::vector<int> mainAnswers(kMainQuestions.size(), -1);
// 运行主卷
if (!runQuizSession(kMainQuestions, mainAnswers, true, "主卷提交结果"))
return;
updateNotebookFromMain(mainAnswers);
int mainScore = computeMainScore(mainAnswers);
int totalScore = mainScore;
bool didBonus = false;
if (readYes("\n是否继续挑战附加题?")) {
std::vector<int> bonusAnswers(kBonusQuestions.size(), -1);
if (runQuizSession(kBonusQuestions, bonusAnswers, false, "附加题作答完成")) {
totalScore += kBonusTotalPoints;
didBonus = true;
}
}
// 输出最终成绩
// ...
}
- 答案全填检查辅助函数
点击查看代码
static bool allAnswered(const std::vector<int> &answers) {
for (int v : answers)
if (v < 0) return false;
return true;
}
四、运行结果
-
答题过程中的实时反馈
![image]()
-
提交时提示
如果用户未答完所有题就输入 s,系统会提示:
![image]()
-
主卷提交结果与附加题询问
![image]()
![image]()
五、结对编程作业体会
这次结对编程让我对代码的可读性和可维护性有了更深的理解。
- 在另一位同学编程时,我需要跳出具体实现,从更高的视角审视 A 同学写的代码。例如,在最初的题库定义中,每个题目是一个独立的变量,导致后续难以遍历。我建议改用 std::vector
统一存储,这一改动使得后续所有答题逻辑都可以用循环处理,大大减少了重复代码 - 当我编写通用答题引擎时,另一位同学在旁边不断提醒我注意边界条件:“如果当前在第一题,按 p 怎么办?”“如果用户输入大写字母怎么办?”“如果答案还没选完就按 s 怎么办?”这些问题迫使我在编码时就加入相应判断,最终产出的 runQuizSession 函数非常健壮,几乎能应对所有异常输入
- 在娱乐题衔接部分,我最初直接将奖励分累加,但 A 同学指出应该只在用户完成娱乐题时才给分,如果中途按 q 退出则不应奖励。我们随即调整了返回值判断逻辑,保证了规则严谨
这次经历让我认识到,结对编程不是简单的分工写代码,而是一种持续的思维碰撞和互相监督。两人的知识盲区可以互补,最终代码质量远超单人独立完成。我会将这种方法应用到今后的课程项目和工作中。








浙公网安备 33010602011771号