软件工程第五次作业

四则运算生成器

1.题目要求

  • 自动生成四则运算练习题
  • 以定制题目数量
  • 用户可以选择运算符
  • 用户设置最大数(如十以内、百以内等)
  • 用户选择是否有括号、是否有小数
  • 用户选择输出方式(如输出到文件、打印机等)
  • 最好能提供图形用户界面(根据自己能力选做,以完成上述功能为主)

2.分工

核心代码分为两部分,算术生成部分和算术求解部分

算术生成部分:

驾驶员:单文聪

领航员:林亚超

算术求解部分:

驾驶员:林亚超

领航员:单文聪

3.代码审查表

功能模块名称 算术生成模块 
审查人 林亚超  审查日期 2019.5.2 
代码名称 四则运算生成器  代码作者 单文聪 
文件结构
重要性 审查项 结论
头文件和定义文件的名称是否合理? 是 
  头文件和定义文件的目录结构是否合理? 是 
  版权和版本声明是否完整? 否 
重要 头文件是否使用了 ifndef/define/endif 预处理块? 是 
  头文件中是否只存放“声明”而不存放“定义” 否 
     
程序的版式
重要性 审查项 结论
  空行是否得体? 是 
  代码行内的空格是否得体? 是 
  长行拆分是否得体? 是 
  “{” 和 “}” 是否各占一行并且对齐于同一列? 是 
重要 一行代码是否只做一件事?如只定义一个变量,只写一条语句。 是 
重要 If、for、while、do等语句自占一行,不论执行语句多少都要加 “{}”。 否 
重要 在定义变量(或参数)时,是否将修饰符 * 和 & 紧靠变量名?注释是否清晰并且必要? 是 
重要 注释是否有错误或者可能导致误解? 否 
重要 类结构的public, protected, private顺序是否在所有的程序中保持一致? 未用类结构 
     
命名规则
重要性 审查项 结论
重要 命名规则是否与所采用的操作系统或开发工具的风格保持一致? 是 
  标识符是否直观且可以拼读? 是 
  标识符的长度应当符合“min-length && max-information”原则? 是 
重要 程序中是否出现相同的局部变量和全部变量? 是 
  类名、函数名、变量和参数、常量的书写格式是否遵循一定的规则?
  静态变量、全局变量、类的成员变量是否加前缀? 否 
     
表达式与基本语句
重要性 审查项 结论
重要 如果代码行中的运算符比较多,是否已经用括号清楚地确定表达式的操作顺序?
  是否编写太复杂或者多用途的复合表达式? 否 
重要 是否将复合表达式与“真正的数学表达式”混淆? 否 
重要 是否用隐含错误的方式写if语句? 例如 否 
  (1)将布尔变量直接与TRUE、FALSE或者1、0进行比较。 否 
  (2)将浮点变量用“==”或“!=”与任何数字比较。 是 
  (3)将指针变量用“==”或“!=”与NULL比较。 是 
  如果循环体内存在逻辑判断,并且循环次数很大,是否已经将逻辑判  
  断移到循环体的外面? 否,未涉及 
重要 Case语句的结尾是否忘了加break? 否 
重要 是否忘记写switch的default分支? 是 
重要 使用goto 语句时是否留下隐患? 例如跳过了某些对象的构造、变量的初始化、重要的计算等。 否,未涉及 
     
常量
重要性 审查项 结论
  是否使用含义直观的常量来表示那些将在程序中多次出现的数字或字符串? 是 
  在C++ 程序中,是否用const常量取代宏常量? 否 
重要 如果某一常量与其它常量密切相关,是否在定义中包含了这种关系? 是 
  是否误解了类中的const数据成员?因为const数据成员只在某个对象 否 
  生存期内是常量,而对于整个类而言却是可变的。 否,未使用类 
     
函数设计
重要性 审查项 结论
  参数的书写是否完整?不要贪图省事只写参数的类型而省略参数名字。 是 
  参数命名、顺序是否合理? 是 
  参数的个数是否太多? 否 
  是否使用类型和数目不确定的参数? 否 
  是否省略了函数返回值的类型? 否 
  函数名字与返回值类型在语义上是否冲突? 否 
重要 是否将正常值和错误标志混在一起返回?正常值应当用输出参数获得,而错误标志用return语句返回。 否 
重要 在函数体的“入口处”,是否用assert对参数的有效性进行检查? 否,未涉及assert 
重要 使用滥用了assert? 例如混淆非法情况与错误情况,后者是必然存在的并且是一定要作出处理的。 否,未涉及assert 
重要 return语句是否返回指向“栈内存”的“指针”或者“引用”? 否 
  是否使用const提高函数的健壮性?const可以强制保护函数的参数、返回值,甚至函数的定义体。“Use const whenever you need” 否 
     
内存管理
重要性 审查项 结论
重要 用malloc或new申请内存之后,是否立即检查指针值是否为NULL?(防止使用指针值为NULL的内存) 否,未涉及指针 
重要 是否忘记为数组和动态内存赋初值?(防止将未被初始化的内存作为右值使用) 否 
重要 数组或指针的下标是否越界? 否 
     
其它常见问题
重要性 审查项 结论
重要 数据类型问题:  
  (1)变量的数据类型有错误吗? 没有 
  (2)存在不同数据类型的赋值吗? 存在 
  (3)存在不同数据类型的比较吗? 存在 
重要 变量值问题:  
  (1)变量的初始化或缺省值有错误吗? 没有 
  (2)变量发生上溢或下溢吗? 没有 
  (3)变量的精度够吗? 够 
重要 逻辑判断问题:  
  (1)由于精度原因导致比较无效吗? 没有 
  (2)表达式中的优先级有误吗? 没有 
  (3)逻辑判断结果颠倒吗? 没有 
重要 循环问题:  
  (1)循环终止条件不正确吗? 正确 
  (2)无法正常终止(死循环)吗? 没有 
  (3)错误地修改循环变量吗? 没有 
  (4)存在误差累积吗? 没有 
重要 错误处理问题:  
  (1)忘记进行错误处理吗? 是 
  (2)错误处理程序块一直没有机会被运行? 没有 
  (3)错误处理程序块本身就有毛病吗?如报告的错误与实际错误不一致,处理方式不正确等等。 是 
  (4)错误处理程序块是“马后炮”吗?如在被它被调用之前软件已经出错。 没有 
重要

4.源代码

逆波兰表达式算法:

  • 首先,分配2个栈,栈s1用于临时存储运算符(含一个结束符号),此运算符在栈内遵循越往栈顶优先级越高的原则;栈s2用于输入逆波兰式,为方便起见,栈s1需先放一个优先级最低的运算符;
  • 从中缀式的左端开始逐个读取字符x,逐序进行如下步骤:
    • 若x是操作数,则分析出完整的运算数(在这里为方便,用字母代替数字),将x直接压入栈s2;
    • 若x是运算符,则分情况讨论:
    • 若x是'(',则直接压入栈s1;
    • 若x是')',则将距离栈s1栈顶的最近的'('之间的运算符,逐个出栈,依次压入栈s2,此时抛弃'(';
    • 若x是除'('和')'外的运算符,则再分如下情况讨论:
    • 若当前栈s1的栈顶元素为'(',则将x直接压入栈s1;
    • 若当前栈s1的栈顶元素不为'(',则将x与栈s1的栈顶元素比较,若x的优先级大于栈s1栈顶运算符优先级,则将x直接压入栈s1。否则,将栈s1的栈顶运算符弹出,压入栈s2 中,直到栈s1的栈顶运算符优先级别低于(不包括等于)x的优先级,或栈s2的栈顶运算符为'(',此时再则将x压入栈s1;
  • 在进行完(2)后,检查栈s1是否为空,若不为空,则将栈中元素依次弹出并压入栈s2中(不包括'#');      
  • 完成上述步骤后,栈s2便为逆波兰式输出结果。但是栈s2应做一下逆序处理,因为此时表达式的首字符位于栈底;
//求逆波兰表达式
float* Rpn_expression(float*arr)
{
    stack<float> s1;                    //存运算符
    stack<float> s2;                  //存数
	s1.push(1000000);          // 栈底,是最低优先级
	float* a = arr;
	float ch_s1;      //接收s1的栈顶元素返回值
	for (; *a!=-1;a++)
	{
		switch ((int)*a)
		{
			//左括号符号直接入栈
		case 1000005:                      //'('
			s1.push(1000005); break;
			//把和距离右括号最近的左括号之间的字符依次出栈,送入S2,将左括号弹出
		case 1000006:                        //')'
			while (s1.top() != 1000005)
			{
				ch_s1 = s1.top();
				s1.pop();
				s2.push(ch_s1);
			}
			s1.pop();
			break;
			//遇下列运算符,则分情况讨论:
			//1.若当前栈s1的栈顶元素是'(',则当前运算符直接压入栈s1;
			//2.否则,将当前运算符与栈s1的栈顶元素比较,若优先级较栈顶元素大,则直接压入栈s1中,
			//  否则将s1栈顶元素弹出,并压入栈s2中,直到栈顶运算符的优先级别低于当前运算符,然后再将当前运算符压入栈s1中
		case 1000001: //   '+'
		case 1000002://    '-'
			while (s1.top() != 1000000)
			{
				if (s1.top() == 1000005)
					break;
				ch_s1 = s1.top();
				s1.pop();
				s2.push(ch_s1);
			}
			s1.push(*a);
			break;
		case 1000003:    //'*'
		case 1000004:   //'/'
				while (s1.top() != 1000001 && s1.top() != 1000002 && s1.top() != 1000000)
				{
					if (s1.top() == 1000005)
					{
						break;
					}
					else
					{
						ch_s1 = s1.top();
						s1.pop();
						s2.push(ch_s1);
					}
				}
			s1.push(*a);
			break;
			/*如果*a在0-9之间,判断*(a+1)是否在0-9之间,如果是,将*a存在数组trans里,依次判断以后的*(a+i);*/
			/*用atof函数以trans为参数将字符串转化为浮点数,并存入栈s2;否则直接存入s2栈*/
		default:
		{
			s2.push(*a);
			break;
		}
		}
	}
	while (!s1.empty() && s1.top() != 1000000)   //如果s1非空,则剩余元素全部入栈s2
	{
		ch_s1 = s1.top();
		s1.pop();
		s2.push(ch_s1);
	}
	int length = s2.size();
	float* reverse;
	reverse = (float*)malloc(sizeof(float)*length);
	reverse[length] = '\0';
	Number_print = length;
	for (int i = length - 1; i >= 0; i--)
	{
		reverse[i] = s2.top();
		s2.pop();
	}
	return reverse;
}

根据逆波兰表达式求解源代码

float Calculate(float* a)
{
	float*arr = a;
	int length = Number_print;
	stack<float> s;
	for ( int i=0;i<length; i++)
	{
		if (arr[i] != 1000001 &&arr[i] != 1000002 &&arr[i]!= 1000003 &&arr[i]!= 1000004)
		{
			s.push(arr[i]);
		}
		else
		{
			float right = s.top();
			s.pop();
			float left = s.top();
			s.pop();
			switch ((int)arr[i])
			{
			case 1000001:s.push(right + left);
				break;
			case 1000002:s.push(left - right);
				break;
			case 1000003:s.push(right*left);
				break;
			case 1000004:s.push(left / right);
				break;
			default:break;
			}
		}
	}
	return s.top();
}

5.单元测试

本次对程序中的核心代码进行单元测试,采用条件测试:

   	TEST_CLASS(UnitTest1)
	{
	public:
		//表达式:25/5+(6.5-2.5)*3
		//逆波兰表达式:25,5,'/',6.5,4.5,'-',3,'*'
		TEST_METHOD(TestMethod1)   //逆波兰表达式求值测试
		{
			// TODO: 在此输入测试代码
			float a[11] = { 25,5,1000004,6.5,2.5,1000002,3,1000003,1000001 };
			int num=Calculate(a);
			Assert::AreEqual(17, num);
		}
		//表达式:(25+15)*3/(25-15)*4.6
		//逆波兰表达式:25,15,'+',3,'*',25,15,'-','/',4.6,'*'
		TEST_METHOD(TestMethod2)   //逆波兰表达式求值测试
		{
			// TODO: 在此输入测试代码
			Number_print = 9;
			float a[12] = { 40,3,1000003,25,15,1000002,1000004,4.6,1000003};
			float num = Calculate(a);
			float n = 55.199997;
			Assert::AreEqual(num, n);
		}

	};
  • 测试结果

5.程序结果

  • 小数,包含加减乘除与括号,并且输出到文档

  • 文档内容:

  • 问题:

  • 答案:

  • 正数,包含加减乘除与括号

6.界面设计

  在我们完成基本功能之后,采用MFC制作了简易界面,如下图所示:(界面功能尚不完善)

7.结果

合作照片


  本次作业完成过程中,花了两天时间完成核心代码的编写,编写过程中我和单文聪采用两个人互换领航员和驾驶员的角色。前期我们制定计划确认将代码的开发工作分为两部分,即分为算术表达式的生成与算术表达式的求解,单文聪负责算术表达式的生成,在这期间他为驾驶员,我为领航员,我负责算术表达式的求解以及输出到文档,在这期间我是驾驶员,他是领航员,经过不断地讨论,我们都很好的完成了各自的工作。
  在这次编写代码的过程中,我深刻体会到了交流合作的重要性。在第一天,我们讨论编写方案时,最开始我们互相交流思路时我并没有理解清他的思路方向,导致在写代码的过程中我写的代码不能完全移植到他的代码中,没有交流好参数的传递方法以至于重新改写了代码,浪费了些时间,后来我们吸取教训,在之后的编写过程中我们保持交流,保证了工作没有偏移计划方向。
  在这次合作过程中,单文聪同学及时和我交流编写思路,在我写完逆波兰表达式的生成函数与表达式的计算函数后,我们测试后发现算术结果错误,并且表达式的生成也有错误,单文聪同学和我按照代码的逻辑重新分析了一遍代码,将逻辑可能有错的地方逐一改写并测试,用了一个上午将所有的BUG解决,完成了老师的任务要求。在编写代码的过程中,由于最开始我们制定了参数传递方案以及相关函数的调用方案,所以我们的代码在最后很好的完成了移植。并且单文聪同学的代码写的逻辑很清晰,并且比较严谨((⊙﹏⊙)他的BUG比较少),函数的设计采用模块化设计,函数简洁易读,所以说在这个过程中很好的完成了工作。

posted @ 2019-05-02 19:57  林亚超  阅读(372)  评论(1编辑  收藏  举报