“软件开发与创新课程设计”第七周结对编程作业及感想

数学考试系统开发总结:从零到一构建C++控制台应用

📌 项目概述

本项目实现了一个数学考试系统,支持单选题、多选题和判断题的题库管理、在线考试、自动判分与成绩报告导出。系统采用纯C++控制台界面,适合初学者理解面向对象设计、文件I/O、随机算法等核心概念。
本项目的合作伙伴学号为:2452814

下附完整代码

点击查看代码
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <ctime>
#include <map>
#include <cstdlib>   // for rand, srand
#include <cstdio>    // for getchar
#include <fstream>   // 文件操作
using namespace std;

// 题目结构体
struct Question {
    int id;
    int type; // 0单选 1多选 2判断
    string content;
    vector<string> options;
    vector<int> answer; // 正确答案索引
    string explanation;
};

// 全局题库
vector<Question> bank;
int nextId = 1;

// 初始化数学题库
void initBank() {
    Question q1;
    q1.id = nextId++; q1.type = 0; q1.content = "3^2 + 4^2 = ?^2";
    q1.options.push_back("5"); q1.options.push_back("6"); q1.options.push_back("7"); q1.options.push_back("8");
    q1.answer.push_back(0); q1.explanation = "勾股定理";
    bank.push_back(q1);

    Question q2;
    q2.id = nextId++; q2.type = 0; q2.content = "√2是?";
    q2.options.push_back("有理数"); q2.options.push_back("无理数"); q2.options.push_back("整数"); q2.options.push_back("分数");
    q2.answer.push_back(1); q2.explanation = "不能表示为分数";
    bank.push_back(q2);

    Question q3;
    q3.id = nextId++; q3.type = 1; q3.content = "哪些是质数?";
    q3.options.push_back("2"); q3.options.push_back("4"); q3.options.push_back("7"); q3.options.push_back("9");
    q3.answer.push_back(0); q3.answer.push_back(2); q3.explanation = "2和7是质数";
    bank.push_back(q3);

    Question q4;
    q4.id = nextId++; q4.type = 2; q4.content = "可导必连续";
    q4.options.push_back("正确"); q4.options.push_back("错误");
    q4.answer.push_back(0); q4.explanation = "可导一定连续";
    bank.push_back(q4);
}

// 清屏
void cls() { system("clear||cls"); }

// 显示题目
void showQuestion(const Question& q) {
    cout << "ID:" << q.id << " [" << (q.type==0?"单选":q.type==1?"多选":"判断") << "] ";
    cout << q.content << endl;
    for (size_t i = 0; i < q.options.size(); ++i)
        cout << "  " << char('A'+i) << ". " << q.options[i] << endl;
}

// 导出所有题目到文件
void exportQuestions() {
    string filename;
    cout << "请输入导出文件名(如 questions.txt): ";
    cin >> filename;
    ofstream fout(filename.c_str());
    if (!fout) {
        cout << "错误:无法创建文件!" << endl;
        return;
    }

    for (size_t i = 0; i < bank.size(); ++i) {
        const Question& q = bank[i];
        fout << q.type << endl;
        fout << q.content << endl;
        fout << q.options.size() << endl;
        for (size_t j = 0; j < q.options.size(); ++j) {
            fout << q.options[j] << endl;
        }
        string ansStr;
        for (size_t j = 0; j < q.answer.size(); ++j) {
            ansStr += char('0' + q.answer[j]);
        }
        fout << ansStr << endl;
        fout << q.explanation << endl;
        fout << endl;
    }

    fout.close();
    cout << "成功导出 " << bank.size() << " 道题目到文件 " << filename << endl;
}

// 考试判分
double check(const Question& q, vector<int> stuAns) {
    if (stuAns.empty()) return 0;
    if (q.type == 2) return stuAns[0] == q.answer[0] ? 1 : 0;
    if (q.type == 0) return stuAns == q.answer ? 1 : 0;
    // 多选判分
    for (size_t i = 0; i < stuAns.size(); ++i) {
        int a = stuAns[i];
        bool found = false;
        for (size_t j = 0; j < q.answer.size(); ++j) {
            if (q.answer[j] == a) { found = true; break; }
        }
        if (!found) return 0;
    }
    return stuAns.size() == q.answer.size() ? 1 : 0.5;
}

// 开始考试(增加答题输入验证)
void exam() {
    if (bank.empty()) {
        cout << "错误:题库为空,请先添加题目!" << endl;
        return;
    }
    int limit, cnt;
    cout << "考试时长(分钟): ";
    cin >> limit;
    if (limit <= 0) {
        cout << "时长必须为正数,已设为默认5分钟。" << endl;
        limit = 5;
    }

    cout << "题目数(最多" << bank.size() << "): ";
    cin >> cnt;
    if (cnt <= 0) {
        cout << "题目数至少为1,已设为1。" << endl;
        cnt = 1;
    }
    if (cnt > (int)bank.size()) cnt = bank.size();

    vector<Question> paper;
    for (int i = 0; i < cnt; ++i) paper.push_back(bank[i]);
    // 随机打乱
    for (int i = 0; i < (int)paper.size(); ++i) {
        int r = i + rand() % (paper.size() - i);
        swap(paper[i], paper[r]);
    }

    map<int, vector<int> > answers;
    time_t start = time(0);

    cls();
    cout << "=== 考试开始 时长" << limit << "分钟 ===\n";
    for (size_t i = 0; i < paper.size(); ++i) {
        int remain = limit * 60 - (time(0) - start);
        if (remain <= 0) { cout << "\n时间到!\n"; break; }

        Question& q = paper[i];
        cout << "\n[" << i+1 << "/" << paper.size() << "] ";
        cout << "剩余" << remain/60 << "分" << remain%60 << "秒\n";
        showQuestion(q);

        // 答案输入验证
        string ans;
        bool valid = false;
        while (!valid) {
            cout << "答案(0跳过): ";
            cin >> ans;
            if (ans == "0") {
                valid = true;
                break;
            }
            bool allValid = true;
            for (size_t j = 0; j < ans.size(); ++j) {
                char c = toupper(ans[j]);
                if (c < 'A' || c > char('A' + q.options.size() - 1)) {
                    cout << "错误:答案 " << c << " 超出选项范围(A-" << char('A'+q.options.size()-1) << ")!请重新输入。" << endl;
                    allValid = false;
                    break;
                }
            }
            if (allValid) valid = true;
        }

        vector<int> stuAns;
        if (ans != "0") {
            for (size_t j = 0; j < ans.size(); ++j) {
                stuAns.push_back(toupper(ans[j]) - 'A');
            }
        }
        answers[q.id] = stuAns;
    }

    // 判分并显示报告
    cls();
    cout << "========== 成绩报告 ==========\n";
    double total = 0;
    vector<double> scores(paper.size(), 0);
    for (size_t i = 0; i < paper.size(); ++i) {
        Question& q = paper[i];
        double score = check(q, answers[q.id]);
        scores[i] = score;
        total += score;
        cout << "\n" << q.content << "\n你的答案:";
        vector<int>& stu = answers[q.id];
        for (size_t j = 0; j < stu.size(); ++j)
            cout << char('A' + stu[j]) << " ";
        cout << "| 正确答案:";
        for (size_t j = 0; j < q.answer.size(); ++j)
            cout << char('A' + q.answer[j]) << " ";
        cout << "| 得分:" << score;
        if (score < 1) cout << "\n解析:" << q.explanation;
        cout << endl;
    }
    cout << "\n总分:" << total << "/" << paper.size();
    cout << " 正确率:" << total/paper.size()*100 << "%\n";

    // 询问导出报告
    cout << "\n是否导出本次考试报告?(y/n): ";
    char ch;
    cin >> ch;
    if (ch == 'y' || ch == 'Y') {
        string filename;
        cout << "请输入导出文件名(如 report.txt): ";
        cin >> filename;
        ofstream fout(filename.c_str());
        if (!fout) {
            cout << "错误:无法创建文件!" << endl;
            return;
        }

        fout << "========== 考试报告 ==========\n";
        fout << "考试时间: " << ctime(&start);
        fout << "题目数量: " << paper.size() << "\n";
        fout << "总分: " << total << " / " << paper.size() << "\n";
        fout << "正确率: " << total/paper.size()*100 << "%\n\n";
        fout << "详细答题情况:\n";
        for (size_t i = 0; i < paper.size(); ++i) {
            Question& q = paper[i];
            fout << "----------------------------------------\n";
            fout << "题目" << i+1 << ": " << q.content << "\n";
            fout << "类型: " << (q.type==0?"单选":q.type==1?"多选":"判断") << "\n";
            fout << "选项:\n";
            for (size_t j = 0; j < q.options.size(); ++j) {
                fout << "  " << char('A'+j) << ". " << q.options[j] << "\n";
            }
            fout << "正确答案: ";
            for (size_t j = 0; j < q.answer.size(); ++j) {
                fout << char('A' + q.answer[j]) << " ";
            }
            fout << "\n你的答案: ";
            vector<int>& stu = answers[q.id];
            if (stu.empty()) fout << "(未作答)";
            else {
                for (size_t j = 0; j < stu.size(); ++j) {
                    fout << char('A' + stu[j]) << " ";
                }
            }
            fout << "\n得分: " << scores[i] << "\n";
            if (scores[i] < 1) {
                fout << "解析: " << q.explanation << "\n";
            }
            fout << "\n";
        }
        fout << "========== 报告结束 ==========\n";
        fout.close();
        cout << "考试报告已保存到 " << filename << endl;
    }
}

// 题目管理(修改:单选题强制4个选项,题目文本不能为纯数字)
void manage() {
    int opt;
    cout << "1.添加 2.删除 3.查看 0.返回\n选择: ";
    cin >> opt;
    if (opt == 1) {
        Question q;
        q.id = nextId++;

        // 验证题目类型
        int type;
        while (true) {
            cout << "类型(0单选 1多选 2判断): ";
            cin >> type;
            if (type >= 0 && type <= 2) break;
            cout << "错误:类型只能是0、1或2,请重新输入。" << endl;
        }
        q.type = type;
        cin.ignore();

        // 验证题目文本不能为纯数字
        bool contentValid = false;
        while (!contentValid) {
            cout << "题目: ";
            getline(cin, q.content);
            if (q.content.empty()) {
                cout << "错误:题目内容不能为空,请重新输入。" << endl;
                continue;
            }
            // 检查是否为纯数字(只包含0-9,允许负号和小数点?用户要求“不可以是数字”,简单判断全数字即可)
            bool allDigits = true;
            for (size_t i = 0; i < q.content.size(); ++i) {
                if (q.content[i] < '0' || q.content[i] > '9') {
                    allDigits = false;
                    break;
                }
            }
            if (allDigits) {
                cout << "错误:题目内容不能为纯数字,请重新输入。" << endl;
            } else {
                contentValid = true;
            }
        }

        // 处理选项
        if (q.type == 0) {
            // 单选题:固定4个选项
            int n = 4;
            cout << "请输入4个选项(每行一个):" << endl;
            for (int i = 0; i < n; ++i) {
                string s;
                cout << char('A'+i) << ": ";
                getline(cin, s);
                if (s.empty()) {
                    cout << "警告:选项内容为空,将自动填入默认文本。" << endl;
                    s = "选项" + char('A'+i);
                }
                q.options.push_back(s);
            }
        } else if (q.type == 1) {
            // 多选题:用户输入选项数(2-10)
            int n;
            while (true) {
                cout << "选项数: ";
                cin >> n;
                if (n >= 2 && n <= 10) break;
                cout << "错误:选项数应为2~10之间的整数,请重新输入。" << endl;
            }
            cin.ignore();
            for (int i = 0; i < n; ++i) {
                string s;
                cout << char('A'+i) << ": ";
                getline(cin, s);
                if (s.empty()) {
                    cout << "警告:选项内容为空,将自动填入默认文本。" << endl;
                    s = "选项" + char('A'+i);
                }
                q.options.push_back(s);
            }
        } else { // type == 2 判断
            q.options.push_back("正确");
            q.options.push_back("错误");
        }

        // 验证答案格式
        string ans;
        bool ansValid = false;
        while (!ansValid) {
            cout << "答案(字母,如A、AB等): ";
            cin >> ans;
            if (ans.empty()) {
                cout << "错误:答案不能为空,请重新输入。" << endl;
                continue;
            }
            // 检查每个字符是否合法
            bool legal = true;
            for (size_t i = 0; i < ans.size(); ++i) {
                char c = toupper(ans[i]);
                if (c < 'A' || c > char('A' + q.options.size() - 1)) {
                    cout << "错误:字母 " << c << " 超出选项范围(A-" << char('A'+q.options.size()-1) << ")!" << endl;
                    legal = false;
                    break;
                }
            }
            if (!legal) continue;

            if (q.type == 0 && ans.size() != 1) {
                cout << "错误:单选题只能有一个答案,请重新输入。" << endl;
                continue;
            }
            if (q.type == 2 && ans.size() != 1) {
                cout << "错误:判断题只能有一个答案(A或B),请重新输入。" << endl;
                continue;
            }
            ansValid = true;
        }

        q.answer.clear();
        for (size_t i = 0; i < ans.size(); ++i) {
            q.answer.push_back(toupper(ans[i]) - 'A');
        }

        cout << "解析: ";
        cin.ignore();
        getline(cin, q.explanation);
        if (q.explanation.empty()) {
            q.explanation = "无解析";
        }

        bank.push_back(q);
        cout << "题目添加成功!新ID=" << q.id << endl;

    } else if (opt == 2) {
        if (bank.empty()) {
            cout << "题库为空,无法删除。" << endl;
            return;
        }
        int id;
        cout << "输入要删除的题目ID: ";
        cin >> id;
        vector<Question>::iterator it;
        for (it = bank.begin(); it != bank.end(); ++it) {
            if (it->id == id) break;
        }
        if (it != bank.end()) {
            bank.erase(it);
            cout << "题目ID " << id << " 已删除。" << endl;
        } else {
            cout << "错误:未找到ID为 " << id << " 的题目。" << endl;
        }
    } else if (opt == 3) {
        if (bank.empty()) {
            cout << "题库为空。" << endl;
        } else {
            for (size_t i = 0; i < bank.size(); ++i) {
                showQuestion(bank[i]);
                cout << "---\n";
            }
        }
    } else if (opt != 0) {
        cout << "无效选项,请重新输入。" << endl;
    }
}

// 主菜单
int main() {
    srand(time(0));
    initBank();
    while (true) {
        cls();
        cout << "===== 数学考试系统 =====\n";
        cout << "1.题目管理 2.开始考试 3.导出所有题目 0.退出\n选择: ";
        int opt;
        cin >> opt;
        if (opt == 1) {
            cls();
            manage();
            cin.ignore();
            cin.get();
        } else if (opt == 2) {
            cls();
            exam();
            cin.ignore();
            cin.get();
        } else if (opt == 3) {
            cls();
            exportQuestions();
            cin.ignore();
            cin.get();
        } else if (opt == 0) {
            break;
        } else {
            cout << "无效选项,按回车继续..." << endl;
            cin.ignore();
            cin.get();
        }
    }
    return 0;
}
---

算法设计思路

1. 数据结构设计

题目(Question)使用结构体存储:

struct Question {
    int id;                 // 唯一标识
    int type;               // 0单选 1多选 2判断
    string content;         // 题干
    vector<string> options; // 选项列表
    vector<int> answer;     // 正确答案索引(支持多选)
    string explanation;     // 解析
};
  • 设计考量:用vector<int>存储答案索引,兼容单选(1个索引)和多选(多个索引);判断视为选项只有“正确/错误”的特殊单选。

2. 考试判分算法

判分函数 check() 实现了三种题型的差异化评分规则

double check(const Question& q, vector<int> stuAns) {
    if (stuAns.empty()) return 0;
    if (q.type == 2) return stuAns[0] == q.answer[0] ? 1 : 0;  // 判断
    if (q.type == 0) return stuAns == q.answer ? 1 : 0;        // 单选
    // 多选:部分正确得0.5,全对得1,有错误答案得0
    for (int a : stuAns) 
        if (find(q.answer.begin(), q.answer.end(), a) == q.answer.end()) return 0;
    return stuAns.size() == q.answer.size() ? 1 : 0.5;
}
  • 亮点:多选题支持半对半给分,更贴近真实考试场景。
  • 时间复杂度:O(n×m),n为学生答案数,m为标准答案数(实际n和m很小,可忽略)。

3. 随机抽题与时间控制

考试开始前,系统会:

  • 从题库中随机打乱题目顺序(Fisher-Yates洗牌算法)
  • 记录起始时间戳 start = time(0)
  • 每道题作答前计算剩余时间:remain = limit*60 - (time(0)-start)
for (int i = 0; i < paper.size(); ++i) {
    int remain = limit * 60 - (time(0) - start);
    if (remain <= 0) break;
    // 显示倒计时并接收答案
}

4. 文件导出设计

  • 题目导出:逐行存储题目的类型、题干、选项数、选项内容、答案(如"02"表示A和C)、解析,用空行分隔。
  • 成绩报告导出:包含考试时间、总分、正确率、每道题的作答情况与得分。使用ctime()生成时间戳,增强报告可读性。

5. 输入验证机制

为防止用户输入非法数据,代码在以下环节做了严格校验:

  • 题目文本不能为纯数字
  • 单选题答案只能是一个字母
  • 答案字母不能超出选项范围(A~D等)
  • 考试时长和题目数为正数
// 单选题强制4个选项,选项内容不能为空
if (q.type == 0) {
    for (int i = 0; i < 4; ++i) {
        string s; getline(cin, s);
        if (s.empty()) s = "选项" + char('A'+i);
        q.options.push_back(s);
    }
}

运行示例

1. 初始界面

image

2.考试功能

image

3.导出考试报告

image
image

4.查看题目

image

5.添加题目

image
image

6.删除题目

image
image

7.添加题目时的错误提示(主要功能均有错误提示机制,在此以添加题目功能为例)

image

结对编程感想

本次项目采用 结对编程 模式,两人轮流来 编写代码 / 检查代码 。以下是我们合作中的真实体会:

结对编程的优点

  1. 错误率显著降低
    一个人写代码时容易忽略边界条件(比如数组越界、文件打开失败),而另一个人实时审查能及时发现。

  2. 设计思路更开阔
    关于“是否允许考试中跳过题目”,两人产生了不同看法。经过讨论,我们决定增加输入“0”表示跳过,但跳过题目之后该题不得分,并且不中断考试。

  3. 知识互补
    一位成员对C++文件流更熟悉,另一位对随机算法和字符串处理更擅长。分工后,文件导出模块和洗牌算法模块都完成得很快,且代码质量高于单人编写。

结对编程的挑战

  • 1.沟通成本:开发初期,成员对函数命名规范有不同意见,即存 使用英语进行标注使用拼音进行标注 的分歧,后来约定参考现有代码风格。
  • 2.轮换机制问题:成员在开发时往往由于投入或疲劳,导致开发时间 多于 / 少于 约定的开发时间,导致每次轮换的时间点都不固定,工作占比也不统一。

该系统的改进方向

  • 持久化题库:目前每次启动初始化固定题目,可改为从文件加载。
  • 图形界面:控制台在美观和交互上有限,可考虑Qt或Web版本。
  • 防作弊:增加随机选项顺序、题目乱序、倒计时锁屏等。

总结

本次数学考试系统开发项目,全程采用结对编程模式推进,最终完成了功能较为完整、逻辑较为清晰的C++控制台应用。该项目覆盖题库管理、在线考试、自动判分、报告导出四大核心功能,而结对编程作为本次开发的核心合作模式,贯穿项目全流程,不仅直接影响了项目的开发效率与代码质量,更让我们在协作中获得了远超技术本身的成长。

结对编程是本次项目顺利推进的关键支撑,其优势在开发过程中得到了充分体现。不同于单人开发的局限性,两人轮流担任编写者与审查者,实时把控代码质量,有效降低了边界条件遗漏、语法错误、逻辑漏洞等问题的出现概率,比如在编写文件导出模块和输入验证机制时,审查者及时发现了文件流关闭不及时、非法输入校验不全面的问题,避免了后续测试中的大量返工。

posted @ 2026-04-22 21:42  桃子汽水S  阅读(4)  评论(0)    收藏  举报