在上一篇我們添加了對乘除法的支持,也介紹了BNF范式,并且針對當前的算術表達式寫出了對應的范式,同時根據范式給出相應的代碼實現。這篇我們將繼續為算數表達式添加對括號的支持。
對應的BNF 范式
在上一篇我們給出了乘除法對應的范式
<expr>::=<term>{(PLUS|MINUS)<term>}
<term>::=<factor>{(DIV|MUL)<factor>}
<factor>::={(0|1|2|3|4|5|6|7|8|9)}
針對乘除法的優先級比加減法高,我們的做法是將乘除法單獨作為一個部分,然后在最外層表達式中只處理加減法。基于這種思路,我們來看如何處理括號的問題。例如下面的算數表達式
((1+2)*3+4) - (5 - 6 / 3)
這里我們直接給出對應的文法,然后再來分析一下該如何由這個文法得到對應的表達式
<expr>::=<term>{(PLUS|MINUS)<term>}
<term>::=<factor>{(DIV|MUL)<factor>}
<factor>::=({(0|1|2|3|4|5|6|7|8|9|)})|LPAREN<expr>RPAREN
- 首先根據表達式,它應該由兩個term來組成
expr = term - term
- 接著看看兩個term,它們并不是單純的加法運算,所以兩個term應該只有單純的一個factor,也就是
expr = factor - factor
- 因為最外層都有括號,所以再次展開
expr = (expr1) - (expr2)
- 這時就又到了分析expr的過程了,左側的expr最外層是一個加法,所以這里可以得到
expr1 = term + term
- 右側的expr 最外層是一個減法,也就是
expr2 = term - term
- 結合最外層的表達式可以得到
expr = (term1 + term2) - (term3 - term4)
- term1 部分有一個乘法,所以它可以解析為
term1 = factor * factor
- term2 部分就是單獨的數字所以可以得到
term2 = factor
,并且進一步得到term2=4
- term3 部分就是單純的數字,可以得到
term3 = factor
,并且進一步得到term3=5
- term4 部分有一個除法,所以它可以解析為
term3 = factor / factor
- 此時整個表達式可以表示為
expr = (factor1 * factor2 + 4) - (5 - factor3 / factor4)
- factor1 本身也是一個括號,加表達式,所以它可以表示為
factor1 = (expr)
- factor2 是一個數字,所以它表示為
factor2 = 3
- factor3 是一個數字,所以它表示為
factor3 = 6
- factor4 是一個數字,所以它表示為
factor4 = 3
- 此時表達式可以是
expr = ((expr1) * 3 + 4) - (5 - 6 / 3)
- 此時再次分析這個 expr1 可以得到
expr1 = 1+2
- 這個時候,整個表達式就出來了
expr = ((1+2) * 3 + 4) - (5 - 6 / 3)
用圖來表示大概可以表示如下
代碼實現
有了范式,我們就可以按照范式來組織代碼實現。
首先我們先在 ETokenType
中添加針對括號的標簽
typedef enum e_TokenType
{
CINT = 0, //整數
PLUS, //加法
MINUS, //減法
DIV, //乘法
MUL, //除法
LPAREN, //左括號
RPAREN, //右括號
END_OF_FILE // 字符串末尾結束符號
}ETokenType;
然后在 get_next_token
函數中添加對括號進行詞法分析并打標簽的功能
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(is_space(c))
{
skip_whitespace();
return get_next_token(pToken);
}
else
{
switch (c) {
case '+':
pToken->type = PLUS;
break;
case '-':
pToken->type = MINUS;
break;
case '*':
pToken->type = DIV;
break;
case '/':
pToken->type = MUL;
break;
case '(':
pToken->type = LPAREN;
break;
case ')':
pToken->type = RPAREN;
break;
case '\0':
pToken->type = END_OF_FILE;
break;
default:
return false;
}
}
return true;
}
這里我對這個函數進行了一些改寫,針對依靠單個字符就能打上標簽的采用switc
來進行處理,像空白字符、數字這種有多種字符類型的就采用普通的if
處理。
然后在get_oper
中添加對括號的識別
if (get_next_token(&token) && (token.type == PLUS || token.type == MINUS || token.type == DIV || token.type == MUL || token.type == LPAREN || token.type == RPAREN))
{
oper = token.type;
if (pRet)
*pRet = true;
}
然后根據文法,get_factor
需要能夠返回一個 expr
的結果,所以這里需要添加以下代碼
if (token.type == LPAREN)
{
bool bValid = true;
value = expr(&bValid);
if (!bValid)
*pRet = false;
if (get_next_token(&token) && token.type == RPAREN)
*pRet = true;
else
*pRet = false;
}
如果我們得到的標簽不為括號則按照原來的處理方式來處理,如果是括號,則將括號中的內容作為表達式并計算表達式的值,作為整數來返回。之前的expr
函數我們僅僅將結果打印并返回是否解析成功,這里需要做一些改進。我們使用一個傳出參數來返回解析是否成功,而將計算結果作為值進行返回。
另外需要特別注意的是,我們將反括號的判斷放到了 get_factor
函數中,所以在 get_term
和 expr
中,遇到反括號應該考慮對位置索引進行遞減,并且遇到反括號應該認為到達末尾并推出。這里的代碼就不貼出來了。有興趣的小伙伴可以看github上上傳的代碼。地址