回 帖 发 新 帖 刷新版面

主题:第6章 句柄

第六章 句柄的第一部分 
  上一章是讲述的一种称为代理的类.当我们创建代理的时候,将会复制所代理的对象.但有的时候,我们不想,不能够,不允许复制这个对象.
  使用指针,往往能够在一定程度上避免复制.但是指针所带来的风险也是显而易见的.指针使用起来比对象要困难.比如你要时刻记住初始化指针---一个没有初始化的指针是非常的危险的,它就象一颗定时炸弹,不知道什么时候就会让你的程序崩溃,而它可能仅仅就是2行简单的指针代码.还有,当几个指针指向同一个对象的时候,什么时候删除这个对象将会是件让人头痛的事情.
  好,现在我们明白了我们的目的,那就是:要避免指针的缺点,同时又能利用它的优点,还要在保持多态性的前提下避免复制对象的代价.
  C++的方法是定义一个类,将这个类的对象绑定到它所控制的类的对象上.这个类被称为句柄类(handle class).
  因为句柄的行为类似指针,有时也叫它智能指针.但是两者的区别大于相似,在有限的情况下才能视作相同.

  我们将从一个简单的类开始,一步一步的得到我们的句柄类:
    
1,首先,我们设计一个对象类,MyPoint,一个普通的类,简单的3个构造函数,获得和修改属性的函数,封装的数据.没什么好说的.作者特别提醒不要用
    MyPoint(int x = 0, int y = 0):xval(x),yval(y){}
这样的构造函数,因为在这个坐标点类里面,不应该允许只用一个参数就可以构造对象的情况出现.
于是,我们得到的代码大致如下:
// 对象类,这个类将作为句柄所要表示的对象,也就是被抽象的内容
class MyPoint // 这是一个描述坐标点的类,它只有x,y两个坐标值作为成员变量
{
public:
  MyPoint() : xval(0), yval(0) {}       // 缺省构造函数
  MyPoint(int x, int y) : xval(x), yval(y) {} // 带参构造函数
  MyPoint(const MyPoint& p) : xval(p.x()), yval(p.y()) {}// 拷贝构造函数
  int x() const { return xval; }  // 属性
  int y() const { return yval; }  // 属性
  MyPoint& x(int xv) { xval = xv; return *this; }// 修改属性,返回对象的引用,                      // 好处是可以连续使用函数
  MyPoint& y(int yv) { yval = yv; return *this; }// 修改属性    
private:        
  int xval, yval;    // 私有的数据
};

2,现在该考虑下句柄类了.我们首先想到,要将句柄绑定到对象上,是否直接用对象去初始化句柄呢?象这样MyHandle h(p);这会有个很明显的问题----对象直接处于用户的控制之下,用户绕开句柄,对对象的操作将是不可预料的.例如用户删掉了对象p,句柄h怎么知道呢?或者二者怎么协调呢?如果我们让句柄与其所绑定的对象的内存分配和释放无关,那么
和使用一个指针来指向这个对象有什么区别呢?

  我们考虑的是,句柄类应该控制它所绑定的对象---完全的控制,由它创建和销毁对象.好了,那么有2种选择,使用类似于MyPoint的构造函数,让句柄去创建这个对象;或者使用一个对象作为参数构造句柄,最终将创建对象的一个副本,句柄完全控制这个副本.
  其形式大致如下:
      MyHandle h(int x, int y);
   或者   MyHandle h(MyPoint p);

3,我们可以开始动手设计句柄类了.开始我们能确定的构造函数有缺省构造,2个带参的构造,拷贝构造,应该没有问题.
关键的就是成员函数和成员变量的考虑了.句柄类的代码大致如下:
class MyHandle
{
public:
  // 构造
  MyHandle();        // 缺省构造
  MyHandle(int x, int y);    // 带参构造,类似于对象的构造
  MyHandle(const MyPoint& p);    // 带参构造,用对象来构造句柄
  MyHandle(const MyHandle& h);    // 拷贝构造
  ~MyHandle();            // 析构    
  MyHandle& operator=(const Handle& h);    // 赋值操作符
  int x() const;         // 下面是和MyPoint一样的操作
  int y() const;
  Handle& x(int);
  Handle& y(int);
  ......    // 其他的操作
private:
  ......  // 成员变量,这个成员变量的确定将会影响这个类的一切行为.
          // 也许我们会考虑使用对象的指针 MyPoint* p,但是它会带来很多问题
          // 其中之一就是何时删除?;
}

  我们设计句柄的目的,首先就是考虑避免不必要的对象复制.要能够允许多个句柄绑定到一个对象上.这样我们就必须知道,有多少个句柄绑定到了一个对象上.才能确定何时删除对象.我们使用引用计数来达到我们的目的.

  引用计数通常相当于一个计数器,当增加一个句柄绑定到对象上时,计数加1,反之减1,为0时则删除这个对象.这个计数器,放在什么位置呢?开始我们考虑放在句柄类MyHandle里.但是马上就可以想到,当添加一个绑定到对象的句柄时,我们必须给所有绑定到该对象的句柄的计数器加1.要这么做,我们必须知道他们的位置,这是件非常麻烦的事情.
  
  或者我们考虑放在对象类里.好像更不行了,原因?你往往不可以重写对象类,比如库里的类是不能被我们改写的.
 
  我们只有考虑把这个计数器放到除了句柄类,对象类之外的第3个类里去了,我们称之为计数类(引用计数类).当我们确定要添加这个类的时候.我们要考虑的问题有:这个类怎么与句柄类协作?何时创建与删除?

4,我们来好好考虑这个计数类吧.毫无疑问,首先我们能想到的是一个对象对应一个计数器,也就是说这个计数类对象应当只有一份,随着对象的产生而创建,并同时开始计数,并应当随着对象的删除(计数为0)而消亡.于是我们考虑将对象也放进这个计数类,这样能比较好的解决上面的问题.  现在我们可以初步得到计数类的代码了:

// 容纳引用计数和对象的类
class MyUPoint
{    
// 所有成员都是私有的,初始化引用计数为1
private:
  ~MyUPoint() {} 
  MyUPoint() : u(1) {}    // 缺省构造,MyPoint也是缺省构造
  MyUPoint( int x, int y ) : p(x,y), u(1) {}// 带参数,构造一个MyPoint实例
  MyUPoint( const MyPoint& p0 ) : p(p0), u(1) {}// 带参数,拷贝构造一个MyPoint
                     // 实例
  MyPoint p;   // 实际的对象
  int u;       // 计数器    
 
  friend class MyHandle;    // 句柄类声明为友员
};
 
代码不多,特点有3个:
1,主要是构造函数,让这个类的构造函数和对象类MyPoint类似,这样和使用对象类的方法就一致.
2,所有成员都是私有的,同时声明句柄类为友员,这意味着只能由句柄类创建计数类对象,同时相当于计数类对外界来说,是不可见的.
3,构造函数里,引用计数都初始化为1,表示了该计数类的对象创建后肯定会被一个句柄类所保存.
  
  既然计数类对外不可见,而且只能由句柄类来创建,我们就可以将计数类MyUPoint作为句柄类MyHandle的成员了.这样那会保存计数类的多个实例吗?会 存在多个对象类的实例吗?  使用指针,就不会了.
  现在可以给MyHandle类加上一个成员变量了.MyUPoint* up.再加上实现的代码,代码完善一下:
// 所谓的句柄类
class MyHandle
{
public:
// 构造
  MyHandle() : up(new MyUPoint) {} // 构造新的计数类对象
  MyHandle(int x, int y) : up(new MyUPoint(x, y))  {}    // 构造新的计数类对象
  MyHandle(const MyPoint& p) : up(new MyUPoint(p)) {}    // 构造新的计数类对象,                    // 使用的是副本
  MyHandle(const MyHandle& h):up(h.up){++up->u;}//拷贝构造,这个是指针                                //的拷贝,所以引用计数加1
  ~MyHandle()        // 析构,
  {        
    if(--(up->u) == 0)    // 计数为0时,删除计数类对象
      delete up; 
  }
  int x() const { return up->p.x(); }    // 属性
  int y() const { return up->p.y(); }    // 属性

  // 赋值操作符
  MyHandle& operator=(const MyHandle& h)     
  {
     h.up->u++;        // 引用计数加1,这句必须放前面
     if(--up->u == 0)    // 原引用计数减1,如果为0,则删除该对象
     {
       delete up;
     }
   
     up = h.up;        // 重新的绑定
     return *this;        
  }
        

  MyHandle& x(int x) { up->p.x(x); return *this; }    // 修改对象的函数
  MyHandle& y(int y) { up->p.y(y); return *this; }    // 修改对象的函数
private:
  MyUPoint* up;            
}

  仔细考虑下赋值操作符,我们的目的是避免复制.因此赋值操作的意义相当于将等号左边句柄原来绑定的对象断开,原对象引用计数减1,绑定到等号右边的句柄对象上,引用计数加1.  为什么要先将引用计数先加1,是因为如果2个句柄指向同一个对象的时候也能保证正常工作.
 
  我们来看看上面的修改对象的函数.上面的实现方法将改变该句柄所绑定的对象的成员变量的值.所有绑定到这个对象的句柄都会取到同样的x值.
  看如下代码:
    MyHandle h(3,4);
    MyHandle h2 = h;

    h2.x1(5);
    int n = h.x();

  n将等于5.
  这是我们通常能得到的结果.这种结果是因为在修改函数中直接使用计数类对象的指针来实现.可以称为指针语意.它使得句柄使用起来象指针.
 
  有时候,我们又希望句柄使用起来象对象一样,一个句柄内容的改变不影响到另一个句柄,即使它们通过赋值进行了绑定.例如上面的示例代码,我们希望n等于3.这种情况我们称之为值语意.

  那么如何来实现值语意呢?肯定不能使用绑定的计数类指针(up)了.我们要求的是这个句柄不再影响其他绑定到同一对象的句柄了.怎么做?除非这个句柄绑定的对象没有别的句柄引用,否则只有先断开和原对象的绑定,才能停止对绑定对象的影响的.断开了绑定,那就必须绑定新的对象.新的对象从何而来呢?从原来绑定的对象重新创建一个吧.

  具体代码象这样:
  MyHandle& x1(int x)     // 值语意的版本
  {
     if(up->u != 1)    // 引用计数不为1,有别的句柄引用
     {
        --up->u;    // 原绑定对象引用计数减1 
        up = new MyUPoint(up->p);    // 创建新的绑定对象
     }
     up->p.x(x);    // 修改绑定对象的成员变量
     return *this;
  }

  于是我们就得到了值语意的句柄.而且在我们需要值语意的时候,在修改对象的函数中通常都要创建新的对象.但仅当我们要修改对象时才会复制原绑定的对象.这一技术通常称为"写时复制(copy on write)".
  作者提出的私有函数大致就是这样:    
   void CopyOnWrite()// 那个所谓写时复制的函数,当需要写的时候调用这个函数
   {
       if (up->u != 1)
       {
    --up->u;
    up = new MyUPoint(up->p);
       }
    }


  和代理类(Surrogate)比较,句柄类所能体现的优势是减少了复制对象的开销.但是同时增加了引用计数的开销,如果复制操作并不多,那么开销也不会很大.

  还有一种句柄的方案,那就是对一个句柄进行复制的时候,会将该句柄和原绑定的对象断开.
  例如:
    h1 = h;
  h将与绑定的对象断开,h1将会和该对象绑定.这么做是危险,你可能在没有意识到的情况下把句柄和所绑定的对象断开.

所有的代码,列在下面.

#include <stdio.h>


// 对象类,这个类将作为句柄所要表示的对象,也就是被抽象的内容
class MyPoint// 这是一个描述坐标点的类,它只有x,y两个坐标值作为成员变量
{
public:
  MyPoint() : xval(0), yval(0) {}  // 缺省构造函数
  MyPoint(int x, int y) : xval(x), yval(y) {} // 带参构造函数
  MyPoint(const MyPoint& p) : xval(p.x()), yval(p.y()) {} // 拷贝构造函数
  int x() const { return xval; }        // 属性
  int y() const { return yval; }        // 属性
  MyPoint& x(int xv) { xval = xv; return *this; }    // 修改属性
  MyPoint& y(int yv) { yval = yv; return *this; }    // 修改属性    
private:        
  int xval, yval;    // 私有的数据    
};


// 容纳引用计数和对象的类
class MyUPoint
{
// 所有成员都是私有的,初始化引用计数为1
private:
  ~MyUPoint() {} 
  MyUPoint() : u(1) {}    // 缺省构造,MyPoint也是缺省构造
  MyUPoint( int x, int y ) : p(x,y), u(1) {} // 带参数
  MyUPoint( const MyPoint& p0 ) : p(p0), u(1) {} // 带参数        
  MyPoint p;
  int u;
  friend class MyHandle;    // 句柄声明为友员
};

// 所谓的句柄类
class MyHandle
{
public:
 // 构造
  MyHandle() : up(new MyUPoint) {}    // 缺省构造
  MyHandle(int x, int y) : up(new MyUPoint(x, y)) {}    // 带参
  MyHandle(const MyPoint& p) : up(new MyUPoint(p)) {}    // 带参
  MyHandle(const MyHandle& h) : up(h.up) { ++up->u; }     // 拷贝构造
  // 赋值操作符
  MyHandle& operator=(const MyHandle& h)
  {
     h.up->u++;
     if(--up->u == 0)
     {
    delete up;
     }
     up = h.up;
     return *this;        
}

  ~MyHandle()
  { 
     if(--(up->u) == 0)
       delete up; 
  }
  
  int x() const { return up->p.x(); }
  int y() const { return up->p.y(); }
  MyHandle& x0(int x) { up->p.x(x); return *this; }    // 指针语意的版本
  MyHandle& y0(int y) { up->p.y(y); return *this; }    // 指针语意的版本
  MyHandle& x1(int x)     // 值语意的版本
  {
     if(up->u != 1)
     {
    --up->u;
           up = new MyUPoint(up->p);
     }
     up->p.x(x); 
     return *this;
  }

  MyHandle& y1(int y)     // 值语义的版本
  {
     if(up->u != 1)
     {
    --up->u;
    up = new MyUPoint(up->p);
     }
     up->p.y(y); 
     return *this;
   }
private:
   void CopyOnWrite()    // 那个所谓写时复制的函数,当需要写的时候调用这个函数
   {
      if (up->u != 1)
      {
    --up->u;
    up = new MyUPoint(up->p);
      }
   }

   MyUPoint* up;    
};

------------读 C++沉思录 第六章

回复列表 (共2个回复)

沙发

你这是把书都搬过来了吧

板凳

首先请问下看过原书么?
我看的是中文版的,我这本来就类似读书笔记,当然书上的东西很多.我只是按自己的理解和习惯整理了一下,希望和大家讨论~~,看是否有不对的地方,你的回复就不知道什么意思了.

我来回复

您尚未登录,请登录后再回复。点此登录或注册