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

本项目介绍了一个四则运算器的软件工程基础结对项目,涵盖生成随机四则运算题目、求解答案、GUI界面设计及性能分析。项目采用C++和C#实现,包括生成题目、求解、单元测试、性能优化及GUI界面扩展。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

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

队友博客链接:https://blog.youkuaiyun.com/qq_37745978/article/details/86308941

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

PSP2.1Personal 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个算式
2Please input TWO parameters!
3-iPlease input TWO parameters
4-c 5Please input in the correct form!
5-i abcPlease input a NUMBER!
  1. 算式运算符测试,检测四则运算器中每一种算符的运算正确性,该层检测正确才可以进行进一步的程序分析,其中对于^运算符分别进行正常整型运算、幂指数为0以及底数为分数三种情况讨论,其设计如下:
编号操作数1操作数2运算符预期结果
111/2+3/2
211/2-1/2
331/2*3/2
431/2/6
532^9
630^1
71/21^1/2
  1. 题目查重测试
    在四则运算中,由于存在加法交换律、乘法交换律以及左右结合律,故算式之间存在形式不同但逻辑运算相同的情况,在本项目中需要对该类情况进行测试,其设计如下:
编号算式1算式2预期结果
11+2+33+(1+2)重复
21+2+33+2+1不重复
33 * 44 * 3重复
41 * 2 * 33 * 2 * 1不重复
5(1+2)* 33 *(1+2)不重复
61 ** 2 ** 33 ** 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/41/2+(2/3+3/4)重复
11(1/2+2/3)* 3/43/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.1Personal Software Process Stages预估耗时 (分钟)实际耗时(分钟)
Planning计划3035
Estimate估计这个任务需要多少时间3035
Development开发19302005
Analysis需求分析(包括学习新技术)9060
Design Spec生成设计文档6060
Design Review设计复审(和同事审核设计文档)3030
Coding Standard代码规范(为目前的开发制定合适的规范)3015
Design具体设计120120
Coding具体编码12001345
Code Review代码复审6075
Test测试(自我测试,修改代码,提交修改)240300
Reporting报告190175
Test Report测试报告120100
Size Measurement计算工作量1015
Postmortem & Process Improvement Plan事后总结,并提出过程改进计划6060
Total合计21502215

10. 实验总结

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值