结对作业——随机生成四则运算(Core 第7组)
结对作业
——随机生成四则运算(core第7组)
吕佳玲 PB16060145
涂涵越 PB16060282
GITHUB地址
https://github.com/hytu99/homework_2
(7_Arithmetic 文件夹内有最新的.dll、.lib、.h文件以及API文档)
项目简介
这次软件工程结对项目为制作一个给小学生用的四则运算出题软件,然后我们抽到的是Core组,也就是负责计算核心——随机生成四则运算——这一部分,并将其封装成dll模块,供UI组使用。
项目分析
首先,我们对外,就是接受UI传进来的相关参数,然后生成题目,并将生成的题目及相应的结果以字符串数组的形式返回给UI。
而我们自己内部,则是按照要求(表达式类型、运算符数目、运算符种类等)生成随机一定数量题目。就随机生成题目的方式,为了更方便地满足所要求的限定条件,通过讨论,我们采用的是通过循环,按照运算的顺序逐步生成完整的表达式,在生成的过程中通过比较运算符的优先级以及逐步计算中间结果的方式,来保证不出现不必要的括号以及中间结果不出现负数。
代码架构与具体实现
(1)对外为一个arithmetic类
1 class arithmetic { 2 private: 3 typedef struct expNode { 4 string exp; 5 string ans; 6 }expNode; 7 expNode *p; 8 int expNum; 9 int expType; //0 for integers, 1 for decimals, 2 for fractions 10 int oprNum; 11 int oprType[5]; 12 int min, max; 13 int accuracy; //the accuracy of decimals 14 15 public: 16 arithmetic() { 17 18 expNum = 1; 19 expType = 0; 20 oprNum = 1; 21 //oprType = 0; 22 oprType[0] = 1; 23 oprType[1] = 1; 24 oprType[2] = 0; 25 oprType[3] = 0; 26 oprType[4] = 0; 27 28 min = 1; 29 max = 10; 30 accuracy = 2; 31 32 } 33 34 void setExpNum(int n); 35 void setExpType(int n); 36 void setOprNum(int n); 37 void setOprAdd(int n); 38 void setOprSub(int n); 39 void setOprMul(int n); 40 void setOprDiv(int n); 41 void setOprPow(int n); 42 void setOprAll(int a, int s, int m, int d, int p); 43 void setOprByStr(string s); 44 45 void setBounds(int min, int max); 46 void setAccuracy(int n); 47 48 int getExpNum(); 49 50 string* getExpSet(); 51 52 string* getAnsSet(); 53 54 void generate(); 55 56 };
(a)类的成员变量
(b)类的成员函数
(c)使用示例
1 arithmetic test; //定义一个arithmetic类变量 2 3 test.setExpNum(20); //生成20个表达式test.setExpType(DECIMAL); //表达式类型为小数 4 test.setBounds(1, 20); //操作数范围为1~20 5 test.setOprNum(4); //运算符为4个 6 test.setOprAdd(1); 7 test.setOprSub(1); 8 test.setOprMul(1); 9 test.setOprDiv(1); 10 test.setOprPow(0); //运算符包括“+-*/” 11 /*设置运算符种类还可用以下两种方式之一 12 test.setOprAll(1, 1, 1, 1, 0); 13 test.setOprByStr("+-*/"); */ 14 test.setAccuracy(2); //精度为两位小数 15 16 test.generate(); //生成表达式 17 18 string* expSet; //表达式数组 19 string* ansSet; //结果数组 20 expSet = test.getExpSet(); 21 ansSet = test.getAnsSet(); 22 23 for (int i = 0; i < test.getExpNum(); i++) { 24 //屏幕输出表达式及结果 25 cout << expSet[i] << " = " << ansSet[i] << endl; 26 }
(2)设置参数(set系列函数)
函数内部定义了异常,当传入参数不符合要求时,抛出异常,并将题目数量expNum设为0,防止程序崩溃。
1 void arithmetic::setExpNum(int n) { 2 3 try { 4 expNum = n; 5 if (expNum < 0) { 6 throw "The number of expressions must be an integer and at least one."; 7 } 8 } 9 catch (const char* msg) { 10 cout << msg << endl; 11 setExpNum(0); 12 } 13 }
(3)随机生成表达式
generate函数根据表达式的种类expType调用不同的生成表达式的函数。
调用的函数原型:
string newExactDivExp(int oprNum, int oprType, int min, int max, int &result); //返回除法均为整除的表达式,result保存结果字符串
string newExp(int oprNum, int oprType, int min, int max, double &result,int accuracy); //生成结果为小数的表达式,result保存结果字符串
string newFracExp(int oprNum, int oprType, int min, int max, Fraction &result); //生成分数表达式,result保存结果字符串
其中newFracExp函数使用了自定义的Fraction类,其中重载了各种运算符,类定义如下:
1 class Fraction 2 { 3 private: 4 int nume; // numerator 5 int deno; // denominator 6 public: 7 Fraction(int nu = 0, int de = 1) :nume(nu), deno(de) {} 8 void simplify(); 9 string display(); 10 void setNume(int nu); 11 void setDeno(int de); 12 int getNume(); 13 int getDeno(); 14 15 friend Fraction operator+(const Fraction &c1, const Fraction &c2); 16 friend Fraction operator-(const Fraction &c1, const Fraction &c2); 17 friend Fraction operator*(const Fraction &c1, const Fraction &c2); 18 friend Fraction operator/(const Fraction &c1, const Fraction &c2); 19 friend Fraction operator^(const Fraction &c1, const Fraction &c2); 20 21 friend bool operator>(const Fraction &c1, const Fraction &c2); 22 friend bool operator<(const Fraction &c1, const Fraction &c2); 23 friend bool operator==(const Fraction &c1, const Fraction &c2); 24 friend bool operator!=(const Fraction &c1, const Fraction &c2); 25 friend bool operator>=(const Fraction &c1, const Fraction &c2); 26 friend bool operator<=(const Fraction &c1, const Fraction &c2); 27 28 friend Fraction operate(const Fraction &c1, char c, const Fraction &c2); 29 };
测试结果
以下expNum = 20, oprNum = 4,运算符包括“+-*/^”, min = 1, max = 20。
(1)生成整除表达式(expType = 0)
(2)生成一般(非整除)表达式(expType = 1,accuracy = 2)
(3)生成分数表达式(expType = 2)
BUG记录与分析
这部分主要记录一些编程时出现的bug,大多数都是一些细节上的问题。
(1)生成随机数
在这一部分我们还是遇到了很多小问题, 首先就是第一次测试时,发现生成两个操作数经常相等, 后来才发现是因为这句“srand((unsigned) time(NULL));”不能放在我们的random函数里面; 再就是一个double转int的问题, 因为我们计算中间结果是用double类型来计算, 然后定义的变量为int型, 然后出现了结果为负数的情况, 原因可能是double转int丢失了小数, 导致后续计算结果并非准确值。
(2)情况考虑不全
这个主要是在生成表达式函数内部, 为了使表达式更随机, 我们使用了随机函数来决定新产生的随机数是放在左还是放在右, 但有一些情况是必须放在左或者右的, 但是由于初始考虑不周, 就出现了很多非预期的结果。
这里总结一下, 必须放在左边的情况: 运算符为除号, 且tempVal(当前中间结果)为0; 运算符为减号, 且tempVal 大于范围最大值max。必须放在右边的情况: 运算符为减号, 且tempVal 小于 范围最小值min。
再就是关于是否加括号的问题,开始只考虑了运算符的优先级,没考虑减号和除号后面的括号有括号无括号要变号的问题,这导致最后结果与表达式不匹配。
(3)分数比较大小
当分母范围比较大的时候,生成的分数表达式偶尔仍会出现负数的结果,但是按代码的逻辑却并不可能出现这样的结果,开始百思不得其解,一味在代码逻辑上考虑毫无结果,最后才发现是在分数比较上出现了溢出现象,因为当时写分数比较的函数时,考虑了负数情况,为了让代码更简洁,就采取了“(通分后分子a-通分后分子b)*通分后的分母 > 0"来判断分数a大于分数b,结果没想到乘积过大导致溢出变成负数,所以说代码找bug还是不要总以为你以为是对的的地方都是对的。
DLL封装
dll封装这一步开始尝试时也是遇到了很多麻烦,要么是无法重新生成解决方案(报很多错),要么就是生成了.dll、.lib文件后,在新的项目里无法调用。最初还怀疑是不是由于使用了c++的stl,或者有多个类,类里又有友元函数等比较复杂的东西(因为教程里的通常都很简单),后来才发现还是问题出现在一些小问题上(不过不得不说有些教程确实不负责任...)。
用visual studio来进行dll封装还是很方便的,可以新建一个dll项目,添加.cpp、.h文件,将代码复制进去,然后关键就是要在.h文件中在那些需要导出的的类或者函数前加上“__declspec(dllexport)”,这一点很多教程都说到了,但很多却没说生成新的解决方案后,使用.h文件时要把里面的这一句改为“__declspec(dllimport)”,导致调用时显示“无法解析的外部函数”。
为了避免修改的麻烦,也可以用条件编译来实现,在 .h文件中加入以下代码:
1 #ifndef ArithmeticDLL 2 #define ArithmeticAPI __declspec(dllexport) 3 #else 4 #define ArithmeticAPI __declspec(dllimport) 5 #endif
然后在.cpp文件中加入“#define ArithmeticDLL”这一句,这样在你的项目中就是“__declspec(dllexport)”,在别的使用dll的项目中就是“__declspec(dllimport)”。
还有一个问题就是最好所有的函数、类前面都要加这一句,包括你要开放给外部调用的和你的代码内部调用的,不然会报错。另外,类加了这一句后内部的成员函数就不用加了,但它的友元函数仍然要加。
添加语句示例如下:
1 class ArithmeticAPI Fraction 2 { 3 private: 4 int nume; // numerator 5 int deno; // denominator 6 public: 7 Fraction(int nu = 0, int de = 1) :nume(nu), deno(de) {} 8 9 void simplify(); 10 string display(); 11 void setNume(int nu); 12 void setDeno(int de); 13 int getNume(); 14 int getDeno(); 15 16 ArithmeticAPI friend Fraction operator+(const Fraction &c1, const Fraction &c2); 17 ArithmeticAPI friend Fraction operator-(const Fraction &c1, const Fraction &c2); 18 ArithmeticAPI friend Fraction operator*(const Fraction &c1, const Fraction &c2); 19 ArithmeticAPI friend Fraction operator/(const Fraction &c1, const Fraction &c2); 20 ArithmeticAPI friend Fraction operator^(const Fraction &c1, const Fraction &c2); 21 22 Fraction operator+(); 23 Fraction operator-(); 24 25 ArithmeticAPI friend bool operator>(const Fraction &c1, const Fraction &c2); 26 ArithmeticAPI friend bool operator<(const Fraction &c1, const Fraction &c2); 27 ArithmeticAPI friend bool operator==(const Fraction &c1, const Fraction &c2); 28 ArithmeticAPI friend bool operator!=(const Fraction &c1, const Fraction &c2); 29 ArithmeticAPI friend bool operator>=(const Fraction &c1, const Fraction &c2); 30 ArithmeticAPI friend bool operator<=(const Fraction &c1, const Fraction &c2); 31 32 ArithmeticAPI friend Fraction operate(const Fraction &c1, char c, const Fraction &c2); 33 };
重新生成解决方案后就会生成.dll、.lib文件了。
vs中使用方法如下:
1)工程中存在cpp的情况下,修改项目属性:属性--C/C++--预处理器--预处理器命令--添加_CRT_SECURE_NO_WARNINGS(由于使用了sprintf函数);
2)将7_ArithmeticDll.dll,7_ArithmeticDll.lib以及7_ArithmeticDll.h三个文件 复制到存放即将调用core的.cpp的文件夹中;
3)在头文件中添加 现有项 "7_ArithmeticDll.h";
4)在资源文件中 添加 现有项 "7_ArithmeticDll.lib”;
5)在用来调用core的.cpp中添加 #include"7_ArithmeticDll.h";
6)release/debug、x64/x86不能混用。
团队分工&结对编程的意义
结对编程作业大部分的时间都是采取共同编写代码,即“一个做驾驶员,一个做领航员”,驾驶员负责敲键盘,领航员在一侧提供建议、检查错误或帮忙搜索相关的资料。
就这次团队项目而言,我们合作还是很愉快的。清明节的前两天几乎整天都在写代码,也基本上完成了大部分的代码架构。
我(涂)主要是负责calculate函数(不过这个函数后来发现用不上)以及Fraction这个类的编写(编写指做“驾驶员”),而吕佳玲主要是对生成新的表达式的子函数来进行编写。写完后的调试等过程,则是共同完成。
完成大概架构及基本测试后,后面几天主要就是针对一些小问题及一些新出现的要求进行编写,如更改生成随机分数的方式、增加生成整除表达式的功能等等。当然现在可能还是会有一些小问题,我们这边希望通过UI使用后的反馈再进行集中调整。
通过这次作业,我们也体会到了结对编程的优越性。想起第一天晚上讨论时两个人的二脸懵逼, 如果一个人面对这样一个项目, 难度可想而知。结对编程时,出现一个问题时,两个人一起讨论总是能比一个人死磕能更快得出结果。个人编程时,难免会遇上一些“坎”而被绊住,而两个人一起编程时,每个人的“坎”可能不一样,这样一来,“坎”出现的几率也就大大降低了。而且,结对编程时不是一个人整天坐在电脑前敲键盘,时不时更换驾驶员和领航员的身份,大脑没那么容易死机,这也算是一个好处吧。
PSP表格
Status |
Stages |
预估耗时/min |
实际耗时/min |
Accept |
【计划】 Planning |
100 |
120 |
Accept |
——估计时间 Estimate |
100 |
120 |
Accept |
【开发】 Development |
1790 |
2155 |
Accept |
——需求分析 Analysis |
10 |
10 |
Accept |
——设计文档 Design Spec |
40 |
40 |
Accept |
——设计复审 Design Review |
10 |
10 |
Accept |
——代码规范 Coding Standard |
10 |
5 |
Accept |
——具体设计 Design |
120 |
150 |
Accept |
——具体编码 Coding |
1440 |
1600 |
Accept |
——代码复审 Code Review |
40 |
40 |
Accept |
——测试 Test |
120 |
300 |
Accept |
【记录用时】 Record Time Spent |
20 |
10 |
Accept |
【测试报告】 Test Report |
60 |
40 |
Accept |
【算工作量】 Size Measurement |
20 |
15 |
Accept |
【总结改进】 Postmortem |
60 |
120 |
Accept |
【合计】 Summary |
2050 |
2460 |
课程建议
对于课程来说,最主要的建议还是希望能够在发布作业的时候就把详细要求说清楚,这个应该不难吧,很多东西只要想得全面一点就会发现很多不清不楚的问题。动不动就在群里或者博客里加一句真的很不友好啊。虽然还不至于“上面一张嘴,下面跑断腿”,但是还是很难受的。
还一点就是觉得这门课把大家都当成未来当码农(过于面向就业)也不是很友好。当然编程能力对于信院来说当然非常重要,但是无论怎么说,这门课毕竟还是一门课,虽然能学到很多东西,但代价就是每周在上面花的时间比别的所有课的时间加起来还多。现在结对项目结束了,要继续开启团队项目,我们首先要花很多时间去积累所需的知识(基本是从零开始),还要去看书写读书笔记,还有课后作业,每一项都不是每周一两个小时就能搞完的(可能我比较慢吧)。其实我觉得上学期电设二团队项目的模式挺好的,每周向助教汇报进展,助教提供建议,同时制定下周的目标。当然可能软工更高级一点、范围更广一些,性质也不一样,不过有些方面还是能借鉴一下的吧。
对团队项目的启发
团队项目即将开始,从这次结对编程的经验来看,还是要先完成大的架构,先出来一个能用的再说,细节上的东西可以不断地去补充,这也是“敏捷原则”的体现之一吧。还有就是细节方面、鲁棒性方面还是要尽量考虑的全面一点。再就是像我们core组是要提供接口给UI组使用,这就涉及到一个用户体验的问题,用户体验永远是应该摆在第一位的。