软件工程2017第2次作业—— 个人项目:四则运算题目生成程序(基于控制台)...

1.Github项目地址

https://github.com/zhouhuiwei/Homework/blob/master/w2.cpp

 

2.PSP表格

记录在程序的各个模块的开发上估计和实际耗费的时间:

PSP2.1

Personal Software Process Stages

预估耗时(分钟)

实际耗时(分钟)

Planning

计划

10

15

· Estimate

· 估计这个任务需要多少时间

900

-

Development

开发

 

 

· Analysis

· 需求分析 (包括学习新技术)

120

300

· Design Spec

· 生成设计文档

30

30

· Design Review

· 设计复审 (和同事审核设计文档)

15

45

· Coding Standard

· 代码规范 (为目前的开发制定合适的规范)

15

15

· Design

· 具体设计

60

240

· Coding

· 具体编码

600

300

· Code Review

· 代码复审

60

480

· Test

· 测试(自我测试,修改代码,提交修改)

60

270

Reporting

报告

 

 

· Test Report

· 测试报告

30

15

· Size Measurement

· 计算工作量

30

15

· Postmortem & Process Improvement Plan

· 事后总结, 并提出过程改进计划

60

30

 

合计

1000

1635

 

 

3.解题思路描述

    由于只接触过C语言,时间有限,决定选用C++编程

    在填写了PSP表格的预估时间后,认真阅读了老师分享的两个示例。

    边读边了解了文中介绍的一些方法,我摸索学习了结构体的使用方法;特别去了解了一下后缀表达式(逆波兰)方法,这个方法我尝试学习;但有经验的同学告诉我,现在的我使用容易越学越乱。在网上搜索了“字符串+数组+四则运算”之类的案例,还了解了个位数的运算式利用指针运算的方法。

    因为一个式子计算的次数不算很多,也可以不考虑括号,数组应该可以达到要求,所以我最终决定利用数组完成计算部分的设计。

  • 把所有数都用结构体定义为分数形式,整数是a/1,分数是a/b
  • l 把两个数的四则运算先用函数定义出来,后续数组中两两运算只需引用该函数。因为已经定义好每个数都是分数,所以这个计算函数也好定义。
  • l 计算:定义两个数组,一个是字符、一个是结构体,字符数组装运算符,结构体数组装分数。一个运算式中运算符的个数是随机生成的,这两个数组里的元素也是随机生成的。

     因为定义了分数的结构体,所以现在每个结构体的数就像是加了括号;而运算符的数组里本应该包含“+,-,*,/”,运算时保持不变,输出时很方便改输出为“+,-,×,÷”。

    1)先算乘除:利用循环,把乘除运算符的前面的数赋为0,把后面的数赋为两数运算的结果,运算后把这个运算符变为加号;并且检测这个运算符前面的符号是否为减号,如果是,把后面的数的分子取反。

    2)再算加减:此时式子中只有加减号,把每个运算符后面的数赋为这个运算符前后两数运算之结果即可,存放数的数组中最后一个即为运算结果。

     例如:1-2×3÷5×5/6+4=?(6个数:1,2,3,5,5/6,4,5个操作符:-,*,/,*,+)

     先乘除:1+0+0+0+(-1)+4

     后加减:1 ,1 ,1 ,1,   0 , 4 ——最后一个数4即为答案

 

4.设计实现过程

    设计包括代码如何组织,比如会有几个类,几个函数,他们之间关系如何,关键函数是否需要画出流程图?

1)定义结构体:分数,包括两个整数——分子和分母,一个字符“/”,这是程序的基础;

2)定义最大公约数函数:用辗转相除法,可以求两数最大公约数,可用来化简全程都使用到的“分数”形式;

3)随机生成函数:100以内整数和真分数——全部定义为分数形式,利用srandrand函数,真分数生成两个整数合成,整数只用一个,把分母直接定义为1即可;

随机生成:运算符——排序任意性;

4)四则运算函数:两个“分数”之间两两相乘时遵循的四则运算规律,分别赋值给分子、分母,得到结果后要记得约分,并且要保证分数值为负时,负号加在分子上;

5)主函数:让用户选择每套题目所含题数,然后按之前所述,定义两个数组,然后先乘除后加减计算得到结果即可;

for (q = 0; q < j; q++){
				/*每循环一次,计算乘除运算符前后两数*/
				if (op[q] == '*' || op[q] == '/') {	               //这里的错误找得好苦				
					num[q + 1] = cal(num[q], op[q], num[q + 1]);   //利用前面定义的函数进行两数的乘除,得到的是一个结构体
					if (q > 0 && op[q - 1] == '-') {                    //从第2个运算符开始,检测前一个运算符是否为负号						
						op[q - 1] = '+';                           //如果前一个运算符是负号,把它改为加号
						num[q + 1].a = -num[q + 1].a;              //把负号放到这个运算符后面的数的分子上
						num[q + 1].line = '/';
						num[q + 1].b = num[q + 1].b;
					}                                              //这样负号就被移动到正确的位置,不会因为计算方法的缺陷失效
					num[q].a = 0;num[q].line = '/';num[q].b = 1;
					op[q] = '+';                                   //把已经被计算了的前一个数赋值为0,操作符改为+号
				}			
			}
			/*后算加减:*/
			for (h = 0; h < j; h++)
				num[h + 1] = cal(num[h], op[h], num[h + 1]);

  

比对答题人的答案,答对一题计数器加一,最后答对题数与总题数相比,可得正确率与分数。

 

5.代码说明

1)定义结构体:分数,包括两个整数——分子和分母,一个字符“/”,这是程序的基础;

/*定义一个结构体:分数*/
struct fs
{
	int a;//above分子 
	char line;
	int b;// below分母 
};

  

2)定义最大公约数函数:用辗转相除法,可以求两数最大公约数,可用来化简全程都使用到的“分数”形式;

/*求最大公约数*/
int gys(int a, int b)
{
	if (!b) return a;
	else return gys(b, a%b);
}  //辗转相除法 

  

3)a.随机生成函数:100以内整数和真分数——全部定义为分数形式,利用srand和rand函数,真分数生成两个整数合成,整数只用一个,把分母直接定义为1即可;

/*生成随机数(100以内、真分数)*/
fs generateNum()
{
	int a, b, t;
	fs num;
	if (rand() % 4 == 1)//随机产生0,1,2,3,当刚好是1时,产生分数 (这里概率一定是1/4吗?有没有让它概率不固定的办法?) 
	{
		a = rand() % 10 + 1;
		b = rand() % 10 + 1;
		if (a>b) { t = a; a = b; b = t; }//使分子小于分母,确保真分数 
		t = gys(b, a);
		a = a / t;
		b = b / t;
	}//生成分数 
	else
	{
		a = rand() % 100 + 1;
		b = 1;
	}//生成整数,即分母为1 
	num.a = a;
	num.b = b;
	num.line = '/';
	return num;//返回结构体:分数的值 
}

  

b.随机生成:运算符——排序任意性;

/*生成操作符*/
char generateOp()
{
	char op;
	int n;
	n=rand()%4;//根据随机数的值,确定运算符,然后输出
	if (n==0) op='+';
	if (n==1) op='-';
	if (n==2) op='*'; 
	if (n==3) op='/';
	return op;
}

  

4)四则运算函数:两个“分数”之间两两相乘时遵循的四则运算规律,分别赋值给分子、分母;得到结果后要约分;并且利用abs保证分数值为负时,负号加在分子上;

/*两个“分数”的四则运算*/
fs cal(fs n1, char op, fs n2)
{
	fs num;
	switch (op){
	    case '+':{
				 num.a = n1.a*n2.b + n2.a*n1.b;
				 num.b = n1.b*n2.b;
				 break;
	    }
	    case '-':{
				 num.a = n1.a*n2.b - n2.a*n1.b;
				 num.b = n1.b*n2.b;
				 break;
	    }
	    case '*':{
				 num.a = n1.a*n2.a;
				 num.b = n1.b*n2.b;
				 break;
	    }
	    case '/':{
				 num.a = n1.a*n2.b;
				 num.b = n1.b*n2.a;
				 break;
	    }
		default:{
				num.a = 0;
				num.b = 1;
		}
	}
	int t;
	if (num.a<num.b)
		t = gys(num.b, num.a);
	else
		t = gys(num.a, num.b);
	num.a = num.a / t;
	num.b = num.b / t;//约分
	if (num.a*num.b < 0) {
		num.a = -abs(num.a);
		num.b = abs(num.b);
	}//保证分数为负数时,负号加在分子上 (是不是要考虑不让题目中出现任一负数结果?)
	return num;

  

5)主函数:让用户选择每套题目所含题数;

然后按之前所述,定义两个数组,然后先乘除后加减计算得到结果即可(具体可看代码内详细注释);

比对答题人的答案,答对一题计数器加一;最后答对题数与总题数相除,可得正确率与分数。

int main(){
	int begin = 1;	
	while (begin == 1) {
		srand((unsigned)time(NULL));           //随机数生成函数初始化 
		char op[10];
		struct fs num[10];
		int m, n, i, j, q, h, k = 0;
		float s;
		cout << "_________________\n本次满分100。" << endl << "请输入题目数:";
		cin >> n;//用户选择的题数
		for (m = 1; m <= n; m++){
			j = rand() % 10 + 1;               // 随机生成该题的操作符数目:1~10
			/*随机生成数和运算符,写入数组*/
			for (i = 0; i < j; i++){
				num[i] = generateNum();
				op[i] = generateOp();
			}
			num[i] = generateNum();                              
			
			/*输出计算式*/
			cout << endl << m << ':';
			for (i = 0; i < j; i++){
				/*每循环一次,输出一个“分数”*/
				if (num[i].b == 1) cout << num[i].a;              //当生成整数时,只输出结构体fs中的a,也就是分子部分
				else cout << num[i].a << num[i].line << num[i].b; //生成分数时,输出a/b形式的数
				/*分别输出运算符号的书面形式*/
				if (op[i] == '*')cout << "×";
				else if (op[i] == '/')cout << "÷";
				else if (op[i] == '-')cout << "-";
				else cout << "+";
			}
			/*运算式的最后一个数,在循环外单独输出,并输出等号*/
			if (num[i].b == 1)cout << num[i].a << '=';
			else cout << num[i].a << num[i].line << num[i].b << '=';;

			/*进行计算:先算乘除*/
			for (q = 0; q < j; q++){
				/*每循环一次,计算乘除运算符前后两数*/
				if (op[q] == '*' || op[q] == '/') {	               //这里的错误找得好苦				
					num[q + 1] = cal(num[q], op[q], num[q + 1]);   //利用前面定义的函数进行两数的乘除,得到的是一个结构体
					if (q > 0 && op[q - 1] == '-') {                    //从第2个运算符开始,检测前一个运算符是否为负号						
						op[q - 1] = '+';                           //如果前一个运算符是负号,把它改为加号
						num[q + 1].a = -num[q + 1].a;              //把负号放到这个运算符后面的数的分子上
						num[q + 1].line = '/';
						num[q + 1].b = num[q + 1].b;
					}                                              //这样负号就被移动到正确的位置,不会因为计算方法的缺陷失效
					num[q].a = 0;num[q].line = '/';num[q].b = 1;
					op[q] = '+';                                   //把已经被计算了的前一个数赋值为0,操作符改为+号
				}			
			}
			/*后算加减:*/
			for (h = 0; h < j; h++)
				num[h + 1] = cal(num[h], op[h], num[h + 1]);

			/*答题者输入答案,此时num[h]为正确答案*/			
			fs answer;
			if (num[h].b == 1){
				cin >> answer.a;
				answer.b = 1;
			}
			else cin >> answer.a >> answer.line >> answer.b;
			
			/*判断答案对错*/
			if (answer.a == num[h].a && answer.b == num[h].b){
				printf("正确!");
				k += 1;//正确题数计数器
			}
			else {
				cout << "不正确!正确答案=";
				if (num[h].b == 1) cout << num[h].a;
				else cout << num[h].a << '/' << num[h].b;
			}
		}
		s = 100 * k / n;
		cout << endl << "正确率:" << k << '/' << n << endl;
		cout << "本次得分:" << s << endl << "再来一套?Y/N:";
		char yes;
		cin >> yes;
		if (yes == 'y' || yes == 'Y')begin = 1;
		else begin = 0;
	}
	system("pause");
	return 0;
}

  

6.测试运行

程序运行的截图:

 

7.项目小结:

前期看的资料比较多,并且和人讨论过思路,思考得比较全面才开始写自己的代码,所以最初的大部分代码写的过程中比较顺畅。(虽然由于对编程语言的生疏,还是出现了符号、花括号之类的错误,但需要的函数和实现设计的过程没有问题。)

由于使用的软件(DEV-C++)最开始不兼容,一直没有编译,到初稿写完才换软件(VS2013),所以写的过程中没有及时发现各种小BUG,正式运行调试前修改了一些。单元测试真的很重要啊。排查的重要问题:

  • 第一个BUG:for语句循环结束后,计数参数“i”还加了1,但最开始输出正确答案时输出的是有定义的数组元素之外的数;而且自己查错总是在思维定式中,写的时候明明对“i++”语句重新学习过,先人后己,但放到for循环里就忘了;此外编写的时候有过疑问,还有注释,怎么查错的时候,找出错误了才想起来。
  • 第二个BUG:计算的方法最开始没有考虑到负号,出现了计算错误才思考改正,按计算机语言人工走了一遍才发现“-”号会丢失,走了很多弯路。
  • 第三个BUG:由于太久没有练习编程,出现了输入和想法的出入。“if (op[q] == '*' || op[q] == '/')”写成了”if (op[q] == '*' ||  '/')”,大概找各种地方的问题自己鼓捣了一天都没发现。真的是思维定式,找了他人复审才最终查出。解决问题,自己费了很多的时间和精力,非常没有效率。但是尽管BUG很傻,改完了心情不要太好。
  • 编程的时候还出现了:“我本来没这两句,找不到错误加了这两句;终于找到之后我又删了这两句……想不得什么时候又加上了,但加的时候是直接复制的上面的那句的,所以参数不对”这种错误。

改的过程中通过思考、和他人交流,得到了类似于这样的的排除错误的经验:

“可以输出每一次循环之后的计算式,来查看到底哪一步出了问题。”

“为了查if语句的毛病,可以在if之前输出判断条件的值,在if语句里面输出‘com into if’这样的标识字眼来排查。”

这些经验看着很普通,但体会过才知道,要是一开始就知道这么做会多么好。也许系统学习C语言的时候老师说过,或者哪本书里、哪个论坛里会提,但这会儿的我不知道,真的平添了好多阻碍。只想以后多用点功夫钻研一下编程语言及其应用,好让写作业时间紧张的时候轻松一点。

出的错误看着也很小很小,甚至有点可笑,但无一不困住了我好久好久,3个小时、一个白天天、一天加一晚上……也许我的学习方法还有点问题,不知道及时向什么人求助,不知道更加积极主动……希望能好好改进

※程序特点:

  1. 答完一套题后可以选择是否继续答题。
  2. 随机生成的真分数暂时限定在分子分母都在10以内。
  3. 运算式里操作符的个数也随机生成。
  4. 利用数组和结构体解决四则运算的优先级问题。

※程序不完善的地方:

  1. 附加功能暂时只做了操作符号的个数能随机生成,明天继续完善。
  2. 生成的题目有时过于简单,大部分时候过于复杂,还在思考怎么控制难易度(以及对应分数)。
  3. 最后询问“再来一套?”用户的输入是按字符读的,如果一不小心多输了一个空格或者回车,它就存在缓存区,默认为是这个字符了。这个容错准备用“字符串剥离数字”解决。但一般情况正常输入没有问题。
  4. 答题者输入答案的时候,如果正确答案是分数,一定要输入“int a”,"char line","int b"才行;如果算错了,答题者准备输入整数,打完回车仍然不会比对答案是否正确,一定要输入“/”和另一个数才行。后续准备按“先读入一个字符串,然后再根据字符串里面是否包含‘/’来给answer赋值”解决。

 

转载于:https://www.cnblogs.com/vv06160/p/8146264.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值