自制计算器(一):Scanner

  今天来讲第一部分Scanner,俗称扫描器,也叫词法分析器。想要了解Scanner究竟做了什么,我们要从整个流程讲起。 首先,计算器得到的输入的是一串字符,如 ”1 + 2“。 如果不学编译原理,应该如何计算出结果呢?可能会利用栈,一个数字栈一个符号栈云云,但这样处理简单运算还好,如果有大于10的数,小数或者含有括号的情况,情况会很糟糕,何况编程语言本就比四则运算复杂许多。一般编译器的做法,则是将字符串以Token为单位分割开来。Token,中译为”标记、令牌“,不直观,你可以理解为表达式中的最小类型单位(《编译语言实现模式》中举例自然语言的句子成分也很贴切)。例如上文提到的”1 + 2“可以分解为“数字+符号+数字”的组合。得出Token之后,我们就可以在接下来的语法分析部分(Parser)中根据文法对其进行处理,具体方法我们下节再说。

词法分析可以采用lex等工具自动实现,但为了学习,我们还是人肉实现。这里引入一个概念,状态机(state machine),也叫自动机(automation),顾名思义,就是拥有状态集合的机器,并可以通过边进行状态的迁移(类似图,我突然想是否能直接用图实现状态机,等活干完试一下)。

先举一个简单的例子,下图表示一个个位整数,state 1为开始状态,此时向状态机输入一个0-9的整数,状态机迁移到state 2结束(接受)状态,当字符输入完毕并达到结束状态时,表示匹配成功。

  

多位整数应该如何表示?可以在结束状态上添加一个循环,表示接受多次输入,相当于正则中的”+“。下方的边表示单独的0。

PS:其实状态机也可以表示状态。如下图,表示二元运算式子,当然,这里不讲,只是让大家了解下Token到底是用来干什么的。

好了,状态机的概念理解后,我们来讲如何实现它。先理清思路,我们刚才讲到,Scanner的目的就是构造出Token,所以,首先要创建class Token

    class Token
    {
        // 略

    private:
        TokenType type_;
        int intValue_;
    };

type_代表Token的类型,这里把Token分成四大类,即数字(暂只支持整数)、符号(+-*/)、左右括号以及无效状态(invalid)

intValue_表示整数的值

    enum class TokenType
    {
        INT,
        FLOAT,
        ADD,        // +
        SUB,        // -
        MUL,        // *
        DIV,        // /
        LEFT_PAR,    // (
        RIGHT_PAR,    // )

        INVALID,    // 无效类型
    };

其次,由于operator多而杂,我们可以写一个dictionary以键值来存储它们。它有HasToken、FindToken等方便以供调用(太长不写了,详见代码)。

有了Token之后,就可以构建提取Token的函数了。这里主要讲一下GetNextToken,他是整个状态机的核心,处于while循环中,共分为两部分。第一部分为状态处理,根据当前状态选择处理方式,第二部分state迁移。此阶段对char进行识别,判定迁移state。初始状态为start,会直接进入state迁移,所以本质上就是根据前一个 or 几个char确定状态后,交由handle函数处理。

    

    Token Scanner::GetNextToken(stringstream &expression)
    {
        // first char
        auto currectChar = GetNextChar(expression);

        // state judge
        while (!expression.eof())
        {
            // 第一部分
            switch (state_)
            {
            case State::START:
                break;

            case State::NUMBER:
                return HandleNumberState(expression, currectChar);
                break;

            case State::OPERATOR:
                return HandleOperatorState(expression, currectChar);
                break;

            case State::ERROR:
                ErrorToken("error input");
                return Token(TokenType::INVALID);
            }

            string currectStr;
            currectStr.push_back(currectChar);

            //第二部分
            if (iswdigit(currectChar))
            {
                state_ = State::NUMBER;
            }
            else if (dict_.HasToken(currectStr))
            {
                state_ = State::OPERATOR;
            }
            else
            {
                state_ = State::ERROR;
            }
        }
        return Token(TokenType::INVALID);
    }        

两个handle函数的会把符合条件的char存入buffer,返回构建成Token。

HandleNumberState:接收两个参数字符stream和currectChar(刚才iswdigit()的数字)。构建后重置state为start表示匹配结束。代码如下:

  

    Token Scanner::HandleNumberState(stringstream &expression, char currectChar)
    {
        // first char
        string buffer;
        buffer.push_back(currectChar);

        while (!expression.eof() && isdigit(expression.peek()))
        {
            buffer += GetNextChar(expression);
        }

        // reset state
        state_ = State::START;

        // string to int
        int value;
        std::stringstream stream(buffer);
        stream >> value;

        return Token(TokenType::INT, value);
    }

HandleOperatorState:与number唯一不同的是Token通过自建的Dictionary类构建。

  

    Token Scanner::HandleOperatorState(stringstream &expression, char currectChar)
    {
        // first char
        string buffer;
        buffer.push_back(currectChar);
        auto token = dict_.FindToken(buffer);

        //reset state
        state_ = State::START;

        return token;
    }

  GetTokenList、GetNextTokenList就是以此为基础获得一行内的所有Token。

  Scanner的内容至此就讲完了,接下来是Parser部分。另外,写东西果然好难,磕磕巴巴,很多思路根本无法表达( ╯□╰ )

时间: 2024-10-08 21:28:08

自制计算器(一):Scanner的相关文章

HTML自制计算器

1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <title>自制计算器</title> 6 <link href="css/bootstrap.css" rel="stylesheet" /> 7 <script src="js/bootstrap.js"&g

自制计算器(零)

最近对编译器产生了兴趣,打算根据书上的思想裸写一个Python解释器,但过多的细节以及对编译过程的不了解,迟迟未有进展.正巧此时想到以前在sicp上看到过计算器的实现方法,想着是否能用简单的四则运算入门,所以有了这个计算器. 因作者水平问题,实现方法.代码设计上多有漏洞.写文也是为了能整理思路,查缺补漏,同时或与能为遇到同样问题的童鞋提供个思路吧. 进度:这个计算器目前实现到语法树阶段,错误处理还没弄明白. 代码架构: Scanner:使用了状态机,参考蓝色大大的文章http://frozeng

自制计算器

package com.cpinfo.his.web.nus; import java.awt.BorderLayout; import java.awt.Color; import java.awt.GridLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import javax.swing.JButton; impo

用php实现简单的自制计算器

存档: 1 <!DOCTYPE html> 2 <html> 3 <head> 4 <title>PHP实现计算器</title> 5 </head> 6 7 <body> 8 <?php 9 $mess = ""; 10 if(isset($_POST["sub"])){ 11 if($_POST["num1"] == ""){ 12

java 程序,通过scanner 编写计算器

package test; import java.util.Scanner; import java.util.regex.Matcher; import java.util.regex.Pattern; class Calculator { private Double num1; private Double num2; private String var; private Scanner input; public Double getNum1() { return num1; } p

《自制编程语言》笔记:使用yacc与lex制作简单计算器

1.代码 1.1)test.l 1.2)test.y 1.3)Makefile (因为是在linux环境下,所以使用了Makefile) 2.编译与运行 2.1)编译 2.2)运行 1.代码(也可以在我的百度网盘下载:http://pan.baidu.com/s/1o65k7v8) 1.1)lex文件 test.l 1 %{ 2 #include <stdio.h> 3 #include "y.tab.h" 4 5 int 6 yywrap(void) 7 { 8 retu

JS计算器(自制)

<!doctype html><html><header><meta charset="utf-8"><script src="jquery-1.9.1.min.js"></script><style>button{ width:40px; background-color:#2B91D5; color:white;}input[type='text']{ width:98%;}&l

通过键盘接收数值和字符,实现计算器功能。

import java.util.Scanner; /** * @author 蓝色以太 * 通过键盘接收数值和字符,实现计算器功能. */ public class Calculator { public static void main(String[] args) { Scanner sc=new Scanner(System.in); System.out.println("请输入第一个数值:"); double num1=sc.nextDouble(); System.out

自己写的计算器(加减乘除)代码

首先是Calculator计算器类 package test; public class Calculator { public int addition(int number1, int number2) { return number1+number2; } public int subtraction(int number1, int number2) { return number1-number2; } public int multiplication(int number1, in