结对作业

结对项目

这个作业属于哪个课程 信安1912-软件工程 (广东工业大学 - 计算机学院)
这个作业要求在哪里 结对项目
这个作业的目标 四则运算生成/检查器+效能分析+样例测试+总结
成员1介绍 张孟聪,3119005486
成员2介绍 杨析睿,3119005482

GitHub:

成员 学号 GitHub地址
张孟聪 3119005486 https://github.com/Rushmmmc/TeamWork
杨析睿 3119005482 https://github.com/ORonaldinhoO/Software-Engineering/tree/master/partner

程序设计

主程序流程图:

各模块关系图:

代码说明

关键部分

除法处理

static void doDivideFraction(string& fraction1, const string& fraction2) {
    vector<string> temp2 = substring(fraction2, '/');
     doMultiplyFraction(fraction1, temp2[1] + "/" + temp2[0]);  // 转换成乘法 
  }

该部分通过substring()先将原分号去除,再调换被除数的分子分母,重新构成分数,从而将除法巧妙转换成乘法,然后借助通用乘法模块实现除法。

通用乘法模块

static void doMultiplyFraction(string& fraction1, const string& fraction2) {
    vector<string> temp1 = substring(fraction1, '/');
    vector<string> temp2 = substring(fraction2, '/');
    // 分子
    int molecule1 = stoi(temp1[0]),molecule2 = stoi(temp2[0]);
    // 分母
    int denominator1 = stoi(temp1[1]), denominator2 = stoi(temp2[1]);
    int res1 = molecule1 * molecule2;
    int res2 = denominator1 * denominator2;
    fraction1 = to_string(res1) + "/" + to_string(res2);
  }

该部分通过subsrting()先将乘数和被乘数的分号去除,下一步提取出各自的分子和分母,分子相乘,分母相乘,组合成新的分数结果。

其中,stoi()将 n 进制的字符串转化为十进制,to_string将数字常量转换为字符串。

需要提及的是,这一部分最后并未进行化简处理。化简步骤放在最后所有运算小项相加得到结果时,这样即可一步处理到位。

通用加法模块

static void doAddFraction(string& fraction1, const string& fraction2) {
    vector<string> temp1 = substring(fraction1, '/');
    vector<string> temp2 = substring(fraction2, '/');
    // 分子
    int molecule1 = stoi(temp1[0]),molecule2 = stoi(temp2[0]);
    // 分母
    int denominator1 = stoi(temp1[1]), denominator2 = stoi(temp2[1]);
    // 获得最小公倍数
    int multiple = getLeastCommonMultiple(denominator1, denominator2);
    int res = molecule1 * (multiple / denominator1) + molecule2 * (multiple / denominator2);
    fraction1 = to_string(res) + "/" + to_string(multiple);

该部分思想与通用乘法模块大体相似,只是多了获得最小公倍数的步骤,从而匹配加法结果。

化简

 private:
  // 化简 
  static void simplyFraction(string& fraction) {
    vector<string> temp1 = substring(fraction, '/');  // 去除分号 
    string res;
    int m = stoi(temp1[0]), n = stoi(temp1[1]);
    int check = m / n;  // 以5/2为例 
    if (check >= 1) {
      res += to_string(check) + "'";
      m -= check * n;
    }
    if (m == 0)  {     // 如果除得尽,不需分数表示 
      fraction = to_string(check);
      return;
    }
    int gcd = getGcd(n, m);
    fraction = res;
    fraction += n / gcd == 1 ? to_string(m / gcd) : to_string(m / gcd) + "/" + to_string(n / gcd);  // 真分数形式的分数/带分数表示 
  }

该部分依然在去除分号后得到分子和分母。

先计算check值,若不小于1,结果为带分数或整数,res += to_string(check) + "'"便是添加(可能是带分数的)整数项。

下一步if(m==0),判断原分子分母是否除得尽,除得尽则不需要分数表示。

最后,借助getGcd()求余数,并拼接计算结果,同时实现结果的化简。

计算过程(Caculator.cpp)

#include "Calculator.h"

string Calculator::calculate(string text) {
  // 上一运算符 默认为+
  char mark = '+';
  const vector<string>& texts = substring(text, ' ');
  stack<string> numStack;
  string fraction;
  string num;
  string result;
  // 循环推进文本 
  for (int i = 0; i < texts.size(); i++) {
    string cur = texts[i];
    if(stringIsNum(cur)) {
      num = cur + "/1";
    }
    // 如果是分数
    else if (cur.size() >= 3){
      num = cur;
    }
    // 如果是运算符或者到达末尾
    if ((cur.size() == 1 && !stringIsNum(cur)) || i == texts.size() - 1) {
      switch (mark) {
        case '+': {
          numStack.push(num);
          break;
        }
        case '-': {
          numStack.push("-" + num);
          break;
        }
        case 'x': {
            multiplyFraction(num, numStack.top());
            numStack.pop();
            numStack.push(num);
          break;
        }
        case '/': {
          string temp = numStack.top();
          divideFraction(temp, num);
          numStack.pop();
          numStack.push(temp);
          break;
        }
        default: break;
      }
      mark = cur[0];
    }
  }
  // 整理数据栈,最后统一相加 
  while (!numStack.empty()) {
    // cout << numStack.top() << endl;
    addFraction(result, numStack.top());
    numStack.pop();
  }

  simplyFraction(result);  // 化简 
  return result;

前面提及的四个模块,均属于Caculator.h头文件中的模块组成,而Caculator.cpp通过引用头文件定义一道题目的计算过程

大体步骤分为三步:

  • 循环推进题目文本text

    • 判断是否为十进制数字,是则将其分数化
    • 判断是否为运算符或者到达末尾,
      • 是则再进行switch分支判断
        • 加减直接入栈,乘除先得到结果小项再入栈
        • 到达题目末尾,break跳出循环
      • 否则继续推进题目
  • 整理数据栈,最后以逐个相加的方式(addFraction()上一结果与栈顶结果相加,再出栈),实现统一相加

  • 化简最终结果并返回

生成题目集并求解(Examination.cpp)

#include "Examination.h"
#include "Calculator.h"

unordered_map<int, char> Examination::operatorMap_ = {
        {1, '+'},
        {2, '-'},
        {3, 'x'},
        {4, '/'}
};  // 运算符 

// Examination::generate,生成count道数目运算题 
vector<std::string> Examination::generate(int count, int maxNum, vector<string>& results) {
  srand((int)time(0));
  // 用于判断结果是否重复 如果重复则重新生成
  unordered_set<string> checkResults;

  vector<std::string> examinations;
  for (int i = 0; i < count; i++) {
    // 生成本题目的运算符个数 最多3个
    int operatorCount = random(4);
    // 生成本题目分数个数
    int fractionCount = random(operatorCount + 2);
    // 分数所在位置的序列
    unordered_set<int> fractions;
    while (fractions.size() != fractionCount) {
      int fractionsIndex = random(operatorCount + 2) - 1;
      fractions.insert(fractionsIndex);
    }
    string curExamination;
    for (int j = 0; j < operatorCount + 1; j++) {
      // 本次需要插入的不是分数
      if (!fractions.count(j)) {
        curExamination += to_string(random(maxNum + 1));
      }
      else {
        curExamination += to_string(random(maxNum + 1)) + "/" + to_string(random(maxNum + 1));
      }
      // 最后一轮不需要再加操作运算符了
      if (j != operatorCount) {
        curExamination += " ";
        curExamination += operatorMap_[random(5)];
        curExamination += " ";
      }
    }
    // 对这一道题目求解 
    string examinationRes = Calculator::calculate(curExamination);
    // 查重
    bool flag = checkResults.count(examinationRes);
    // flag为true就重复了,不需要再次添加该题信息 
    if (flag) {
      i--;
      continue;
    }
    // 不重复则添加题目信息和对应答案 
    else {
      examinations.push_back(curExamination);
      checkResults.insert(examinationRes);
      results.push_back(examinationRes);
    }
  }
  return examinations;
}

指定题目数量和数值范围,主要通过一个外层for循环一一生成题目,生成完每一道题目后立即查重,不重复再添加信息。

亮点

1.编程语言选择高性能的C++,在C的基础上增加面向对象的特点,代码可读性好,同时在方便使用现有类的基础上,具有更小的内存资源消耗,提高运行速度;

2.一般的数值运算需要两个栈。该程序优化后只需用一个数据栈,记录每次运算的前一个操作符和当前操作符,因为操作符的优先级只发生在前一个和当前,只用记录这两个即可保证运算的正确性,具体做法是按比较两个操作符的优先级来处理;

3.模式选择步骤加入异常处理,防止因命令行传参造成的程序错误,

4.文件读取环节添加异常处理

异常处理

模式选择

模式选择取决于命令行传递的参数。

runFlag: 布尔类型标志变量。

第一个if,如果成立,runFlag为真,模式一激活。

相应的else if,模式一与模式二对立,runFlag为真,模式二激活。

第二个if,当传递参数不合规或runFlag为假时,提示重新在命令行传参,程序结束。

文件读取

如果文件读取为空,则提前提示并退出。

尽管else if语句已经判断了非空,但为了避免打开失败,再做一次判断,可提前提示文件不存在。

传参不合规的提示。

效能分析与改进

效能分析时输入的参数:

调用树:

Flame Graph(火焰图):

all:

caculate:

generate:

main:

可以看到,由Caculator::caculate到Examination:: generate再到main,占用率越来越高,符合程序各部分关系图所描述的情况。

测试运行

正确输入规范:

其中,模式一中n表示题目数目,r表示最大数值。

样例一

题目根据要求成功生成,解答结果经验证正确,成功保存至文件。

样例二

题目根据要求成功生成,解答结果经验证正确,成功保存至文件(重新覆盖之前的文件)。

样例三

题目根据要求成功生成,解答结果经验证正确,成功保存至文件(重新覆盖之前的文件)。

样例四

输入不合规,提示重新输入。

样例五

检测至模式二,对题目及其结果验证,并统计正确/错误信息,成功保存至文件。

样例六

先模式一重新生成题目和结果,再选择模式二,对题目及其结果验证,并统计正确/错误信息,成功保存至文件(重新覆盖之前的文件)。

样例七

输入不合规,提示重新输入。

样例八

输入不合规,提示重新输入,验证了模式一和模式二的区分。

样例九

输入不合规,提示重新输入,验证了模式一和模式二的区分。

样例十

将结果文件内容清空:

因为解答为空,所以无法验证正确/错误情况,与预期一致。

样例十一

模式二中特意输入错误的文件名参数:

应该是Exercises.txt,而非Exercise.txt。检测到文件不存在,异常处理成功。

PSP表格

*PSP2.1* *Personal Software Process Stages* *预估耗时(分钟)* *实际耗时(分钟)*
Planning 计划
· Estimate · 估计这个任务需要多少时间 30 30
Development 开发 490 450
· Analysis · 需求分析 (包括学习新技术) 200 100
· Design Spec · 生成设计文档 30 30
· Design Review · 设计复审 (和同事审核设计文档) 20 20
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 10 10
· Design · 具体设计 50 50
· Coding · 具体编码 30 20
· Code Review · 代码复审 30 20
· Test · 测试(自我测试,修改代码,提交修改) 40 30
Reporting 报告 20 20
· Test Report · 测试报告 20

项目小结

项目收获

1.花费的时间比想象的要长,说明思路想法和代码工程的互通难免遇到问题,因此除了加强自身代码能力,还要提前预留好时间和策略;另外,本次合作暂未用到git的分支管理,下次可以学习。

2.技术选型上使用了C++语言,排查了一些内存泄漏问题以及空悬指针问题,收获良多;

3.多次commit的敏捷开发能快速反馈,没必要第一次就追求完整完善。根据想法快速地初步落实好,再从这一次提交的结果出发,可以让思路、存在问题、优化方向更清晰;

4.项目的最后开发阶段,鼓励在算法、性能方向进行优化,思考有没有更高效率的代码实现;

5.git语法记录:1. git rm * --cached:清空暂存区所有缓存;2.git reset --soft HEAD^,撤销未push的commit,例如git reset --soft HEAD~2为撤销回到前两个版本。

结对感受与相互建议

张孟聪:

这次结对项目我主要负责代码设计,感觉还是有不少的收获的,一开始看到题目感觉比较简单,但是真正要实现维持分数的而不是浮点数才发现有很多麻烦的事情。这次和队友合作总体比较愉快,使用C++进行开发真的很nice。

杨析睿:

这次自己主要是辅助,更多地参与代码检查和文档撰写。其实一开始自己也可以和搭档共同探讨实现思路,但本次这一点做的并不是很好。代码检查方面,要多向搭档确认好代码功能,避免理解偏差,有建议就及时指出。文档撰写方面,设计、代码说明、测试等等都应有所说明,同时语言表达要简明清晰,这样文档才具备良好可读性。

对于搭档,自己的搭档代码能力很强,代码规范和相关注释也做的很到位,他的开发能力是自己需要学习的地方。另外,搭档对细节的一些优化处理也值得自己品味学习,例如计算部分从两个栈简化到单个栈的优化。

最后,结对项目需要两个人多沟通,保持联系,以提高项目开发效率和完整度。

posted @ 2021-10-25 23:19  好好踢球的小Ray  阅读(54)  评论(1编辑  收藏  举报