您的位置:首页 > 其它

复制控制----句柄类与继承

2015-07-19 10:36 260 查看
C/C++拾遗(十七):面向对象补充——复制控制

     昨天粗略地看了《面向对象编程》这一章,简单地梳理了一下自己感觉重要的几个知识点,今天就昨天没有来得及仔细看的部分做些补充,主题是涉及类继承的复制控制以及类作用域的讨论。

一、构造函数与复制控制

     我们知道对于一个类而言,构造函数、复制构造函数、赋值运算符与析构函数是必需的部分,换句话说,如果我们没有显式定义,C++会启用默认的系统版本。在类的继承中,以上四种函数同样是不能被继承的,如果在派生类中没有定义,结果就是使用默认的版本。为了便于说明,我们首先引入一个基类和派生类。基类用来描述计算一般图书销售的价格,而派生类则用来描述数量复合要求的图书的折扣价格总额。具体定义如下:

点击(此处)折叠或打开

//图书基类

class Item_base

{

      public:

             Item_base(const std::string &book = "",

                       double sales_price = 0.0):

                       book_no(book), price(sales_price) {}

             std::string book() const {return
book_no;}

             virtual double net_price(std::size_t n) const

                       {return n*price;}

             virtual ~Item_base() {}

      private:

              std::string book_no;

      protected:

                double price;

                       

      } ;

//含有折扣的图书类,必须满足最小的min_qyt数量要求才可以享有折扣 

class Bulk_item : public Item_base

{

      public:

             double net_price(std::size_t) const;

      private:
<
4000
/li>
              std::size_t min_qyt;

              double discount;

      }

double Bulk_item::net_price(std::size_t
cnt) const

{

       if (cnt >= min_qty)

          return cnt * (1 - discount ) * price;

       else

          return cont * price;

       }

1. 构造函数      

     在上面的类定义中,可以看到基类定义了自身的带有默认参数值的构造函数,一般来说,基类的构造函数受继承关系的影响不大,唯一需要考虑的是允许哪些成员被派生类访问,即确定是否需要protected标号。派生类的构造函数与基类的构造函数唯一的不同是:初始化派生类成员的同时还要初始化派生类对象的基类部分,基类部分由基类提供的构造函数初始化。即我们可以定义派生类的构造函数如下:

点击(此处)折叠或打开

Bulk_item::Bulk_item(const std::string& book, double
sales_price, 

                     std::size_t qty = 0, double
disc_rate = 0.0):

                     Item_base(book, sales_prices),min_qty(qyt), discount(disc_rate) {}

      上述的构造函数接受实参用来初始化基类对象和自身的成员,需要记住的一点是,当调用派生类的构造函数时,首先调用基类的构造函数初始化基类成员,然后按照类定义中成员的声明顺序依次初始化派生类成员。析构函数调用时顺序则与此相反。另一个很重要的是:每一个派生类都只能初始化其直接基类。即若A派生B,B派生C,则在C的构造函数中只能初始化B,调用B的构造函数时会自动调用A的构造函数。构造函数只能初始化其直接基类的原因是每个类都定义了自己的接口,C++的原则是一旦某个类定义了自身的接口,那么所有与该类对象的交互都应该通过这些接口实现。

2. 复制控制与继承

     派生类的复制构造、赋值运算以及析构函数,都需要考虑进基类的影响。当定义派生类的复制构造函数时,必须显示的调用基类的复制构造函数,如:

Derived (const Derived& d): Base(d)  {}

     注意这里的Base(d)的作用在于将派生类对象d转换为其基类部分的引用,然后调用基类的复制构造函数初始化派生类对象的基类部分。派生类的赋值操作与此类似,但是不同的是需要防止自身赋值的出现,因此要增加一步自身赋值的判断:

Derived& Derived::opreator=(const Derived &rhs)

{

    if (this != rhs)

        Base::operator=(rhs);

     return *this;

}

二、虚析构函数与构造函数/析构函数中的虚函数

     一般情况下,派生类的析构函数调用时会自动调用其基类的析构函数,但是对于多态性质的动态绑定而言,我们析构的对象是一个基类的指针或者引用,但是其代表的往往可能是一个派生类对象。基类的析构函数无法自动析构一个派生类对象,因此导致内存泄漏。解决这类办法的便捷途径是使得析构函数也可以实现动态绑定,没错!就是将基类的析构函数声明为虚函数就可以啦!可以看看下面的例子程序:

点击(此处)折叠或打开

//Check out Virtual Destructor

#include <iostream>

#include <cstdlib> 

using namespace std;

class Base

{

      public:

             Base(int a):num(a) {cout << "Base
Constructor..."<<endl;}

             virtual void print() {cout << "num
is "<< num<<endl;}

             ~Base() {cout << "Base
Destructor..."<<endl;}

      protected:

                int num;

      };

class Derived : public Base

{

      public:

             Derived(int a, int b):Base(a), data(b) {cout << "Derived
Constructor..."<<endl;}

             void print() {cout << "data
is "<<data<<endl;}

             ~Derived() {cout << "Derived
Destructor..."<<endl;}

      private:

              int data;

      };

int mai
1a640
n()

{

    Base *pB = new Derived(100, 200);

    pB->print();

    delete pB;

    

    

    system("pause");

    return 0;

}

      函数运行结果如下:对象析构时竟然只调用了基类的析构函数!



     只要将12行修改为:virtual ~Base() {cout << "Base
Destructor..."<<endl;},析构时就会自动线调用派生类的析构函数然后是基类的析构函数:



     问题解决,需要注意区分只有析构函数可以声明为虚函数,构造函数与复制构造、赋值运算都不能作为虚函数。说完了虚析构函数,我们再来看看构造函数和析构函数中的虚函数。这里大致有两种情况:一种是构造函数/析构函数调用虚函数,另一种是构造函数/析构函数调用的函数调用了虚函数,但是不管哪一种,虚函数都绑定到构造函数或者析构函数自身类型定义的版本。具体来说,就是对于一个派生类对象的构造与析构,C++编译器认为在这个过程中对象类型会发生变化,一开始是调用派生类对象的虚函数(以析构的情况为例),析构基类部分时就认为是一个基类型的对象,调用基类的析构函数。

三、继承情况下的类作用域

     说到作用域,大致涉及对象名称解析的查找规则以及由此带来的名称冲突解决方案。说起来麻烦,但是只要平时在编程时小心避免出现对象成员同名的情况,一般就不会出现这部分的问题。在列出名称解析规则前,先感性地说几个规则:

1. 名称查找在编译时发生:除了动态绑定以外,更多的情况是对象、指针与引用的静态绑定,即在编译时即确定了对象;

2. 当发生成员或函数名称冲突时,派生类的成员会覆盖基类成员,成员函数只看函数名,无视其他;

     好了,是不是与函数的变量声明作用域规则很像?接下来我们给出函数名称查找与继承的规则:
<1>首先确定进行函数调用的对象、引用或指针的静态类型;

<2>在该类中查找函数,如果找不到就直接在基类中查找,如此依次向上递推,直到找到最后一个类,找不到返回错误;

<3>一旦找到该名称即进行常规检查(原型),检查调用是否合法;

<4>若函数调用合法,C++编译器生成代码直接调用函数;若存在动态绑定,则生成代码以确定根据对象类型进行运行时绑定;

句柄类与继承 

如何实现一个类似“购物车”的数据结构呢?用过淘宝的同学们一定都晓得“购物车”应用,可以记录不同的商品,并且相同的商品可以显示次数,最后计算出总额。如果用C++来实现的话,当然是首选容器对象了。由于是统计可以重复的对象,所以可以使用multiset关联容器。现在你有两个选择:一是将商品对象放入容器中成为容器对象;二是容器中存储指向商品对象的指针或引用。第一种方法明显是不合适的,因为商品之间可能存在着继承关系,那么我们的容器类型时基类还是派生类呢?派生类型的容器元素的派生部分就会是未定义状态,而基类型的容器元素则会失去派生类部分成员。如果使用指针或者引用呢?可以避免上面的问题,但是用户必须负责管理指针或引用,尤其是防止“悬垂指针”的出现,即要确保指针在,对象在;对象失,指针无。这无疑会加重用户的开发负担。那么有没有更好的解决办法呢?有的,就是我们今天要讨论的句柄类。
一、指针型句柄
     其实说白了句柄类一点都不神秘,前面我们接触过“计数类”用来管理指针,这里的句柄类类似,但是比简单的计数类增加了一些其他的功能,因为我们除了利用它管理指针,还希望便捷的使用其指向的对象。这里的主要idea就是将指针的管理工作封装到一个类中,类中起码具有两个数据成员,一个指向对象的指针和一个计数指针,计数归零时意味着要释放对象和句柄类。这里我们使用的例子还是上一节中的图书类,这里简单再次给出:

点击(此处)折叠或打开

//图书基类

class Item_base

{

      public:

             Item_base(const std::string &book = "",

                       double sales_price = 0.0):

                       book_no(book), price(sales_price) {}

             std::string book() const {return
book_no;}

             virtual double net_price(std::size_t n) const

                       {return n*price;}

             virtual ~Item_base() {}

      private:

              std::string book_no;

      protected:

                double price;

                       

      } ;

//含有折扣的图书类,必须满足最小的min_qyt数量要求才可以享有折扣 

class Bulk_item : public Item_base

{

      public:

             double net_price(std::size_t) const;

      private:

              std::size_t min_qyt;

              double discount;

      }

double Bulk_item::net_price(std::size_t
cnt) const

{

       if (cnt >= min_qty)

          return cnt * (1 - discount ) * price;

       else

          return cont * price;

       }

      我们即将定义的句柄类,除了封装指针以外,还希望用户能够像使用一个指针一样去使用它,如:

点击(此处)折叠或打开

Handle_item item(Bulk_item("0-201-42778-2", 42, 2, 201));

item->net_price() //对net_price函数的虚调用

      接下来我们来定义句柄类Handle_item:

点击(此处)折叠或打开

//定义一个句柄类

class Handle_item

{

      public:

             Handle_item():p(0), cnt(new
std::size_t(1)) {}
      //默认构造函数

             Handle_item(const Item_base&);
                      //自定义的接受基类引用为参数,进行绑定的构造函数

             Handle_item(const Handle_item &i):
                  //复制构造函数

                               p(i.p), cnt(i.cnt) {++*cnt;}
     

             ~Handle_item() {des_use();}
                         //析构函数

             Handle_item& operator=(const Handle_item);
          //定义赋值操作

             

             const Handle_item *oprator->()const {
               //定义->运算符

                               if (p) return p;

                               else 

                                   throw std::logic_error("未绑定Handle_item"));

                                   )

             const Handle_item &operator*() const {
              //定义解引用运算符

                               if (p) return p;

                               else

                                   throw std::logic_error("未绑定Handle_item");

                                   }

      private:

              Item_base *p;

              std::size_t *cnt;

              void des_use()

              {

                   if (--*cnt == 0)

                    {

                              delete p;

                              delete cnt;

                              }

               }

             

             

      }

      从上面的定义中,可以看到Handle_item类具有两个数据成员p和cnt,分别指向基类对象和计数对象,并且定义了构造函数和复制控制函数,为了能够使得我们的句柄类对象像指针那样使用,我们还单独重载了->和*运算符。下面是复制构造函数的实现:

点击(此处)折叠或打开

Handle_item&

  Handle_item::oprator=(const Handle_item &rhs)

  {

         ++*rhs.cnt;

         des_use();

         p = rhs.p;

         use = rhs.use;

         return *this; 

  }

      当我们实现类的复制构造函数时,必须要考虑用户调用函数为自身赋值的情况。因为赋值操作时析构和复制构造二者的结合,因此必须小心实现的操作步骤。为了使得为自身赋值时程序依旧能够正确运行,在des_use()之间首先将计数器加一,避免为自身赋值时首先调用des_use()导致自身对象被析构,从而引发程序错误。
     实现构造函数的时候,特别说明接受Item_base引用绑定一个句柄类的构造函数,因为这里存在的问题是Item_base引用的实际对象类型只有在运行时才能够确定,那如何为其分配空间呢?解决的思路是:既然参数是动态绑定,那么我们的行为也实现动态绑定。实现动态绑定的方法自身是使用虚函数,这就需要我们定义一个新的虚函数clone(),从而能够实际的对象类型开辟合适的空间。为了使用虚函数clone(),我们为基类和派生类添加函数:

点击(此处)折叠或打开

Item_base::

virtual Item_base* clone() const {return
new Item_base(*this);}

Bulk_item::

Bulk_item* clone() const {return
new Bulk_item(*this);}

      这里也许有细心的童鞋会奇怪,不是说虚函数的原型必须一致才能动态绑定吗?其实这里是一个例外,如果虚函数的基类实例返回类类型的引用或指针,那么该虚函数的派生类实例可以返回基类实例返回的类型的派生类(或指针|引用)。有了clone函数,我们就可以给出Handle_item类构造函数的定义了:

点击(此处)折叠或打开

Handle_item::Handle_item(const Item_base &item):

                               p(item.clone()), cnt(new
std::size_t(1)) {}

二、句柄的使用
     定义了handle_item,接下来使用的时候就要将句柄类对象放入multiset容器中成为容器元素,从而实现利用指针访问商品对象的最终目的。这里有一个小问题需要解决,放入multiset中的元素必须能够比价大小从而确定排列顺序,我们不会重载<运算符,那样太费事了,我们选择的方法是直接提供一个比较函数,就比较图书的book号码就可以。

点击(此处)折叠或打开

inline bool

compare(const Handle_item &lhs, const Handle_item &rhs)

{

              return lhs->book() < rhs->book();

              }

      经过了这么多,现在我们终于可以来定义我们的“购物车”啦:

点击(此处)折叠或打开

class Basket {

      typedef bool (*comp)(const Handle_item&, const Handle_item&);

public:

       typedef std::multiset<Handle_item, comp> set_type;

       typedef set_type::size_type size_type;

       typedef set_type::const_iterator const const_iter;

       Basket():items(compare) {}

       void add(const Handle_item &item)

       {

            items.insert(item);

        }

      size_type size(cosnt Handle_item &i)const

      {

            return items.count(i);

                }

      double total() const;

private:

        std::multiset<Handle_item, comp> items;

        }

double Basket::total() const

{

       double sum = 0.0;

       for (const_iter iter = items.begin();

            iter != items.end();

            iter = items.upper_bound(*iter))

       {

            sum += (*iter)->net-price(items.count(*iter));

        }

       }

      在定义Basket类时为了书写方便,我们使用typedef进行了简化,typedef、对象和函数可以成为类的成员。计算总价的total函数比较有意思,首先是for循环的递增式使用了upper.bound(),目的是使得跳过相邻的相同的商品直接到达下一个不同类的商品的迭代器位置,而最后的sum式子中的count()则用来计算同类商品的数目,因为我们的折扣是在同种商品达到最低的数目之后才会享有的。
PS:
     最后谈谈自己对于句柄类的感觉。句柄类听名字就让自己想到了windows中的句柄概念,记得当时看的时候也只是知道那是一个数据结构,里面肯定封装了对象的指针,但是还有更多的对象属性在里面,比如访问权限控制等等。现在想来原型结构上系统的“句柄”也许与我们变成使用的句柄类有些类似吧。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: