在Bjarne Stroustrup写作的C++程序设计的基本功能部分,作者主要实现了一个桌面计算器的例子。可以认为这是作者精挑细选的一个例程,作者用这个例子演绎了基本的表达式(我们可以把这个部分内容类比为人类语言的语句,学说学写一句话);函数(我们可以把这部分内容类比为人类语言的功能段落,一段话有一个中心思想,表达一个或几个观点意思);名字空间和异常(对异常的处理体现了一个程序员的成熟程度和软件的鲁棒性,名字空间有点类似人类语言段落的标题或文章标题);源文件和程序(源文件结构或文件组织类似于人类语言的文章大纲和结构或者谋篇布局)。
写作C++语言基本功能部分有很多方面可以写,这里钱能的C++程序设计教程从C++性能的角度给出了C++的基本功能;Stanley B Lippman的C++Primer着重于函数的角度描写了程序执行的机制;而作为C++语言的发明者,却是从编译器的角度选取了桌面计算器的例子作为C++基本功能的切入点。Bjarne Stroustrup无疑关注的角度更加接近底层,同时他还关心了程序语言的歧义性等多个比较吊诡或难以理解的地方,这些都是初学C++者想不到的地方,但是无疑C++的初学者如果能够运行一下Bjarne Stroustrup的案例,这将大大地开阔C++初学者的眼界,同时可以使得C++的初学者一开始就接受了正规的训练,少走了很多弯路。其实对于C++的初学者,作者Bjarne Stroustrup一直在引导读者思考为什么这里这么写而不是那么写,所以带着思考,带着锤炼语言的目的来学习Bjarne Stroustrup的C++程序设计语言自然是最有效的道路。这里,C++初学者不为提升C++编程效率,这有算法和其他工具帮忙;C++初学者不为深入理解C++的机理,诚然理解了机理会更深入地理解计算机的运行;C++的初学者在这里只为写出好的C++程序,这需要着力于模仿和实践,我也深深认同这才是学习语言最最重要的。固然我若是懂得了很多语言的机理对于我理解别人的程序很有帮助,然而若是让我自己来写一段语言,确实还相差了很多。
对于实现一个桌面计算器,从软件开发的角度标准化开发流程可以把该过程分解为三个阶段:
一、分析:定义需要解决的问题的范围。这里的问题为实现加、减、乘、除四种浮点数的中缀运算符的标准算术运算。例如给定输入r=2.5和area=pi*r*r(pi预先有定义),计算器程序将输出2.5和19.635。
二、设计:建立系统的整体结构。 这里建立系统的整体结构为四个模块:分析器(实现语法分析功能)、输入函数(实现输入和词法分析功能)、符号表(保存持久性信息)和驱动程序(处理初始化、输出和错误处理功能)。这有点类似一个最小的编译器。这里对于编译原理不熟悉的读者会有一些障碍,所以我从Alfred,V.A.的编译原理一书中给出关于编译器的一个基本模型,扩展相关的知识。 先给出编译器的流程图如下:
图1 编译器编译流程图
这里还是以area=pi*r*r这条语句为例,词法分析步骤读入源程序的字符流,并将它们组织成为有意义的词素(lexeme)的序列。词法分析器产生形如<token-name,attribute-value>的词法单元作为输出。这里area是一个词素,对应的词法单元为<id,1>,其中id是表示标识符的抽象符号,而1指向符号表中area对应的条目。一个标识符对应的符号表条目存放该标识符有关的信息,比如它的名字和类型。赋值符号=是一个词素,对应的词法单位为<=>。因为这个词法单元不需要属性值,所以这里省略了第二个分量。pi是一个常数,也是一个词素,对应的词法单元为<3.1415926535897932385>。*是一个词素,被影射成词法单元<*>。r是一个词素,被映射为词法单元<id,2>,其中2指向r对应的符号表条目。经过词法分析后area=pi*r*r被表示为如下的词法单元序列:<id,1><=><3.1415926535897932385><*><id,2><*><id,2>。
编译器的第2个步骤称为语法分析或解析(parsing)。语法分析器使用由词法分析器生成的各个词法单元的第一个分量来创建树形的中间表示。如下图所示:
这里需要着重谈一下符号表管理。编译器的重要功能之一就是记录源程序中使用的变量的名字,并收集和每个名字的各种属性有关的信息。这些属性可以提供一个名字的存储分配、它的类型、作用域等信息。符号表数据结构为每个变量名字创建一个记录条目。记录的字段就是名字的各个属性。这个数据结构应该允许编译器迅速查找到每个名字的记录,并向记录中快速存放和获取记录中的数据。
三、实现:写出代码并完成测试。这里最终将给出桌面计算器的完整实现并测试其完整性。
// The desk calculator
// includes character-level input (sec6.1.3), but
// no command line input (sec6.1.7),
// no namespaces, and
// no exceptions
// pp 107-117, sec 6.1, A Desk calculator
// uses += rather than push_back() for string
// to work around standard library bug
// No guarantees offered. Constructive comments to bs@research.att.com
/*
program:
END // END is end-of-input
expr_list END
expr_list:
expression PRINT // PRINT is semicolon
expression PRINT expr_list
expression:
expression + term
expression - term
term
term:
term / primary
term * primary
primary
primary:
NUMBER
NAME
NAME = expression
- primary
( expression )
*/
#include<iostream>
#include<string>
#include<map>
#include<cctype>
using namespace std;
int no_of_errors; // note: default initialized to 0
double error(const string& s)
//double error(const char* s)
{
no_of_errors++;
cerr << "error: " << s << '\n';
return 1;
}
enum Token_value {
NAME, NUMBER, END,
PLUS='+', MINUS='-', MUL='*', DIV='/',
PRINT=';', ASSIGN='=', LP='(', RP=')'
};
Token_value curr_tok = PRINT;
double number_value;
string string_value;
/* The simplest token reader
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: // NAME, NAME =, or error
if (isalpha(ch)) {
cin.putback(ch);
cin>>string_value;
return curr_tok=NAME;
}
error("bad token");
return curr_tok=PRINT;
}
}
*/
Token_value get_token()
{
char ch;
do { // skip whitespace except '\en'
if(!cin.get(ch)) return curr_tok = END;
} while (ch!='\n' && isspace(ch));
switch (ch) {
case ';':
case '\n':
return curr_tok=PRINT;
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: // NAME, NAME=, or error
if (isalpha(ch)) {
string_value = ch;
while (cin.get(ch) && isalnum(ch))
string_value += ch; // string_value.push_back(ch);
// to work around library bug
cin.putback(ch);
return curr_tok=NAME;
}
error("bad token");
return curr_tok=PRINT;
}
}
map<string,double> table;
double expr(bool); // cannot do without
double prim(bool get) // handle primaries
{
if (get) get_token();
switch (curr_tok) {
case NUMBER: // floating-point constant
{ 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: // unary minus
return -prim(true);
case LP:
{ double e = expr(true);
if (curr_tok != RP) return error(") expected");
get_token(); // eat ')'
return e;
}
default:
return error("primary expected");
}
}
double term(bool get) // multiply and divide
{
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("divide by 0");
default:
return left;
}
}
double expr(bool get) // add and subtract
{
double left = term(get);
for (;;) // ``forever''
switch (curr_tok) {
case PLUS:
left += term(true);
break;
case MINUS:
left -= term(true);
break;
default:
return left;
}
}
int main()
{
table["pi"] = 3.1415926535897932385; // insert predefined names
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;
}
程序运行结果如下所示:
r=2.5
2.5
area=pi*r*r
19.635