结对编程:在线答题系统 —— 金铲铲知识测验

第一部分:学号2452829
一、分工与负责模块

  1. 提出核心模块划分,将系统拆分为:题库数据层、答题引擎层、持久化层、交互菜单层
  2. 编写题库数据结构、主菜单框架、文件读写函数
  3. 实现错题本查看功能viewWrongNotebook(),及重做错题功能 redoWrongNotebook()
  4. 负责整体流程测试、边界情况测试

二、算法设计思路

  1. 数据结构设计 —— Question 结构体
点击查看代码
struct Question {
    std::string stem;                  // 题干
    std::array<std::string, 4> choices;// 四个选项
    int correct;                       // 0-3 表示正确选项索引,-1 表示不计对错(娱乐题目)
};

这样无论是主卷还是娱乐题都可以使用同一套逻辑处理

  1. 错题本持久化设计
    为了实现“错题解析”与“重做错题”功能,需要将主卷做错的题目标记下来并保存到文件。选择纯文本文件 wrong_notebook.txt,格式如下:

错题本(主卷题号 0–9),可删除本文件清空
MAIN|2
MAIN|5

设计思路:

  • 读取时,按行解析以 MAIN| 开头的行,提取数字索引存入 std::set(自动去重且有序)
  • 更新时,遍历主卷作答结果:答对的从集合中移除,答错的插入集合,最后覆盖写回文件
  • 错题本查看时,直接根据索引从题库常量数组中取出题目信息并打印
  1. 错题重做算法流程(保证错题本始终反映用户当前掌握薄弱的题目)
  • 从文件中读取错题索引集合 book
  • 若 book 为空,提示用户并返回
  • 根据 book 构建子题库 subset(只包含错题)
  • 调用通用答题引擎 runQuizSession 进行作答
  • 提交后,根据作答结果更新 book:
    • 答对的题目从 book 中移除
    • 依然答错的题目保留
      将更新后的 book 写回文件
  1. 主菜单循环设计
    使用了一个无限循环来显示主菜单并处理用户选择:
点击查看代码
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;
}

三、关键代码展示

  1. 题库定义(部分主卷题目)
点击查看代码
static const std::vector<Question> kMainQuestions = {
    {"下面选项中哪一件装备的合成材料与心之钢相同( )",
     {"斯塔缇克电刃", "卢安娜的飓风", "救赎", "静止法衣"},
     3},
    {"一局游戏中同一种二费牌一共有多少张( )",
     {"30", "27", "25", "18"},
     2},
    // ... 其余题目略
};
  1. 错题本加载函数
点击查看代码
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;
}
  1. 错题本查看函数
点击查看代码
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";
    }
}

四、运行结果

  1. 主菜单界面
    image

  2. 错题本查看功能测试
    当主卷答错题目后,选择菜单 [2] 可看到如下输出:
    image

  3. 重做错题功能测试
    选择菜单 [3] 后,进入答题界面,只包含错题。答对后错题本自动移除该题:
    image

五、结对编程作业体会
通过本次结对编程,我收获颇丰。

  1. 在代码设计方面,我意识到提前规划数据结构的重要性。例如一开始我试图用多个数组分别存储题干、选项和答案,导致代码难以维护。经讨论后,我们重构为Question 结构体,整个逻辑瞬间清晰。
  2. 在协作模式上,我编写错题本功能时,另一位同学及时发现了我未考虑文件不存在情况的漏洞,并建议我使用std::set来避免重复记录同一错题。这些实时审查极大提升了代码的健壮性。
  3. 测试环节也让我学到很多。我原本认为重做错题逻辑简单,但实际运行时发现错题索引与子题库下标映射容易搞混。经过反复调试,我们采用了indices向量来桥接原始索引与子题库位置,问题迎刃而解。

总的来说,结对编程虽然需要不断交流,但确实能减少低级错误,产出更高质量的代码。以后在团队项目中我会更积极地推行这种方式。

第二部分:学号2452609
一、分工与负责模块

  1. 补充交互细节,设计了命令行指令集(n/p/a-d/s/q/h),提出UTF‑8控制台兼容方案
  2. 审查另一同学的题库定义和文件读写函数,提出改进建议
  3. 编写通用答题引擎函数runQuizSession()、题目打印函数printQuestion()、答案选择与提交逻辑
  4. 实现主卷考试流程 runFullExam()及娱乐题衔接逻辑>

二、算法设计思路

  1. 通用答题引擎 ——runQuizSession设计
    为了同时支持主卷、附加题、以及错题重做,我需要一个与具体题库内容无关的答题循环。因此我设计了如下状态机:

初始化:cur = 0,显示第 cur 题
循环读取用户输入:
├── 'n' → cur 向后移动(边界检查)
├── 'p' → cur 向前移动(边界检查)
├── a/b/c/d → 将当前题答案存入 answers[cur]
├── 's' → 检查是否所有答案均已填写,是则提交并返回;否则提示未完成
├── 'h' → 显示帮助
└── 'q' → 退出当前答题会话
每次翻题或选答案后,刷新当前题目显示及已选答案。

关键点:

  • 使用 std::vector &answers 作为输入输出参数,既可保留之前的作答,又可在提交后供外部判分
  • 通过 bool objective 参数区分主卷(需判对错)和娱乐题(不计对错)
  • 提交时统一打印每道题的作答结果,主卷计算总分
  1. 题目打印与答案状态显示
    编写了printQuestion函数,它除了显示题目和选项外,还会显示当前题目已选答案。如果未选则显示“(未选择)”。这为用户提供了清晰的实时反馈,符合“实时保存答案”的要求

  2. 主卷与附加题衔接逻辑
    在 runFullExam 中,设计了如下流程:

  1. 运行主卷答题会话 runQuizSession(主卷题库)
  2. 根据作答更新错题本
  3. 询问用户是否挑战附加题
    • 若选是:运行附加题答题会话,完成后奖励10分
    • 若选否:跳过
  4. 输出最终合计得分
    这样既保证了核心功能,又增加了趣味性拓展
  1. 跨平台中文显示处理
    考虑到 Windows 控制台默认编码为 GBK,直接输出中文会乱码,我在程序入口添加了 UTF‑8 设置:
点击查看代码
#ifdef _WIN32
    SetConsoleOutputCP(65001);
    SetConsoleCP(65001);
#endif
确保在 Windows、Linux、macOS 下均能正常显示中文菜单和题目

三、关键代码展示

  1. 通用答题引擎函数
点击查看代码
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;
}
  1. 主卷考试与附加题衔接
点击查看代码
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;
        }
    }
    // 输出最终成绩
    // ...
}
  1. 答案全填检查辅助函数
点击查看代码
static bool allAnswered(const std::vector<int> &answers) {
    for (int v : answers)
        if (v < 0) return false;
    return true;
}

四、运行结果

  1. 答题过程中的实时反馈
    image

  2. 提交时提示
    如果用户未答完所有题就输入 s,系统会提示:
    image

  3. 主卷提交结果与附加题询问
    image
    image

五、结对编程作业体会
这次结对编程让我对代码的可读性和可维护性有了更深的理解。

  1. 在另一位同学编程时,我需要跳出具体实现,从更高的视角审视 A 同学写的代码。例如,在最初的题库定义中,每个题目是一个独立的变量,导致后续难以遍历。我建议改用 std::vector 统一存储,这一改动使得后续所有答题逻辑都可以用循环处理,大大减少了重复代码
  2. 当我编写通用答题引擎时,另一位同学在旁边不断提醒我注意边界条件:“如果当前在第一题,按 p 怎么办?”“如果用户输入大写字母怎么办?”“如果答案还没选完就按 s 怎么办?”这些问题迫使我在编码时就加入相应判断,最终产出的 runQuizSession 函数非常健壮,几乎能应对所有异常输入
  3. 在娱乐题衔接部分,我最初直接将奖励分累加,但 A 同学指出应该只在用户完成娱乐题时才给分,如果中途按 q 退出则不应奖励。我们随即调整了返回值判断逻辑,保证了规则严谨

这次经历让我认识到,结对编程不是简单的分工写代码,而是一种持续的思维碰撞和互相监督。两人的知识盲区可以互补,最终代码质量远超单人独立完成。我会将这种方法应用到今后的课程项目和工作中。

posted @ 2026-04-22 16:24  我不要起床  阅读(32)  评论(0)    收藏  举报