從0開始自制解釋器——實現(xiàn)多位整數(shù)的加減法計算器

上一篇我們實現(xiàn)了一個簡單的加法計算器,并且了解了基本的詞法分析、詞法分析器的概念。本篇我們將要對之前實現(xiàn)的加法計算器進行擴展,我們?yōu)樗砑右韵聨讉€功能

  1. 計算減法
  2. 能自動識別并跳過空白字符
  3. 不再局限于單個整數(shù),而是能計算多位整數(shù)

提供一些工具函數(shù)

首先為了支持減法,我們需要重新定義一下TokenType這個類型,也就是需要給 - 定義一個標志?,F(xiàn)在我們的TokenType的定義如下

typedef enum e_TokenType
{
    CINT = 0,
    PLUS,
    MINUS,
    END_OF_FILE
}ETokenType;

由于需要支持多個整數(shù),所以我們也不知道最終會有多少個字符,因此我們提供一個END_OF_FILE 表示我們訪問到了最后一個字符,此時應該退出詞法分析的過程。

另外因為整數(shù)個數(shù)不再確定,我們也就不能按照之前的提供一個固定大小的數(shù)組。雖然可以提供一個足夠大的空間來作為存儲數(shù)字的緩沖,但是數(shù)字少了會浪費空間。而且考慮到之后要支持自定義變量和函數(shù),采用固定長度緩沖的方式就很難找到合適的大小,太大顯得浪費空間,太小有時候無法容納得下用戶定義的變量和函數(shù)名。因此這里我們采用動態(tài)長度的字符緩沖來保存。我們提供一個DyncString 的結構來保存這些內容

#define DEFAULT_BUFFER_SIZE 16

// 動態(tài)字符串結構,用于保存任意長度的字符串
typedef struct DyncString
{
    int nLength; // 字符長度
    int capacity; //實際分配的空間大小
    char* pszBuf; //保存字符串的緩沖
}DyncString, *LPDyncString;

// 動態(tài)字符串初始化
// str: 被初始化的字符串
// size: 初始化字符串緩沖的大小,如果給0則按照默認大小分配空間
void dyncstring_init(LPDyncString str, int size);
// 動態(tài)字符串空間釋放
void dyncstring_free(LPDyncString str);
//重分配動態(tài)字符串大小
void dyncstring_resize(LPDyncString str, int newSize);
//往動態(tài)字符串中添加字符
void dyncstring_catch(LPDyncString str, char c);
// 重置動態(tài)數(shù)組
void dyncstring_reset(LPDyncString str);

它們的實現(xiàn)如下

/*----------------------------動態(tài)數(shù)組的操作函數(shù)-------------------------------*/
void dyncstring_init(LPDyncString str, int size)
{
    if (NULL == str)
        return;

    if (size == 0)
        str->capacity = DEFAULT_BUFFER_SIZE;
    else
        str->capacity = size;
    str->nLength = 0;
    str->pszBuf = (char*)malloc(sizeof(char) * str->capacity);
    if (NULL == str->pszBuf)
    {
        error("分配內存失敗\n");
    }

    memset(str->pszBuf, 0x00, sizeof(char) * str->capacity);
}

void dyncstring_free(LPDyncString str)
{
    if (NULL == str)
        return;

    str->capacity = 0;
    str->nLength = 0;
    if (str->pszBuf == NULL)
        return;

    free(str->pszBuf);
}

void dyncstring_resize(LPDyncString str, int newSize)
{
    int size = str->capacity;
    for (; size < newSize; size = size * 2);
    char* pszStr = (char*)realloc(str->pszBuf, size);
    str->capacity = size;
    str->pszBuf = pszStr;
}

void dyncstring_catch(LPDyncString str, char c)
{
    if (str->capacity == str->nLength + 1)
    {
        dyncstring_resize(str, str->capacity + 1);
    }

    str->pszBuf[str->nLength] = c;
    str->nLength++;
}

void dyncstring_reset(LPDyncString str)
{
    dyncstring_free(str);
    dyncstring_init(str, DEFAULT_BUFFER_SIZE);
}
/*----------------------------End 動態(tài)數(shù)組的操作函數(shù)-------------------------------*/

另外提供一些額外的工具函數(shù),他們的定義如下

void error(char* lpszFmt, ...)
{
    char szBuf[1024] = "";
    va_list arg;
    va_start(arg, lpszFmt);
    vsnprintf(szBuf, 1024, lpszFmt, arg);
    va_end(arg);

    printf(szBuf);
    exit(-1);
}

bool is_digit(char c)
{
    return (c >= '0' && c <= '9');
}
bool is_space(char c)
{
    return (c == ' ' || c == '\t' || c == '\r' || c == '\n');
}

主要算法

我們還是延續(xù)之前的算法,一個字符一個字符的解析,只是現(xiàn)在需要額外的將多個整數(shù)添加到一塊作為一個整數(shù)處理。而且需要添加跳過空格的處理。

首先我們對上次的代碼進行一定程度的重構。我們添加一個函數(shù)專門用來獲取下一個字符

char get_next_char()
{
    // 如果到達字符串尾部,索引不再增加
    if (g_pPosition == '\0')
    {
        return '\0';
    }
    else
    {
        char c = *g_pPosition;
        g_pPosition++;
        return c;
    }
}

expr() 函數(shù)里面大部分結構不變,主要算法仍然是按次序獲取第一個整數(shù)、獲取算術運算符、獲取第二個整數(shù)。只是現(xiàn)在的整數(shù)都變成了采用 dyncstring 結構來存儲

int expr()
{
    int val1 = 0, val2 = 0;
    Token token = { 0 };
    dyncstring_init(&token.value, DEFAULT_BUFFER_SIZE);

    if (get_next_token(&token) && token.type == CINT)
    {
        val1 = atoi(token.value.pszBuf);
    }
    else
    {
        printf("首個操作數(shù)必須是整數(shù)\n");
        dyncstring_free(&token.value);
        return -1;
    }

    int oper = 0;
    if (get_next_token(&token) && (token.type == PLUS || token.type == MINUS))
    {
        oper = token.type;
    }
    else
    {
        printf("第二個字符必須是操作符, 當前只支持+/-\n");
        dyncstring_free(&token.value);
        return -1;
    }

    if (get_next_token(&token) && token.type == CINT)
    {
        val2 = atoi(token.value.pszBuf);
    }
    else
    {
        printf("操作符后需要跟一個整數(shù)\n");
        dyncstring_free(&token.value);
        return -1;
    }

    switch (oper)
    {
    case PLUS:
        {
            printf("%d+%d=%d\n", val1, val2, val1 + val2);
        }
        break;
    case MINUS:
        {
            printf("%d-%d=%d\n", val1, val2, val1 - val2);
        }
        break;
    default:
        printf("未知的操作!\n");
        break;
    }

    dyncstring_free(&token.value);
}

最后就是最終要的 get_next_token 函數(shù)了。這個函數(shù)最主要的修改就是添加了解析整數(shù)和跳過空格的功能

bool get_next_token(LPTOKEN pToken)
{
    char c = get_next_char();

    dyncstring_reset(&pToken->value);
    if (is_digit(c))
    {
        dyncstring_catch(&pToken->value, c);
        pToken->type = CINT;
        parser_number(&pToken->value);
    }
    else if (c == '+')
    {
        pToken->type = PLUS;
        dyncstring_catch(&pToken->value, '+');
    }
    else if (c == '-')
    {
        pToken->type = MINUS;
        dyncstring_catch(&pToken->value, '-');
    }
    else if(is_space(c))
    {
        skip_whitespace();
        return get_next_token(pToken);
    }
    else if ('\0' == c)
    {
        pToken->type = END_OF_FILE;
    }
    else
    {
        return false;
    }
    return true;
}

在這個函數(shù)中我們先獲取第一個字符,如果字符是整數(shù)則獲取后面的整數(shù)并直接拼接為一個完整的整數(shù)。如果是空格則跳過接下來的空格。這兩個是可能要處理多個字符所以這里使用了單獨的函數(shù)來處理。其余只處理單個字符可以直接返回。

parser_numberskip_whitespace 函數(shù)比較簡單,主要的過程是不斷從輸入中取出字符,如果是空格則直接將索引往后移動,如果是整數(shù)則像對應的整數(shù)字符串中將整數(shù)字符加入。

void skip_whitespace()
{
    char c = '\0';
    do
    {
        c = get_next_char();
    } while (is_space(c));

    // 遇到不是空白字符的,下次要取用它,這里需要重復取用上次取出的字符
    g_pPosition--;
}

void parser_number(LPDyncString dyncstr)
{
    char c = get_next_char();
    while(is_digit(c))
    {
        dyncstring_catch(dyncstr, c);
        c = get_next_char();
    }

    // 遇到不是數(shù)字的,下次要取用它,這里需要重復取用上次取出的字符
    g_pPosition--;
}

唯一需要注意的是,最后都有一個 g_pPosition-- 的操作。因為當我們發(fā)現(xiàn)下一個字符不符合條件的時候,它已經過了最后一個數(shù)字或者空格了,此時應該已經退回到get_next_token 函數(shù)中了,這個函數(shù)第一步就是獲取下一個字符,因此會產生字符串被跳過的現(xiàn)象。所以這里我們執(zhí)行 -- 退回到上一個位置,這樣再取下一個就不會有問題了。

最后為了能夠獲取空格的輸入,我們將之前的scanf 改成 gets。這樣就大功告成了。

我們來測試一下結果


1.png

最后的總結

最后來一個總結。本篇我們對上一次的加法計算器進行了簡單的改造,支持加減法、能跳過空格并且能夠計算多位整數(shù)。

在上一篇文章中,我們提到了Token,并且說過,像 get_next_token 這樣給字符串每個部分打上Token的過程就是詞法分析。get_next_token 這部分代碼可以被稱之為詞法分析器。這篇我們再來介紹一下其他的概念。

  1. 詞位(lexeme):
    詞位的中文解釋是語言詞匯的基本單位。例如漢語的詞位是漢字,英語的詞位是基本的英文字母。對于我們這個加法計算器來說基本的詞位就是數(shù)字以及 +\- 這兩個符號
  2. parsing(語法分析)和 parser(語法分析器)
    我們所編寫的expr函數(shù)主要工作流程是根據(jù)token來組織代碼行為。它的本質就是從Token流中識別出對應的結構,并將結構翻譯為具體的行為。例如這里找到的結構是 CINT oper CINT。并且將兩個int 按照 oper 指定的運算符進行算術運算。這個將Token流中識別出對應的結構的過程我們稱之為語法分析,完成語法分析的組件被稱之為語法分析器。expr 函數(shù)中即實現(xiàn)了語法分析的功能,也實現(xiàn)了解釋執(zhí)行的功能。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容