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...
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值