资讯专栏INFORMATION COLUMN

如何写一个计算器?

zsy888 / 3292人阅读

摘要:考虑这样一个问题,给定一个字符串,,如何将它转化为如下形式换句话说,就是如何将字符串按照四则运算计算出来,如何写一个计算器。语义分析使用语法树检查源程序是否和语言定义的语义一致。

考虑这样一个问题,给定一个字符串,“1+1+(3+4)-2*3+8/2”,如何将它转化为如下形式:

“1+1=2”

“3+4=7”

“2+7=9”

“2*3=6”

“9-6=3”

“8/2=4”

“3+4=7”

换句话说,就是如何将字符串按照四则运算计算出来,如何写一个计算器。

拿 java 来举例,并且为了简单,我们只考虑个位数。这个过程大概分为这几个步骤,首先需要扫描字符串去除空白字符,其次将各个字符转换成对应的操作符或操作数,然后按照四则运算规则逐次计算并输出。

好,我们首先构造一个 scanner,它主要功能是顺序扫描字符串,返回字符并跳过其中的空白字符,如下

2015年就要结束了

public class Scanner {

    public Scanner(String source){
       this.source = source.toCharArray();
    }

    private char[] source;
    private int index = 0;
    private static char END = "
";
    public char getNext(){
        char result;

        do{
            if (index >= source.length){
                return END;
            }
            result = source[index];
            index += 1;
        }while (Character.isWhitespace(result));

        return result;
    }

}

在进行下一步之前,让我们思考一下这个算式的规律,算式中存在两种对象,一种是数字,一种是操作符,由于存在运算的优先级,我们分成三种对象,并用下面的形式来说明。

expr —> term + expr | term - expr | term
term —> factor * term | factor/term | factor
factor—> digit |(expr)

—>的意思是由...组成 代表或关系,expr 代表加减法运算式,term 代表乘除法运算式,factor 代表操作的最小元素,最后一句的意思就是 factor 由数字或者带括号的 expr 组成。这三个定义式是递归的,它可以代表任意深度的算式。让我们用树的形式来观察一下:

有了这三种抽象对象我们可以写出对应方法了,我们在parser类里定义三个函数,来代表三种对象的产生过程,并且定义char类型变量head代表正在被扫描的字符。

public class Parser {
    private Scanner scanner;
    public Parser(Scanner scanner){
        this.scanner = scanner;
    }

    private char head;

    public void parse(){
        if (Scanner.END != (head = scanner.getNext())){
            expr();
        }
        if (head != Scanner.END){
            throw new RuntimeException(“syntax error at "+head);
        }
    }

    public int expr(){
        int result = term();
        int tempResult;
        char operate;
        while ((operate = head) == "+" || operate == "-") {
            head = scanner.getNext();
            tempResult = term();
            switch (operate) {
                case "+":
                    System.out.println(result + "+" + tempResult + "=" + (result + tempResult));
                    result += tempResult;
                    break;
                case "-":
                    System.out.println(result + "-" + tempResult + "=" + (result - tempResult));
                    result -= tempResult;
            }
        }
        return result;
    }
    public int term(){
        int result = factor();
        int tempResult;
        char operate ;
        while ((operate=head) == "*" ||operate == "/") {
            head = scanner.getNext();
            tempResult = factor();
            switch (operate) {
                case "*":
                    System.out.println(result + "*" + tempResult + "=" + (result * tempResult));
                    result *= tempResult;
                    break;
                case "/":
                    System.out.println(result + "/" + tempResult + "=" + (result / tempResult));
                    result /= tempResult;
            }
        }
        return result;
    }

    public int factor(){
        int factor;

        if (Character.isDigit(head)){
            factor = head - 48; //字符变量-48可以转换成对应的字面数字
            head = scanner.getNext();
        } else {
            match("(");
            factor = expr();
            match(")");

        }
        return factor;
    }

//match 方法用来断言 head 是什么字符,如果为真,将读取下一个字符赋值给 head

public boolean match(char symbol){
    if (symbol == head){
        head = scanner.getNext();
        return true;
    }
    throw new  RuntimeException("syntax error at "+head);
}

public static void main(String... args){
    Scanner scanner = new Scanner("1+1+(3+4)-2*3+8/2");
    Parser parser = new Parser(scanner);
    parser.parse();
}

}

如果回过头来重新考虑这件事情,你会发现我们这个小程序的本质是将一段文本转化成可以执行的程序,正如我们的编译器一样。而实际上编译器要复杂的多,它的基本工作过程可以分为几个步骤:

词法分析 (scanning),读入源程序字符流,将字符转换成有意义的词素 (lexeme) 的序列,并生成对应的词法单元 (token)

语法分析 (parsing),主要目的是生成词法单元的语法结构,一般会使用树形结构来表示,称为语法树。

语义分析 (semantic analysis),使用语法树检查源程序是否和语言定义的语义一致。其中一个重要部分是类型检查。

生成中间代码,语义分析完成后,编译器会将语法树生成为一种接近机器语言的中间代码。我们程序最后产生的一系列小的表达式与之类似。

代码优化,编译器会尝试改进中间代码,用以生成更高效的机器代码。

代码生成,将优化过对中间代码生成机器代码。

在这些过程中,递归的方法起到了非常重要的作用,有一句话说明了编译器的本质,编译器就是让你的源程序变成可执行程序的另一个程序。你会发现这个定义本身就是递归的。透过这些编译原理,可以让我们更加深入的理解编程语言,甚至发明一种编程语言。

OneAPM Mobile Insight 以真实用户体验为度量标准进行 Crash 分析,监控网络请求及网络错误,提升用户留存。访问 OneAPM 官方网站感受更多应用性能优化体验,想阅读更多技术文章,请访问 OneAPM 官方技术博客。
本文转自 OneAPM 官方博客

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/65347.html

相关文章

  • 用PHP一个最简单的解释器Part5(算器最后一节,下节开始如何个脚本语言)

    摘要:经过几天的努力,用已经实现了一个完整的基础计算器,如下图上代码定义整数类型描述定义操作符号类型描述加法定义操作符号类型描述减法定义操作符号类型描述乘法定义操作符号类型描述除法定义操作符号类型描述定义操作符号类型描述定义空格用来存储输入字符的 showImg(https://segmentfault.com/img/bVbdNO5?w=900&h=377); 经过几天的努力,用PHP已经...

    yanest 评论0 收藏0
  • 如何懒惰的代码

    摘要:考虑下面这段的代码现在的问题是是否计算答案大概是是的。如果你需要代码变得懒一些,你需要绕过它的勤奋。这样的代码太差了。可惜的是,虽然是懒惰的,它却很勤奋的计算它的参数。里有一亿个数,所以这样的代码太贵了。 懒和勤奋:懒是一个很广泛的词,在程序员的世界里,它指只做需要做的工作。而勤奋在这里指做很多的工作为了未来。 考虑下面这段 JavaScript 的代码: showImg(https:...

    betacat 评论0 收藏0
  • 如何一个移动端的联动选择器(四)

    摘要:写在前面之前写了一篇为移动端而生的自定义多级联动选择器,得到了很多人的关注。具体实现步骤如下先传入一个需要计算深度的对象给,判断如果还有则迭代,并计算深度。如果增加了联动级数需要来判断,则为新增加的联动绑定新的事件。 写在前面 之前写了一篇 MultiPicker -『为移动端而生』的自定义多级联动选择器,得到了很多人的关注。鉴于很多人对这种手写插件的过程很好奇,所以写了几篇文章,来说...

    ssshooter 评论0 收藏0
  • 如何一个移动端的联动选择器(四)

    摘要:写在前面之前写了一篇为移动端而生的自定义多级联动选择器,得到了很多人的关注。具体实现步骤如下先传入一个需要计算深度的对象给,判断如果还有则迭代,并计算深度。如果增加了联动级数需要来判断,则为新增加的联动绑定新的事件。 写在前面 之前写了一篇 MultiPicker -『为移动端而生』的自定义多级联动选择器,得到了很多人的关注。鉴于很多人对这种手写插件的过程很好奇,所以写了几篇文章,来说...

    cgspine 评论0 收藏0
  • 如何一个移动端的联动选择器(四)

    摘要:写在前面之前写了一篇为移动端而生的自定义多级联动选择器,得到了很多人的关注。具体实现步骤如下先传入一个需要计算深度的对象给,判断如果还有则迭代,并计算深度。如果增加了联动级数需要来判断,则为新增加的联动绑定新的事件。 写在前面 之前写了一篇 MultiPicker -『为移动端而生』的自定义多级联动选择器,得到了很多人的关注。鉴于很多人对这种手写插件的过程很好奇,所以写了几篇文章,来说...

    leiyi 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<