Bjarne Stroustrup的《The C++ Programming Language》果然与众不同。在讲表达式和语句的时候,先举了一个桌面计算器的例子——其中涉及到编译的一些常识。我没有学过编译原理,所以其中的语法分析搞得我一头雾水。好在经过柯老师讲解,对“递归下降”有了那么一点感觉。但对那个例子本身还是有一点疑问。
这是个很简单的计算器,只有四则运算,尽管有声明变量之类的功能,程序还是很短的。然而,我却一直搞不清楚BS为什么要在他的语法分析函数expr(), term(), prim(), 里面都设置一个布尔参数。按BS的原话,这是为了“指明是否需要调用get_token()去取得下一个单词”。这么说是很抽象的。只好通过分析源代码,来看一看这个bool参数到底起了什么作用。
//LiuKai @ HUST
//2006-7-20
/***************************************************************************************
dc1.cpp:桌面计算器版本1,来自《TC++PL》第六章的源程序,未作改动,仅仅使其可以运行
使用全局变量过多
***************************************************************************************/
#pragma warning(disable:4786)
#include <iostream>
#include <string>
#include <map>
using namespace std;
enum Token_value
{
NAME, NUMBER, END,
PLUS='+', MINUS='-', MUL='*', DIV='/',
PRINT=';', ASSIGN='=', LP='(', RP=')'
};
Token_value curr_tok = PRINT;
map<string,double> table;
double expr( bool );
double term( bool );
double prim( bool );
Token_value get_token( void );
double error( const string& );
/
double expr( bool get )
{
double left = term( get );
for(;;)
{
switch( curr_tok )
{
case PLUS:
left += term( true );
break;
case MINUS:
left -= term( true );
break;
default:
return left;
}
}
}
double term( bool get )
{
double left = prim( get );
for(;;)
{
switch( curr_tok )
{
case MUL:
left *= prim( true );
break;
case DIV:
if( double d = prim(true) )
{
left /= d;
break;
}
else
return error("divide by 0");
default:
return left;
}
}
}
double number_value;
string string_value;
double prim( bool get )
{
if(get) get_token();
switch( curr_tok )
{
case NUMBER:
{
double v = number_value;
get_token();
return v;
}
case NAME:
{
double &v = table[string_value];
if( get_token()==ASSIGN )
v = expr(true);
return v;
}
case MINUS:
{
return -prim( true );
}
case LP:
{
double e = expr( true );
if( curr_tok != RP )
return error( ")expected" );
get_token();
return e;
}
default:
return error( "primary expected" );
}
}
Token_value get_token()
{
char ch = 0;
cin >> ch;
switch( ch )
{
case 0:
return curr_tok = END;
case ';':
case '*':
case '/':
case '+':
case '-':
case '(':
case ')':
case '=':
return curr_tok=Token_value(ch);
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
case '.':
cin.putback(ch);
cin >> number_value;
return curr_tok = NUMBER;
default:
if( isalpha(ch) )
{
cin.putback(ch);
cin>>string_value;
return curr_tok = NAME;
}
error("bad token");
return curr_tok = PRINT;
}
}
int no_of_errors;
double error( const string &s )
{
no_of_errors++;
cerr << "error:" << s << '/n';
return 1;
}
int main()
{
table["pi"] = 3.1415926535897932385;
table["e"] = 2.7182818284590452354;
while( cin )
{
get_token();
if( curr_tok == END ) break;
if( curr_tok == PRINT ) continue;
cout << expr(false) << '/n';
}
return no_of_errors;
}
从控制流的角度来说,布尔量的作用一般是改变流程。对于本例,简单地说,这个传进去的布尔量对expr()、term()是没有任何控制作用的,因为它们一上来就只是简单地把bool get这个参数传给了循环递归的下一层函数( expr() 里面第一句就是double left = term(get), 而 term()函数也类似 ),这两个函数中没有任何关于get的判断机制。这个布尔量真正起作用的地方是在prim()函数里面。而这个函数里也只有一句和get的值有关,那就是第一条语句:
if(get) get_token();
这就很明显了,get这个布尔参数层层传递下来,它的唯一作用就是判断在执行prim()里的switch之前要不要先取一个单词出来。
按照“正常”的情况,这个单词是要取的——取了这个单词才能进行switch,细心观察不难发现,对于expr()、term()、prim()这三个函数来说,除了“最初”传进expr()的那个布尔量有可能为false之外,它们之间互相调用时,都直接传进了true( 和最初传给expr()的布尔量的值无关 )。这就是说,一旦第一次调用prim(),对bool get进行过判断以后,程序的“流程”就完全不再受那个“最初由main()传进”的布尔量的控制。这时我就可以大胆的给出一个结论——给expr()传进去一个false的唯一作用就是把prim()函数里的第一条get_token()语句“屏蔽”一次——在每个表达式的求值过程里仅仅一次。
现在把注意力集中到main函数上来,我们只看循环体部分。首先执行的是get_token()这个动作。有一点需要说明的是,get_token()虽然又返回值,但它的结果却也全部存到了全局变量里面,因此,在main()函数里调用它和在prim()函数里调用是完全等价的。然后对第一次token的结果做两个判断,如果是分号就在循环一次,如果是END就跳出来。紧接着,调用了一个expr(false),由前面的分析——这个函数第一次调用prim()时,不执行其中的get_token()函数,这也正是作者的意图,因为前面的main函数部分已经token过一次了,而以后的过程就“步入正轨”了。这样做的好处就是使单个的分号成为一条合法的语句。(这句话的意思后面还有解释)
我不喜欢这么“兴师动众”地搞一个布尔参数出来就为了那么一点点目的。不考虑单个分号问题的话,我们当然可以把cout << expr(false) << '/n';这一句放到循环体的最前面,去掉那个单独调用的get_token(),然后把expr(false)改为expr(true)。这么一来,程序中所有使用bool量的地方都是在使用true——没有false的情况了。那么,bool就是完全多余的了,可以改为void。下面是我修改过的代码:
//LiuKai @ HUST
//2006-7-20
/***************************************************************************************
dc2.cpp:桌面计算器版本2,来自《TC++PL》第六章的源程序,去掉bool
使用全局变量过多
***************************************************************************************/
#pragma warning(disable:4786)
#include <iostream>
#include <string>
#include <map>
using namespace std;
enum Token_value
{
NAME, NUMBER, END,
PLUS='+', MINUS='-', MUL='*', DIV='/',
PRINT=';', ASSIGN='=', LP='(', RP=')'
};
Token_value curr_tok = PRINT;
map<string,double> table;
double expr( void );
double term( void );
double prim( void );
Token_value get_token( void );
double error( const string& );
/
double expr( void )
{
double left = term();
for(;;)
{
switch( curr_tok )
{
case PLUS:
left += term();
break;
case MINUS:
left -= term();
break;
default:
return left;
}
}
}
double term(void)
{
double left = prim();
for(;;)
{
switch( curr_tok )
{
case MUL:
left *= prim();
break;
case DIV:
if( double d = prim() )
{
left /= d;
break;
}
else
return error("divide by 0");
default:
return left;
}
}
}
double number_value;
string string_value;
double prim( void )
{
get_token();
switch( curr_tok )
{
case NUMBER:
{
double v = number_value;
get_token();
return v;
}
case NAME:
{
double &v = table[string_value];
if( get_token()==ASSIGN )
v = expr();
return v;
}
case MINUS:
{
return -prim();
}
case LP:
{
double e = expr();
if( curr_tok != RP )
return error( ")expected" );
get_token();
return e;
}
default:
return error( "primary expected" );
}
}
Token_value get_token()
{
char ch = 0;
cin >> ch;
switch( ch )
{
case 0:
return curr_tok = END;
case ';':
case '*':
case '/':
case '+':
case '-':
case '(':
case ')':
case '=':
return curr_tok=Token_value(ch);
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
case '.':
cin.putback(ch);
cin >> number_value;
return curr_tok = NUMBER;
default:
if( isalpha(ch) )
{
cin.putback(ch);
cin>>string_value;
return curr_tok = NAME;
}
error("bad token");
return curr_tok = PRINT;
}
}
int no_of_errors;
double error( const string &s )
{
no_of_errors++;
cerr << "error:" << s << '/n';
return 1;
}
int main()
{
table["pi"] = 3.1415926535897932385;
table["e"] = 2.7182818284590452354;
while( cin )
{
cout << expr() << '/n';
if( curr_tok == END ) break;
if( curr_tok == PRINT ) continue;
}
return no_of_errors;
}
看,程序并没有经过很大改动。而且绝大多数情况下,运行结果、处理错误等都和原来的程序没有任何区别。惟一的差别就发生在输入单个分号的时候。BS的原版程序在得到一个单分号输入时,不作任何输出。而我修改过的程序会显示一条错误信息——primary expected。这也在我的意料之中,因为我并没有像原版程序那样,先检测第一个token,确定它不是分号或者结束符以后再调用expr(),我把任何输入都“一视同仁”,如果第一个token的结果是分号的话,它将得不到main函数中的特殊处理而是被当作了一个prim(),但prim()又不“认识”分号,这样的话就会报告一个“错误”。
这对一般使用没有太大的影响——只是偏离了原先规定的计算器的语法。为了在我的程序基础上改进这个“小”问题,我试了好几种方法,最后竟然还是觉得用BS的方法比较好。尽管又回到了BS的原版程序,但我现在的看法就不同了。我不认为那个布尔参数是程序构架中必然要用到的东西,它只是为了解决一个不大不小的问题所用的技巧而已。
纯属个人观点,如果谁有异议,一定要告诉我!