[体验编译原理]编写简易计算器

Demo: CaculationTest

前言

有想过自己写一个计算器么?输入一些数学表达式就能自己计算解析生成结果。

如果没有,可以现在开始想想,也许你会发现自己计算要不了几秒钟的表达式,让程序计算却没这么简单。

假定

为了便于理解,我们现在简化需求,数据类型只有整数,运算符只有加减乘除,没有括号。运行结果如图所示:

解析过程

逐个分析表达式字符串的每一个char,将其解析为一系列的token。然后根据token代表的不同含义进行相应的操作,直到计算出最终结果。

(本例中并没有全部解析完token,再遍历token,而是边解析边进行操作。这样做效率稍微高一点,但不能直接查看解析出来的全部token。)

这和我们阅读也比较接近:我们从左往右依次读取信息,读的过程中,我们会根据上下文即前后组成,形成一定的语义,如“其实不忍”,你可能会理解为”他实在不忍心“,或者理解为”他其实是不忍心的“,这得依照你对上下文的理解去选择了。

对照刚才的比喻,可以得出,token是语义的基本组成,或者说对字符组成的一种抽象。程序中将token抽象为以下数据结构:

 enum TokenType
   {
       Add,Sub,
       Mul,Div,
       Int,
       Start,End
   }

   class Token
   {
       public TokenType Type;
       public object Value;

       public Token(TokenType type , object value = null)
       {
           Type = type;
           Value = value;
       }
   }

Token和表达式

Token必须解析为表达式才会有意义。有了表达式,我们才能计算出最终结果。一个表达式,是能表示明确意义的一个或一组token。

在C#中,我们有一元表达式,二元表达式;表达式中有不同的运算,如加减法;在此例中,所有的表达式都可以计算出某个值,而且表达式之间可以相互计算形成新的表达式,如”表达式(1*2)+表达式(2*3)”。基于此,有Expression类,也并不复杂:

abstract class Expression
    {
        public abstract int GetValue();
    }

    class UnaryExpress : Expression
    {
        int _value;

        public UnaryExpress(int value)
        {
            _value = value;
        }

        public override int GetValue()
        {
            return _value;
        }
    }

    class BinaryExpression : Expression
    {
        TokenType _tokenType;
        int _left;
        int _right;

        public BinaryExpression(TokenType tokenType, Expression left, Expression right)
        {
            _tokenType = tokenType;
            _left = left.GetValue();
            _right = right.GetValue();
        }

        public override int GetValue()
        {
            switch (_tokenType)
            {
                case TokenType.Add:
                    return _left + _right;
                case TokenType.Sub:
                    return _left - _right;
                case TokenType.Mul:
                    return _left * _right;
                case TokenType.Div:
                    return _left / _right;
                default:
                    throw new Exception("unexceptional token!");
            }
        }
    }

此例中,并没有真正意义上的“一元表达式”,仅将数字看作它而已。二元表达式的值计算相对复杂,但类别也不多。

算法优先级

如果不算括号,恐怕对我们来说,用自己的“原始方式”解析表达式,最大的麻烦就是解决算法优先级的问题。

为什么“1+2*3” 不解析为1+2再乘以3呢,如何才能将其正确解析为2*3再和1相加呢?

首先我们需要顺序分析每个token, 表达式的解析顺序决定了最后的运算顺序,看下原代码中比较重要的这3个方法:

//解析加减
        Expression ParseAddSub()
        {
            //左操作数为优先级较高的运算符
            var l = ParseMulDiv();
            while (_token.Type == TokenType.Add || _token.Type == TokenType.Sub)
            {
                var t = _token.Type;
                ParseToken();
                var r = ParseMulDiv();//解析右操作数
                l = new BinaryExpression(t, l, r);
            }
            return l;
        }

        //解析乘除
        Expression ParseMulDiv()
        {
            var l = ParseUnary();
            while (_token.Type == TokenType.Mul || _token.Type == TokenType.Div)
            {
                var t = _token.Type;
                ParseToken();
                var r=ParseUnary();
                l = new BinaryExpression(t, l, r);
            }
            return l;
        }

        //解析一元表达式(目前只有单个整数)
        Expression ParseUnary()
        {
            Expression ret = null;
            if (_token.Type == TokenType.Int)
            {
                ret= new UnaryExpress((int)_token.Value);
            }

            //解析完int后,移到下一个token处,即+-*/
            ParseToken();
            return ret;
        }

1*2+2*2,我们可以看作两个乘法表达式相加,所以,解析加法运算的左右操作符前,程序尝试读取其是否是乘法表达式。

如果是1+2*3呢,左操作符当乘法运算解析时,并没有匹配上,由最高优先级的一元表达式决定其值,返回1,所以左操作符就是1了,当解析到+时,程序尝 试解析右操作符,从优先级比加法高一级的乘除开始,往上搜索匹配。很明显,2*3命中了乘法表达式,计算出右操作数的结果后,再和1相加,结果就正确了。

练习

如果研究透了demo,可以尝试把括号,取模运算,一无表达式(正负号)。

扩展

在IL代码和LinqExpression API中,“表达式”,“二元表达式”,“赋值表达式”,“成员获取表达式(.运算)”等,都很常见,解析的表达后对应的操作也很多,新建实例,引用实 例,访问局部变量等,赋值等。有兴趣可以查看一下Dynamic Linq的原代码(后续章节中,我们会写一个简单易版的Dynamic Linq动态计算 IQueryable.Where(string))。

时间: 2024-10-13 23:58:09

[体验编译原理]编写简易计算器的相关文章

JS编写简易计算器

<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/html"> <head lang="en"> <meta charset="UTF-8"> <title></title> <script> function add(){ var a=parseInt(document.getElementById(&quo

用antlr4来实现《按编译原理的思路设计的一个计算器》中的计算器

上次在公司内部讲<词法分析--使用正则文法>是一次失败的尝试--上午有十几个人在场,下午就只来了四个听众. 本来我还在构思如何来讲"语法分析"的知识呢,但现在看来已不太可能. 这个课程没有预想中的受欢迎,其原因可能是: 1.课程内容相对复杂,听众知识背景与基础差异比较大. 2.授课技巧不够,不能把复杂的知识简单化的呈现给基础稍差一点的人. 针对这两个可能的原因,我要尝试做出以下调整: 1.使用antlr来实现词法和语法的部分. 2.暂时把"编译"过程改为

编译原理-如何使用flex和yacc工具构造一个高级计算器

Flex工具的使用方法 Lex 是一种生成扫描器的工具. Lex是Unix环境下非常著名的工具,主要功能是生成一个扫描器(Scanner)的C源码. 扫描器是一种识别文本中的词汇模式的程序. 这些词汇模式(或者常规表达式)在一种特殊的句子结构中定义.一种匹配的常规表达式可能会包含相关的动作.这一动作可能还包括返回一个标记. 当 Lex 接收到文件或文本形式的输入时,它试图将文本与常规表达式进行匹配. 它一次读入一个输入字符,直到找到一个匹配的模式. 如果能够找到一个匹配的模式,Lex 就执行相关

c语言:编写一个简易计算器,打印菜单界面,实现加减乘除运算,可以退出菜单界面

.编写一个简易计算器 程序: #include<stdio.h> enum  OP { EXIT,//0 ADD,//1 SUB,//2 MUL,//3 DIV//4 }; void menu()//menu表示菜单 { printf("**** 1.add  ****\n"); printf("**** 2.sub  ****\n"); printf("**** 3.mul  ****\n"); printf("**** 

编译原理大作业(用java编写小型GCC 编译器)

以前只用编译器编译程序,现在学完编译原理这门课以后,通过编译大作业,我对编译器的工作原理有了比较清晰的认识 编译器的工作原理 编译器 (Compiler) 是一种将由一种语言编写的程序转换为另一种编程语言的可执行程序. 现代软件对于编译器的需求远甚从前, 究其原因很简单: 作为中间层, 编译器是构建更高层抽象的基础设施. 编译器意欲将人类可阅读的高阶代码, 翻译为机器能运行的低阶代码. 现代编译器的主要工作流程为: 源代码(source code)→ 预处理器(preprocessor)→ 编译

小白说编译原理-5-变量支持计算器

简介 本章依然专注于使用yacc实现计算器,主要的特点是给算术运算增加变量支持. 模块拆分 它主要分为3个模块 1. lex词法分析器 2. yacc语法分析器 3. 符号表 功能描述 1. lex词法分析器 正规式的定义如下: delim [ \t] ws {delim}+ letter [a-zA-Z] digit [0-9] id {letter}({letter}|{digit})* /* can support 12.34 */ number {digit}+(\.{digit}+)?

Java(随笔)——利用HTML,CSS,JavaScript,JQuery编写的简易计算器

一.利用刚学过的前端知识做一个简易的计算器里边使用到了HTML,CSS,JavaScript以及JQuery的知识代码如下:(1)利用CSS设计了计算器外形样式:(2)利用JavaScript实现了建议计算器的功能:代码: <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>简易计算器</title> <link rel="styl

编译原理实战——使用Lex/Flex进行编写一个有一定词汇量的词法分析器

编译原理实战--使用Lex/Flex进行编写一个有一定词汇量的词法分析器 by steve yu 2019.9.30 参考文档:1.https://blog.csdn.net/mist14/article/details/486413492.https://wenku.baidu.com/view/1c6398903868011ca300a6c30c2259010202f3a4.html 1.Flex工具的概述 Flex工具是生成C语言的工具,我们在日常生活中,如果直接使用C语言进行编写词法分析

使用HTML+CSS,jQuery编写的简易计算器后续(添加了键盘监听)

之前发布了一款简易的计算器,今天做了一下修改,添加了键盘监听事件,不用再用鼠标点点点啦 JS代码: var yunSuan = 0;// 运算符号,0-无运算;1-加法;2-减法;3-乘法;4-除法 var change = 0;// 属于运算符后需要清空上一数值 var num1 = 0;// 运算第一个数据 var num2 = 0;// 运算第二个数据 var cunChuValue = 0;// 存储的数值 $(function() { $(".number").click(f