面向对象的表达式计算器
一、面向对象的表达式计算器的功能
1.能够计算普通表达式
2.支持变量
3.支持函数
4.支持保存变量和函数
二、巴科斯范式(BNF)
1.在双引号中的字("word")代表着这些字符本身。而double_quote用来代表双引号。
2.在双引号外的字(有可能有下划线)代表着语法部分。
3.尖括号( < > )内包含的为必选项。
4.方括号( [ ] )内包含的为可选项。
5.大括号( { } )内包含的为可重复0至无数次的项。
6.竖线( | )表示在其左右两边任选一项,相当于"OR"的意思。
7. ::= 是“被定义为”的意思。
表达式的BNF表示
一个表达式分解为:表达式(Expr)、项 (Term)、因子(Factor)
Expr ::= Term{("+"| "-") Term }
表达式=项+项-项............
Expr ::= Term "=" Expr
表达式=项
Term ::= Factor {("*" | "/") Factor }
项=因子*因子/因子.......
Factor ::=
1.Number 数字
2.Identifier 类别
2.1.Function 函数
2.2.Variable 变量
3."-"Factor 因子
4."("Expr ")" 表达式
可以看表达式的表示是一个递归的过程,这种解析表达式的方法称之为:逆向递归法
表达式的解析
目的:解析表达式,生成表达式树
例子
1 + x +3 *(sin(20)+7)*4
表达式的解析过程
解析后的语法树如下图
三、基于对象编程与面向对象编程
我们的最终目的是用C++设计一个面向对象的表达式计算器,所以非常有必要弄清楚,什么是基于对象编程和面向对象的编程。而要弄清楚这一点,又要先弄明白什么是值语义,什么是对象语义。
值语义:对象的拷贝与原对象无关,拷贝后与原对象脱离关系,互不影响。这种拷贝叫深拷贝。拷贝之后脱离关系,只要在拷贝的时候都为对象分配内存空间就行了。某种些情况下算是一种对资源的浪费。
值语义例子
}
对象语义:1.对象拷贝是禁止的
2.对象拷贝允许,对象与被复制的对象之间共享资源
拷贝后,对象与被拷贝对象之间有有联系,这只需要拷贝的过程中,只是拷贝指针或引用, 这样的话,对象与被拷贝对象都“指向”同一资源。这种拷贝叫浅拷贝。当然这对资源的释放造成了一定的困难,不过有RAII技术来解决这个问题。
Tips:
1.C++编译器默认提供的拷贝构造函数就是对象语义的。
2.JAVA是纯面向对象的语言,因此都是对象语义,这点是与C++的其中一个不同点
对象语义例子
class Test
{
private:
int * pNum_;
public:
Test(int num)
{ pNum_ = new int(num); };
Test(const Test& src)
{ pNum_ = src.pNum_; //都指向同一块内存区,存在联系, }
int * GetpNum()const { return pNum_; }
};
int main(void)
{
Test t1(3); Test t2(t1); //拷贝过后,t2与t1存在联系
std::cout << hex << t1.GetpNum() << std::endl;
std::cout<< hex << t2.GetpNum() << std::endl;
return 0;
}
四、计算表达式
我们的最终目的是计算出表达式中的值,因此就需要定义一个抽象类用于计算表达式的值,该抽象类定义为:Node
下面所有的类图不使用UML建模语言画的,是通过visual studio自动生成的类关系图。
Node的类图
它继承了个Noncpyable类,由于我们是要做面向对象的表达式计算器,所以呢,通过一个小小的手段,把拷贝给禁止掉。这个小小的手段就是让一个类继承一个把拷贝构造函数和赋值操作符重载设置为private权限的函数,把无参构造函数和析构函数设置为权限protected。
例如:
class Noncpyable { protected: Noncpyable() {} ~Noncpyable() {} private: //子类继承后,并没有权限访问这两个函数,而子类的拷贝又需要调用到这两个函数,故把拷贝给禁止了。 Noncpyable(const Noncpyable &); const Noncpyable& operator= (const Noncpyable &) const; };
Noncpyable的类图
现在对运算符进行抽象,运算符可以分为一元运算符,二元运算符,
一元运算符:函数,取反(-)
二元运算符:加,减,乘,除,赋值运算
这样的话,我们应该把一元运算符和二元运算符定义成抽象类,而具体的运算符设置为具体类继承它们。当然它们都要继承Node。
一元运算符
二元运算符
这是表达式的BNF表示
Expr ::= Term{('+' | '-') Term }
Expr ::= Term = Expr
Term ::= Factor {('*' | '/') Factor }
factor ::=
1.number
2.identifier
2.1.function
2.2.variable
3.'-'factor
4.'('expression')'
现在我们需要支持红色字体中的特性,先来 Expr ::= Term{('+' | '-') Term } 、Term ::= Factor {('*' | '/') Factor }这两个吧
它们都要一个共同的特性:包含的为可重复0至无数次的项。因此设置一个抽象类MultipleNode来表示这种特性对
Expr ::= Term{('+' | '-') Term }设置一个SumNode的类,Term ::= Factor {('*' | '/') Factor }设置一个ProductNode类
支持number,把设置一个NumberNode保存一个数字
支持variable,设置一个VariableNode保存变量信息
现在把所有的关于计算的类都设计完了,下面来个“完整的”的类图
五、符号表,函数表,变量存储表设置
表达式计算器,需要支持变量和函数,而变量和函数都是些符号,因此设置一个SymbolTable类来存储这些符号。符号有两种,一种是变量,一种是函数,故在设置一个Storage类存储变量中的值,设置一个FunctionTable类来存储函数。由于这三中类存在着联系,现在在设置一个Calc类来管理它们。
SymbolTable类,FunctionTable类,Storage类,Calc类的关系
根据符号表中的符号对应的的ID,在Storage中根据ID找到变量的值,或者是在FunctionTable中根据ID找到相对应得数学函数
SymbolTable类,FunctionTable类,Storage类,Calc类的设计
SymbolTable类 class SymbolTable : public Serializable { private: std::map<const std::string, unsigned int> dictionary_; //存储符号与id相对应得关系 unsigned int curId_; //存储将要加入符号表的符号id public: enum { IDNOTFOUND = 0xFFFFFFFF }; //IDNOTFOUND代表在符号表中找不到相应的符号 SymbolTable() : curId_(0) {} unsigned int Add(const std::string& str); //向符号表中加入符号 unsigned int Find(const std::string& str) const; //根据符号寻找符号表中是否存储该符号 unsigned int GetCurId()const; //获取下一个的加入符号表的符号id void Clear(); //清楚符号表 const std::string &GetSymbolName(unsigned int id) const;//根据id查找并获取符号表中的符号 virtual void Serialize(Serializer& out) const; //文件存储 virtual void DeSerialize(DeSerializer& in); //文件加载 };
//Storage类 class Storage : public Serializable { private: std::map<unsigned int, double> cells_; //存储符号表中id与值多对应的关系 public: Storage(SymbolTable& tbl); void Clear(); //清除数据 void AddConstants(SymbolTable& tbl); //增加常量,目前只支持pi e double GetValue(unsigned int id) const; //根据id获得想对于的值 unsigned int GetSize() const; //获取map中有多少个数据 void SetValue(unsigned int id, double val); //设置map中的值 void AddValue(unsigned int id, double val); //想map中添加值 virtual void Serialize(Serializer& out) const;//将Storage类中的值存入文件存储 virtual void DeSerialize(DeSerializer& in); //将文件中的值加载到Storage类中 }; FunctionTable类
typedef double(*PtrFun)(double); //重命名了一种函数指针类 class FunctionTable { private: std::map<unsigned int, PtrFun> funsMap_; //存储符号表中的id与对应的数学函数指针 public: FunctionTable(SymbolTable& tbl); void Init(SymbolTable& tbl); //存储是函数表 PtrFun GetFunction(unsigned int id) const; //根据id获取函数表中对应的函数 unsigned int GetSize() const; //获得函数表中有多少个函数 ~FunctionTable();
}; Calc类 class Calc : public Serializable { friend class Parser; //使到Parser类方便的访问Calc的私有成员 private: //由于Storage的构造函数需要用到SymbolTable的引用,故声明顺序SymbolTable在Storage的前面 SymbolTable symTbl_; FunctionTable functionTbl_; //由于函数表中的函数是固定的,因此把函数表的构造放在符号表的前面 Storage storage_; //变量的个数是不固定的 unsigned int FindSymbol(const std::string& str) const; //在符号表中查找str符号 unsigned int AddSymbol(const std::string& str); //往符号表中添加符号 Storage& GetStorage(); //获取stroage_的引用 bool IsFunction(unsigned int id) const; //判断是否是函数 PtrFun GetFunction(unsigned int id) const; //获取函数指针 double GetVariableValue(unsigned int id) const; //获取变量的值 public: Calc() : symTbl_(), functionTbl_(symTbl_), storage_(symTbl_) {} void ListFun() const; //打印函数表 void ListVariable() const; //打印变量表 virtual void Serialize(Serializer& out) const; //将函数表和变量表中的内容存储到文件中 virtual void DeSerialize(DeSerializer& in); //从文件中加载内容到符号表和变量表中 };
六、解析表达式
概述
有了构建语法的类,存储符号的类,现在就可以对表达式进行扫描,解析了。扫描可以抽象出一个Scanner类来完成这一个功能,而解析可以抽象出一个Parser类来完成这一个功能。这两个类存在一定的关系,扫描与解析的互动是这样子的:扫描到一个标识符,然后解析它是什么标识符。由于该表达式计算器是要支持一些命令的,命令的解析和表达式的解析过程完全不一样,所以,又要设置一个CommandParser类,来解析命令。
Scanner类,Parser类,CommandParser类的设计
Scanner类
enum EToken { TOKEN_COMMAND, TOKEN_ERROR, TOKEN_END, TOKEN_NUMBER, TOKEN_IDENTIFIER, TOKEN_PLUS, TOKEN_MINUS, TOKEN_MUTIPLY, TOKEN_DIVIDE, TOKEN_LPARENTHESIS, TOKEN_RPARENTHESIS, TOKEN_ASSIGN, }; class Scanner { private: std::istream& in_; //标准输入流 bool isEmpty_; //是否为空 EToken token_; //记录扫描结果 double number_; //扫描到的数字 std::string symbol_; //扫描到标识符 int look_; //扫描到的字符 void ReadChar(); //从标准输入流中读取字符 public: explicit Scanner(std::istream& in); void Accept(); //扫描一个标识符or操作数or操作数 void AcceptCommand(); //扫描命令 void CleanIstream(); //清除标准输入流缓存 double Number() const; //获取扫描到的数字 bool IsEmpty()const; //判断是否为空 bool IsDone() const; //判断是否扫描完毕 bool IsCommand() const; //判断是否是命令 std::string GetSymbol() const; //获取扫描到的标识符 EToken Token() const; //获取扫描结果 };
Parser类
enum STATUS { STATUS_OK, STATUS_ERROR, STATUS_QUIT, }; class Parser { private: Scanner& scanner_; std::auto_ptr<Node> tree_; //表达式语法树 STATUS status_; //状态 Calc &calc_; //要处理的符号信息 public: Parser(Scanner& scanner, Calc& calc); /*与scanner类相关联*/ ~Parser(); STATUS Parse(); //解析一个表达式生成表达式树 std::auto_ptr<Node> Expr(); //解析表达式 std::auto_ptr<Node> Term(); //解析项 std::auto_ptr<Node> Factor(); //解析因子 double Calculate() const; //计算出表达式的值 };
CommandParser类
class CommandParser { private: enum Ecommand //由于该宏只会在CommandParser类内部用到,故定义在内部 { CMD_HELP, CMD_QUIT, CMD_VAR, CMD_FUN, CMD_LOAD, CMD_SAVE, CMD_ERROR, }; private: Scanner& scanner_; Calc &calc_; ECommand cmd_; //解析到的命令 std::string cmdName_; //命令名 void Help() const; //帮助命令 void ListVar() const; //打印出变量表 void ListFun() const; //打印出函数表 STATUS Load(const std::string& fileName);//从文件中加载内容到变量表和函数表中 STATUS Save(const std::string& fileName);//存储变量表和函数表到文件中 public: CommandParser(Scanner& scanner, Calc& calc_); /*与scanner类相关联*/ STATUS Execute(); //根据解析到的命令执行命令 };
七、文件保存
概述
把符号表和变量表中的内容保存到一个文件中,通过IO文件流,来把符号表和变量表存储到文件中。在这之前需要弄明白什么是序列化和反序列化
对象的序列化
序列化:把对象转换为字节序列的过程
反序列化:把字节序列恢复为对象的过程
我们要把SymbolTable类的对象(符号表)和Storage类的对象(变量表)转换成字节序列保存到文件中,这时就可以设置Serializer类来完成这样的功能,同样的设置一个DeSerializer类来完成把保存到文件当中的字节序列恢复为对象的功能。这里要注意的是,所有的复杂类型都是由内置类型组合而成的,因此不是为某一个类设置专门一个序列化类,而是设置一个序列化的基类,这个类可以把相关的数据类型转换成字节序列存入文件中,再通过这样的一个基类,去把某一个类的对象的内容存储到文件中。
在表达式计算器中,有20多个个类,仅仅是SymbolTable类的对象和Storage类的对象需要保存or加载的,有的类可以有这功能,有的类可以没有这个功能。这样的话,可以定义一个”接口类“来表达这一功能,有的这个功能的就继承,没有就不继承,当然C++里头没接口的概念,不过我们可以用抽象类来模拟这样的功能。这个抽象类就定义为Serializable类。
相关类之间的关系
注意:由于Storage类和SymboTable类是由Calc类管理的,故Calc类也需要继承Serializable类
Serializer类,DeSerializer类,Serializable类的设计
Serializer类
class Serializer { private: std::ofstream stream_; public: Serializer(const std::string& fileName); Serializer& Put(int x); Serializer& Put(unsigned int x); Serializer& Put(long x); Serializer& Put(unsigned long x); Serializer& Put(double x); Serializer& Put(const std::string& x); Serializer& Put(bool x); Serializer& operator<< (int x); Serializer& operator<< (unsigned int x); Serializer& operator<< (long x); Serializer& operator<< (unsigned long x); Serializer& operator<< (double x); Serializer& operator<< (const std::string& x); Serializer& operator<< (bool x); };
DeSerializer类
class DeSerializer { private: std::ifstream stream_; public: DeSerializer(const std::string& fileName); DeSerializer& Get(int& x); DeSerializer& Get(unsigned int& x); DeSerializer& Get(long& x); DeSerializer& Get(unsigned long& x); DeSerializer& Get (double& x); DeSerializer& Get(std::string& x); DeSerializer& Get(bool& x); DeSerializer& operator>> (int& x); DeSerializer& operator>> (unsigned int& x); DeSerializer& operator>> (long& x); DeSerializer& operator>> (unsigned long& x); DeSerializer& operator>> (double& x); DeSerializer& operator>> (std::string& x); DeSerializer& operator>> (bool& x); };
Serializable类
class Serializable { public: virtual void Serialize(Serializer& out) const = 0; virtual void DeSerialize(DeSerializer& in) = 0; };
八、异常处理
概述
表达式计算器的类基本已经设计完成了,由于在程序运行的时候总会有这样那样的异常,例如:a +2, a没有初始化,对于异常的管理一般而言是需要自定义异常类。这个自定义异常类也是在继承了系统已经定义好的exception类,然后再重新定义内容。
异常的种类
语法异常---->SyntaxError类
赋值时左操作数不是变量:1 = 2;
缺少括号:1 + (2*2
不认识的函数:
函数缺少括号:sin(20
符号表中不存在的符号:
输入一个变量但没有设置它的值:a 回车
不是表达式:)1-2
文件读写异常---->FileStreamError类
文件读异常
文件写异常
除零异常--->DevisorZero类
加载时与存储的版本号不一致--->VersionUnMatch类
现在已经有了需要自定义的异常类,为了方便管理,再设置一个Exception的基类用于继承。所以类图就变成这个样子了
Exception类、SyntaxError类、FileStreamError类、DevisorZero类、VersionUnMatch类的设计
Exception类
class Exception : public std::exception { private: std::string message_; //异常信息 public: explicit Exception(const char* message) //char *类型赋值给string类型,printf(%s)会出问题,而用cout不会 : message_(message) {} explicit Exception(const std::string& message) : message_(message) {} virtual ~Exception() throw() {} virtual const char * what() const throw(); //获取异常信息 };
SyntaxError类
class SyntaxError : public Exception { public: explicit SyntaxError(const char* message) : Exception(message) {} explicit SyntaxError(const std::string& message) : Exception(message) {} virtual ~SyntaxError() throw(){} };
FileStreamError类
class FileStreamError : public Exception { public: explicit FileStreamError(const char* message) : Exception(message) {} explicit FileStreamError(const std::string& message) : Exception(message) {} virtual ~FileStreamError() throw() {} };
DevisorZero类
class DevisorZero : public Exception { public: explicit DevisorZero(const char* message) : Exception(message) {} explicit DevisorZero(const std::string& message) : Exception(message) {} virtual ~DevisorZero() throw() {} };
VersionUnMatch类
class VersionUnMatch : public Exception { public: explicit VersionUnMatch(const char* message) : Exception(message) {} explicit VersionUnMatch(const std::string& message) : Exception(message) {} virtual ~VersionUnMatch() throw() {} };