软件工程第三次作业——结对项目
------------恢复内容开始------------
一、作业信息
这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/Class12Grade23ComputerScience |
---|---|
这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class12Grade23ComputerScience/homework/13470 |
这个作业的目标 | 实现一个自动生成小学四则运算题目的命令行程序 |
其余信息
GitHub仓库:
https://github.com/Scaler1024/math-quiz-maker
小组成员 | 学号 |
---|---|
高扬鹏 | 3123004349 |
彭耿立 | 3123004364 |
二、PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 60 |
Estimate | 估计这个任务需要多少时间 | 30 | 30 |
Development | 开发 | 200 | 400 |
Analysis | 需求分析(包括学习新技术) | 2000 | 2000 |
Design Spec | 生成设计文档 | 100 | 120 |
Design Review | 设计重审 | 45 | 70 |
Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 100 | 120 |
Design | 具体设计 | 100 | 250 |
Coding | 具体编码 | 2000 | 3000 |
Code Review | 代码复审 | 200 | 300 |
Test | 测试(自我测试,修改代码,提交修改) | 200 | 500 |
Reporting | 报告 | 100 | 170 |
Test Repor | 测试报告 | 100 | 70 |
Size Measurement | 计算工作量 | 30 | 30 |
Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 60 | 120 |
合计 | 5295 | 7240 |
三、效能分析
时间投入:
在项目开发过程中,我们花费约两个晚上用于性能分析与优化,主要集中在表达式生成、去重校验和文件读写三个关键模块。
改进思路:
表达式生成优化:减少无效生成,在构建表达式树时增加预校验逻辑,提前过滤明显不符合条件的表达式(如除数为 0、负数结果等),降低无效尝试次数。
去重算法优化:通过标准化表达式(将表达式转换为唯一字符串表示),使用set容器存储已生成的表达式,将去重校验的时间复杂度从 O (n) 降低到 O (log n)。
文件操作优化:采用批量写入方式,减少磁盘 IO 次数,提高文件保存效率。
性能测试图:
参数: -n 100 -r 10
文件读取:
文件写入:
CPU使用率:
四、设计说明
项目采用面向对象设计,包含6个核心类,类间关系如下:
Fraction类(分数处理):封装分数的四则运算及约分逻辑,提供toString()方法转换为字符串
各类运算符重载:实现分数的加减乘除
toString():分数格式化输出
simplify():分数化简
Expression类(表达式处理):储存表达式字符串、运算符、操作数、提供evaluate()计算结果和isValid()验证是否满足项目要求
evaluate():计算表达式结果
isValid():验证表达式有效性
standardize():标准化表达式(用于去重)
toString():转换为字符串表示
ExpressionGenerator类(题目生成):递归生成随机符合项目要求的四则运算。
generate():生成表达式
generateNumber():生成数字节点
generateOperator():生成运算符
buildExpressionTree():构建表达式树
DuplicateChecker类(重复检测):通过标准化表达式(处理交换律等)去重,使用哈希集合储存已经生成的表达式。
isDuplicate():判断表达式是否重复
FileProcessor类(文件处理):负责读写题目/答案文件,是实现评分功能(重新计算表达式与答案比对)。
saveExercises():保存练习题
saveAnswers():保存答案
gradeAnswers():批改答案
CommandLineParser类(命令行解析):解析用户输入的命令行参数。
parse():解析命令行参数
has():检查是否包含指定参数
get():获取参数值
关键函数ExpressionGenerator::generate()流程图:
五、关键代码
1.表达式生成代码(ExoressionGenerator.h)
// 生成表达式
Expression generate() {
int operatorCount = rand() % maxOperators + 1;
Expression expr;
int attempts = 0;
const int MAX_ATTEMPTS = 100;//最大尝试次数
while (attempts < MAX_ATTEMPTS) {
// 构建表达式树
ExpressionNode* root = buildExpressionTree(operatorCount);
expr.setRoot(root);
// 检查表达式是否有效
if (expr.isValid(maxRange)) {
return expr;
}
attempts++;
}
// 如果多次尝试失败,返回一个简单的表达式
return Expression(new ExpressionNode('+', generateNumber(), generateNumber()));
}
// 构建表达式树
ExpressionNode* buildExpressionTree(int operatorCount) {
if (operatorCount == 0) {
return generateNumber();
}
// 随机分配左右子树的运算符数量
int leftCount = rand() % operatorCount;
int rightCount = operatorCount - leftCount - 1;
ExpressionNode* left = buildExpressionTree(leftCount);
ExpressionNode* right = buildExpressionTree(rightCount);
return new ExpressionNode(generateOperator(), left, right);
}
代码说明:
采用递归的方式构建表达式树,通过控制运算符数量来控制表达式复杂度
每次生成后通过isValid()方法来验证表达式是否符合要求(数值范围、非负性、除法有效性等)
设置最大尝试次数,避免了无限循环
2.表达式标准化与去重(DuplicateChecker.h)
class DuplicateChecker {
private:
// 存储已生成的标准化表达式字符串,用于快速查重
set<string> generatedExpressions;
// 标准化表达式为唯一字符串表示
string standardizeExpression(const Expression& expr) {
// 调用Expression类的标准化方法获取规范化表达式
Expression normalized = expr.standardize();
//将标准话的字符串转换为字符串
return normalized.toString();
}
public:
bool isDuplicate(const Expression& expr) {
//先将表达式转换为标准字符串
string standardized = standardizeExpression(expr);
//检查该字符串是否已经在集合中存在
if (generatedExpressions.find(standardized) != generatedExpressions.end()) {
return true;
}
generatedExpressions.insert(standardized);
return false;
}
void clear() {
generatedExpressions.clear();//清空已经储存的所有表达式记录
}
};
代码说明:
通过standardize()方法将表达式转换为标准化形式(如加法和乘法的操作数排序)
使用set容器存储已生成的表达式字符串,实现高效的重复检查。
时间复杂度为O(logn),相比线性检查大大提高了效率
3.表达式验证(Expression.h)
bool isValid(int maxRange) const {
// 检查数值范围
function<bool(ExpressionNode*)> checkRange = [&](ExpressionNode* node) {
if (node->type == NUMBER) {
Fraction val = node->value;
if (val.getNumerator() < 0 || val.getNumerator() >= maxRange ||
val.getDenominator() <= 0 || val.getDenominator() >= maxRange) {
return false;
}
return true;
}
return checkRange(node->left) && checkRange(node->right);
};
if (!checkRange(root)) return false;
// 检查减法结果非负
function<bool(ExpressionNode*)> checkNonNegative = [&](ExpressionNode* node) {
if (node->type == OPERATOR && node->op == '-') {
Fraction leftVal = evaluate(node->left);
Fraction rightVal = evaluate(node->right);
if (leftVal < rightVal) {
return false;
}
}
if (node->left && !checkNonNegative(node->left)) return false;
if (node->right && !checkNonNegative(node->right)) return false;
return true;
};
if (!checkNonNegative(root)) return false;
// 检查除法结果为真分数
function<bool(ExpressionNode*)> checkDivision = [&](ExpressionNode* node) {
if (node->type == OPERATOR && node->op == '/') {
Fraction result = evaluate(node);
if (!result.isProperFraction()) {
return false;
}
}
if (node->left && !checkDivision(node->left)) return false;
if (node->right && !checkDivision(node->right)) return false;
return true;
};
if (!checkDivision(root)) return false;
return true;
}
代码说明:
采用递归方式检查表达式各节点是否符合要求
包含三个检查维度:数值范围、减法结果非负、除法结果为真分数
使用函数对象(function)封装检查逻辑,使代码结构更清晰
4.命令行解析与主函数(main.cpp)
int main(int argc, char* argv[]) {
CommandLineParser parser;
parser.parse(argc, argv);
// 生成题目模式
if (parser.has("-n")) {
int count = stoi(parser.get("-n"));
if (count <= 0 || count > 10000) {
cout << "题目数量必须在1到10000之间" << endl;
return 1;
}
if (!parser.has("-r")) {
cout << "错误: 必须使用 -r 参数指定数值范围" << endl;
return 1;
}
int range = stoi(parser.get("-r"));
ExpressionGenerator generator(range);
DuplicateChecker checker;
vector<Expression> exercises;
vector<Fraction> answers;
for (int i = 0; i < count; i++) {
int attempts = 0;
const int MAX_ATTEMPTS = 1000;
while (attempts < MAX_ATTEMPTS) {
Expression expr = generator.generate();
if (!checker.isDuplicate(expr)) {
exercises.push_back(expr);
answers.push_back(expr.evaluate());
break;
}
attempts++;
}
}
// 保存题目和答案
string execDir = getExecutableDirectory();
string exercisesPath = combinePath(execDir, "Exercises.txt");
string answersPath = combinePath(execDir, "Answers.txt");
FileProcessor::saveExercises(exercises,exercisesPath);
FileProcessor::saveAnswers(answers, answersPath);
}
// 批改模式
else if (parser.has("-e") && parser.has("-a")) {
string exerciseFile = parser.get("-e");
string answerFile = parser.get("-a");
string execDir = getExecutableDirectory();
string gradePath = combinePath(execDir, "Grade.txt");
FileProcessor::gradeAnswers(exerciseFile, answerFile, gradePath);
}
else {
cout << "用法:" << endl;
cout << "生成题目: " << argv[0] << " -n <题目数量> -r <数值范围>" << endl;
cout << "批改题目: " << argv[0] << " -e <题目文件> -a <答案文件>" << endl;
return 1;
}
return 0;
}
六、测试说明
测试用例:
#include <gtest/gtest.h>
#include "Fraction.h"
#include "Expression.h"
// 测试Fraction类的基本运算
TEST(FractionTest, BasicOperations) {
// 测试1:分数加法
Fraction f1(1, 2);
Fraction f2(1, 3);
EXPECT_EQ((f1 + f2).toString(), "5/6");
// 测试2:分数减法
Fraction f3(3, 4);
Fraction f4(1, 4);
EXPECT_EQ((f3 - f4).toString(), "1/2");
// 测试3:分数乘法
Fraction f5(2, 3);
Fraction f6(3, 4);
EXPECT_EQ((f5 * f6).toString(), "1/2");
// 测试4:分数除法
Fraction f7(1, 2);
Fraction f8(3, 4);
EXPECT_EQ((f7 / f8).toString(), "2/3");
// 测试5:带分数转换
Fraction f9("1'1/2");
EXPECT_EQ(f9.getNumerator(), 3);
EXPECT_EQ(f9.getDenominator(), 2);
}
// 测试Expression类的表达式计算
TEST(ExpressionTest, Evaluation) {
// 测试6:简单整数表达式
ExpressionNode* node1 = new ExpressionNode('+', new ExpressionNode(Fraction(2,1)), new ExpressionNode(Fraction(3,1)));
Expression expr1(node1);
EXPECT_EQ(expr1.evaluate().toString(), "5");
// 测试7:分数表达式
ExpressionNode* node2 = new ExpressionNode('*', new ExpressionNode(Fraction(1,2)), new ExpressionNode(Fraction(2,3)));
Expression expr2(node2);
EXPECT_EQ(expr2.evaluate().toString(), "1/3");
// 测试8:混合运算
ExpressionNode* node3 = new ExpressionNode('+',
new ExpressionNode('*', new ExpressionNode(Fraction(1,2)), new ExpressionNode(Fraction(2,3))),
new ExpressionNode(Fraction(1,3))
);
Expression expr3(node3);
EXPECT_EQ(expr3.evaluate().toString(), "2/3");
// 测试9:表达式标准化(去括号和排序)
ExpressionNode* node4 = new ExpressionNode('+', new ExpressionNode(Fraction(3,1)), new ExpressionNode(Fraction(2,1)));
Expression expr4(node4);
EXPECT_EQ(expr4.standardize().toString(), "2 + 3 = ");
// 测试10:表达式合法性检查
ExpressionNode* node5 = new ExpressionNode('-', new ExpressionNode(Fraction(1,1)), new ExpressionNode(Fraction(2,1)));
Expression expr5(node5);
EXPECT_FALSE(expr5.isValid(10)); // 结果为负,不合法
}
int main(int argc, char **argv) {
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
测试结果:
1.输入错误参数:
2.输入错误题目数目:
3.输入错误算术数值范围:
4.输入错误的路径:
思路:
基础运算测试:选择简单分数(如 1/2、1/3)和整数,验证四则运算结果的正确性
带分数测试:通过字符串构造带分数(如 "1'1/2"),验证内部数值转换
表达式测试:构建不同复杂度的表达式树,覆盖整数运算、分数运算、混合运算场景
标准化测试:构造可交换运算(加法、乘法)的表达式,验证标准化后是否按规则排序
合法性测试:构造结果为负数的表达式,验证isValid()能正确识别不合法情况
操作测试:输入错误的路径,错误命令参数,错误的数目进行尝试。
七、项目小结
- 成败得失
成功之处:
代码结构清晰,类的职责划分明确,便于维护和
实现了所有核心功能,包括表达式生成、去重、计算和判分
不足之处:
表达式生成效率仍有提升空间,生成大量题目时耗时较长
错误提示信息可以更详细,提高用户体验 - 结对感受
通过本次结对项目,我们深刻体会到团队协作的优势。两人分工合作,效率明显高于单独开发。在遇到问题时,通过讨论往往能更快找到解决方案,同时也能互相发现代码中的问题,提高代码质量。 - 个人闪光点与建议
改进建议:
可以增加更多的沟通环节,每天花 15 分钟同步进度和问题
在编码前可以更详细地设计接口,减少后续整合时的修改
可以尝试使用单元测试框架,提高测试效率和覆盖率
通过本次项目,我们不仅完成了一个功能完整的数学练习程序,更重要的是学会了如何在团队中协作,如何发挥各自的优势,共同解决问题。这些经验对于今后的软件开发工作将大有用处。