• 编译器--简单数学表达式计算器(一)


    做了一个能够计算简单数学表达式值的小计算器,算不上是编译器,但用到了编译器的知识。

    近期在看一些编译器的东西,所以动手写这个最简单的计算器,既是对那些抽象的编译器知识有个形象的认识,也为后面加入复杂的东西--语句打下基础。此计算器是以《编译原理与实践》中实现的tiny编译器为參考写的。tiny是一个值得去研究的编译器,能够说是麻雀虽小,五脏俱全。从词法分析到代码生成都有,而且代码很清晰易懂。我认为想要了解编译器。能够从tiny入手,去将它跑起来并分析。废话不多说,開始记录这个小计算器。

    先说下需求:

    1、仅仅支持最简单的+ -*/  运算

    2、支持括号嵌套

    3、仅仅支持正数


    需求就这么简单的三条,能够将思路集中在与编译器相关的知识上面。

    比方能够计算(5+3)*2这个表达式。得到值16。或者计算 7- 5*3得到值-3。

    接下来说说实现的的思路,在此之前先扯一个我在大学里面学过的一个方法,是学数据结构的栈时。老师举了一个利用栈来计算简单数学表达式值的方法。其方法就是依次扫描这个表达式。依据操作符的优先级来决定其入栈的顺序。最后得到表达式的一个后缀表达式。最后利用这个后缀表达式来求值。

    这里要介绍的方法与上面的方法有点类似,也是要先扫描一遍表达式。只是扫描完之后得到的不是一个后缀表达式,而是一棵语法树,然后对这棵语法树进行递归求解。以下就是表达式(5+3)*2这个表达式相应的语法树。对这棵语法树进行后序遍历事实上也能得到一个后缀表达式,所以说两种方法还是相通的。

                             *

                          /      

                        +         2

                     /      

                   5        3


    学过编译原理的都知道。要实现某种语言。就先要定义其语法。语法的作用是用来定义语言是什么样子的(废话),比方计算器的语法就定义了操作的优先级、结合性等。假设扫描过程中发现表达式不符合语法的定义,就觉得表达式是非法的,比方 表达式“5+3-”就是非法的。由于减号后面没有被减数。

    以下放出语法:

    expr -> term |  term+term | term-term                     

    term -> factor | factor * factor | factor/factor

    factor -> number | (expr)

    number -> (0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9)*


    我认为定义语法是一个比較有腩的事情,至少这个语法不是我造的。是从tiny编译器中拿过来的。这个语法看起来比較清晰,三条语法出现的先后顺序代表了计算的优先级。括号最高,乘除次之。加减最低。factor代表一个最小的计算因子。能够是数字或者一个括号括起来的表达式。term代表一个乘法或者除法表达式,exprt代表一个加法或者减法表达式,term和expr也能够直接是num或者(expr)

    定义语法要考虑消除左递归的问题,比方假设将第一条语法expr -> term |  term+term | term-term 写成expr -> term | expr+term | expr-term。这样是能体现加减法具有左结合性,可是这条语法假设直接写成递归函数。就会有死循环的问题。

    tiny的这个语法消除了左递归,把加减法的左结合性问题丢给了expr相应的函数来处理。


    以下開始上代码,先看下处理expr的函数

    TreeNode *exp()
    {
    	TreeNode *node;
    	TreeNode *lnode, *rnode;
    	
    	node = term();
    
    	/*假设下一个符号是+ 或者-。那么就以操作符作为根节点
    	两个操作数作为子节点*/
    	while ((ADD == token) || (MINUS == token))
    	{
    		/*左节点*/
    		lnode = node;
    		
    		/*操作符节点,即根节点*/
    		node = newNode();
    		node->attr.e = OpK;
    		node->val.tt = token;
    		node->child[0] = lnode;
    		
    		match(token);
    
    		/*右节点*/
    		rnode = term();
    
    		node->child[1] = rnode;
    	}
    	
    	return node;
    }
    这段代码先调用term()函数来处理一个乘法或者除法表达式,接下来推断下一个token是否是加减号操作符。假设是的话,就将该操作符作为根节点,把term()函数返回的节点作为根节点的左节点。然后再调用term()函数返回一个表达式节点,作为右节点。左右两个节点代表操作符的两个操作数。


    以下是term函数的代码,与exp函数很相似,不须要再具体说明。

    TreeNode *term()
    {
    	TreeNode *node;
    	TreeNode *lnode, *rnode;
    
    	node = factor();
    
    	/*将乘法或者除法操作符作为根节点。并得到左右节点*/
    	while ((MUL == token) || (DIV == token))
    	{
    		lnode = node;
    		node = newNode();
    		node->attr.e = OpK;
    		node->val.tt = token;
    		node->child[0] = lnode;
    		
    		match(token);
    
    		rnode = factor();
    
    		node->child[1] = rnode;
    	}
    	
    	return node;
    }
    


    接下来是factor函数,代表一个最小的计算因子。

    TreeNode *factor()
    {
    	TreeNode *node;
    
    	switch (token)
    	{
    		/*一个数字*/
    		case NUM:	
    			/*生成并返回一个节点。节点类型就是常数*/
    			node = newNode();
    			node->attr.e = ConstK;
    			node->val.num = atoi(tval);
    			match(NUM);
    			break;
    		/*左括号*/
    		case LPAREN:
    			match(LPAREN);
    			/*调用exp来解析一个表达式*/
    			node = exp();
    			match(RPAREN);
    			break;		
    		default:
    			printf("<Error>factor: Token cann't handled.
    ");
    			exit(1);
    			break;
    	}
    	
    	return node;
    }
    factor函数推断下一个token是数字还是括号。假设是数字。那么就直接返回数字所代表的节点。假设是左括号,括号中面是一个表达式,则调用exp来分析这个表达式,然后返回表达式的节点。


    通过上面几个函数,能够返回一棵语法树,以下的calc函数通过这个语法树来递归地进行求值。

    int calc(TreeNode *node)
    {
    	int val;
    	int val1, val2;
    
    	if (NULL == node)
    	{
    		printf("<Error>calc: syntax error.
    ");
    		exit(1);
    	}
    
    	/*依据节点的属性返回对应的值。眼下节点有两种
    	属性:数字或者操作符*/
    	switch (node->attr.e)
    	{
    		/*数字属性节点直接返回值*/
    		case ConstK:
    			return node->val.num;
    			break;
    		/*操作符属性节点值须要先计算两个操作数的值,
    			再依据操作符来计算最后的结果*/
    		case OpK:
    			val1 = calc(node->child[0]);
    			val2 = calc(node->child[1]);
    			switch (node->val.tt)
    			{
    				case ADD:
    					val = val1 + val2;
    					break;
    				case MINUS:
    					val = val1 - val2;
    					break;
    				case MUL:
    					val = val1 * val2;
    					break;
    				case DIV:
    					val = val1 / val2;
    					break;
    				default:
    					printf("<Error>cal: Unknown operation.
    ");
    					exit(1);
    					break;
    			}
    			break;
    		default:
    			printf("<Error>calc: Unknown expression type.
    ");
    			exit(1);
    			break;
    	}
    	
    	return val;
    }


    这个小计算器的主体代码介绍完了。其他的就剩下一些支撑函数,如getToken函数用来获取一个token。match函数用来推断获取出来token与当前语法要求的token是否一致,假设不一致。就说明出现了语法错误。


    将代码进行编译:

    gcc -fno-builtin mycomplier.c -o mycomplier


    然后建立一个文件exprtest,里面的内容为要计算的表达式。如 3+(10-2) * 5 

    保存exprtest文件后。输入mycomplier   exprtest,即会输出The result is 43.


    好了,这个小计算器就总结完了。它还有非常多数值方面的功能有待完好,比方支持负数和其他操作符。但正如開始所说。这里重点关注编译器方面的知识,我是个急性子,所以没太花时间去处理这些数值操作。后面会完好这个计算器,在当中增加处理语句的功能。让它更像是在编译一个语言。


    完整代码下载路径编译器原理--一个小计算器    

    有什么问题欢迎发邮件交流学习:)

    Email:    robin.long.219@gmail.com















  • 相关阅读:
    洛谷——P1141 01迷宫
    洛谷——P1781 宇宙总统
    洛谷——P1608 路径统计
    洛谷——P1144 最短路计数
    洛谷—— P1162 填涂颜色
    python(22)- 递归和函数式编程
    android驱动例子(LED灯控制)
    Android之SDK、NDK、JNI和so文件
    NDK 与 JNI 的关系
    Android之NDK开发
  • 原文地址:https://www.cnblogs.com/llguanli/p/6775376.html
Copyright © 2020-2023  润新知