数学考试系统开发总结:从零到一构建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,有错误答案得0for (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. 初始界面
2.考试功能
3.导出考试报告
4.查看题目
5.添加题目
6.删除题目
7.添加题目时的错误提示(主要功能均有错误提示机制,在此以添加题目功能为例)
结对编程感想
本次项目采用 结对编程 模式,两人轮流来 编写代码 / 检查代码 。以下是我们合作中的真实体会:
结对编程的优点
-
错误率显著降低
一个人写代码时容易忽略边界条件(比如数组越界、文件打开失败),而另一个人实时审查能及时发现。 -
设计思路更开阔
关于“是否允许考试中跳过题目”,两人产生了不同看法。经过讨论,我们决定增加输入“0”表示跳过,但跳过题目之后该题不得分,并且不中断考试。 -
知识互补
一位成员对C++文件流更熟悉,另一位对随机算法和字符串处理更擅长。分工后,文件导出模块和洗牌算法模块都完成得很快,且代码质量高于单人编写。
结对编程的挑战
- 1.沟通成本:开发初期,成员对函数命名规范有不同意见,即存 使用英语进行标注 和 使用拼音进行标注 的分歧,后来约定参考现有代码风格。
- 2.轮换机制问题:成员在开发时往往由于投入或疲劳,导致开发时间 多于 / 少于 约定的开发时间,导致每次轮换的时间点都不固定,工作占比也不统一。
该系统的改进方向
- 持久化题库:目前每次启动初始化固定题目,可改为从文件加载。
- 图形界面:控制台在美观和交互上有限,可考虑Qt或Web版本。
- 防作弊:增加随机选项顺序、题目乱序、倒计时锁屏等。
总结
本次数学考试系统开发项目,全程采用结对编程模式推进,最终完成了功能较为完整、逻辑较为清晰的C++控制台应用。该项目覆盖题库管理、在线考试、自动判分、报告导出四大核心功能,而结对编程作为本次开发的核心合作模式,贯穿项目全流程,不仅直接影响了项目的开发效率与代码质量,更让我们在协作中获得了远超技术本身的成长。
结对编程是本次项目顺利推进的关键支撑,其优势在开发过程中得到了充分体现。不同于单人开发的局限性,两人轮流担任编写者与审查者,实时把控代码质量,有效降低了边界条件遗漏、语法错误、逻辑漏洞等问题的出现概率,比如在编写文件导出模块和输入验证机制时,审查者及时发现了文件流关闭不及时、非法输入校验不全面的问题,避免了后续测试中的大量返工。
。










