【软件工程基础】结对项目之四则运算题目生成
一,项目介绍
项目的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电脑图形界面程序,增加倒计时功能,判断用户是否能在20秒内按时完成,并设置用户得分,增加历史记录,把用户做题的成绩记录并展示
二,项目分析
1.PSP表格预估时间
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 |
2.思路分析
首先我们进行的是最基础的代码编写,即第一阶段的进行。本阶段的重点在于出题和解题两个方面。
首先是在生成题目方面,我们应该首先建立一个文件存储算式或者在程序中有创立一个相关文件,然后生成操作者规定生成的个数的随机数字(题目要求为1000,这里为了方便调试我们可采用较小的数字),并且在他们之间穿插括号,四则运算符号等生成四则运算,并将他们写入文件中 ,以满足题目要求。
然后是解决题目方面,这里我们阅读了很多学长学姐的代码,大多数都是建立一个类来存储数字信息来实现支持分数运算,然后使用堆栈来完成算式的读取和运算。经过我们的讨论,决定采用与他们不同的递归方式来完成运算。具体算法如下:
先判断是否有括号,有的话,找到最内部的括号,优先运算括号内部,返回括号左边内容+括号内运算结果+括号右边内容,并去掉这个括号,将返回值作为新题目处理;
没有括号的话再找乘方运算,如果有乘方先判断是否加减乘除都没有,如果都没有,进行所有乘方运算,直接退出递归。既有乘方又有加减时,先进行乘方运算,将结果与其他部分串联起来作为新题目处理。这里为了处理分数的乘方问题,将分数单独列出来分类讨论。
下面是乘除运算,需要考虑参与运算的是否为分数,并在运算完成后化简分数到最简形式。
在这一阶段,我主要负责的是生成题目与保存题目相关代码的编写,刁宇杰同学主要负责的是解决题目方面代码的编写。
于是整个函数的运行流程图就如下所示
后来在我们两个人设计的函数对接在一起的过程中我们又发现了一些其他的问题,首先是每次输入的题目都会一样,这样的话我们使用 time函数来获得系统时间,然后将time_t型数据转化为(unsigned)型再传给随机数引擎,因为系统的时间一直在变,所以rand()获得的数,也就一直在变,相当于是随机数了;然后就是出题与解题的数组无法传递,后来经过核实是传输过程中数组名字发生了变化,我们统一重新定义了一下,解决了问题。
三,重要代码
1.生成题目代码
char* chuti(int a)
{
double nums[100];
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() % 99+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()%5+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() % 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() % 99+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()%5+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.主函数代码
if (argc != 3)
{
cout << "命令行参数格式错误!" << endl;
cout << "请输入两个参数,第一个为出题数量(1000以内),第二个为乘方表示形式(1表示^,2表示**)" << endl;
return 0;
}
int a1len = strlen(argv[1]);
for (int i = 0;i < a1len;i++)
{
if (!('0' <= argv[1][i]&& argv[1][i] <= '9'))
{
cout << "参数1错误,请输入数字" << endl;
return 0;
}
}
int a1num = atoi(argv[1]);
if (!(0 < a1num <= 1000))
{
cout << "参数1错误,出题数量应为1-1000之内的整数" << endl;
return 0;
}
if (strlen(argv[2]) != 1)
{
cout<<"参数2长度错误,请输入1或2"<<endl;
return 0;
}
if (!(argv[2][0] == '1' || argv[2][0] == '2'))
{
cout<<"参数2数值错误,请输入1或2"<<endl;
return 0;
}
int a2num = atoi(argv[2]);
double v = 0, x = 0;
for (int i = 1;i <= a1num;i++)
{
char *timu=chuti(a2num);
save(timu);
char *correct= jieti(a2num, timu);
char answer[100];
cout<<"请输入你的答案"<<endl;
cin >> answer;
getchar();
if (!strcmp(correct, answer))
{
x+=1;
cout<<"答案错误"<<endl;
cout << "正确答案为" <<correct<< endl;
}
else
{
v += 1;
cout << "答案正确" << endl;
}
}
cout<<"答题完成"<<endl;
cout<<"正确数:"<<v<<"错误数:"<<x<<endl;
double lv = v / (v + x);
cout << "正确率:" << lv << endl;
return 0;
3.解题代码
这一部分代码主要由刁宇杰同学编写,由于排版原因传到博客上总是会出问题,具体请看github的代码库。
int len = strlen(timu);
if (kuohao)
{
int lpos = 0,rpos = 0;
for (int i = 0;i <= len;i++)//有括号时获取括号位置
{
if (timu[i] == '(')
{
lpos = i;
}
if (timu[i] == ')')
{
rpos = i;
break;
}
}
char ret[100]; //要返回的数组
char newtimu[100]; //要参与递归的数组
memset(newtimu, '\0', sizeof(newtimu));
memset(ret, '\0', sizeof(ret));
for (int i = 0;i < lpos;i++)
{
ret[i] = timu[i];
}
for (int i = lpos+1;i < rpos-1;i++)
{
newtimu[lpos - i + 1] = timu[i];
}
kuohao--;
int kuohao2=0, chengfang2=0, chengchu2=0, jiajian2=0;//统计括号内的运算符
for (int i = lpos+1;i < rpos ;i++)//获得运算符个数
{
if (timu[i] == '+' || timu[i] == '-')
{
jiajian2++;
}
else if (timu[i] == '*' || timu[i] == '/')
{
chengchu2++;
}
else if (timu[i] == '^')
{
chengfang2++;
}
else if (timu[i] == '(')
{
kuohao2++;
}
}
strcat(ret, calculate(newtimu, kuohao2, chengfang2, chengchu2, jiajian2));
int retlen = strlen(ret);
for (int i = 0;i < len - rpos - 1;i++)
{
ret[retlen + i] = timu[rpos + i + 1];
}
return ret;
}
四、性能分析
通过使用VisualStudio的性能分析功能,我们得到了以上结果。
从中可以看出,出题函数、保存函数占用CPU较多,可以着重对此进行优化。
在出题函数中,我们采取了减小数组长度、优化结构等方式进行了优化。
保存函数较为简单,不方便进行优化。
在解题函数中,我们优化了算法,提前判断不需要递归的情况并将其剪枝,以此来减小内存开销。
五、测试数据
对主函数的测试
主函数主要负责处理命令行参数、比对用户输入、并将其它函数结合起来。首先是命令行判断部分,我们使用了黑盒测试的因果图法,画了用户不同输入导致的各种结果,并根据该图,选取合适的命令行输入值进行测试。然后是输入比对部分,由于比较复杂,我们画出了程序的流图,并根据该图找出相应的基本路径,从而选择合适的测试用例。
对出题函数的测试:
该函数的输入为乘方表示形式,输出为题目字符串,在内部大量运用了rand函数,不方便进行白盒测试,我们对其进行了黑盒测试:多次输入1或2,观察其输出题目是否符合要求。
对保存函数的测试:
该函数输入为题目字符串,无返回值,对其进行不同字符串的输入,观察写入文件的值是否与输入一致。
对解题函数的测试:
该函数输入为乘方表示形式和题目,输出为答案,我们对其采用了路径覆盖测试法,选取合适的用例使程序中的路径都执行一遍,观察输出是否符合预期。
六、运行结果
这是程序一次运行的结果示例,这里控制台输入的题目数为5,乘方表示为1,可以看到,程序能够自动出题并得出答案,并能与用户输入进行比较,统计正确错误数,并且能支持乘方、分数、括号运算。
下面是C#运行的结果
七、个人总结
首先从课程的角度来讲我认为这次软件工程的大作业对于我对这门课的理解与学习有着很大的帮助,无论是将课上学到的知识融会贯通运用到项目的要求中去,还是在有些知识没有掌握的情况下通过互联网以及请教同学,学长学姐来完善项目,抑或是扬长避短,运用简单的算法来解决实际问题降低出现bug的可能性,都是我在软件开发方面宝贵的经验,而且在课堂中学到的知识也能被我加以运用,颇有一种无用之用是为大用的感觉,非常的好。
然后就是个人方面,大大加强了我得编程能力,有一段时间没有敲代码了感觉好多知识都忘记了,也出现了许多令人啼笑皆非的bug,不过工程中的编程部分大大提高了我的编程水平,好多平时不会用的函数在“压力”之下使我不得不掌握他们的用法。然后也非常感谢队友的帮助,这么大一个工程光靠一人之力感觉是很难做好的但是两个人合作的话在完成的过程中就不会感到那么的枯燥无力。
总而言之按要求完成了项目还是一件很有成就感的事情,希望这门课给我带来的帮助会让我在未来走的越来越远。