《Programming: Principles and Practice Using C++》读书笔记(二)
文法分析代表了一个简单的解释器的实现,调试过程着实抓狂,膜拜下写编译器的大牛们 Orz
程序具有良好的可扩展性,放在Git上,可以Fork一个改改玩:https://github.com/leowww24/simple_cal
程序设计的过程:
分析:程序规模越大,需求分析愈重要
设计:系统总体结构图,各部分实现及其相互关系,重要的是考虑具体实现方式,如需要什么数据结构,用什么库函数等
实现:编码,调试,测试
设计:系统总体结构图,各部分实现及其相互关系,重要的是考虑具体实现方式,如需要什么数据结构,用什么库函数等
实现:编码,调试,测试
Bjarne Stroustrup的几点建议:
1、首先分析时间、技巧、工具是否足以解决问题,无法确保完成就先不要启动
2、宏观分析问题,从问题的一个简化版本入手,先实现一个程序原型,让他跑起来
2、宏观分析问题,从问题的一个简化版本入手,先实现一个程序原型,让他跑起来
反复测试原型程序,引出理解、思想或工具中存在的问题
只有经过充分思考和反复实验(设计、实现)才能深入理解要求解的问题
如果程序原型很难求解问题的某个方面,应该果断丢弃,从新设计
只有经过充分思考和反复实验(设计、实现)才能深入理解要求解的问题
如果程序原型很难求解问题的某个方面,应该果断丢弃,从新设计
3、将问题分解为多个模块
通过设计基本模块可以帮助理解各部分的功能与实现难点
思考各模块的实现方式,选择数据结构和库函数
思考各模块的实现方式,选择数据结构和库函数
4、寻找程序各模块通用的解决方案,设计通用类
5、通过main函数来组织模块,实现程序的主体
5、通过main函数来组织模块,实现程序的主体
困难之处:
在模块设计的初期,既要防止功能的蔓延导致的困难重重以至于无法下手,又要防止过度简化以至在设计中遗漏核心环节导致不断生产无用的程序原型,可能只有积累经验方能应对此困难吧
构建计算器程序
首先给出用例&伪代码
用例:
Expression:2+2*3
Result:8
Expression:2-2*3
Result:-4
Expression:5*(1.2+0.34e3)
Result:1706
Expression:23c90
Result:对异常输入给出错误提示
Result:8
Expression:2-2*3
Result:-4
Expression:5*(1.2+0.34e3)
Result:1706
Expression:23c90
Result:对异常输入给出错误提示
伪代码:
read_a_line
calculate
write_result
calculate
write_result
设计过程:
1、程序应包含输入、计算等模块,各模块之间如何交换信息?
通过设计数据交换类实现
2、输入异常交给哪个模块处理?
在输入模块处理:不合理的表达式形式无法预判,不可行
在计算模块处理:如何从异常中恢复?
在计算模块处理:如何从异常中恢复?
将异常处理放在while(cin)中,只输出异常信息,不会引起程序中断
3、如何处理读入的计算表达式?
可能涉及的设据类型:
浮点数:3.14或0.24e3
运算符:+ - * / %(注意操作数不能是浮点数)
括号:()
空格、换行等字符:使用标准的cin将忽略这些字符
运算符:+ - * / %(注意操作数不能是浮点数)
括号:()
空格、换行等字符:使用标准的cin将忽略这些字符
如何识别表达式的结尾?如何退出程序?
增加控制字符,如使用‘=’表示一个表达式结束,使用‘q’退出程序
分词(编译处理源代码的方式)
增加控制字符,如使用‘=’表示一个表达式结束,使用‘q’退出程序
分词(编译处理源代码的方式)
设计分词类来存储单词:
//存储操作数、运算符及控制字符的单词类
//对于运算符及控制字符value为空,规定数字的type用‘0’表示
class Token
{
public:
Token(char ch):type(ch){}
Token(char ch,double val):type(ch),value(val){}
char type;
double value;
};
设计函数getToken()从输入流获取一个单词
文法分析时发现数据交换类必须具有缓冲(可读又可写)功能,可将getToken()放在缓冲存储类中实现读的功能
4、如何处理计算模块中表达式的优先级?
文法分析(顺序读入单词,分析语义)
定义计算器的文法:
Expression:
Expression:
Term
Expression "+" Term
Expression "-" Term
Expression "+" Term
Expression "-" Term
Term:
Primary
Term "*" Primary
Term "/" Primary
Term "%" Primary
Term "*" Primary
Term "/" Primary
Term "%" Primary
Primary
Number
"(" Expression ")"
"(" Expression ")"
Number:
floating-point-literal
通过用例测试文法,寻找缺陷,同时熟悉文法的执行细节
文法设计的关注点:
文法设计的关注点:
- 如何区分文法规则和单词
- 如何排列文法规则(顺序)
- 如何表达可选模式(多选)
- 如何表达重复模式(重复)
- 如何识别文法的开始
5、保证文法的正确实现,需要设计输入流的缓冲存储类
对计算器问题,设计一个Token大小的缓冲区即可
//输入流与单词类的中间缓冲区类
//其get方法直接与从输入流获取单词,所有分词工作由其完成
//push_back()方法取STL的同名函数,将单词退回缓冲区
//标志符用于表示当前缓冲区状态
class Token_Stream
{
private:
Token buffer;
bool null;
public:
Token_Stream():buffer(0),null(true){}
void push_back(Token t);
Token get();
};
6、通过main函数组织模块
Token_Stream ts;
int main()
{
double val=0;
while (cin)
{
try
{
Token token=ts.get();
if (token.type=='q')
{
break;
}
else if (token.type=='=')
{
cout<<"Result:"<<val<<endl;
}
else
{
ts.push_back(token);
val=expression();
}
}
catch(exception &e)
{
cerr<<"Error:"<<e.what()<<endl;
}
catch(...)
{
cerr<<"Unkwon error."<<endl;
}
}
}
实现文法规则(通过函数直接实现,以Expression为例)
Expression:
Term()
Expression "+" Term
Expression "-" Term
Expression "+" Term
Expression "-" Term
//错误的实现(程序将逆序进行计算)
Token_Stream ts;
double expression()
{
double lef=term();
Token t=ts.get();
switch(t.kind)
{
case '+':
return expression()+lef;
case '-':
return expression-lef;
default:
return lef;
}
}
//修正版
//任何一个expression均以一个Term开始后跟随一个”+“或”-“,直到Term后面跟随的不是”+“或”-“时返回Term
double expression()
{
double val=term();
Token t=ts.get();
while(t.type=='+'||t.type=='-')
{
if(t.type=='+')
{
val+=term();
t=ts.get();
}
if(t.type=='-')
{
val-=term();
t=ts.get();
}
}
ts.push_back(t);
return val;
}
或
double expression()
{
double lef=term();
while (true)
{
Token token=ts.get();
switch (token.type)
{
case '+':
lef+=term();
break;
case '-':
lef-=term();
break;
default:
ts.push_back(token);
return lef;
}
}
}
函数调用关系图:
程序完整代码:
/*************************************************************************
> File Name: cal.cpp
> Title: 基于文法分析的简单计算器实现
************************************************************************/
#include<iostream>
#include<stdexcept>
#include<string>
using namespace std;
//异常包裹函数
void error(const string& s)
{
cerr<<s<<endl;
throw runtime_error(s);
}
//存储运算符和操作数的单词类
class Token
{
public:
Token(char ch):type(ch){}
Token(char ch,double val):type(ch),value(val){}
char type;
double value;
};
//输入流和单词类Token的媒介
//对输入流进行分词,根据输入类型返回一个运算符或运算数的单词
class Token_Stream
{
private:
Token buffer;
bool null;
public:
Token_Stream():buffer(0),null(true){}
void push_back(Token t);
Token get();
};
void Token_Stream::push_back(Token t)
{
if (!null)
{
error("Buffer not null,cannot push_back.");
}
buffer=t;
null=false;
}
Token Token_Stream::get()
{
if (!null)
{
null=true;
return buffer;
}
char ch;
cin>>ch;
switch (ch)
{
case 'q':case '=':
return Token(ch);
case '+':case '-':case '*':case '/':case '(':case ')':
return Token(ch);
case '0':case '1':case '2':case '3':case '4':
case '5':case '6':case '7':case '8':case '9':
cin.putback(ch);
double val;
cin>>val;
return Token('0',val);
default:
return Token(ch);
}
}
//全局对象
Token_Stream ts;
//文法实现
double term();
double primary();
double expression()
{
double val=term();
Token t=ts.get();
while(t.type=='+'||t.type=='-')
{
if(t.type=='+')
{
val+=term();
t=ts.get();
}
if(t.type=='-')
{
val-=term();
t=ts.get();
}
}
ts.push_back(t);
return val;
}
double term()
{
double val=primary();
while (true)
{
Token token=ts.get();
switch (token.type)
{
case '*':
val*=primary();
break;
case '/':
val/=primary();
break;
default:
ts.push_back(token);
return val;
}
}
}
double primary()
{
double val;
Token token=ts.get();
switch (token.type)
{
case '0':
val=token.value;
break;
case '(':
val=expression();
token=ts.get();
if (token.type!=')')
{
error("Missing ).");
}
break;
default:
char invalid=token.type;
string wrong_msg=" is not defined.";
error(invalid+wrong_msg);
}
return val;
}
//主函数
int main()
{
double val=0;
cout<<"WELCOME TO LEO'S SIMPLE CALCULATOR!"<<endl
<<"Please enter expressions using float-point numbers."<<endl
<<"Enter '=' to show the result."<<endl
<<"Enter 'q' to quit the program."<<endl
<<"**************************************"<<endl;
while (cin)
{
try
{
Token token=ts.get();
if (token.type=='q')
{
break;
}
else if (token.type=='=')
{
cout<<"Result:"<<val<<endl;
}
else
{
ts.push_back(token);
val=expression();
}
}
catch(exception &e)
{
cerr<<"Error:"<<e.what()<<endl;
}
catch(...)
{
cerr<<"Unkwon error."<<endl;
}
}
}