浅谈一个布尔参数的作用

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的原版程序,但我现在的看法就不同了。我不认为那个布尔参数是程序构架中必然要用到的东西,它只是为了解决一个不大不小的问题所用的技巧而已。
纯属个人观点,如果谁有异议,一定要告诉我!
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值