C++沉思录读书笔记(8章)-一个面向对象程序范例

来源:岁月联盟 编辑:exp 时间:2011-11-11

 

这个例子很好的展示了面向对象编程的三个要素:数据抽象、继承和动态绑定。

问题:算术表达式的表达式树, 如(-5)*(3+4)对应的表达式树为:

/

 

我们希望通过调用合适的函数来创建这样的树,然后打印该树完整的括号化形式。例如:

Expr t = Expr("*", Expr("-",5), Expr("+", 3, 4));

cout << t << endl;

输出结果为:((-5)*(3+4))

 

面向对象的解决方案:

一 节点如何表示?

仔细观察上图,根据子节点的个数可以抽象出三种节点,分别有0,1,2个子节点的节点。如果让我来设计的话我很可能会这样描述节点:

 

class Node 

private: 

       string value;    //节点值 

       int num;         //子节点个数 

       Node* son[2]; //储存子节点指针 

public: 

       Node(); 

       Node(string&, int);                             //0 个子节点 

       Node(string&, int, Node*);                 //1 个子节点 

       Node(string&, int, Node*, Node*);      //2 个子节点 

       ~Node(); 

}; 

 

这种表示方法需要设置专门的字段类指示子节点的个数。然而书中用继承解决了这个问题,让人拍案叫绝!

(1)考虑三种节点类的共同点与不同点:都具有节点值,都需要打印节点,但节点值类型不同,打印节点方式不同,子节点个数不同。

(2)考虑三种节点之间的关系: 每一种节点都与其他节点相互独立,一个没有子节点的节点不是“一种”有一个子节点的节点,反之亦然。

(3)综合上面两点,定义一个抽象基类,动态绑定在这里就有用武之地了。

二 几种节点类的具体定义:

 

#include <iostream> 

#include <string> 

using namespace std; 

class Expr_node 

    friend ostream& operator<<(ostream&, const Expr_node&); 

protected: 

    virtual void print(ostream&) const = 0;//动态绑定 

    virtual ~Expr_node(){} 

}; 

 

ostream& operator<<(ostream& o, const Expr_node& e) 

    e.print(o); 

    return o; 

 

class Int_node: public Expr_node                //0 个子节点 

private: 

    int n;   

    friend ostream& operator<<(ostream&, const Expr_node&);                                 

public: 

    Int_node(int k) : n(k) {} 

    void print(ostream &o) const {o << n;} 

}; 

 

class Unary_node:public Expr_node     //1 个子节点 

private: 

    string op; 

    Expr_node *opnd; 

    friend ostream& operator<<(ostream&, const Expr_node&); 

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    //2 个子节点 

private: 

    string op; 

    Expr_node *left; 

    Expr_node *right; 

    friend ostream& operator<<(ostream&, const Expr_node&); 

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<< ")";} 

}; 

 

void main() 

    Binary_node * t = new Binary_node("*", 

        new Unary_node("-", new Int_node(5)), 

        new Binary_node("+",new Int_node(3), new Int_node(4))); 

    cout << *t; 

    //节点未删除 

 

三 类定义的缺陷

上面方案已经基本上解决问题了,但是创建表达式树的方式还有待改进。按照上面的方案,我们只能这样定义表达式:

Binary_node *t = new Binary_node("*",

                                                   new Unary_node("-", new Int_node(5)),

                                                   new Binary_node("+",new Int_node(3),new Int_node(4)));

cout << *t << endl;

这个改进离我们理想的表达方式还有差距,并且我们不再拥有指向内层new的对象的指针,因此上述代码的情形会造成内存泄露,如果我们通过定义好析构函数来解决这个问题,则又可能会多次删除对象,因为理想情况下可能有多个Expr_node指向同一个下层表达式对象,这种形式把内存管理的事情都交给了用户。

 

四 用句柄类改进。

既然用户关心的只是树,而不是树中的单个节点,就可以用Expr来隐藏Expr_node的继承层次。这里又回到了前面讨论的句柄类的内容,用户可见的只有Expr了,内存管理的事情就完全由Expr掌控!改进后代码如下:

 

#include <iostream> 

#include <string> 

using namespace std; 

 

class Expr_node 

    friend class Expr;  //友元类可以被继承 

    int use;                  //引用计数 

public: 

    virtual void print(ostream&) 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&); 

    ~Expr() 

    {  

        if(--p->use == 0) 

            delete p; 

    } 

}; 

 

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: 

    //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 << ")"; 

    } 

}; 

 

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 << ")"; 

    } 

}; 

 

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; 

 

 

五 总结

       这个例子很好的展示了面向对象的三个要素,这样设计出的类具有很好的扩展性,比如再增加有多个子节点的节点,只要添加个新类,然后在Expr中添加个新构造函数就行了。用户完全不必知道底层的代码是怎么实现的。以后面对问题的时候要好好的借鉴这种思想!

 

作者 yucan1001