结对项目作业

这个作业属于哪个课程 https://edu.cnblogs.com/campus/gdgy/Class34Grade23ComputerScience
这个作业要求在哪里 https://edu.cnblogs.com/campus/gdgy/Class34Grade23ComputerScience/homework/13479
这个作业的目标 实现一个自动生成小学四则运算题目的命令行程序

项目成员:莫圣韬 戴宏翔

1.github链接

https://gitee.com/elysiak/Paired-project

2.psp表格

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 40 50
Estimate 估计这个任务需要多少时间 10 15
Requirements Analysis 需求分析 30 35
Development 开发 590 580
Analysis 需求分析(包括学习新技术) 70 100
Design Spec 生成设计文档 40 35
Design Review 设计复审 30 25
Coding Standard 代码规范制定 20 15
Architecture Design 架构设计 60 70
Detailed Design 详细设计 40 45
Coding 具体编码 180 200
Code Review 代码复审 40 30
Unit Test 单元测试 30 20
Integration Test 集成测试 30 20
System Test 系统测试 30 20
Reporting 报告 80 135
Test Report 测试报告 40 45
Performance Analysis 性能分析报告 30 35
Size Measurement 计算工作量 10 15
Postmortem & Process Improvement Plan 事后总结,并提交过程改进计划 30 40
Total 合计 710 765

3.效能分析

屏幕截图 2025-10-16 230041

由图可以看出性能瓶颈是exression类中的evaluate函数
原来的问题:

  • 在乘除法处理阶段,每次删除操作都会导致向量中元素的移动,这在表达式较长时会造成较大的性能开销。
  • 循环中嵌套了删除操作,导致最坏情况下时间复杂度为O(n^2)。

优化后
image

优化思路

使用两个栈:一个存储操作数(numStack),一个存储运算符(opStack)。
遍历表达式中的每个元素(操作数和运算符):

  • 遇到操作数,压入操作数栈。
  • 遇到运算符,与运算符栈顶的运算符比较优先级:
    如果栈顶运算符优先级不低于当前运算符,则从操作数栈弹出两个操作数,从运算符栈弹出一个运算符,进行计算,并将结果压入操作数栈。重复此过程直到栈顶运算符优先级低于当前运算符或 栈为空。然后将当前运算符压入运算符栈。

遍历结束后,将运算符栈中剩余的运算符依次弹出并计算,直到运算符栈为空。
操作数栈中剩下的最后一个数就是表达式的结果。

4.设计实现过程

类1: Fraction (分数类)
功能:表示和操作分数
方法:

  • 构造函数(整数、字符串)
  • 算术运算(+、-、×、÷)
  • 比较运算(==、<、>、<=、>=、!=)
  • 简化分数
  • 字符串转换

类2: Expression (表达式类)
功能:表示和计算数学表达式
核心方法:

  • evaluate():表达式求值(双栈算法)
  • isValid():表达式有效性检查
  • toString():表达式字符串表示

类3: ProblemGenerator (题目生成器)
职责:生成随机的有效表达式
核心方法:

  • generate():生成随机表达式
  • generateNumber():生成随机数
  • generateOperator():生成随机运算符

类4: Application (应用程序)
功能:协调整个程序流程
核心方法:

  • generateProblems():生成题目文件
  • checkAnswers():检查答案
  • parseExpression():解析表达式字符串
  • run():主运行逻辑

关键函数

Expression::evaluate()
deepseek_mermaid_20251020_78b435


ProblemGenerator::generate()
deepseek_mermaid_20251020_d8368b


Application::generateProblems()
deepseek_mermaid_20251020_d6f1db


Application::checkAnswers()
deepseek_mermaid_20251020_905d76

5.代码说明

Fraction

点击查看代码
class Fraction {
private:
    int numerator;      // 分子
    int denominator;    // 分母

    // 分数简化函数,将分数约分到最简形式
    // 通过计算分子和分母的最大公约数,将分数化为最简形式
    // 如果分子为0,则将分母设为1,表示0值分数
    void simplify() {
        if (numerator == 0) {
            denominator = 1;  // 0值分数统一表示为0/1
            return;
        }
        // 计算分子和分母的最大公约数,注意分子取绝对值
        int gcd_val = gcd(std::abs(numerator), denominator);
        numerator /= gcd_val;     // 分子约分
        denominator /= gcd_val;   // 分母约分
    }

    // 计算两个整数的最大公约数(GCD)
    // 使用欧几里得算法递归计算最大公约数
    int gcd(int a, int b) {
        return b == 0 ? a : gcd(b, a % b);
    }

public:
    // 构造函数,通过分子分母创建分数
    // 自动处理分母为0或负数的情况,并自动简化分数
    Fraction(int num = 0, int den = 1) : numerator(num), denominator(den) {
        if (denominator == 0) denominator = 1;  // 防止除零错误,分母设为1
        if (denominator < 0) {
            // 确保分母始终为正,负号转移到分子
            numerator = -numerator;
            denominator = -denominator;
        }
        simplify();  // 构造完成后自动简化分数
    }

    // 字符串构造函数,支持多种分数格式
    // 自动识别并解析带分数、真分数和整数格式
    Fraction(const std::string& str) {
        if (str.find('\'') != std::string::npos) {
            // 处理带分数格式:a'b/c (如 "2'1/2" 表示 2又1/2)
            size_t pos1 = str.find('\'');      // 定位单引号位置
            size_t pos2 = str.find('/');       // 定位斜杠位置
            int integer = std::stoi(str.substr(0, pos1));           // 提取整数部分
            int num = std::stoi(str.substr(pos1 + 1, pos2 - pos1 - 1)); // 提取分子
            int den = std::stoi(str.substr(pos2 + 1));              // 提取分母
            // 将带分数转换为假分数:a'b/c = (a×c + b)/c
            numerator = integer * den + (integer >= 0 ? num : -num);
            denominator = den;
        }
        else if (str.find('/') != std::string::npos) {
            // 处理真分数格式:a/b (如 "3/4")
            size_t pos = str.find('/');
            numerator = std::stoi(str.substr(0, pos));     // 提取分子
            denominator = std::stoi(str.substr(pos + 1));  // 提取分母
        }
        else {
            // 处理整数格式 (如 "5")
            numerator = std::stoi(str);
            denominator = 1;  // 整数分母为1
        }
        simplify();  // 解析完成后自动简化
    }

    // 将分数转换为字符串表示
    // 根据分数的值自动选择最合适的显示格式
    std::string toString() const {
        if (numerator == 0) {
            return "0";  // 0值分数直接返回"0"
        }
        if (denominator == 1) {
            return std::to_string(numerator);  // 整数直接返回数字字符串
        }

        int integer = numerator / denominator;           // 整数部分
        int remainder = std::abs(numerator) % denominator; // 余数部分(取绝对值)

        if (integer == 0) {
            // 真分数格式:分子/分母 (如 "3/4")
            return std::to_string(numerator) + "/" + std::to_string(denominator);
        }
        else if (remainder == 0) {
            // 整数格式 (如 "2")
            return std::to_string(integer);
        }
        else {
            // 带分数格式:整数'分子/分母 (如 "2'1/2")
            return std::to_string(integer) + "'" + std::to_string(remainder) + "/" + std::to_string(denominator);
        }
    }

    // 分数加法运算符重载
    // 通过通分后分子相加实现分数加法
    Fraction operator+(const Fraction& other) const {
        int num = numerator * other.denominator + other.numerator * denominator;
        int den = denominator * other.denominator;
        return Fraction(num, den);  // 返回新分数,会自动简化
    }

    // 分数减法运算符重载
    // 通过通分后分子相减实现分数减法
    Fraction operator-(const Fraction& other) const {
        int num = numerator * other.denominator - other.numerator * denominator;
        int den = denominator * other.denominator;
        return Fraction(num, den);
    }

    // 分数乘法运算符重载
    // 分子乘分子,分母乘分母实现分数乘法
    Fraction operator*(const Fraction& other) const {
        int num = numerator * other.numerator;
        int den = denominator * other.denominator;
        return Fraction(num, den);
    }

    // 分数除法运算符重载
    // 通过乘以倒数实现分数除法,有除零保护
    Fraction operator/(const Fraction& other) const {
        if (other.numerator == 0) {
            return Fraction(0, 1);  // 除零保护,返回0
        }
        // 分数除法:a/b ÷ c/d = a/b × d/c
        int num = numerator * other.denominator;
        int den = denominator * other.numerator;
        return Fraction(num, den);
    }

    // 分数相等比较运算符
    // 比较化简后的分子和分母是否完全相同
    bool operator==(const Fraction& other) const {
        return numerator == other.numerator && denominator == other.denominator;
    }

    // 分数小于比较运算符
    // 通过交叉相乘比较分数大小,避免浮点数精度问题
    bool operator<(const Fraction& other) const {
        return numerator * other.denominator < other.numerator * denominator;
    }

    // 分数大于比较运算符
    // 基于小于运算符实现:a > b 等价于 b < a
    bool operator>(const Fraction& other) const {
        return other < *this;
    }

    // 分数小于等于比较运算符
    // 基于小于运算符实现:a <= b 等价于 !(b < a)
    bool operator<=(const Fraction& other) const {
        return !(other < *this);
    }

    // 分数大于等于比较运算符
    // 基于小于运算符实现:a >= b 等价于 !(a < b)
    bool operator>=(const Fraction& other) const {
        return !(*this < other);
    }

    // 分数不等于比较运算符
    // 基于等于运算符实现:a != b 等价于 !(a == b)
    bool operator!=(const Fraction& other) const {
        return !(*this == other);
    }

    // 检查分数是否为真分数
    // 真分数定义:|分子| < 分母
    bool isProper() const {
        return std::abs(numerator) < denominator;
    }

    // 检查分数是否为非负数
    // 仅检查分子,因为分母始终为正
    bool isNonNegative() const {
        return numerator >= 0;
    }

    // 检查分数是否为零
    // 检查分子是否为0
    bool isZero() const {
        return numerator == 0;
    }
};

Expression

点击查看代码
class Expression {
private:
    std::vector<Fraction> numbers;     // 存储表达式中的所有操作数
    std::vector<std::string> operators; // 存储表达式中的所有运算符

    // 缓存机制:存储已计算的结果,避免重复计算
    mutable Fraction cachedResult;      // 缓存的计算结果
    mutable bool isResultCached;        // 标记结果是否已缓存

    // 预分配的栈空间:用于双栈算法,避免重复内存分配
    mutable std::vector<Fraction> numStack;     // 操作数栈
    mutable std::vector<std::string> opStack;   // 运算符栈

    // 获取运算符的优先级
    // 返回运算符的优先级数值,数值越大优先级越高
    int getPrecedence(const std::string& op) const {
        if (op == "*" || op == "/") return 2;  // 乘除法优先级最高
        if (op == "+" || op == "-") return 1;  // 加减法优先级较低
        return 0;  // 其他运算符默认优先级
    }

    // 应用具体的运算操作
    // 根据运算符类型执行相应的分数运算
    Fraction applyOperation(const Fraction& left, const Fraction& right, const std::string& op) const {
        if (op == "+") return left + right;  // 加法运算
        if (op == "-") return left - right;  // 减法运算
        if (op == "*") return left * right;  // 乘法运算
        if (op == "/") {
            if (right.isZero()) return Fraction(0, 1);  // 除零保护,返回0
            return left / right;  // 除法运算
        }
        return Fraction(0, 1);  // 未知运算符返回0
    }

    // 表达式求值函数,使用双栈算法
    // 实现运算符优先级处理,确保乘除法先于加减法执行
    Fraction evaluate() const {
        // 如果已经计算过结果,直接返回缓存的值,避免重复计算
        if (isResultCached) {
            return cachedResult;
        }

        // 处理空表达式的情况
        if (numbers.empty()) {
            cachedResult = Fraction(0, 1);
            isResultCached = true;
            return cachedResult;
        }

        // 清空栈空间但保留预分配的容量,避免重复内存分配
        numStack.clear();
        opStack.clear();

        // 第一个操作数直接压入操作数栈
        numStack.push_back(numbers[0]);

        // 遍历所有运算符和对应的操作数
        for (size_t i = 0; i < operators.size(); i++) {
            const std::string& currentOp = operators[i];  // 当前运算符
            const Fraction& currentNum = numbers[i + 1];  // 当前操作数

            // 处理运算符优先级:当栈顶运算符优先级不低于当前运算符时,先计算栈顶运算
            // 这确保了乘除法在加减法之前执行
            while (!opStack.empty() &&
                getPrecedence(opStack.back()) >= getPrecedence(currentOp)) {
                // 从栈中弹出运算符和操作数进行计算
                Fraction right = numStack.back(); numStack.pop_back();
                Fraction left = numStack.back(); numStack.pop_back();
                std::string op = opStack.back(); opStack.pop_back();

                // 执行运算并将结果压回操作数栈
                Fraction result = applyOperation(left, right, op);
                numStack.push_back(result);
            }

            // 当前运算符和操作数入栈,等待后续处理
            opStack.push_back(currentOp);
            numStack.push_back(currentNum);
        }

        // 处理栈中剩余的运算(从左到右顺序)
        while (!opStack.empty()) {
            Fraction right = numStack.back(); numStack.pop_back();
            Fraction left = numStack.back(); numStack.pop_back();
            std::string op = opStack.back(); opStack.pop_back();

            Fraction result = applyOperation(left, right, op);
            numStack.push_back(result);
        }

        // 栈顶元素即为最终计算结果,存入缓存
        cachedResult = numStack.back();
        isResultCached = true;
        return cachedResult;
    }

    // 获取运算符的显示形式
    // 将内部运算符转换为用户友好的显示形式
    std::string getDisplayOperator(const std::string& op) const {
        if (op == "*") return "×";  // 乘法显示为×
        if (op == "/") return "÷";  // 除法显示为÷
        return op;  // 加减法保持原样
    }

public:
    // 构造函数:使用操作数和运算符列表初始化表达式
    // 同时预分配栈空间以提高性能
    Expression(const std::vector<Fraction>& nums, const std::vector<std::string>& ops)
        : numbers(nums), operators(ops), isResultCached(false) {
        // 预分配足够的栈空间,避免计算过程中的重复内存分配
        numStack.reserve(numbers.size());      // 操作数栈预留空间
        opStack.reserve(operators.size());     // 运算符栈预留空间
    }

    // 将表达式转换为字符串形式,用于显示和输出
    // 格式:操作数1 运算符1 操作数2 运算符2 ... 操作数n =
    std::string toString() const {
        std::stringstream ss;
        for (size_t i = 0; i < numbers.size(); i++) {
            if (i > 0) {
                // 在运算符前后添加空格,提高可读性
                ss << " " << getDisplayOperator(operators[i - 1]) << " ";
            }
            ss << numbers[i].toString();  // 添加操作数的字符串表示
        }
        ss << " = ";  // 表达式以等号结束
        return ss.str();
    }

    // 获取表达式的计算结果
    // 如果结果已缓存则直接返回,否则调用evaluate()计算
    Fraction getResult() const {
        return evaluate();
    }

    // 检查表达式是否满足所有约束条件
    // 包括数值范围、非负结果和真分数除法等要求
    bool isValid(int range) const {
        // 检查所有操作数是否在指定范围内 [0, range)
        for (const auto& num : numbers) {
            if (num < Fraction(0) || num >= Fraction(range)) {
                return false;  // 数值超出范围
            }
        }

        // 检查中间计算结果是否非负
        // 使用try-catch防止计算过程中出现异常
        try {
            Fraction result = evaluate();
            if (!result.isNonNegative()) return false;  // 结果为负数
        }
        catch (...) {
            return false;  // 计算过程中出现异常
        }

        // 检查所有除法运算的结果是否为真分数
        for (size_t i = 0; i < operators.size(); i++) {
            if (operators[i] == "/") {
                // 确保有足够的操作数
                if (i + 1 >= numbers.size()) return false;
                
                Fraction left = numbers[i];    // 被除数
                Fraction right = numbers[i + 1]; // 除数
                
                if (right.isZero()) return false;  // 除数为零
                
                Fraction div_result = left / right;
                if (!div_result.isProper()) return false;  // 除法结果不是真分数
            }
        }

        return true;  // 所有检查都通过
    }

    // 获取表达式的规范化形式,用于去重比较
    // 格式:操作数1运算符1操作数2运算符2...操作数n(无空格)
    std::string getNormalizedForm() const {
        std::stringstream ss;
        for (size_t i = 0; i < numbers.size(); i++) {
            if (i > 0) {
                ss << operators[i - 1];  // 添加运算符(无空格)
            }
            ss << numbers[i].toString();  // 添加操作数
        }
        return ss.str();
    }

    // 访问器方法:获取操作数列表,主要用于测试
    const std::vector<Fraction>& getNumbers() const { return numbers; }

    // 访问器方法:获取运算符列表,主要用于测试
    const std::vector<std::string>& getOperators() const { return operators; }

    // 清除缓存:当表达式被修改时需要调用此方法
    void clearCache() {
        isResultCached = false;  // 重置缓存标记
    }
};

ProblemGenerator

点击查看代码
class ProblemGenerator {
private:
    int range;           // 数值范围,控制生成的数字大小
    std::mt19937 gen;    // 随机数生成器,用于产生各种随机值

    // 生成随机数,支持生成整数和分数两种类型
    // 根据概率控制决定生成整数还是分数,确保数值在指定范围内
    Fraction generateNumber() {
        std::uniform_int_distribution<> dis(1, std::max(1, range - 1));

        // 概率控制:70%概率生成整数,30%概率生成真分数
        // 只有在范围大于等于2时才可能生成分数
        if (range >= 2 && std::uniform_real_distribution<>(0, 1)(gen) < 0.3) {
            // 生成真分数:分母在[2, range]范围内,分子在[1, 分母-1]范围内
            int den = std::uniform_int_distribution<>(2, range)(gen);      // 随机分母
            int num = std::uniform_int_distribution<>(1, den - 1)(gen);    // 随机分子,确保是真分数
            return Fraction(num, den);
        }
        else {
            // 生成整数:数值在[0, range-1]范围内
            return Fraction(std::uniform_int_distribution<>(0, range - 1)(gen), 1);
        }
    }

    // 生成随机运算符
    // 从四种基本运算符中随机选择一种
    std::string generateOperator() {
        static std::vector<std::string> ops = { "+", "-", "*", "/" };  // 可用的运算符集合
        std::uniform_int_distribution<> dis(0, ops.size() - 1);        // 随机索引生成器
        return ops[dis(gen)];  // 返回随机选择的运算符
    }

public:
    // 构造函数:初始化数值范围和随机数生成器
    // 使用真随机数种子确保每次运行生成不同的题目
    ProblemGenerator(int r) : range(r) {
        std::random_device rd;  // 真随机数设备,用于生成种子
        gen.seed(rd());         // 用真随机数初始化伪随机数生成器
    }

    // 生成有效的数学表达式
    // 通过多次尝试确保生成的表达式满足所有约束条件
    Expression generate() {
        int attempts = 0;                    // 尝试计数器
        const int maxAttempts = 1000;        // 最大尝试次数,防止无限循环

        // 尝试生成有效表达式,直到成功或达到最大尝试次数
        while (attempts < maxAttempts) {
            // 随机决定运算符数量(1-3个),对应2-4个操作数
            int opCount = std::uniform_int_distribution<>(1, 3)(gen);
            std::vector<Fraction> numbers;       // 操作数容器
            std::vector<std::string> operators;  // 运算符容器

            // 构建表达式结构:生成操作数和运算符
            for (int i = 0; i <= opCount; i++) {
                numbers.push_back(generateNumber());  // 生成操作数
                if (i < opCount) {
                    operators.push_back(generateOperator());  // 生成运算符
                }
            }

            // 创建表达式对象并验证其有效性
            Expression expr(numbers, operators);
            if (expr.isValid(range)) {
                return expr;  // 返回有效的表达式
            }
            attempts++;  // 增加尝试计数
        }

        // 保底机制:如果多次尝试都失败,返回一个简单的有效表达式
        // 确保函数始终有返回值,避免生成失败
        return Expression({ Fraction(1), Fraction(1) }, { "+" });
    }
};

Application

点击查看代码
class Application {
private:
    // 生成指定数量的四则运算题目并保存到文件
    // 同时生成对应的答案文件,确保题目不重复且符合所有约束条件
    void generateProblems(int count, int range) {
        ProblemGenerator generator(range);  // 创建题目生成器,指定数值范围
        std::set<std::string> generatedExpressions;  // 用于存储已生成表达式的规范化形式,实现去重
        std::vector<Expression> problems;   // 存储有效的题目表达式
        std::vector<Fraction> answers;      // 存储对应的答案

        // 打开输出文件,用于保存题目和答案
        std::ofstream exerciseFile("Exercises.txt");
        std::ofstream answerFile("Answers.txt");

        // 检查文件是否成功打开
        if (!exerciseFile.is_open() || !answerFile.is_open()) {
            std::cerr << "无法打开输出文件!" << std::endl;
            return;
        }

        int generated = 0;                  // 已生成的题目数量
        const int maxAttempts = count * 10; // 最大尝试次数,防止无限循环
        int attempts = 0;                   // 当前尝试次数

        // 循环生成题目,直到达到指定数量或超过最大尝试次数
        while (generated < count && attempts < maxAttempts) {
            Expression problem = generator.generate();  // 生成一个题目表达式
            std::string normalized = problem.getNormalizedForm();  // 获取规范化形式用于去重

            // 检查表达式是否重复,基于规范化形式进行比较
            if (generatedExpressions.find(normalized) == generatedExpressions.end()) {
                generatedExpressions.insert(normalized);  // 添加到已生成集合
                problems.push_back(problem);              // 添加到题目列表
                answers.push_back(problem.getResult());   // 计算并存储答案
                generated++;                              // 增加已生成计数
            }
            attempts++;  // 增加尝试计数
        }

        // 将题目和答案写入文件
        for (size_t i = 0; i < problems.size(); i++) {
            // 写入题目文件:序号. 表达式 = 
            exerciseFile << (i + 1) << ". " << problems[i].toString() << std::endl;
            // 写入答案文件:序号. 答案
            answerFile << (i + 1) << ". " << answers[i].toString() << std::endl;
        }

        // 关闭文件流
        exerciseFile.close();
        answerFile.close();

        // 输出生成结果信息
        std::cout << "已生成 " << problems.size() << " 道题目到 Exercises.txt 和 Answers.txt" << std::endl;
    }

    // 检查答案文件的正确性,对比题目文件和答案文件
    // 生成统计报告,显示正确和错误的题目编号
    void checkAnswers(const std::string& exerciseFile, const std::string& answerFile) {
        // 打开题目文件和答案文件
        std::ifstream exFile(exerciseFile);
        std::ifstream ansFile(answerFile);

        // 检查文件是否成功打开
        if (!exFile.is_open() || !ansFile.is_open()) {
            std::cerr << "无法打开输入文件!" << std::endl;
            return;
        }

        std::vector<int> correct, wrong;  // 存储正确和错误题目的编号
        std::string exLine, ansLine;      // 用于读取文件的每一行
        int lineNum = 1;                  // 当前处理的题目编号

        // 逐行读取题目文件和答案文件
        while (std::getline(exFile, exLine) && std::getline(ansFile, ansLine)) {
            // 提取表达式和答案字符串,跳过序号部分
            size_t exPos = exLine.find(". ");
            size_t ansPos = ansLine.find(". ");

            // 确保找到了序号分隔符
            if (exPos != std::string::npos && ansPos != std::string::npos) {
                std::string expressionStr = exLine.substr(exPos + 2);  // 提取表达式部分
                std::string givenAnswer = ansLine.substr(ansPos + 2);  // 提取答案部分

                // 移除表达式末尾的 "= ",还原为纯表达式
                if (expressionStr.find(" = ") != std::string::npos) {
                    expressionStr = expressionStr.substr(0, expressionStr.length() - 3);
                }

                try {
                    // 解析表达式字符串为Expression对象
                    Expression expr = parseExpression(expressionStr);
                    // 计算表达式的正确结果
                    Fraction computedAnswer = expr.getResult();
                    // 将答案文件中的字符串转换为Fraction对象
                    Fraction givenAnswerFrac(givenAnswer);

                    // 比较计算结果与给定答案
                    if (computedAnswer == givenAnswerFrac) {
                        correct.push_back(lineNum);  // 答案正确,记录题目编号
                    }
                    else {
                        wrong.push_back(lineNum);    // 答案错误,记录题目编号
                    }
                }
                catch (...) {
                    // 解析或计算过程中出现异常,视为错误答案
                    wrong.push_back(lineNum);
                }
            }
            lineNum++;  // 处理下一题
        }

        // 关闭文件流
        exFile.close();
        ansFile.close();

        // 生成批改结果文件
        std::ofstream gradeFile("Grade.txt");
        
        // 写入正确答案统计
        gradeFile << "Correct: " << correct.size() << " (";
        for (size_t i = 0; i < correct.size(); i++) {
            gradeFile << correct[i];  // 写入题目编号
            if (i < correct.size() - 1) gradeFile << ", ";  // 添加逗号分隔
        }
        gradeFile << ")" << std::endl;

        // 写入错误答案统计
        gradeFile << "Wrong: " << wrong.size() << " (";
        for (size_t i = 0; i < wrong.size(); i++) {
            gradeFile << wrong[i];  // 写入题目编号
            if (i < wrong.size() - 1) gradeFile << ", ";  // 添加逗号分隔
        }
        gradeFile << ")" << std::endl;

        gradeFile.close();  // 关闭批改结果文件
        std::cout << "统计结果已输出到 Grade.txt" << std::endl;
    }

    // 解析表达式字符串,将其转换为Expression对象
    // 支持从文件读取的表达式格式转换为内部表示
    Expression parseExpression(const std::string& expr) {
        std::vector<Fraction> numbers;        // 存储解析出的操作数
        std::vector<std::string> operators;   // 存储解析出的运算符

        std::stringstream ss(expr);  // 使用字符串流进行分词
        std::string token;           // 存储每个分词

        // 逐个处理分词
        while (ss >> token) {
            if (token == "+" || token == "-" || token == "×" || token == "÷") {
                // 处理运算符:将显示格式转换为内部格式
                if (token == "×") operators.push_back("*");    // 乘法符号转换
                else if (token == "÷") operators.push_back("/"); // 除法符号转换
                else operators.push_back(token);  // 加减法符号保持不变
            }
            else {
                // 处理数字:使用Fraction的字符串构造函数解析各种格式
                numbers.push_back(Fraction(token));
            }
        }

        // 使用解析出的操作数和运算符创建Expression对象
        return Expression(numbers, operators);
    }

    // 显示程序使用帮助信息
    // 说明命令行参数的使用方法和示例
    void showHelp() {
        std::cout << "使用方法:" << std::endl;
        std::cout << "生成题目: Myapp.exe -n <题目数量> -r <数值范围>" << std::endl;
        std::cout << "检查答案: Myapp.exe -e <题目文件> -a <答案文件>" << std::endl;
        std::cout << "示例:" << std::endl;
        std::cout << "  Myapp.exe -n 10 -r 10" << std::endl;
        std::cout << "  Myapp.exe -e Exercises.txt -a Answers.txt" << std::endl;
    }

public:
    // 应用程序主入口函数
    // 解析命令行参数并执行相应的功能
    void run(int argc, char* argv[]) {
        // 如果没有提供参数,显示帮助信息
        if (argc < 2) {
            showHelp();
            return;
        }

        // 解析命令行参数,存储为键值对
        std::map<std::string, std::string> params;
        for (int i = 1; i < argc; i += 2) {
            if (i + 1 < argc) {
                params[argv[i]] = argv[i + 1];  // 参数名作为键,下一个参数作为值
            }
        }

        // 检查是否为生成题目模式
        if (params.find("-n") != params.end() && params.find("-r") != params.end()) {
            // 提取题目数量和数值范围参数
            int count = std::stoi(params["-n"]);
            int range = std::stoi(params["-r"]);

            // 参数验证:必须为正整数
            if (count <= 0 || range <= 0) {
                std::cerr << "参数必须为正整数!" << std::endl;
                return;
            }

            // 参数验证:题目数量不能超过限制
            if (count > 10000) {
                std::cerr << "题目数量不能超过10000!" << std::endl;
                return;
            }

            // 执行题目生成功能
            generateProblems(count, range);

        }
        // 检查是否为答案检查模式
        else if (params.find("-e") != params.end() && params.find("-a") != params.end()) {
            // 执行答案检查功能
            checkAnswers(params["-e"], params["-a"]);

        }
        // 参数不匹配,显示错误信息和帮助
        else {
            std::cerr << "参数错误!" << std::endl;
            showHelp();
        }
    }
};

6.测试运行

测试结果

屏幕截图 2025-10-16 215439
image

测试部分例子

image
image

exercise.txt

屏幕截图 2025-10-20 124501

answer.txt

image

grade.txt

image

  1. 计算正确性验证
  • 运算符优先级:确保乘除法优先于加减法
  • 结果验证:每个测试用例都通过手动计算验证
  1. 约束条件
  • 非负结果:所有表达式验证确保不产生负数
  • 真分数除法:除法运算结果是真分数
  • 数值范围:所有数值都在指定范围内
    运算符数量:不超过3个运算符
  1. 边界情况处理
  • 零的处理:正确处理零的运算和显示
  • 负分数:负分数的运算和显示无错误
  • 除零保护:防止除零出错

7.项目小结

感想

1.

这次合作开发程序,是一次非常宝贵和高效的体验。
在合作初期,我们首先对任务进行了拆分,并明确了各自负责的模块。过程中,我们不可避免地遇到了一些技术思路和实现方式上的分歧。但通过定期的代码审查和集中讨论,我们不仅高效地解决了问题,还相互借鉴了更好的编程习惯和算法思路。我的搭档莫圣韬在架构设计上很有远见,而我在细节处理和调试方面比较擅长,这种优势互补让我们的代码质量远超个人独立完成的水准。
更重要的是,这次合作让我深切体会到团队协作的力量。当遇到棘手的技术难题时,能够随时与伙伴探讨,极大地减轻了心理压力,也加速了问题的解决。最终程序的成功,是我们共同智慧和努力的结晶。
总而言之,这是一次非常成功的合作。它不仅圆满完成了开发任务,更让我在沟通协作和技术层面都获得了新的成长。

2.

在这次结对开发小学四则运算程序的过程中,我与队友深刻体会到了协作编程的魅力。我们分工明确,通过频繁的代码审查和设计讨论,确保了各模块的无缝衔接。。双栈算法的实现让我们领略了数据结构的美妙,而分数运算的完整性则考验着我们对细节的把握。面对复杂的业务约束,我们共同探讨解决方案,在争论中达成共识,在合作中相互学习。这个项目不仅提升了我们的编程能力,更培养了团队协作精神。当看到程序成功生成万道题目并准确批改时,所有的调试艰辛都化为了成就的喜悦。这次经历让我们明白,优秀的软件不仅是技术的结晶,更是团队智慧与默契的产物。

建议

代码规范与风格统一:

  • 使用一致的命名规范(如变量、函数、类名的命名)。
  • 统一代码格式(如缩进、括号风格等)。
  • 可以使用代码格式化工具(如ClangFormat)来自动格式化代码。

模块化设计:

  • 将系统划分为清晰的模块,每个模块由一个人负责。
  • 定义好模块之间的接口,便于并行开发。

版本控制:

  • 使用Git等版本控制工具,并制定分支管理策略(如Git Flow)。
  • 定期提交代码,并编写清晰的提交信息。

代码审查:

  • 互相审查代码,确保代码质量,并分享知识。
posted @ 2025-10-21 19:02  elysia。。。  阅读(7)  评论(0)    收藏  举报