【软件工程基础】结对项目之四则运算题目生成
一,项目介绍
项目的github地址:https://github.com/qqqqqianru/sizeyunsuantimushengcheng
二、项目要求
第一阶段:写一个能够自动生成小学四则运算题目的命令行软件,分别满足下面的各种需求,下面的这些需求都可以用命令行参数的形式来指定:
a)一次可以出1000道题目,并且没有重复的,把题目写入一个文件中
b)当有多于一个运算符的时候,如何对一个表达式求值?逐步扩展功能和可以支持的表达式类型,最后希望支持下面类型的题目(最多10个运算符,括号的数量不限制)
25-34-2/2+89=?
1/2+1/3-1/4=?
(5-4)(3+28)=?
c)除了整数以外,还要支持真分数的四则运算
d)让程序能接受用户输入答案,并判定对错,最后给出总对/错的数量。
第二阶段:
增加一个运算符,要求支持乘方运算。乘方运算的优先级高于乘除法。
如何表示乘方,有两种表示方法:^或**。
第三阶段:结对的同学商量一下,从以下几个方向中选择一个,对程序进行扩展。
把程序变成一个windows/mac/linux电脑图形界面的程序(取决于你目前使用的电脑)同时增加倒计时功能,每个题目必须在20秒钟完成,如果完不成,则得0分并进入下一题。增加“历史纪录”功能,把用户做题的成绩记录下来并可以展现历史纪录。
把程序变成一个智能手机程序,增加倒计时和历史纪录功能。
把程序变成一个网页程序,用户通过设定参数,就可以得到各种题目。
选一个你从来没有学过的编程语言,试一试实现基本功能。估计这个软件所需要的时间,并且写出大概的设计步骤和实现算法。
把这个程序的思路变成一个可以一步一步演示的动画,写一个带有图形界面的程序。
三、项目分析
| PSP2.1 | Personal Software Process Stages | 预估耗时(min) | 实际耗时 |
|---|---|---|---|
| Planning | 计划 | 30 | 40 |
| Estimate | 估计这个任务需要多少时间 | 1800 | 1900 |
| Development | 开发 | 1200 | 1000 |
| Analysis | 需求分析(包括学习新技术) | 360 | 300 |
| Design Spec | 生成设计文档 | 120 | 170 |
| Design Review | 设计复审(和同事审核设计文档) | 30 | 40 |
| Coding Standard | 代码规范(为目前的开发制定合适的规范) | 30 | 40 |
| Design | 具体设计 | 180 | 200 |
| Coding | 具体编码 | 600 | 650 |
| Code Review | 代码复审 | 60 | 50 |
| Test | 测试(自我测试,修改代码,提交修改) | 180 | 200 |
| Reporting | 报告 | 60 | 50 |
| Test Report | 测试报告 | 20 | 20 |
| Size Measurement | 计算工作量 | 10 | 30 |
| Postmortem & Process Improvement Plan | 事后总结,并提出过程改进计划 | 60 | 60 |
| 合计 | 1800 | 2000 |
第一、二阶段: 第一阶段是项目的核心部分,主要包含出题、保存、解题、比对四个部分,第二阶段则是第一阶段的补充。
出题部分,要求生成不重复的四则运算,最多1000道,要求支持括号、分数运算功能。
保存部分,将出的题目保存到文件中,这一部分比较容易实现。
解题部分,要求通过相应的算法将前一部分所出的题目解出正确答案。
比对部分,将解出的答案与用户输入相比对,得出结果并进行统计,这一部分比较容易实现。

本阶段的重点在于出题和解题部分,下面将详细分析:
首先是在生成题目方面,我们应该首先建立一个文件存储算式或者在程序中有创立一个相关文件,然后生成操作者规定生成的个数的随机数字(题目要求为1000,这里为了方便调试我们可采用较小的数字),并且在他们之间穿插括号,四则运算符号等生成四则运算,并将他们写入文件中 ,以满足题目要求。
然后是解决题目方面,这里我们阅读了很多学长学姐的代码,大多数都是建立一个类来存储数字信息来实现支持分数运算,然后使用堆栈来完成算式的读取和运算。经过我们的讨论,决定采用与他们不同的递归方式来完成运算。具体算法如下:
先判断是否有括号,有的话,找到最内部的括号,优先运算括号内部,返回括号左边内容+括号内运算结果+括号右边内容,并去掉这个括号,将返回值作为新题目处理;
没有括号的话再找乘方运算,如果有乘方先判断是否还有加减乘除,如果都没有,进行所有乘方运算,直接返回值。既有乘方又有加减时,先进行乘方运算,将结果与其他部分串联起来作为新题目处理。这里为了处理分数的乘方问题,将分数单独列出来分类讨论。要注意的是,乘方右边的符号一定要是整数,否则会出现根号的情况,无法处理,这一点在出题部分已经考虑过了。
下面是乘除运算,需要考虑参与运算的是否为分数,并在运算完成后化简分数到最简形式。为了支持分数运算,引入了一个符号#,代表它前后的数字是一个分数。围绕#号作分类讨论,并返回不同的值。
然后是加减运算,思路与乘除基本一致,需要注意加减的两个数都是分数的情况,需要进行通分再运算。
当式子中没有任何运算,只有数字和代表分数的#号时,即可返回答案。
第三阶段:
这一阶段我们选择了C#来开发图形界面程序,并增加倒计时、历史纪录功能。C#与C++的语法有着很大的不同,为了使前面的代码能够正确应用,我们将之前的出题、解题函数稍加改进,做成了两个可执行文件,并写了一个调用函数来实现跨语言的调用。
在windows图形界面程序题目的生成中我们只有一个窗体,在窗体左侧可以设置出题数量、乘方表示形式,出题数量默认为10,如果输入不合理的数字或非数字则会提示输入错误信息。右侧从上到下分别为所出的题目、用户答案输入区域、确定按钮、以及点击确定后会出现的提示信息显示区域。下方为倒计时、开始/重置按钮、历史纪录按钮,倒计时由timer控件控制,在点击开始/重置按钮后会运行/重置,在倒计时结束后会触发事件提示时间到;点击历史纪录按钮则会弹出相应历史信息

四、项目重要代码(完整代码在GitHub上,这里只列出重要部分的代码)
1.生成题目代码
char* chuti(int a)
{
double nums[100];
srand((unsigned)time(NULL));
char str[100];
char chuti[3000];
int ii=0;
int flag, q = 1;
int pos1 = -1, ///pos1,pos2为括号位置
pos2 = -1;
if(a==1)
{
int dd,ee,ff,gg;
str[0] = '+' ;
str[1] = '-';
str[2] = '*';
str[3] = '/';
flag = rand() % 5 + 2;
int aa;
aa=0;
int bb;
bb=rand()%flag-1;
int cc=-1;
for(int i = 0; i < flag; i++)
{
nums[i] = rand() % 20+1;
}
pos1 = -1;
pos2 = -1;
while(1)
{
pos1 = rand() % flag;
pos2 = rand() % flag;
if(abs(pos1-pos2))//绝对值
{
pos1 = min(pos1, pos2);
pos2 = max(pos1, pos2);
break;
}
}
if(flag == 2)
{
pos1 = -1;
pos2 = -1;
}
//ofile<<"(" << q << ")"<< " ";
//cout << "(" << q++ << ")"<< " ";
for(int i = 0; i < flag; i++)
{
int k = rand()% 4;
if(i == pos1 && pos1!=pos2)
{
if(pos1!=0||pos2!=flag-1)
{
cout<< "("<<" ";
chuti[ii]='(';
ii++;
//ofile<<"("<<" ";
}
}
cout << nums[i] << " ";
if(nums[i]<10)
{
chuti[ii]=nums[i]+48;
ii++;
}
else
{
gg=nums[i];
chuti[ii]=nums[i]/10+48;
ii++;
ff=gg%10;
chuti[ii]=ff+48;
ii++;
}
//ofile<<nums[i]<<" ";
if(i == pos2 && pos1!=pos2)
{
if(pos1!=0||pos2!=flag-1)
{
int dd=rand()%5+2;
cout<<"^"<<" "<<dd<<" ";
chuti[ii]='^';
ii++;
chuti[ii]=dd+48;
ii++;
aa=1;
cout<< ")" <<" ";
chuti[ii]=')';
ii++;
//ofile<<")"<<" ";
}
}
if(aa==0)
{
if(i==bb)
{
ee=rand()%3+2;
cout<<"^"<<" "<<ee<<" ";
chuti[ii]='^';
ii++;
chuti[ii]=ee+48;
ii++;
}
}
if(i != flag-1)
{
cout<< str[k] << " ";
chuti[ii]=str[k];
ii++;
//ofile<<str[k]<<" ";
}
}
cout<< endl;
}
else
{
int dd,ee,ff,gg;
str[0] = '+' ;
str[1] = '-';
str[2] = '*';
str[3] = '/';
flag = rand() % 3 + 2;
int aa;
aa=0;
int bb;
bb=rand()%flag-1;
int cc=-1;
for(int i = 0; i < flag; i++)
{
nums[i] = rand() % 20+1;
}
pos1 = -1;
pos2 = -1;
while(1)
{
pos1 = rand() % flag;
pos2 = rand() % flag;
if(abs(pos1-pos2))//绝对值
{
pos1 = min(pos1, pos2);
pos2 = max(pos1, pos2);
break;
}
}
if(flag == 2)
{
pos1 = -1;
pos2 = -1;
}
//ofile<<"(" << q << ")"<< " ";
//cout << "(" << q++ << ")"<< " ";
for(int i = 0; i < flag; i++)
{
int k = rand()% 4;
if(i == pos1 && pos1!=pos2)
{
if(pos1!=0||pos2!=flag-1)
{
cout<< "("<<" ";
chuti[ii]='(';
ii++;
//ofile<<"("<<" ";
}
}
cout << nums[i] << " ";
if(nums[i]<10)
{
chuti[ii]=nums[i]+48;
ii++;
}
else
{
gg=nums[i];
chuti[ii]=nums[i]/10+48;
ii++;
ff=gg%10;
chuti[ii]=ff+48;
ii++;
}
//ofile<<nums[i]<<" ";
if(i == pos2 && pos1!=pos2)
{
if(pos1!=0||pos2!=flag-1)
{
int dd=rand()%5+2;
cout<<"**"<<" "<<dd<<" ";
chuti[ii]='*';
ii++;
chuti[ii]='*';
ii++;
chuti[ii]=dd+48;
ii++;
aa=1;
cout<< ")" <<" ";
chuti[ii]=')';
ii++;
//ofile<<")"<<" ";
}
}
if(aa==0)
{
if(i==bb)
{
ee=rand()%3+2;
cout<<"**"<<" "<<ee<<" ";
chuti[ii]='*';
ii++;
chuti[ii]='*';
ii++;
chuti[ii]=ee+48;
ii++;
}
}
if(i != flag-1)
{
cout<< str[k] << " ";
chuti[ii]=str[k];
ii++;
//ofile<<str[k]<<" ";
}
}
cout<< endl;
}
return chuti[];
}
2、解题代码(由于代码过长这里只展示主要结构)
int gongyueshu(int m,int n);//获得两个数最大公约数
int gongbeishu(int m,int n);//获得两个数最小公倍数
char *calculate(char *timu,int kuohao,int chengfang,int chengchu,int jiajian)
{
if(kuohao)
{
char newtimu[100]=括号左边内容+calculate(括号内算式,括号内括号数,括号内乘方数,括号内乘除数,括号内加减数);
return calculate(newtimu,新题目括号数,新题目乘方数,新题目乘除数,新题目加减数);
}
else if(chengfang)
{
乘方运算;
用运算结果替换原相应内容;
return calculate(新题目,……);
}
else if(chengchu)
{
乘除运算(只需考虑前一个运算数是否为分数);
用运算结果替换原相应内容;
return calculate(新题目,……);
}
else if(jiajian)
{
加减运算 (考虑前后是否为分数);
用运算结果替换原相应内容;
return calculate(新题目,……);
}
else
{
判断正负;
化简分数;
将代表分数的#号改为/号;
返回答案;
}
}
五、性能分析与代码优化

通过使用Visual Studio的性能分析功能,我们得到了以上结果。
从中可以看出,出题函数、保存函数占用CPU较多,可以着重对此进行优化。
在出题函数中,我们采取了减小数组长度、优化结构等方式进行了优化。
保存函数较为简单,不方便进行优化。
在解题函数中,我们优化了算法,提前判断不需要递归的情况并将其剪枝,以此来减小内存开销。
六、代码测试
对主函数的测试
主函数主要负责处理命令行参数、比对用户输入、并将其它函数结合起来。首先是命令行判断部分,我们使用了黑盒测试的因果图法,画了用户不同输入导致的各种结果,并根据该图,选取合适的命令行输入值进行测试。然后是输入比对部分,由于比较复杂,我们画出了程序的流图,并根据该图找出相应的基本路径,从而选择合适的测试用例。
对出题函数的测试:
该函数的输入为乘方表示形式,输出为题目字符串,在内部大量运用了rand函数,不方便进行白盒测试,我们对其进行了黑盒测试:多次输入1或2,观察其输出题目是否符合要求。
对保存函数的测试:
该函数输入为题目字符串,无返回值,对其进行不同字符串的输入,观察写入文件的值是否与输入一致。
对解题函数的测试:
该函数输入为乘方表示形式和题目,输出为答案,我们对其采用了路径覆盖测试法,选取合适的用例使程序中的路径都执行一遍,观察输出是否符合预期。
七、运行结果
这是程序一次运行的结果示例,这里控制台输入的题目数为5,乘方表示为1,可以看到,程序能够自动出题并得出答案,并能与用户输入进行比较,统计正确错误数,并且能支持乘方、分数、括号运算。


下面是C#程序的运行结果截图
主界面
答题

答题超时

答题完成

历史纪录


八、个人总结
在这次结对项目中我认为收获最大的就是锻炼了我们的编程能力,因为我们之前写代码大多都是按照题目要求来完成相关功能的实现,而这一次是我们为数不多的运用学过的知识同时学习没学过的知识来解决实际问题,工作环境和我们以后的工作应该是极为相像的,是一次很好的锻炼机会。在这次代码的编写中我们碰到了好多这样那样的问题,有很多是我们应用所学知识解决不了的,这个时候就是对我们能力的一次极好的锻炼。同时我们也明白了软件测试也是软件工程的重要一环,软件工程不只是敲代码,其他的部分也很重要,如果不能通过测试就不是一次成功的工程。队友的重要性也不言而喻,只有两个人有明确的分工,才能加快项目的完成。一个人想做好这么大的工程是非常困难的,多了一个人发挥的效率可能不仅仅是1+1=2那么简单。总之通过这次项目我深刻理解了课堂中讲的许多令我理解的模棱两可的东西,加深了我对相关概念的融会贯通,令人受益匪浅。
本文介绍了结对项目——四则运算题目生成器的开发过程,包括生成不重复的四则运算题、支持括号、分数运算、乘方运算,以及解题算法。项目分为三个阶段,从命令行程序到图形界面应用,涵盖多种功能如倒计时、历史纪录等。项目使用C#实现图形界面,并通过黑盒和白盒测试确保代码质量。

717

被折叠的 条评论
为什么被折叠?



