zoukankan      html  css  js  c++  java
  • 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...
  • 相关阅读:
    sqlserver数据库备份还原时出现3241问题
    ssms安装失败_拒绝访问0x80070005解决方法
    Linux strace命令
    争议 | 要不要去IT外包公司工作?
    sqlserver计算时间差DATEDIFF 函数
    CodeForces
    [HNOI 2016] 网络
    CodeForces
    AtCoder Beginner Contest 209
    学习4412开发板+项目实战+配套视频+每日指导
  • 原文地址:https://www.cnblogs.com/zhuyp1015/p/2613624.html
Copyright © 2011-2022 走看看