软件工程结对项目——四则运算题目生成(GUI)

本项目实现了四则运算题目的随机生成、求解及用户答案判定,涵盖后缀表达式转换、代码覆盖率测试及GUI界面设计,适用于教育软件开发。

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

目录

1.源代码的github链接:

2.PSP表格

3.题目要求

第一阶段:

第二阶段:

第三阶段:

4.解题思路

5.设计实现过程

第一二阶段:

第一部分:operation类的构建

第二部分:四则运算生成函数

第三部分:四则运算求解及判断函数

第四部分:main函数部分

在powershell上的运行结果:

单元测试部分

代码覆盖率部分

第三阶段:

6.性能分析

7.代码说明

四则运算题目生成部分:

求解函数部分:

判断用户输入结果部分:

GUI部分:

设置框的核心部分:

运算框的核心部分:

历史记录框的核心部分:

8.总结

1.期间遇到的问题

2.个人小结

 


1.源代码的github链接:

https://github.com/mwl0811/Arithmetic

队友博客地址:https://blog.youkuaiyun.com/qq_41843511/article/details/86511573

2.PSP表格

 

 

personal software 

process stage

预估耗时(分钟)实际耗时(分钟)
planning计划6090
Estimate估计这任务需要多长时间2030
development开发6090
analysis需求分析(包括学习新技术)180150
design spec生成设计文档12090
design review设计复审(和同事审核设计文档)6090
coding standard代码规范(为目前的开发制定合适的规范)12090
design具体设计180180
coding具体编码15001820
code review代码复审300240
test测试(自我测试,修改代码,提交修改)300300
reporting报告6060
test report测试报告6060
size measurement计算工作量3020
postmortem&process improvement plan事后总结,并提出过程改进计划180120
 合计30303430

3.题目要求

第一阶段:

  • 一次可以生成一千道题目,并且没有重复的,把题目写入一个文件夹中
  • 当有多与一个运算符的时候,如何对一个表达式求值?逐步扩展功能和可以支持的表达式类型,最后可以支持以下运算:

         25 - 3 * 4 - 2 / 2 + 89 = ?
         1/2 + 1/3 - 1/4 = ?
      (5 - 4 ) * (3 +28) =?

  • 除了整数外,还要支持真分数的四则运算
  • 让程序可以接受用户输入答案,并判定对错

第二阶段:

  • 支持乘方运算,乘方的优先级大于乘法和除法,并有两种表达形式,‘^’或‘**’
  • 两种方法都要支持,可以通过设置来选择

第三阶段:

  • 把程序变成一个Windows/Mac/Linux电脑图形界面程序,同时增加倒计时功能,每个题目必须在20秒内完成,如果完不成,则得零分并进入下一题,增加“历史记录”功能,把用户做题的成绩记录下来,并可以展现历史记录

4.解题思路

首先第一阶段相对比较好实现,随机生成一千道题目,需要获取随机数以及随机运算符,并将其放在适当的位置,表达式求值部分稍微有点麻烦,但主要思路还是运用后缀表达式来进行运算,其中需要的辅助数据结构有栈,队列等。将随机生成的四则运算题目即中缀表达式利用堆栈转换成后缀表达式,再进行计算。在与用户输入交互部分,主要是对用户生成答案的判断,这里有一点就是要支持真分数的运算,以及小数,对于负数还要判定前面有没有负号。所以在跟正确答案的对比部分,必须要识别好用户所给出的答案到底是什么,主要是对小数点,‘/’,以及负号的识别处理。

第二阶段其实就是在第一阶段中首先生成四则运算题目部分中随机运算符产生部分加入‘^’,'**'这个可以在‘^’生成后将其替换。模式选择在命令行中实现,有模式一和模式二,模式一表示乘方用‘^’表示,模式二表示乘方用‘**’表示。在计算结果部分只要在运算部分加一个case:^就行,具体计算由pow()函数实现。

第三阶段我们选择的第一个任务也就是将其变成一个图形界面程序。这一部分就是使用前面生成的.exe可执行文件进行修改,分别变成三个.exe文件,第一个是题目生成的.exe,第二个是对用户输入结果判定的.exe,第三个计算正确题数和错误题数的.exe文件。最后用c#写成windows窗体界面,在其中调用我们所修改后形成的三个文件。

5.设计实现过程

第一二阶段:

前两阶段中,我跟队友是分开实现题目生成和解题以及判别这两个部分的,最终合而为一拼起来。我负责的一部分是解题和判别答案部分,队友负责题目生成部分。在第一二阶段中,最后拼合好的程序主要封装在四部分中:

第一部分:operation类的构建

这一部分我将其封装在operation.h的头文件中,并命名一个名为Operation的类。类里面是关于四则运算项目所要实现的函数声明以及数独需要的私有变量。

公有函数:

  • int solve(string str);  //计算四则运算题目的结果
  • void Generate(int mode, int N);//生成四则运算题目
  • bool isPra(char c); //判断是否为括号
  • int getPri(char c);//获取符号优先级
  • void check(char c, stack<char>& coll2, deque<char>& coll3);//判断优先级
  • void allocate(deque<char>& coll1, stack<char>& coll2, deque<char>& coll3);//生成后缀表达式
  • void calculate(deque<char>& coll3, stack<double>& coll4);//计算后缀表达式结果
  • void judge();//判断用户输入答案是否正确
  • double getans()//为了设计单元测试而加入的获取正确答案函数
  • Operation()//构造函数,在其中打开problem.txt

私有变量:

  • ofstream out;  //打开的写入四则运算题目的文件
  • double ans[2000] = { 0 };//存储生成的正确结果
  • int ansnum = 0;//记录答案数目

第二部分:四则运算生成函数

这一部分我将其封装在Generate.cpp中,此部分主要是根据主函数传来的生成四则运算表达式题目的数目以及乘方的类型随机生成运算数和运算符,以及括号。为了防止最终得出的结果过大,我们对乘方的随机出现部位进行了限制,避免多个乘方的产生,并且还要避免除数为零的情况,以及0^0情况的产生,另外在生成括号时,括号需要成对出现,左括号和右括号要相匹配,但是避免不了最终生成的表达式中有重复括号的出现,这就需要对生成的表达式最终进行检验,去掉多余的括号。函数生成的四则运算式的质量直接决定了solve()函数能否成功调用。

第三部分:四则运算求解及判断函数

这一部分我将其封装在Solve.cpp中,此部分是实现四则运算的求解以及对用户输入结果的判断。首先由generate()函数传入生成的四则运算表达式四则运算题目,deque<char> coll1用来盛放中缀表达式,stack<char> coll2用来盛放操作符,deque<char> coll3用来盛放后缀表达式,stack<double>coll4用来计算后缀表达式的辅助容器,接着使用allocate()函数来生成后缀表达式,接着使用calculate()函数来计算表达式的结果,期间需要判定符号优先级也要考虑括号的存在,最终得到结果之后与以往的存在ans[]数组中的结果进行比较,如果没有相等的,那么solve()函数返回1,并将此题目写入文件problem.txt中,将答案存放在ans[]数组中,如果出现重复的情况,那么返回值为0,提示generate()函数重新生成表达式。

判断函数是judge()函数,此函数首先从problem.txt中一行一行的读入四则运算题目,提示用户输入答案,将其保存在字符串as中,由于用户输入的结果包括负数,小数和分数,所以字符串中可能出现‘-’,'/','.'。首先需要对用户输入结果的符号进行判定,将判定结果保存在syb中,接着读入数字放在as1中,如果碰到上述点或者除号,则继续读入的数据放在as2中,最后姜永辉输入的结果计算后保存在final_ans中并与ans[]数组中的正确答案进行对比,判断用户给出答案的正误。

注:这里我们简化两个表达式的判断相等,我们令生成表达式的结果不一样就保证了表达式的不等

第四部分:main函数部分

main函数部分封装在Arithmetic,cpp中,这一部分主要是对命令行传入的数据进行分析,并调用generate()函数。

在powershell上的运行结果:

单元测试部分

我们一共设置了十二个单元测试用例,其中前四个是对命令行传入参数的测试,第五个是针对题目要求不能生成重复的题目的测试,第六个到第十个是对solve()函数能否生成正确结果的测试,后两个是对judge()函数能否正确判断用户输入结果正误的测试。具体设置如下:

  • 输入格式不正确
  • 输入数字过大
  • 检查输入模式1时是否使用的是"^"
  • 检查输入模式2时是否使用的是“**”
  • 检查输入重复的四则运算题目,结果是否提示重新生成

检查出"3+(2+1)="和 "1+2+3="表达式是重复的并能够提示generate()函数重新生成四则运算表达式

  • 检查输入带括号的四则运算题目,结果是否正确
  • 检查输入带'^'四则运算题目,结果是否正确
  • 检查输入带'**'型的乘方的四则运算题目,结果是否正确
  • 检查输入复杂的四则运算题目,结果是否正确
  • 检查结果不是整数的四则运算,结果是否正确
  • 检查输入结果为真分数,判断是否正确

正确答案为0.25,用户输入"1/4"或者“0.25”都正确

  • 检查输入结果为负数,判断是否正确

测试结果:

可以看到,十二个测试用例全部通过

代码覆盖率部分

可以看到代码的覆盖率还是比较好的

第三阶段:

这一部分使用c#写的图形界面程序,主要调用第一二阶段的可执行程序,但是一二阶段的可执行程序不能很好契合我们所需要实现的功能,因而在此基础上我们又重现实现了三个.exe可执行程序,分别是:

  • Generator_wm.cpp   这个是用于根据命令行输入的题目个数和乘方类型,生成不重复的四则运算题目,并将其保存在problem.txt中
  • Solve.cpp    这个是用于根据命令行传入的四则运算表达式和用户给出的答案,算出正确结果并判断用户结果是否正确,并将判断结果追加写入judgements.txt中,并且如果结果正确在ansJug.txt中追加写入1,否则写入0
  • Count_num.cpp   这个是一个字符一个字符读入ansJug.txt中的数据,如果为1,则right_num++,否则wrong_num++,最终等用户所有题目做完后,打开历史记录可以看到自己的正确题数和错误题数

运行结果:

设置题目数量和乘方类型:

每题限时20秒,没有答完的超时警告:

如果题目做完,继续点击下一题时会弹出的警告:

历史记录查看:

6.性能分析

首先是对生成1000个四则运算题目进行的性能分析(不包括与用户的交互输入判断部分),可以看到最耗时的是generte()函数和solve()函数,其中generate()函数的耗时还是由于调用的solve()函数,其自身的生成运算式部分并没有耗费太多时间,而solve()函数则是由于其调用了allocate()函数和calculate()函数及其所调用函数。

接着是对少数运算题目进行测试,这里用10 个,包括与用户的交互输入以及判别部分,可以看到judge()函数即判断用户输入答案是否正确最为耗时。

 

7.代码说明

四则运算题目生成部分:

void Operation::Generate(int mode, int N)
{
	int repeat;
	char ch[5] = { '+','-','*','/','^' };
	srand((unsigned)time(NULL));    //初始化随机数种子
	for (int i = 0; i < N; i++)
	{
		char str[50];   //存放一行计算式
		char sym[11];  //符号保存
		int num[11];   //数字保存
		int symbolnum;   //符号个数
		int bracket1[3], bracket2[3];   //符号随机加的位置
		symbolnum = (rand() % 10) + 1;
		//printf("%d", symbolnum);
		for (int j = 0; j <= symbolnum; j++)
		{
			int symbol;
			if (sym[j - 1] == '^')   //为避免多次乘方而导致结果过大
			{
				if (num[j - 1] == 0)
				{
					num[j] = (rand() % 50) + 1;
				}
				else
				{
					num[j] = (rand() % 4);   //乘方后的数字小于等于3
				}
				symbol = (rand() % 4);   //不再生成乘方
			}
			else if (sym[j - 1] == '/')
			{
				symbol = (rand() % 5);
				num[j] = (rand() % 50) + 1;
			}
			else
			{
				symbol = (rand() % 5);
				num[j] = (rand() % 50);
			}
			sym[j] = ch[symbol];
		}
		sym[symbolnum] = '=';
		int btemp = 0;
		if (symbolnum >= 2)
		{
			btemp = (rand() % 4);   //加括号的个数0-3个
			for (int j = 0; j < btemp; j++)
			{
				do
				{
					bracket1[j] = (rand() % (symbolnum - 1));
					bracket2[j] = (rand() % (symbolnum - bracket1[j] - 1)) + bracket1[j] + 2;
				} while (sym[bracket1[j] - 1] == '^' || sym[bracket1[j] - 1] == '/' || sym[bracket2[j]] == '^');
				//乘方的情况下不加括号,防止过大
				//除法后不加括号,防止除数为0的情况
			}
			for (int j = 0; j < btemp; j++)
			{
				for (int k = 0; k < btemp; k++)
				{
					if (bracket1[j] == bracket2[k])
					{
						bracket1[j] = 20;
						bracket2[k] = 20;   //设置一个较大的数,使同一数字两旁括号其不输出
					}
				}
			}
			for (int j = 0; j < btemp; j++)
			{
				for (int k = j + 1; k < btemp; k++)
				{
					if (bracket1[j] == bracket1[k])
					{
						for (int m = 0; m < btemp; m++)
						{
							for (int n = m + 1; n < btemp; n++)
							{
								if (bracket2[m] == bracket2[n])
								{
									if (bracket1[k] != 20 && bracket2[n] != 20)
									{
										bracket1[k] = 20;
										bracket2[n] = 20;   //删除重复括号
									}
								}
							}
						}
					}
				}
			}
		}
		int len = 0;
		for (int j = 0; j <= symbolnum; j++)
		{
			for (int k = 0; k < btemp; k++)
			{
				if (bracket1[k] == j)
				{

					str[len] = '(';
					len++;
				}
			}
			if (num[j] <= 9 && num[j] >= 0)
			{
				str[len] = num[j] + '0';
				len++;
			}
			else
			{
				str[len] = num[j] / 10 + '0'; len++;
				str[len] = num[j] % 10 + '0'; len++;
			}
			for (int k = 0; k < btemp; k++)
			{
				if (bracket2[k] == j)
				{
					str[len] = ')';
					len++;
				}
			}
			if (mode == 2 && sym[j] == '^')
			{
				str[len] = '*'; len++;
				str[len] = '*'; len++;
			}
			else
			{
				str[len] = sym[j]; len++;
			}
		}
		str[len] = '\0';
		//printf("%s", str);
		//solve(str, out);
		repeat = solve(str);
		if (repeat == 1)
			N++;

	}
}

求解函数部分:

  • 中缀表达式转换为后缀表达式
void Operation::allocate(deque<char>& coll1, stack<char>& coll2, deque<char>& coll3)
{
	while (!coll1.empty())
	{
		char c;
		c = coll1.front();
		coll1.pop_front();

		if (c >= '0'&&c <= '9')
		{
			coll3.push_back(c);
			if ((!coll1.empty() && !isdigit(coll1.front())) || coll1.empty())//由于一个数字不一定只有一位长,故整个数字结尾加空格
				coll3.push_back(' ');
		}
		else
		{
			char d = 0;
			if (!coll1.empty())
				d = coll1.front();

			if (c == '*'&&d == '*')   //**转换为^
			{
				coll1.pop_front();
				coll2.push('^');
			}
			else
				check(c, coll2, coll3);//调用check函数,针对不同情况作出不同操作
		}

	}

	//如果输入结束,将coll2的元素全部弹出,加入后缀表达式中
	while (!coll2.empty())
	{
		char c = coll2.top();
		coll3.push_back(c);
		coll2.pop();
	}
}
  • 求解后缀表达式
void Operation::calculate(deque<char>& coll3, stack<double>& coll4)
{
	double num = 0;
	while (!coll3.empty())
	{
		int flag = 0;
		char c = coll3.front();
		coll3.pop_front();

		char d = 0;
		if (!coll3.empty())
			d = coll3.front();
		else
		{
			flag = 1;
		}


		//如果是操作数,压入栈中
		if (c >= '0'&&c <= '9')
		{
			num = num * 10 + c - '0';
			if ((d == ' '&&flag == 0) || flag == 1)
			{
				coll4.push(num);
				num = 0;
				if (flag == 0)
					coll3.pop_front();
			}

		}
		else	 //如果是操作符,从栈中弹出元素进行计算
		{

			double op1 = coll4.top();
			coll4.pop();
			double op2 = coll4.top();
			coll4.pop();
			switch (c)
			{
			case '+':
				coll4.push(op2 + op1);
				break;
			case '-':
				coll4.push(op2 - op1);
				break;
			case '*':
				coll4.push(op2*op1);
				break;
			case '/':
				coll4.push(op2 / op1);
				break;
			case '^':
				coll4.push(pow(op2, op1));
				break;
			}
		}
	}
}

判断用户输入结果部分:

void Operation::judge()
{
	string as;
	int syb;
	ifstream problem;
	double as1, as2;
	double final_ans;
	problem.open("question.txt");
	string str;
	int num = 0, wrong_num = 0, right_num = 0;
	while (getline(problem, str))
	{
		as1 = 0;
		as2 = 0;
		final_ans = 0;
		syb = 0;//初始值为正数
		cout << str << endl;
		cout << "Your answer:";
		cin >> as;
		int flag = 1;
		int dot = 0;

		//处理输入结果
		if (as[0] == '-')//负数
		{
			syb = 1;
		}

		for (int i = 0; i < as.length(); i++)
		{
			if (i == 0 && syb == 1)
				continue;
			if (as[i] <= '9'&&as[i] >= '0'&&flag == 1)
				as1 = as1 * 10 + (as[i] - '0');
			else if (as[i] == '.')
				flag = 2;
			else if (as[i] <= '9'&&as[i] >= '0'&&flag == 2)
			{
				as2 = as2 * 10 + (as[i] - '0');
				dot++;
			}
			else if (as[i] == '/')
				flag = 3;
			else if (as[i] <= '9'&&as[i] >= '0'&&flag == 3)
				as2 = as2 * 10 + (as[i] - '0');
			else
			{
				flag = 0;
				break;
			}

		}

		//计算输入的最终结果
		if (flag == 1)
			final_ans = as1;
		else if (flag == 2)
			final_ans = as1 + as2 / pow(10, dot);
		else if (flag == 3)
			final_ans = as1 / as2;

		if (syb == 1)
			final_ans = 0 - final_ans;

		//判断输入结果是否正确
		if (flag == 0 || final_ans != ans[num])
		{
			cout << "wrong!" << endl;
			wrong_num++;
		}
		else
		{
			cout << "right!" << endl;
			right_num++;
		}
		num++;
	}
	cout << "正确数:" << right_num << endl;
	cout << "错误数:" << wrong_num << endl;
}

GUI部分:

设置框的核心部分:

private void button1_Click(object sender, EventArgs e)
        {
            string para = quenum + " " + mode;
            System.Diagnostics.Process.Start("Generator_wm.exe", para).WaitForExit();
            Form2 f2 = new Form2();
            this.Hide();
            f2.Show();   //调用做题窗口
        }

运算框的核心部分:

public partial class Form2 : Form
    {
        private int count = 0;
        TimeSpan ts = new TimeSpan(0, 0, 20);
        public Form2()
        {
            InitializeComponent();
            timer1.Interval = 1000;
        }

        private void Form2_Load(object sender, EventArgs e)
        {
            FillGrid();
        }

        void FillGrid()
        {
            ts = new TimeSpan(0, 0, 20);
            this.timer1.Enabled = true;
            StreamReader str1 = new StreamReader(@"question.txt");
            string quebefore;
            for (int i = 0; i < count; i++) 
            {
                quebefore = str1.ReadLine();
            }
            string que = str1.ReadLine();
            if(que==null)
            {
                System.Diagnostics.Process.Start("Count_num.exe");
                MessageBox.Show("no question left! Please quit!", "警告", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
            label5.Text = que;
            if (que != null)
            {
                count++;
            }
            label6.Text = count.ToString();
            textBox3.Text = "Input your answer here";
            label7.Text = ts.Seconds.ToString();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            string que = label5.Text;
            string ans = textBox3.Text;
            string para = que + " " + ans;
            System.Diagnostics.Process.Start("Solve.exe", para).WaitForExit();
            FillGrid();
        }

        private void button3_Click(object sender, EventArgs e)
        {
            System.Environment.Exit(0);
        }

        private void textBox3_TextChanged(object sender, EventArgs e)
        {

        }

        private void label6_Click(object sender, EventArgs e)
        {

        }

        private void button2_Click(object sender, EventArgs e)
        {
            Form3 f3 = new Form3();
            f3.Show();   //调用做题窗口
        }

        private void label7_Click(object sender, EventArgs e)
        {

        }

        private void timer1_Tick(object sender, EventArgs e)
        {
            label7.Text = ts.Seconds.ToString();
            ts = ts.Subtract(new TimeSpan(0, 0, 1));   //隔一秒
            if(ts.TotalSeconds<0.0)
            {
                timer1.Enabled = false;
                string que = label5.Text;
                string ans = textBox3.Text;
                string para = que + " " + ans;
                System.Diagnostics.Process.Start("Solve.exe", para).WaitForExit();
                MessageBox.Show("You have used out of the time!", "超时警告", MessageBoxButtons.OK, MessageBoxIcon.Error);
                FillGrid();
            }
        }

        private void label8_Click(object sender, EventArgs e)
        {

        }
    }

历史记录框的核心部分:


        private void Form3_Load(object sender, EventArgs e)
        {
            StreamReader sr = new StreamReader("Judgements.txt", Encoding.Default);//将选中的文件在textBox2中显示
            richTextBox1.Text = sr.ReadToEnd();
            sr.Close();
        }

8.总结

1.期间遇到的问题

1、在第三阶段的编码过程中,我们总共需要打开三个txt文件一个是problem.txt这个实在生成题目的时候直接写入的,而另外两个judgements.txt和ansJug.txt则需要追加写入,因为我们是对用户的输入结果一个一个进行判断的,最开始在solve.cpp中每次调用一次打开judgements.txt,导致上一次写入的结果都被第二次所覆盖,所以最终我们在四则运算题目生成的generate.cpp中一次性打开这三个文件,另外两个进行追加写入就可以解决这个问题了。

2.在第一第二阶段我们两部分代码合并的时候,总是跳出白框提示错误,由于题目每次是随机生成的因此我们不能针对具体的四则运算式子找bug,最终决定还是回到合并前,两部分代码分开的时候各自调试,检查,最终发现是由于题目生成部分在删除多余括号时出现的问题,最终修改成功。

2.个人小结

通过这次的结对项目,过程中比第一次更加容易一些了,而且我跟队友两个人配合也比较默契,分别实现的部分能够比较好的拼接,节约了很多的时间,但是在第二阶段乘方‘**’这个的转换上其实还是做了一些无用的工作的,就是队友把随机生成的四则运算式中的‘^’换为‘**’,而我又把“**”在我的那一部分又换为‘^’,事实上,是可以她的第一次生成的原始运算式直接给我,我就可以直接计算了,这一点还是团队沟通不够充分导致。另外这次项目开始之前我与队友进行了代码模块的前期设计,为了提高模块的内聚性和减少模块之间的耦合度我们将每一部分的代码细化再细化,每一个函数都有一个独立的功能。最重要的是我与队友配合十分默契,能够清晰地表达想要什么,做成的产品能实现什么功能。这次的结对项目相对于第一次明显轻松了很多,但是也避免不了遇到新的问题,不管怎么说,每次都是一次知识学习的过程,做出最后产品来也很令人愉快,希望以后在其他项目中能够吸取前几次项目的教训,更加出色的完成项目!

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值