软件工程第三次作业——结对项目

------------恢复内容开始------------

一、作业信息

这个作业属于哪个课程 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
文件读取
image
文件写入
image
CPU使用率
image

四、设计说明

项目采用面向对象设计,包含6个核心类,类间关系如下:
image

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()流程图
image

五、关键代码

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();
}

测试结果
屏幕截图 2025-10-21 214002

1.输入错误参数:
image

2.输入错误题目数目:
image

3.输入错误算术数值范围:
image

4.输入错误的路径:
image
image

思路:
基础运算测试:选择简单分数(如 1/2、1/3)和整数,验证四则运算结果的正确性
带分数测试:通过字符串构造带分数(如 "1'1/2"),验证内部数值转换
表达式测试:构建不同复杂度的表达式树,覆盖整数运算、分数运算、混合运算场景
标准化测试:构造可交换运算(加法、乘法)的表达式,验证标准化后是否按规则排序
合法性测试:构造结果为负数的表达式,验证isValid()能正确识别不合法情况
操作测试:输入错误的路径,错误命令参数,错误的数目进行尝试。

七、项目小结

  1. 成败得失
    成功之处:
    代码结构清晰,类的职责划分明确,便于维护和
    实现了所有核心功能,包括表达式生成、去重、计算和判分
    不足之处:
    表达式生成效率仍有提升空间,生成大量题目时耗时较长
    错误提示信息可以更详细,提高用户体验
  2. 结对感受
    通过本次结对项目,我们深刻体会到团队协作的优势。两人分工合作,效率明显高于单独开发。在遇到问题时,通过讨论往往能更快找到解决方案,同时也能互相发现代码中的问题,提高代码质量。
  3. 个人闪光点与建议
    改进建议:
    可以增加更多的沟通环节,每天花 15 分钟同步进度和问题
    在编码前可以更详细地设计接口,减少后续整合时的修改
    可以尝试使用单元测试框架,提高测试效率和覆盖率
    通过本次项目,我们不仅完成了一个功能完整的数学练习程序,更重要的是学会了如何在团队中协作,如何发挥各自的优势,共同解决问题。这些经验对于今后的软件开发工作将大有用处。
posted on 2025-10-21 22:49  枫芃  阅读(13)  评论(0)    收藏  举报