如何寫一個計算器?

OneAPM官方技術部落格發表於2015-12-31

考慮這樣一個問題,給定一個字串,“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 組成。這三個定義式是遞迴的,它可以代表任意深度的算式。讓我們用樹的形式來觀察一下, ![如何寫一個計算器](http://news.oneapm.com/content/images/2015/12/-----2015-12-22---6-21-06-1.png) 有了這三種抽象物件我們可以寫出對應方法了,我們在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 官方部落格

相關文章