调试
使用 visual studio 2017(免费工具)
https://www.visualstudio.com/zh-hans/downloads/
设置断点
https://msdn.microsoft.com/zh-cn/library/5557y8b4.aspx
-
设置断点的时候
-
调试的过程中
我使用的用例
[用例1]
实际输入 1-2-3;
预期输出 =-4
期待的BUG =2
BUG产生的原因 1- (2-3); 错误的优先级
[用例2]
1+2*3;
=7
想看一下 乘法 * 的 优先级 是否高于 加法+
[用例3]
1.1 *(2+3);
=5.5
想看一下 括号 是否改变了优先级
具体的数据如何在代码之间流淌(函数先后调用关系)
- 我一开始最困惑的就是一点“计算到底是怎么开始的?”以
1-2-3;
作为实际的用例,这里期待的正确输出应该是满足从左到右运算优先顺序的= -4
; - 在这个具体的例子里面,函数的调用关系和语句的执行顺序大概是下面这样的
1-2-3;
调用函数 main()
执行
Token t = ts.get();
调用函数 Token_stream::get()
得到
t = |'8'| 1 |
回到函数 main()
执行
ts.putback(t);
状态
buffer = t;
full = true;
执行
val = expression();
调用函数expression()
执行
double left = term();
调用函数 term()
执行
double left = primary();
调用函数 primary()
执行
Token t = ts.get();
由于这时候 buffer 是满的,
所以直接会从 buffer 里面取出存着数字 1 的Token对象,
故而返回double类型 1
并且依次返回给
term()
expression()
回到 expression()
执行
Token t = ts.get();
计算由此就彻底开始了
自己实现过程中的BUG总结
primary() 只处理一次
Primary:
Number
"(" Expression ")"
- 我本来以为primary()也是放在
while(true){}
里面 - 这说明这时候要面对的已经是3.14 或者 (3.14)这样的整体了,只需要处理一次;
expression() 要面对1+2+3+4+5+6;
所以需要while(true){}
Expression:
Term
Expression "+" Term
Expression "–" Term
term() 要面对1*2*3*4*5*6;
所以也需要while(true){}
Term:
Primary
Term "*" Primary
Term "/" Primary
Term "%" Primary
- 无论是3.14还是1 2 3 4 5 6 都是表示一个右值;
右值是一个可以被放到赋值号 = 右边的值;
所有的switch语句我都没有加break
- 一是不熟悉switch的语法
- 二是完全把switch当成if语句来用了
缓存区使用之后要设置标识符为true
void Token_stream::putback(Token t) {
buffer = t;
isfull = true;
}
1 + 2 * 3; 的运算优先级到底是如何保证的?
- main() 使用ts.get()率先获得了存着 数值1 的Token;
- 计算的入口是expression(), 该函数处理+ - 运算,意味着 + 号 会作为本层(这个函数)中的switch语句的判断条件在函数调用进入下一层之前就被使用掉,造成的结果就是当函数返回时要做一个left+=term()的计算;
- 从term() 进入下一层计算term 也有自己的left,这时候term得到了 数值2,紧接着,term并不急着返回数值2,而是看看数值2之后是否跟着term可以处理的运算,也就是* /运算,term利用自己的ts.get语句发现2之后有一个乘法,于是开始进入自己的循环,着手*乘法的处理,课件对于标题里的表达式来说,乘法的入口是在 数值2 :
- 有一些使用栈的表达式计算算法(可以参考我之前的博客 点这里),面对标题里的表达式,会先处理成
1 2 3 + *
的后缀形式,然后不断把元素和运算符弹出弹栈来计算,这种实现乘法的入口其实是在 数值3;
- 有一些使用栈的表达式计算算法(可以参考我之前的博客 点这里),面对标题里的表达式,会先处理成
- 回到term这里,在处理好2*3并且成功得到6之后,term继续读取Token,马上遇到了标识运算结束的符号,分号,于是利用ts.putback()将分号放回到全局变量ts之中,并且返回了运算结果6;
- 递归调用返回到expression,会得到 left+= 6,最后算出正确答案,并返回;
- 对于上面这个看似复杂的过程,其实可以总结成, 加减号下不去,乘除号回不来;
使用文法设计了乘除优先级 > 加减优先级的运算规则,在实际代码编写过程中,将expression作为计算的入口保证了在函数递归调用返回的时候,运算会先遇到乘除,最后遇到加减;
1+2-3;揭示了term() ts.putback()的双重作用
default:
ts.putback(t); // put t back into the token stream
return left;
}
- putback 语句有一个显而易见的作用,就是在读到分号 ; 的时候,表示自己无法处理,返回给全局变量,用以最终结束计算;
- 然而,term函数中的putback有第二个重要作用,那就是putback(+) 以及 putback(-),本质就是putback(除了* / 以外全都不能处理的符号) ;
- 事实上,与上方的例子相同,数值2同样是被term获取的,并且term也获取到了2之后的运算符,一个负号,这时候就出现了与上方的用例相反的情况,term是无法处理负号的,而我们都知道expression才可以处理负号,因此需要“把负号交还给expression”,但是term又如何知道谁是expression以及怎么返回,term只是一个函数而已,函数自己怎么能知道谁调用了自己;
- 幸运的是,程序知道,程序知道谁调用了谁,栈知道怎么返回,栈知道这时候返回的东西会交给expression的,因此当term执行了return left之后,term拿到的数值2就被返回给expression了,那么负号怎么办,这可是接下来重要的运算符啊!
- 搞了一个大动作, ts.putback(t); 还给了全局变量 ts,不用怀疑,这个负号一定会保存在ts的buffer里面去;
- 很好,这时候烫手山芋数值2 回到了expression,但是数值2 首先要面对是自己作为term的返回值参与的left+=term,也就是left += 2,这时候expression里面的left 是1,所以会先进行1+2的运算,left变成3;
- 至于保存在ts 缓冲区buffer里面的那个负号,可别忘了+= 运算之后的, t = ts.get();
while(true) {
switch(t.kind) {
case '+':
left += term(); // 计算一个 Term 并且 相加
t = ts.get();
break;
........
}
- 这时候, t = ts.get();本质就是从缓冲区里面拿到的负号,并成功进入while的循环,下一个要计算的是left -= term了;
- 于是,我们继续跟踪数据,实际上, term 再一次拿到了数值3,但是这次term又拿到了之后的分号,也是只能把数值3老老实实地返回给expression,把分号还给全局的ts;
- 这时候,终于好了,3 -= 3,也就是3 - 3 嘛,答案是0,又遇到了分号,输出答案;
term 里面的 ts.putback(t); 本质上putback的就是term自己无法处理的任意字符,更具体地说是,优先级低于自己本层的运算符以及结束运算符(这里不考虑无效输入),同时,expression的入口身份,意味着expression里面的 ts.putback(t);本质上只能putback结束符,因为没有比它优先级更低的运算符了。
两个用例的区别与联系
- 全局变量 ts 的缓冲区,仅在main函数执行后,存储过一次 数值类型的Token;
- 计算从入口expression 开始之初就从缓冲区取回了数值,此后的计算,缓冲区要么存着某个函数无法处理的运算符、要么存着标识计算结束的分号;
- 计算的过程中,数值 本质上并不以Token的形式传递,而是以函数的return与调用传递,即以自己的本身的double类型值进行传递;
基于文法的简单算术表达式计算器 系列索引
-
[C++][第0篇] 系列索引 基于文法的算术表达式解释器
关键词 系列索引