Github地址:
https://github.com/feimo49/four-operations.
队友博客地址:
https://blog.csdn.net/weixin_40629184.
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | |
· Estimate | · 估计这个任务需要多少时间 | 30 | |
Development | 开发 | 1930 | |
· Analysis | · 需求分析(包括学习新技术) | 90 | |
· Design Spec | · 生成设计文档 | 60 | |
· Design Review | · 设计复审(和同事审核设计文档) | 30 | |
· Coding Standard | · 代码规范(为目前的开发制定合适的规范) | 30 | |
· Design | · 具体设计 | 120 | |
· Coding | · 具体编码 | 1200 | |
· Code Review | · 代码复审 | 60 | |
· Test | · 测试(自我测试,修改代码,提交修改) | 240 | |
Reporting | 报告 | 190 | |
· Test Report | · 测试报告 | 120 | |
· Size Measurement | · 计算工作量 | 10 | |
· Postmortem & Process Improvement Plan | · 事后总结,并提出过程改进计划 | 60 | |
· | 合计 | 2150 |
对于本次结对项目,我们小组选择了实现四则运算题目生成项目。
该项目要求实现基于控制台的四则运算题目生成器,共分为三个阶段:
I.第一阶段:
II.第二阶段
III.第三阶段
对程序进行扩展,我们选择了第一个扩展方向,使用C#将程序变为一个Windows电脑图形界面的程序,要求:
程序的核心算法主要为两部分,生成运算题目和求解答案。
生成题目的主要思路为生成随机运算数、生成随机运算符和括号、检测并调整生成式。
具体流程为首先生成一个随机的算式长度,之后逐个字符生成算式。当当前字符不是算式的最后一个字符时,生成一个随机数存入当前字符数组中,并在生成算式的过程中调整其格式,随机生成运算符;当当前字符是算式的最后一个字符时,增加了强制匹配右括号的机制。
在生成题目的过程中还需要考虑很多细节问题,在分析后改进如下:
求解四则运算表达式以前就实现过,思路很简单,将表达式转换为逆波兰表达式:将操作数和运算符分别存储在两个栈中,按规则弹出操作数和运算符进行运算。
\ | 运算符优先级 |
---|---|
^、( | 1 |
*、\ | 2 |
+、- | 3 |
) | 4 |
编号 | 输入格式 | 预期输出 |
---|---|---|
1 | -i 10 | 正常处理,随机生成10个算式 |
2 | Please input TWO parameters! | |
3 | -i | Please input TWO parameters! |
4 | -c 5 | Please input the correct form! |
5 | -i abc | Please input a number! |
2.运算符测试
检测四则运算器中每一种算符的运算正确性,该层检测正确才可以进行进一步的程序分析,其中对于^运算符分别进行正常整型运算、幂指数为0以及底数为分数三种情况讨论,其设计如下:
编号 | 操作数1 | 操作数2 | 运算符 | 预期结果 |
---|---|---|---|---|
1 | 1 | 1/2 | + | 3/2 |
2 | 1 | 1/2 | - | 1/2 |
3 | 3 | 1/2 | * | 3/2 |
4 | 3 | 1/2 | / | 6 |
5 | 3 | 2 | ^ | 9 |
6 | 3 | 0 | ^ | 1 |
7 | 1/2 | 1 | ^ | 1/2 |
3.题目查重测试
在四则运算中,由于存在加法交换律、乘法交换律以及左右结合律,故算式之间存在形式不同但逻辑运算相同的情况,在本项目中需要对该类情况进行测试,其设计如下:
编号 | 算式1 | 算式2 | 预期结果 |
---|---|---|---|
1 | 1+2+3 | 3+(1+2) | 重复 |
2 | 1+2+3 | 3+2+1 | 不重复 |
3 | 3*4 | 4*3 | 重复 |
4 | 123 | 321 | 不重复 |
5 | (1+2)*3 | 3*(1+2) | 不重复 |
6 | 123 | 321 | 不重复 |
7 | (1+2)*(3+4) | (3+4)*(1+2) | 不重复 |
8 | (2-1)/(5-3) | (5-3)/(2-1) | 不重复 |
9 | (3+6)/(5-3) | (6+3)/(5-3) | 重复 |
10 | (1/2+2/3)+3/4 | 1/2+(2/3+3/4) | 重复 |
11 | (1/2+2/3)* 3/4 | 3/4 *(1/2+2/3) | 重复 |
在性能分析阶段,由于在诊断过程中需要不断与用户进行交互,故其诊断会话时间较长,达到了1分钟20秒。
由分析报告可见,在程序中主函数main()占用的CPU比例最大,其主要原因是在主函数中进行了文件的打开和读操作,占用CPU比例较大;将用户输入答案与正确结果进行比较的judge()函数其次,在其中进行对于用户输入答案的比对与反馈工作,需要与用户界面进行交互;打印当前生成的算式的PrintExp()函数再次之,其实现打印算式的同时还需要将生成的算式写入文件ques.txt中,需要消耗较多的CPU资源。这三个函数为项目中消耗CPU最多的三个主要函数。
在主函数中实现了对输入参数的解析与判断、模式选择以及主要的生成、求解、输出接口设计等,是程序与用户交互的接口。
int main(int argc, char * argv[])
{
if (argc < 3)
{
printf("Please input TWO parameters!\n");
system("pause");
return 0;
}
if (!strcmp(argv[1], "-i"))
{
srand((unsigned)time(NULL));
int n = atoi(argv[2]);
cout << "您希望使用哪种方式表示乘方?(输入1选择模式1,输入2选择模式2)mode-1:^/mode-2:**" << endl;
int m;
cin >>m;
getchar();
ofstream OutputFile("ques.txt");
int ac = 0;
for (int i = 0; i < n; i++)
{
char *t;
num useranswer;
int * save = BuildExp(3);
t = PrintExp(m);
OutputFile << t;
if(judge(get_ans(save)))
ac++;
}
printf("本轮题目正确率:%d/%d\n", ac, n);
}
if (strcmp(argv[1], "-i")) //输入格式不合理的情况
{
printf("Please input in the correct form!\n");
system("pause");
return 0;
}
int flag = 0;
for (int i = 0; i < strlen(argv[2]);i++)
{
if (argv[2][i]<'0' || argv[2][i]>'9')
flag = 1;
}
if (flag == 1) //输入非数字的情况
printf("Please input a NUMBER!\n");
system("pause");
return 0;
}
class num {
private:
int numerator; //分子
int denominator; //分母
int gcd; //最大公约数
int symbol; //运算符
int flag; //设置化简标志,防止重复化简
void get_gcd(int x, int y) //求最大公约数
{
if (y == 0)
gcd = x;
else
get_gcd(y, x%y);
}
void reduction() //化简
{
if (numerator != 0) //分子不为0
{
symbol = symbol * (numerator / abs(numerator))*(denominator / abs(denominator));
numerator = abs(numerator);
denominator = abs(denominator);
get_gcd(numerator, denominator);
}
else //分子为0
{
denominator = 1;
gcd = 1;
symbol = 1;
}
flag = 1;
}
public:
num();
num(int x);
num(int x, int y, int sign);
void print();
void print(char * formula, ofstream & outtofile);
friend num operator +(num &a, num &b);
friend num operator -(num &a, num &b);
friend num operator *(num &a, num &b);
friend num operator /(num &a, num &b);
friend num operator ^(num &a, num &b); //保证b的分母为1
friend int operator == (num &a, num &b);
};
该类中定义了分子、分母、最大公约数几个属性,以及输出数字的方法,并对运算符进行了重载。
通过num类本程序中所有操作数都用分数表示,简化了计算。
//随机化设计
default_random_engine generator(time(NULL));
normal_distribution<double> lendis(5, 3);
normal_distribution<double> numdis(5, 2);
auto lendice = bind(lendis, generator);
auto numdice = bind(numdis, generator);
int Exp[50];
int p = 0;
//mode=1 基础,mode=2 包含分数,mode=3,包含乘方。
//随机生成式子长度
int RandExpLen()
{
int randnum = lround(lendice());
if (randnum < 2)
randnum = 2;
else if (randnum > 10)
randnum = 10;
return randnum;
}
//随机生成算符
int RandSymbol(int mode)
{
int randnum;
if (mode == 1)
randnum = rand() % 4 + 101;
else if (mode == 2)
randnum = rand() % 4 + 101;
else if (mode == 3)
randnum = rand() % 5 + 101;
else if (mode == 4)
randnum = rand() % 2;
return randnum;
}
//随机生成式子中数字个数
int RandExpNum(int maxnum)
{
int randnum = lround(numdice());
if (randnum < 0)
randnum = 0;
else if (randnum > maxnum)
randnum = maxnum;
return randnum;
}
//随机生成一个1~3的数字
int GetEasy()
{
return rand() % 3 + 1;
}
//生成算式
int* BuildExp(int mode)
{
memset(Exp, 0, sizeof(Exp));
bool HavePow = false;
int expnum = RandExpLen();
int lastbracket = 0;
p = 0;
for (int j = 1; j <= expnum; j++)
{
if (j == expnum)//最后一个数字的判断
{
Exp[p++] = RandExpNum(10);
if (Exp[p - 2] == 105)
Exp[p - 1] = GetEasy(); //返回一个1~3的数
if (Exp[p - 2] == 104 && Exp[p - 1] == 0)//判断分母0
Exp[p - 1] = 1;
if (lastbracket != 0)//若有未匹配左括号,则最后一位强制添加右括号
Exp[p++] = 107;
break;
}
else
{
Exp[p++] = RandExpNum(10);//生成随机数
if (Exp[p - 2] == 105)
Exp[p - 1] = GetEasy();
if (Exp[p - 2] == 104 && Exp[p - 1] == 0)//判断分母0
Exp[p - 1] = 1;
if (RandSymbol(4) && lastbracket > 2)//右括号
{
Exp[p++] = 107;
lastbracket = 0;
}
Exp[p++] = RandSymbol(mode);//生成随机符号
{//检查乘方个数
if (Exp[p - 1] == 105 && HavePow)
Exp[p - 1] = RandSymbol(1);
else if (Exp[p - 1] == 105)
HavePow = true;
}
if (RandSymbol(4) && j < expnum - 1 && lastbracket == 0 && Exp[p - 1] < 104)//左括号
{
Exp[p++] = 106;
lastbracket = 1;
}
}
if (lastbracket != 0)
lastbracket++;
}
return Exp;
}
上述代码用于随机生成指定个数个运算表达式。
extern int p;
//运算符和可处理十进制数之间的转换
num cal(num n1, num n2, int opera)
{
if (opera == 101)
return n1 + n2;
else if (opera == 102)
return n1 - n2;
else if (opera == 103)
return n1 * n2;
else if (opera == 104)
return n1 / n2;
else if (opera == 105)
return n1 ^ n2;
}
//将四则运算映射到一串十进制数,0-100为运算数
//其中101-104分别代表+,-,*,/,105为乘方,106为左括号,107为右括号
num get_ans(int * operation)
{
stack <int> operators;
stack <num> operand;
for (int i = 0; i < p; i++)
{
if (operation[i] >= 0 && operation[i] <= 100)
{
num temp(operation[i]);
operand.push(temp);
}
else if (operation[i] == 105 || operation[i] == 106) //左括号与乘方必定入栈
operators.push(operation[i]);
else if (operation[i] == 103 || operation[i] == 104) //乘除会弹出乘方与乘除
{
while (!operators.empty() && (operators.top() == 103 || operators.top() == 104 || operators.top() == 105))
{
int opera = operators.top();
operators.pop();
num n1 = operand.top();
operand.pop();
num n2 = operand.top();
operand.pop();
operand.push(cal(n2, n1, opera));
}
operators.push(operation[i]);
}
else if (operation[i] == 101 || operation[i] == 102) //加减可能弹出乘除与乘方
{
while (!operators.empty() && (operators.top() != 106 && operators.top() != 107))
{
int opera = operators.top();
operators.pop();
num n1 = operand.top();
operand.pop();
num n2 = operand.top();
operand.pop();
operand.push(cal(n2, n1, opera));
}
operators.push(operation[i]);
}
else if (operation[i] == 107) //右括号会一直弹出直至左括号
{
while (operators.top() != 106)
{
int opera = operators.top();
operators.pop();
num n1 = operand.top();
operand.pop();
num n2 = operand.top();
operand.pop();
operand.push(cal(n2, n1, opera));
}
operators.pop();
}
}
while (!operators.empty())
{
int opera = operators.top();
operators.pop();
num n1 = operand.top();
operand.pop();
num n2 = operand.top();
operand.pop();
operand.push(cal(n2, n1, opera));
}
return operand.top();
}
上述代码用求解题目。
我们选择了第一个扩展方向,使用C#将程序变为一个Windows电脑图形界面的程序。
主界面效果如下:
点击START按钮开始答题。
答题界面如下:
开始答题后界面中央随机生成一道运算题目
我们使用C#实现winform图形界面。
首先需要把之前c++的代码用c#进行重写并封装。在重写过程中,除了语法上需要修改,还有一些功能需要更进一步的整合和封装。由于C#中每一个文件就是一个类,所以我们把所有相同功能都封装到一个类中。
相关类重写完成后需要进行winform窗口的布局以及控件代码的编写。
核心代码实现如下:
private void Submit_Click(object sender, EventArgs e)
{
if(Ans.Text=="")
{
Ans.Focus();
return;
}
Judge ans = new Judge();
Num correct_ans = solve.get_ans(save, Generate.p);
int ansflag = ans.judge(correct_ans, this.Ans.Text);
correct_ans_str = correct_ans.c_Tostring();
timu = Generate.C_Tostring();
f3.History_Add(timu, correct_ans_str);
if (ansflag==1)
{
timer1.Stop();
MessageBox.Show("Bingo!");
timer1.Start();
grade+=1;
correct_cnt++;
ggrade.Text = "Grade: "+grade.ToString();
Start_Click(null, null);
this.Ans.Text = "";
totaltime = 20;
}
else if(ansflag==0)
{
timer1.Stop();
MessageBox.Show("Wrong!\n" + "Correct Answer:" + correct_ans_str);
timer1.Start();
Start_Click(null, null);
this.Ans.Text = "";
totaltime = 20;
}
else
{
timer1.Stop();
MessageBox.Show("Error:Please input the correct form!");
timer1.Start();
this.Ans.Text = "";
this.Ans.Focus();
}
f3.Correct_Rate(correct_cnt, cnt-1);
}
//打开历史记录窗口
private void History_Click(object sender, EventArgs e)
{
f3.Show();
}
//每次生成题目时通过f3.History_Add(timu, correct_ans_str),将题目和正确答案传入f3
public void History_Add(String Text1,String Text2)
{
record.Text += Text1 + "=" + Text2+"\r\n";
}
public void Correct_Rate(int c_cnt, int cnt)
{
correct_rate.Text = "Correct Rate : " + c_cnt + "/" + cnt;
}
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 35 |
· Estimate | · 估计这个任务需要多少时间 | 30 | 35 |
Development | 开发 | 1930 | 2005 |
· Analysis | · 需求分析(包括学习新技术) | 90 | 60 |
· Design Spec | · 生成设计文档 | 60 | 60 |
· Design Review | · 设计复审(和同事审核设计文档) | 30 | 30 |
· Coding Standard | · 代码规范(为目前的开发制定合适的规范) | 30 | 15 |
· Design | · 具体设计 | 120 | 120 |
· Coding | · 具体编码 | 1200 | 1345 |
· Code Review | · 代码复审 | 60 | 75 |
· Test | · 测试(自我测试,修改代码,提交修改) | 240 | 300 |
Reporting | 报告 | 190 | 175 |
· Test Report | · 测试报告 | 120 | 100 |
· Size Measurement | · 计算工作量 | 10 | 15 |
· Postmortem & Process Improvement Plan | · 事后总结,并提出过程改进计划 | 60 | 60 |
· | 合计 | 2150 | 2215 |
通过本次结对项目的开发,我学习到了在多人共同开发项目时如何进行有效的分工合作。因为这次是和室友组队,有着一定的默契并且十分了解,我们在计划阶段共同讨论开发计划、制定设计文档,最终形成了较为成熟完整的方案。在开发过程中,经过讨论我们分别负责题目生成和题目求解部分,界面开发则共同完成,跟个人项目相比开发进度明显加快。但是过程中也出现了接口调用错误、封装不完整等问题,在经过检测后都一一解决。
另外在上次个人项目中主要还是使用的结构化设计方法,所以在整个项目的开发过程中遇到一些问题。这次吸取教训之后我们采用了面向对象设计方法,通过类来整合代码,使功能封装的更加好,后期测试和重写都十分方便。
关于winform图形界面的实现,我们先利用c#重写c++部分代码,对c#语言的了解更深一步,发现它在类的整合方面十分方便。因为时间关系,我们的界面还有很多可以优化的地方;另外在几次图形界面开发之后我感觉winform用于GUI开发还有很多不尽人意的地方,我决定以后学习以下基于Python图形界面的第三方库(例如tkinter、Qt、GTK等)的图形界面开发。
本次的结对项目开发总体开发还是十分顺利的,当然也还有许多可以改进的地方。在以后的学习或者工作过程中肯定还有很多需要合作进行开发的项目,有效的交流与合作可以产生1+1>2的效果,为了使开发更加顺利,需要严格按照标准的软件开发流程进行开发,编程时做好代码的封装,能够避免很多问题。