软件工程基础结队项目——四则运算器生成

1.Github项目链接及队友博客链接

GitHub项目链接:
https://github.com/feimo49/four-operations

队友博客链接:https://blog.csdn.net/qq_37745978/article/details/86308941

2.填写表格中的预计时间

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
Total 合计 2150

3.解题思路描述

本次项目的核心算法分为生成四则运算题目和求解答案两个部分。下面分别对这两个部分的设计思路进行描述。

生成题目模块

生成题目模块包括生成随机的算式长度、生成随机的运算符以及生成随机个数的运算数。在算式生成之后,还需要进行括号匹配的检测以及对算式格式的调整。

其具体流程为首先生成一个随机的算式长度,其长度不超过10。之后逐个字符地生成该算式。当当前字符不是算式的最后一个字符时,生成一个随机数存入当前字符数组中,并在生成算式的过程中调整其格式,填入随机生成运算符,同时检测并避免乘方个数过多、乘方后数字过大以及分数分母为0等缺陷情况,同时,为方便同时对整数以及分数进行运算,在程序中设计num类同时表示两种数据类型,设计symbol变量对其进行标记区分,并在该类中设计对分数的化简机制;当当前字符是算式的最后一个字符时,主要流程与前述过程相同,增加了强制匹配右括号的情况。

求解题目模块

在求解题目模块中,主要的实现思想为“堆栈”。将表达式转换为后缀表达式后,通过判断将每一个字符存入操作数栈或操作符栈,存入后运算时分别按规则弹出两个栈中的字符即可。

运算符的优先级定义如下:(注:优先级数字越小优先级越高)

\ 运算符优先级
乘方、左括号 1
乘号、除号 2
加号、减号 3
右括号 4

在实际运算中,为随机生成方便起见,将所有的运算符对应于整型数字,其中101-104分别代表+,-,*,/,105为乘方,106为左括号,107为右括号。将算符、算数均存入堆栈后,直至操作符栈为空时,操作数栈的栈顶数字即为求解结果。

4.设计实现过程

需求分析

四则运算器的生成项目具体的需求可以分为如下三个阶段

  • 第1阶段
    一次性生成1000个不重复的四则运算式至txt文本文件中,通过命令行:可执行文件名.exe -i n(n为需要生成的题目数量)实现。
    例:four_op.exe -i 1000
    生成的四则运算式中最多有10个运算符,括号的数量不作限制。同时,除了整数之外,还要支持真分数的操作。在用户给出当前随机生成算式的答案后,程序能给出正误判断,并在用户完成全部题目之后给出其错误率统计。

  • 第2阶段
    增加可以支持乘方运算的运算符,其中乘方运算符的优先级最高,并且可以有两种表示方法:^/ **,用户可以通过在生成算式前的模式选择决定乘方算符的表示方式。

  • 第3阶段
    为程序设计一个基于Windows窗体程序的GUI界面,增加“倒计时”使得每个题目必须在20秒内完成,如果完不成则计0分进入下一题;同时增加历史记录功能记录用户答过的题及其正确答案。
    在该阶段中本项目使用C#语言进行实现。

程序流程图说明

Created with Raphaël 2.2.0 开始 输入指令 选择模式 生成题目 求解题目 输入答案 判断对错 题目是否做完 输出作答正确率 结束 yes no

类和模块说明

  • Main.cpp
    Main模块为该项目的主模块,用户在该模块中进行输入命令解析、参数设定以及模式选择,同时对于不合法的输入进行报错,并实现与输出模块之间的接口。

  • GenerateExp.cpp
    GenerateExp模块为随机生成表达式模块。包含函数BuildExp()和PrintExp(),其中BuildExp()函数随机生成四则运算题目,将其对应整型符号存入整型数组中;PrintExp()函数将整型算式表达转换为字符型存入字符型数组中并将算式打印,同时返回字符型数组至主函数进行文件输出。

  • Num类
    Num类模块中定义了 分子、分母、最大公约数、标记符号Symbol以及化简标志。同时在该模块中实现了各类算符的重载。

  • Solver.cpp
    Solver模块为求解四则运算题目的模块,其主要函数为get_ans()函数,实现对算式操作数、操作符的入栈、出栈操作,以根据运算规则实现对算式的求解。

  • Judge.cpp
    Judge模块为判断正误模块。该模块的主要函数为judge()函数和Check()函数,其中judge()函数实现用户输入答案与正确答案的比对并将正误信息反馈给用户,Check()函数用作检验用户输入答案的格式是否正确。

函数流程图说明

Created with Raphaël 2.2.0 开始 Main() 输入是否合法 模式选择:1/2? BuildExp()函数 PrintExp()函数 get_ans()函数 judge()函数 结束 输入不合法报错 yes no

5.单元测试

  • 单元测试用例设计如下
  1. 输入测试主要测试程序的合法输入以及不合法输入情况,保证程序的安全性,其设计如下:
编号 输入格式 预期输出
1 -i 10 正常处理,随机生成10个算式
2 Please input TWO parameters!
3 -i Please input TWO parameters
4 -c 5 Please input in the correct form!
5 -i abc Please input a NUMBER!
  1. 算式运算符测试,检测四则运算器中每一种算符的运算正确性,该层检测正确才可以进行进一步的程序分析,其中对于^运算符分别进行正常整型运算、幂指数为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
  1. 题目查重测试
    在四则运算中,由于存在加法交换律、乘法交换律以及左右结合律,故算式之间存在形式不同但逻辑运算相同的情况,在本项目中需要对该类情况进行测试,其设计如下:
编号 算式1 算式2 预期结果
1 1+2+3 3+(1+2) 重复
2 1+2+3 3+2+1 不重复
3 3 * 4 4 * 3 重复
4 1 * 2 * 3 3 * 2 * 1 不重复
5 (1+2)* 3 3 *(1+2) 不重复
6 1 ** 2 ** 3 3 ** 2 ** 1 不重复
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) 重复

6.程序性能分析及改进

在这里插入图片描述
在这里插入图片描述
在性能分析阶段,由于在诊断过程中需要不断与用户进行交互,故其诊断会话时间较长,达到了1分钟20秒。
由分析报告可见,在程序中主函数main()占用的CPU比例最大,其主要原因是在主函数中进行了文件的打开和读操作,占用CPU比例较大;将用户输入答案与正确结果进行比较的judge()函数其次,在其中进行对于用户输入答案的比对与反馈工作,需要与用户界面进行交互;打印当前生成的算式的PrintExp()函数再次之,其实现打印算式的同时还需要将生成的算式写入文件ques.txt中,需要消耗较多的CPU资源。这三个函数为项目中消耗CPU最多的三个主要函数。

7.代码说明

主函数说明

在主函数中实现了输入的解析与判断、参数设定以及主要的生成、求解、输出接口设计等,是该项目最为核心的函数:

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;
}

Num类

Num类定义了算式中数字的数据类型表示:

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);
};

随机生成算式

随机生成指定个数的四则运算式代码如下:

//随机化设计
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();
}

8.程序扩展:GUI界面生成

GUI界面设计显示

在该项目中,我们小组选择了第一个扩展方向,即使用C#语言生成一个基于Windows窗体程序的GUI界面。

主界面呈现:
在这里插入图片描述
在主界面中点击START按钮进入答题界面,该按钮实现了倒计时、成绩记录、题目以及答题文本框显示等功能,同时进行算式的居中设计。

答题界面呈现:
在这里插入图片描述
在答题界面中,左上角实现倒计时功能,每一道题有20s的答题时间,右上角实现成绩记录功能,每答对一道题,Grade++,否则Grade数值不变。在文本框中输入当前显示算式的答案,点击SUBMIT按钮进行提交。如果回答正确,则弹出消息框 “Bingo!” 如下:
在这里插入图片描述
如果回答错误,则弹出消息框 “Wrong!” ,并在下方显示该道题目的正确答案,如下:
在这里插入图片描述
如果答题时间超过了20秒,则弹出超时消息框 “Time Up!” ,并重新开始下一道题:
在这里插入图片描述
在答题过程中,随时可以点击QUIT按钮退出应用程序,也可以点击HISTORY按钮查看历史记录,得以看到自己答过的题目及其正确答案,以及当前的正确率统计。
在这里插入图片描述
在历史记录窗体中可以点击BACK按钮返回答题界面。

GUI界面代码设计

使用Windows窗体项目实现GUI界面的设计。首先将之前撰写的C++项目代码转换为C#语言在该项目中进行重写。在重写过程中需要考虑语法的修改以及GUI中添加的相应功能的正确表述。
在将C++项目中的功能进行成功移接后,进行其与窗体界面的结合,主要在START按钮和SUBMIT按钮中实现窗体程序的功能。

START按钮实现如下:

private void Start_Click(object sender, EventArgs e)
{
	if (IsFirstRound)
	{
		Ans.Visible = true; //点击开始答题按钮后显示答题文本框
		Start.Visible = false;
		label2.Visible = false;
		Submit.Visible = true;
		History.Visible = true;
		ques.Visible = true;
		Timer.Visible = true;
		ggrade.Visible = true;
		Quit.Visible = true;
		ggrade.Text = "Grade: " + grade.ToString();
		timer1.Start();
	}
	Ans.Focus();
	for (int i = 0; i < 1; i++)
	{
		save = Generate.BuildExp(3);
		Generate.PrintExp();
		cnt++;
		ques.Text = Generate.strsave;
		int len = ques.Width;
		int flen = this.Width;
		int x = (flen - len) / 2;
		int y = 100;
		ques.Location = new Point(x, y);
	}
}

SUBMIT按钮实现如下:

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);
        }

HISTORY按钮实现如下:

 //打开历史记录窗口
 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;
        }

9.填写表格中的实际时间

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
Total 合计 2150 2215

10. 实验总结

在本次进行结对项目的完成中,我的收获很多。我掌握了随机生成正确的四则运算式、求解四则运算式以及设计实现完善的GUI界面的方法。同时,我意识到了与队友协作的重要性。之前在进行个人项目的完成时不涉及合作的问题,但是在结对项目中如何与自己的队友进行分工合作显得至关重要。我的队友与我是很熟悉的朋友,因此我们这次的合作十分顺利~

除此之外,在GUI界面设计实现的过程中,我感觉到作为计算机专业的学生,也需要培养自己的设计思想与审美意识,不能只会实现后端的功能而忽视前端界面的美观设计。这次我们在如何设计美观的界面上花了很多的时间,做出了我们认为较为美观的操作界面,希望我们今后可以在这方面有所提高。

原创文章 3 获赞 1 访问量 433

猜你喜欢

转载自blog.csdn.net/weixin_40629184/article/details/86511805
今日推荐