C++ 沉思录——Chap8:一个面向对象程序范例
面向对象编程的三要素:数据抽象、继承以及动态绑定。
这里讨论一个算术表达式树问题,如(-5)*(3+4)对应的表达式树为:
我们希望通过调用合适的函数来创建这样的树,然后打印该树完整的括号化形式。例如:
Expr t = Expr("*", Expr("-",5), Expr("+", 3, 4)); cout << t << endl;
输出结果为:((-5)*(3+4))
此外我们不想为这些表达式的表示形式操心,也不想关心它们内存分配和回收的事宜。从上面图我们可以看出,图中有两种对象节点和箭头。每个节点包含一个值——一个操作数或者一个操作符——并且每个结点又具有零个、一个或者两个子节点。
这些节点既是相同的又是不同的,我们该如何抽象节点的数据结构呢?
这些节点首先有一个共同的特点:每个类都要存储一个值以及一些子节点。同时也可以很容易看出这些节点的一些不同点,比如它们存储的值的种类,子节点的数目。
在设计类的时候继承使得我们可以不中这些类的共同点,而动态绑定又可以帮助各个节点确定身份(从而才去不同的操作)。
仔细分析我们可以把节点分为三种类型:一种表示整数表达式,包含一个整数值,无子节点。另外两种分别表示一元表达式和二元表达式(包含一个操作符,分别有一个或者两个子节点)。因为这些节点的不同,我们在打印的时候也要采取不同的操作,这时候就可以使用动态绑定了:我们可以定义一个virtual 函数来指明应当如何打印各种节点。
注意到这三种节点都成为节点,我们可以先定义一个共同的基类:
class Expr_node { friend ostream & operator << ( ostream &, const Expr_node &); protected: virtual void print(ostream &) const = 0; virtual ~Expr_node() { } };
由于Expr_node 类是一个虚基类,不能实例化对象,只有从其中派生类来实例化对象。Expr_node 类的存在只是为了获得公共的接口。
接着我们先来定义输出操作符,它要调用“对应”的print函数:
ostream & operator << ( ostream & o, const Expr_node & e) { e.print(o); return 0; }
按照前面对三种节点的分析,可以很快定义出第一种节点:
class Int_node : public Expr_node { private: int n; public: Int_node( int k) : n(k) {} void print(ostream & o) const { o << n; } };
对于一元表达式或者二元表达式,因为其中包含有子节点,这时候我们并不知道子节点的类型会是什么,因此不能按值存储子节点,必须存储指针。
class Unary_node : public Expr_node { private: string op; Expr_node *opnd; public: Unary_node( const string &a, Expr_node *b) : op(a), opnd(b) { } void print(ostream & o) const { o << "(" << op << *opnd << ")"; } }; class Binary_node : public Expr_node { private: string op; Expr_node *left; Expr_node *right; public: Binary_node( const string &a, Expr_node *b, Expr_node *c) : op(a),left(b),right(c) { } void print( ostream & o) const { o << "(" << *left << op << *right << ")"; } };
从现有的定义来看,如果我们要创建下面的表达式树:
Expr t = Expr( "*", Expr("-",5), Expr("+",3,4) );
则需要像下面一样来实现:(创建一元和二元表达式树的构造函数期望获得指针,而不是对象)
Binary_node *t = new Binary_node("*", new Unary_node("-",new Int_node(5), new Binary_node("+", new Int_node(3), new Int_node(4) );
这个改进离我们理想的表达方式还有差距,并且我们不再拥有指向内层new的对象的指针,因此上述代码的情形会造成内存泄露,如果我们通过定义好析构函数来解决这个问题,则又可能会多次删除对象,因为理想情况下可能有多个Expr_node指向同一个下层表达式对象,这种形式把内存管理的事情都交给了用户。
既然用户关心的只是树,而不是树中的单个节点,就可以用Expr来隐藏Expr_node的继承层次。这里又回到了前面讨论的句柄类的内容,用户可见的只有Expr。既然用户要乘船的是Expr 而不是Expr_node, 我们就希望Expr的构造函数能代表所有3种Expr_node。每个Expr构造函数豆浆创建Expr_node的派生类的一个合适对象,并且将这个对象的地址存储在正在创建的Expr对象中。Expr 类的用户不会直接看到Expr_node 对象。
class Expr { private: Expr_node *p; friend ostream & operator <<( ostream &, const Expr &); public: Expr(int ); Expr(const string &, Expr); Expr(const string &, Expr, Expr ); Expr(const Expr &); Expr & operator = (const Expr &); ~Expr() { delete p; } };
一系列的构造函数创建适当的Expr_node,并将其地址存储在p中:
Expr :: Expr(int n) { p = new Int_node(n); } Expr :: Expr(const string& op, Expr t) { p = new Unary_node(op, t); } Expr :: Expr(const string &op, Expr left, Expr right) { p = new Binary_node(op, left, right); }
现在使用Expr 为 Expr_node 分配内存,我i类避免不必要的复制,我们依然维护一个引用计数,但这里引用计数包含在Expr_node 里面,Expr 类和 Expr_node 类系统管理引用计数,因此Expr 需要作为Expr_node的友元出现。
class Expr_node { friend ostream & operator <<(ostream &, const Expr &); friend class Expr; int use; protected: Expr_node() : use(1) { } virtual void print( ostream &) const = 0; virtual ~Expr_node() { } };
当Expr 类“复制”一个Expr_node 时,该Expr 将其引用计数增1,当引用为0的时候删除底层的Expr_node:
Expr 类需要增加复制构造函数,析构函数和赋值函数:
class Expr { // 和前面一样 public: Expr (const Expr & t) { p = t.p; ++p->use; } ~Expr() { if( --p->use == 0) delete p; } Expr & operator = (const Expr & t); }; Expr & Expr :: operator = (const Expr &rhs) { rhs.p->use++; if(--p->use == 0) delete p; p = rhs.p; return *this; }
针对Expr 我们还需要定义输出操作符:
ostream & operator << (ostream & o, const Expr & t) { t.p->print(o); return o; }
最后需要更改每个派生自Expr_node的类,令其操作为私有,将Expr 类声明为友元,存储Expr而不是Expr_node的指针,例如:
class Binary_node : public Expr_node { friend class Expr; string op; Expr left; Expr right; Binary_node };
如果我们需要对表达式求值,在现在的构架下也很容易实现,可以在 Expr 类中增加 eval 成员方法,eval 可以将实际的求值工作委托为做出Expr 的结点来完成。
class Expr { private: // 和前面一样 public: // 和前面一样 int eval() const { return p->eval() ; } // 新添加的 }; 这样 Expr_node 类就需要添加另一个纯虚函数: class Expr_node { protected: virtual int eval() const = 0; // 和前面一样 };
针对Expr_node 派生的每一个类添加一个函数来实现求值运算。(这里就不单独列出)
将全部代码列出:
/* 既然用户关心的只是树,而不是树中的单个节点,就可以用Expr来隐藏Expr_node的继承层次。 这里又回到了前面讨论的句柄类的内容,用户可见的只有Expr了,内存管理的事情就完全由Expr掌控! 改进后代码如下: */ #include <iostream> #include <string> using namespace std; class Expr_node { friend class Expr; //友元类可以被继承,句柄类Expr还要操作这里的use,所以是必须的 int use; //引用计数 public: virtual void print(ostream&) const = 0; virtual int eval() const = 0; public: Expr_node():use(1) {} virtual ~Expr_node() {} }; class Expr //句柄类 { friend ostream& operator<<(ostream &o, const Expr &e); private: Expr_node *p; //指向基类的指针 public: Expr(int n); Expr(const string &op, Expr t); Expr(const string &op, Expr left, Expr right); Expr(const Expr &t); Expr& operator=(const Expr&); int eval() const { return p->eval();}; ~Expr() { if(--p->use == 0) delete p; } }; class Int_node: public Expr_node { private: friend class Expr; int n; //public: Int_node(int k):n(k) {} void print(ostream &o) const { o << n; } int eval() const { return n;} }; class Unary_node: public Expr_node { private: friend class Expr; string op; Expr opnd; //public: Unary_node(const string &a, Expr b):op(a), opnd(b) {} void print(ostream &o) const { o << "(" << op << opnd << ")"; } int eval() const { if(op == "-") return -opnd.eval(); throw "error, bad op" + op + "int UnaryNode"; } }; class Binary_node: public Expr_node { private: friend class Expr; string op; Expr left; Expr right; //public: Binary_node(const string &a, Expr b, Expr c):op(a), left(b), right(c) {} void print(ostream &o) const { o << "(" << left << op << right << ")"; } int eval() const { int op1 = left.eval(); int op2 = right.eval(); if(op == "-") return op1 - op2; if(op == "+") return op1 + op2; if(op == "*") return op1 * op2; if(op == "/" && op2 != 0) return op1 / op2; } }; Expr::Expr(int n) { p = new Int_node(n); } Expr::Expr(const string& op, Expr t) { p = new Unary_node(op,t); } Expr::Expr(const string &op, Expr left, Expr right) { p = new Binary_node(op, left, right); } Expr::Expr(const Expr& t) { p = t.p; ++p->use; } Expr& Expr::operator=(const Expr& rhs) { rhs.p->use++; if(--p->use == 0) delete p; p = rhs.p; return *this; } ostream& operator<<(ostream &o, const Expr &e) { e.p->print(o); return o; } void main() { Expr t = Expr("*", Expr("-", Expr(5)), Expr("+", Expr(3), Expr(4))); cout << t << endl; cout << t.eval()<<endl; } /* 这个例子很好的展示了面向对象的三个要素,这样设计出的类具有很好的扩展性,比如再增加有多个子节点的节点, 只要添加个新类,然后在Expr中添加个新构造函数就行了。用户完全不必知道底层的代码是怎么实现的。以后面对 问题的时候要好好的借鉴这种思想! */
运行结果:
C:\Windows\system32\cmd.exe /c chap8_Expr2.exe
((-5)*(3+4))
-35
Hit any key to close this window...
((-5)*(3+4))
-35
Hit any key to close this window...
make it simple, make it happen