如何写一个计算器?

考虑这样一个问题,给定一个字符串,“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 = '\n';
    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();
}

}

如果回过头来重新考虑这件事情,你会发现我们这个小程序的本质是将一段文本转化成可以执行的程序,正如我们的编译器一样。而实际上编译器要复杂的多,它的基本工作过程可以分为几个步骤,
1,词法分析 (scanning),读入源程序字符流,将字符转换成有意义的词素 (lexeme) 的序列,并生成对应的词法单元 (token)
2,语法分析 (parsing),主要目的是生成词法单元的语法结构,一般会使用树形结构来表示,称为语法树。
3,语义分析 (semantic analysis),使用语法树检查源程序是否和语言定义的语义一致。其中一个重要部分是类型检查。
4,生成中间代码,语义分析完成后,编译器会将语法树生成为一种接近机器语言的中间代码。我们程序最后产生的一系列小的表达式与之类似。
5,代码优化,编译器会尝试改进中间代码,用以生成更高效的机器代码。
6,代码生成,将优化过对中间代码生成机器代码。

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,890评论 18 139
  • 简介网络浏览器很可能是使用最广的软件。在这篇入门文章中,我将会介绍它们的幕后工作原理。我们会了解到,从您在地址栏输...
    wengjq阅读 2,071评论 2 15
  • 前言 啰嗦几句没用的,可以跳过。 来了一个公司,老板带我很好,公司同事也不错,公司配了个mac,我的神...
    温走马阅读 3,689评论 20 6
  • 前两天守望第三赛季开始了,酷爱作死的我萌生了一个让自己掉到泥塘然后制霸全场的想法。当然在这之后才真正的理解了一句话...
    虚妄青岚阅读 559评论 3 3
  • 春末夏初。 树荫荫于绿,草尖破于土,不乏旧土新绿。 花开开于色,月圆许十五,不乏旧月新蝶。 怎知,四季始于暖春,始...
    岁月静好陈莹阅读 441评论 0 2