论坛原创
桌面计算器
不知道大家玩c++用什么书,但是我强烈推荐"死戳死踹(B.Stroustrup)"的c++ programming language,并且强烈推荐看英文原版如果你的英语还行,我 凭借我58-59分之间徘徊的英语水平看的很爽,熟练以后基本没有什么障碍了。看了以后才知道这本书是写得真好,国内的一些垃圾教材出的比如 让你说出p++...++q++++23++++4...*0&&45...的结果之类的题目使人一看就想吐,总感觉这些老师智障。B.Stroustrup毕竟是是c++的发明者,对c++的 理解可见,而且里面的例题真是很好,很能体现出c++的特点,又很实际。再次强烈推荐,如果你坚持看下去英文实际上没有太大的问题,我到 现在才体会到看原版的好处,比如可以与国外的人交流,搜索的时候直接用英文,内容肯定比中文的要详尽的多。
言归正传。看书看到Statement and Expression的时候,里面有一个贯穿全文的例子:桌面计算器。对于我这种初学者来说是很难理解的,但是这个 例子又是这么好,只有迎着头皮看下去,于是一个下午是么也不干,就研究它了,好不容易能理解8成,后来又逐渐的理解到位。这个150行左右 的小程序真是五脏俱全,概括了c语言的好多特点(只能说是c)。下面来分析一下,大家探讨,希望能跟深刻的理解它,也希望能帮我改一改我理解上的错误。
程序是一个计算器,有4个部分组成:1.语法分析器(Parser),2.输入函数(input function),3.符号表(symbol table)4.驱动(driver). 我是用的vc编译器,1,2,3用头文件,4用.cpp文件。 第一部分Parser分析器,parser.h.作用是分析用户输入的语句的语法。用户可以在控制台输入以下内容: program: END expr_list END expr_list: expression PRINT expression PRINT expr_list expression: expression+term expression-term term term: term*primary term/primary primary primary NUMBER NAME NAME = expression 注释:在vc中这中间好像必须有空格,不然不行,过一会再讨论这个 -primary (expression)
这里说明了语法,也可以说是给用户说明了使用规则,比如:你在控制台内输入 12*3 ;,计算器就会显示36,其中不管是什么符号(这里有12,*,3,;四个) 都要从primary开始进行分析(具体实现后面说),分析器分析12是NUMBER然后再取*号,向上到term,12附给了term,进行*发运算,primary又要从 输入继续取值,于是取了3,算出来了36以后再向上,这时候没有+,-,就直接把36给了term,这时候分析器遇到了;号后进行输出,然后继续等待输入的值,这样循环。 这是一种递归的方式。以下是Parser.h。 /******************************** Cal_Parser.h the Parser of the desk calculator recursive descend technique *********************************/ #include<map> double prim(bool); //声明 double term(bool); double expr(bool);
double expr(bool get){ //handle + and - operation double left=term(get); for(;;){ switch(curr_tok){ case PLUS: left+=term(true); //get a token to add break; case MINUS: left-=term(true); break; default: return left; } } }
double term(bool get){ //handle * and / operation double left=prim(get); for(;;){ switch(curr_tok){ case MUL: left*=prim(true); break; case DIV: if(double d=prim(true)){ left/=d; break; } return error("devided by 0"); default: return left; } } }
double prim(bool get){ //handle primaries if(get)get_token(); //get a token to decide which type and value it is switch(curr_tok){ //type which is gotten in the get_token() case NUMBER: { double v=number_value;//value gotten in get_token() get_token(); //get next return v; } case NAME: { double& v=table[string_value]; if(get_token()==ASSIGN)v=expr(true); //if the input is like "a=3" return v; } case MINUS: return -prim(true); //"-3" case LP: { double e=expr(true); if(curr_tok!=RP)return error("where is )?"); get_token(); return e; } default: return error("where is primary?"); } }
程序只有3个函数,第1,第2个分别是+ -和* /,第3个是因子,都要经过prim()去取值。
再来说地3个部分,符号表: 我写的整个程序有4个全局变量: Token_Value curr_tok=PRINT; //hold the input type,see enum:Token_Value
double number_value; //hold the value the last NUMBER read
string string_value; //hold the value the last NAME read
int no_of_error; //hold the number of error
第一个的curr_tok是当前的输入字的类型,Token_Value是枚举型:
enum Token_Value{ //input type. NAME, NUMBER, END='!', PLUS='+', MINUS='-', MUL='*', DIV='/', PRINT=';', ASSIGN='=', LP='(', RP=')' }; 表示的是输入字的类型,这里有11种类行,其中跟书上不一样的是END 我附了'!'的值,程序中输入!就退出。 第二个是number_value,意思是如果类型是NUMBER,它就是number的值。 第三个string_value,意思是如果类型是NAME,它就是name的制。 比如说我输入35,number_value就等于35,curr_tok=NUMBER.又比如我输入a=90,识别a的时候,curr_tok就为NAME,string_value就为"a". 第4个的no_of_error是错误的个数,用处相对来说不大。源代码,Cal_SymbolTable.h: /********************************** Cal_SymbolTable.h the symbol table and all the global variables of desk calculator ***********************************/ #include <string> #include <map> using namespace std;
enum Token_Value{ //input type. NAME, NUMBER, END='!', PLUS='+', MINUS='-', MUL='*', DIV='/', PRINT=';', ASSIGN='=', LP='(', RP=')' };
//four global variable Token_Value curr_tok=PRINT; //hold the input type,see enum:Token_Value
double number_value; //hold the value the last NUMBER read
string string_value; //hold the value the last NAME read
int no_of_error; //hold the number of error
//map map<string,double>table;
其中map是来存放初始化的一些固定常量如pi=3.1415926....等等,还有一个功能是存取NAME的值,比如你输入a=9 然后程序就会生成table["a"]=9,table是一个map。
第2部分输入函数,get_token()这一个函数,就是取用户输入的值并且判断值得类型,是NAME?还是一个NUMBER,并且把类型给全局变量curr_tok,把number的值给 number_value如果类型为NUMBER的话。原函数,Cal_Input.h: /********************** Cal_Input.h get the input from user ***********************/ #include<cctype> #include<iostream>
using namespace std;
Token_Value get_token(){ //get the value and type of value from input char ch=0; cin>>ch; switch(ch){ case 0: return curr_tok=END; case ';': //if input is operation case '*': case '/': case '+': case '-': case '(': case ')': case '=': case '!': return curr_tok=Token_Value(ch); case '0': case '1': case '2': case '3': case '4': //if number case '5': case '6': case '7': case '8': case '9': case '.': cin.putback(ch); cin>>number_value; //set value return curr_tok=NUMBER; //set type default: if(isalpha(ch)){ //'a'-'Z' cin.putback(ch); cin>>string_value; return curr_tok=NAME; } error("bad token"); return curr_tok=PRINT; } }
因为输入字是以char的形式输入的,所以没有办法输入两位以上或者是小数,所以程序遇到一个数字的char的时候putback,在用cin把这个值输入完整,NAME也同样是如此
第4个部分是驱动,也就是main函数。先看原文件:Cal_Driver.cpp
#include "Cal_SymbolTable.h" #include "Cal_error.h" #include "Cal_Input.h" #include "Cal_Parser.h" #include <iostream> using namespace std;
int main(){ table["pi"]=3.1415926525897; table["e"]=2.71828182845904; while(cin){ get_token(); if(curr_tok==END)break; if(curr_tok==PRINT)continue; cout<<expr(false)<<'\n'; } return no_of_error; }
先预设了两个map,用table["pi"]和table["e"],都知道是什么意思吧。。。 下面进行无止境的循环,除了用户输入“!”,也就是END的时候,先取数,再送到分析器分析。
这样这个不大的程序就完成了,虽然程序不大,但是可以看出作者把面向过程体现的淋漓尽致,并且也达到了讲statement和expression的目的,不得不佩服Stroustrup的 深厚功底。个人认为去研究这些经典著作的例子是非常有用的,再改一改。一定要把自己放在一个高度去理解程序,不要再过度的扣语法的细节,使自己陷入,语法的困境。 本人也是新手,目的在于交流,请讨论区高手见谅。另外大家可以多发表自己的意见,达到共同进步。
[此贴子已经被作者于2005-9-29 11:27:07编辑过]
----------------解决方案--------------------------------------------------------
???
强啊,看不懂?
----------------解决方案--------------------------------------------------------