句柄类
发布日期:2021-06-28 22:15:36 浏览次数:2 分类:技术文章

本文共 13432 字,大约阅读时间需要 44 分钟。

 

 

一般定义

句柄类(
smart point)是存储指向动态分配(堆)
的类。除了能够在适当的时间自动删除指向的对象外,他们的工作机制很像C++的内置
在面对异常的时候格外有用,因为他们能够确保正确的销毁动态分配的对象。他们也可以用于跟踪被多用户共享的动态分配对象。
在C++中一个通用的技术是定义包装(cover)类或句柄(handle)类,也称
。句柄类存储和管理
所指向对象的类型可以变化,它既可以指向
类型对象又可以指向派生类型对象。用户通过句柄类访问继承层次的操作。因为句柄类使用
执行操作,虚成员的行为将在运行时根据句柄实际绑定的对象类型而变化,即实现c++运行时
。故句柄用户可以获得动态行为但无需操心
的管理。

理解实质

句柄类实际上通过复制指向引用计数器类型的
,来代替复制实际对象;从而使得复制对象变成复制
,实现虚拟复制(即是用复制对象的地址代替复制对象本身),以提高内存效率和访问速度。在涉及到大型、复杂对象以及一些不能复制或是不易复制的对象的复制控制问题时,显得特别有用。
 

引入使用计数

定义句柄类或 的通用技术是采用一个使用计数(use count)。句柄类将一个计数器与类指向的对象相关联。使用计数跟踪该类有多少个 共享同一对象。当使用计数为0时,就删除该类对象,否则再删除类对象时,只要 不为0,就不删除实际的类对象,而是是引用计数减1,实现虚删除。

使用计数类

为了便于理解,我们定义一个实际类(Point),一个引用计数器类(UPoint),一个句柄类(Handle),后面将有例子给以参考。
实现使用计数有两种经典策略:一种是定义一个单独的具体的类用以 使用计数和指向实际类的 ;
另一种是定义一个单独的具体的类用以封装 和类的对象成员。我们称这种类为计数器类(UPoint)。在计数器类中,所有成员均设置为private,避免外部访问,但是将句柄类Handle类声明为自己的 ,从而使句柄类能操纵引用计数器。

写时复制

写时复制(copy on write)技术是解决如何保证要改动的那个引用计数器类UPoint对象不能同时被任何其他的句柄类(Handle类)所引用。通俗的来说,就是当实际对象Point被多个Handle类的 共享时,如果需要通过指针改变实际对象Point,而其他的指针又需要保持原来的值时,这就有矛盾了。打个不恰当的比方来说,两个以上的人共有5W块钱,如果其中一个人想用这5W块钱去消费,那就必须通知其他人。否则在这个人消费了5块钱后,其他人还以为他们仍然有5W块钱,如果这儿时候,他们去买5W的东西,就会发现钱变少了或是没有了,此时他们就陷入债务的泥团。在C++中通过 访问已经删除或是不存在的对象,将是非常危险的。有可能系统提示该行为未定义,也有可以内存非法访问,还有可能使系统崩溃。

句柄类的实例

完成源码程序

#include <iostream>
using namespace std;
// 句柄类中封装的实际对象类型;Point;
class Point
{
public:
// 和带有两个参数的构造函数;
Point():xval(0), yval(0) {}
Point(int x, int y) : xval(x), yval(y) {}
int x() const { return xval; }
int y() const { return yval; }
Point & x(int xv) { xval = xv; return *this; }
Point & y(int yv) { yval = yv; return *this; }
~Point()
{
cout << "~Point!" << endl; //测试时用于分析对象的析构过程;
}
protected:
private:
int xval;
int yval;
};
// 计算器类;
class UPoint
{
//所有的 和函数成员均为:private类型;
friend class Handle; //将句柄类声明为友元类,便于访问引用计数器;
Point p; //声明一个Point实际对象的 ;实现 ;
int u; //引用计数器;
//当创建一个handle句柄对象时,将通过 类自动创建一个Point的真实对象;
//同时使计数器 U 自动为1;
UPoint():u(1) {}
UPoint(int x, int y) : p(x, y), u(1) {}
UPoint (const Point & p0): p(p0), u(1) {}
~UPoint()
{
cout << "~UPoint!" << endl; //测试时用于分析对象的析构过程;
}
};
//句柄类实际上通过复制指向引用计数器类型的指针,来代替复制实际对象;从而使得
//复制对象变成复制 ,提高内存效率和访问速度;
class Handle
{
public:
//句柄类的 ,主要通过复制句柄中指向 的 来实现对象复制的虚拟操作;
//3个 ;
Handle(): up(new UPoint) {}
Handle(int x, int y): up(new UPoint(x,y)) {}
Handle(const Point &p): up(new UPoint(p)) {}
//复制函数;使 自动加 1;
Handle(const Handle &h ) : up(h.up) {++up->u;}
// ;使赋值右侧对象的 自动加 1;
Handle& operator= (const Handle &h)
{
++h.up->u; //赋值右侧引用计数器加 1;
//如果up的引用计数器为1,就直接删除up所指向的对象;
//经验,任何改变实际引用计数器状态变化的操作都应该检查引用计算器
//是否为 0 ,为0就应该删除实际操作对象;
if (--up->u == 0)
delete up;
up = h.up;
return *this; //返回修改 up地址值的自身兑现引用;
}
~Handle()
{
//如果 up 所指的对象 为 u 为最后一个,则应该删除对象;
if (--up->u == 0)
delete up;
cout << "~Handle!" << endl;
}
int x() const
{
return up->p.x();
}
Handle & x(int x0) //采用句柄的值语义;
{
//更改up对象的值时,当 不为 1时,则说明实际对象point 关联多个
//handle的 up ,为了保持原来指针所指向对象的数据不被改变;此时
//需要 将该对象的 减 1, 然后重新定义一个新引用计数器类型;
if (up->u != 1)
{
//将原对象的 减 1;再申请一个引用计数器对象;
//采用写时复制技术解决更改Point对象时所带来的问题;
--up->u;
up = new UPoint(up->p);
}
up->p.x(x0);
return *this;
}
int y() const
{
return up->p.y();
}
Handle & y(int y0)
{
if (up->u != 1)
{
--up->u;
up = new UPoint(up->p);
}
up->p.y(y0);
return *this;
}
// protected:
private:
UPoint *up;
};
int main(int argc, char *argv[])
{
Handle h(23, 34), tmp(h); //定义句柄对象h,和tmp
Handle val; //定义默认的句柄类对象;
//通过句柄类执行实际类Point 成员函数功能;
cout << "handle::h " << h.x() << '\t' << h.y() << endl;
cout << "handle::tmp " << tmp.x() << '\t' << tmp.y() << endl;
cout << "handle::val "<< val.x() << '\t' << val.y() << endl;
//关键的一步val = h;将涉及写时复制;因为h和tmp 共享同一指针;
val = h.x(100); //改变句柄类h所指对象point对象x的值后赋值给val句柄;
cout << "handle::h " << h.x() << '\t' << h.y() << endl;
cout << "handle::tmp " << tmp.x() << '\t' << tmp.y() << endl;
cout << "handle::val "<< val.x() << '\t' << val.y() << endl;
return 0;
}

运行结果分析

handle::h 23 34
handle::tmp 23 34 //h和tmp句柄类对象实际共享同一Point(23,34)对象;
handle::val 0 0 //val默认指向实际对象Point(0,0);
~UPoint! //由于写时复制时:up = new UPoint(up->p);创建了一个临时UPoint
~Point! //对象;调用完后释放,由于Point是UPoint的成员对象,所以先
//UPoint,然后是Point。
handle::h 100 34 //通过修改赋值val = h.x(100);后,h和val共享同一对象Point(100,34)
handle::tmp 23 34
handle::val 100 34
//依次释放内存;
~Handle! //val句柄类对象;val(100,34)
~UPoint!
~Point!
~Handle! //tmp句柄类;tmp(23,34)
~UPoint!
~Point!
~Handle! //只释放了一个句柄类Handle的 ,没有实际对象;
请按任意键继续. . .
 
与句柄类相关的类
代理(surrogate)类,又称委托。(后续完善
[1]
这篇关于句柄类的介绍,在理论部分还是挺不错的,但举的例子就不敢恭维了,UPoint类的目的是从Handle类中将“ ”功能分离出来单独实现,让Handle类专注于实现“ ”功能,因此UPoint类必须能够实现自动计数,而不应该在Handle类中再来对其加减。
上面分析结果的最后一段:
~Handle! //val句柄类对象;val(100,34) ~UPoint! ~Point! ~Handle! //tmp句柄类;tmp(23,34) ~UPoint! ~Point! ~Handle! //只释放了一个句柄类Handle的指针,没有实际对象; 请按 继续. . .
分析有误,改为:
~Handle! //val句柄类对象;val(100,34)
~UPoint! //temp指向的UPoint对象 
~Point! //temp指向的UPoint对象的内嵌Point对象
~Handle! //tmp句柄类;tmp(23,34) 
~UPoint! //val和h共同指向的Upoint对象 
~Point! //val和h共同指向的Upoint对象的内嵌对象Point
~Handle! //释放h句柄类

句柄类例子

#include <iostream>
using namespace std;
//-----------------------------------------
class Point
{
private:
int xval,yval;
public:
Point():xval(0),yval(0){}
Point(int x,int y):xval(x),yval(y){}
int x()const{return xval;}
int y()const{return yval;}
Point& x(int xv){xval=xv;return *this;}
Point& y(int yv){yval=yv;return *this;}
};
//------------------------------------------------------
class UseCount
{
private:
int* p;
UseCount& operator=(const UseCount&);
public:
UseCount();
UseCount(const UseCount&);
~UseCount();
bool only();
bool reattach(const UseCount&);
bool make_only();
};
UseCount::UseCount():p(new int(1)){}
UseCount::UseCount(const UseCount&u):p(u.p){++*p;}
UseCount::~UseCount()
{
if (--*p==0)
{
delete p;
}
}
bool UseCount::only()
{
return *p==1;
}
bool UseCount::reattach(const UseCount& u)
{
++*u.p;
if (--*p==0)
{
delete p;
p=u.p;
return true;
}
p=u.p;
return false;
}
bool UseCount::make_only()
{
if (*p==1)return false;
--*p;
p=new int(1);
return true;
}
//-------------------------------------------
class Handle
{
private:
Point* p;
UseCount u;
public:
Handle();
Handle(int,int);
Handle(const Point&);
Handle(const Handle&);
Handle& operator =(const Handle&);
~Handle();
int x()const;
Handle&x(int);
int y()const;
Handle&y(int);
};
Handle::Handle():p(new Point){}
Handle::Handle(int x,int y):p(new Point(x,y)){}
Handle::Handle(const Point&p0):p(new Point(p0)){}
Handle::Handle(const Handle&h):u(h.u),p(h.p){}
Handle::~Handle()
{
if (u.only())
{
delete p;
}
}
Handle& Handle::operator=(const Handle &h)
{
if (u.reattach(h.u))delete p;
p=h.p;
return *this;
}
int Handle::x()const
{
return p->x();
}
int Handle::y()const
{
return p->y();
}
Handle& Handle::x(int x0)
{
if (u.make_only())p=new Point(*p);
p->x(x0);
return *this;
}
Handle& Handle::y(int y0)
{
if (u.make_only())p=new Point(*p);
{
p->y(y0);
return *this;
}
}
//---------------------------------------------------
int main()
{
Handle h(3,4);
Handle h2 = h;
h2.x(5);
int n = h.x();
cout<<n<<endl;
return 0;
}
 
 
 

为何使用句柄类?

首先就是复制问题.前面有谈到,有些类内部数据很多,采用复制消耗非常大,这种情况下就必须采用句柄类来进行操作.

其次是由于函数的参数和返回值都是采用了复制进行自动传递.虽然c++中引用可以避免,但是很多情况下返回值采用引用并不明智.

对于采用指针的方式,可以解决问题,但是又会引入调用者对于动态管理内存的麻烦.而这往往是很多错误的根源.

 

何为句柄类呢?

句柄类可以理解为采用了引用计数的代理类.

其多个句柄共享了同一个被代理的类.通过引用计数的方式来减少复制以及内存管理.

其行为类似指针,因此也有智能指针之称,但其实差别很大.后面会有讲述.

 

句柄类例子:

先有一个简单的类Point

1 class Point 2 {
/*{
{
{
*/ 3 public: 4 Point():_x(0),_y(0){} 5 Point(int x,int y):_x(x),_y(y){} 6 int x()const {
return _x;} 7 void x(int xv) { _x = xv;} 8 int y()const { return _y;} 9 void y(int yv) { _y = yv;}10 private:11 int _x;12 int _y;13 };/*}}}*/

接下来我们要定义其的Handle类.

我们的Handle类:

1 class Handle 2 { 3 public: 4     Handle():up(new UPoint){} 5     Handle(int x,int y):up(new UPoint(x,y)){} 6     Handle(const Point&p):up(new UPoint(p)){} 7     Handle(const Handle &h); 8     ~Handle(); 9     Handle& operator=(const Handle &h);10     int x() const{ return up->p.x(); }11     int y() const{ return up->p.y(); }12     Handle& x(int);13     Handle& y(int);14 15     16 private:17     UPoint *up;18     void allocup();19 };

这里说明我们的Handle和指针的不同之处.

也许有读者会对Handle有疑问,为什么不采用operator->来直接操作point呢?

其实顾虑就是operator->返回的是point的地址.也就是使用者可以轻易的获得point的地址进行操作,这并不是我们想要的.这也就是Handle也pointer不想同的地方.

UPoint是为了采用引用计数定义的数据结构

1 //all member is private..only assess by Handle 2 class UPoint 3 {
/*{
{
{
*/ 4 friend class Handle; 5 6 Point p; 7 int u;//count 8 9 UPoint():u(0){}10 UPoint(const Point&pv):p(pv){}11 UPoint(int x,int y):p(x,y),u(1){}12 UPoint(const UPoint &up):p(up.p),u(1){}13 };/*}}}*/

 

对于Handle类的操作,我们要在Handle类进行复制的时候,累加Handle指向的UPoint的计数值

即复制构造函数以及赋值函数

1 Handle::Handle(const Handle &h) 2     :up(h.up) 3 { 4   ++up->u; 5 } 6  7 Handle& Handle::operator=(const Handle &h) 8 { 9    ++h.up->u;10   if (--up->u == 0)11       delete up;12   up = h.up;13   return *this;14 }

而对于析构函数,则是减小引用计数,如果减到0了,就说明没有其他的Handle指向了UPoint,因此我们要删除掉.

1 Handle::~Handle()2 {3    if (--up->u == 0)4      delete up;5 }

剩下的就是定义Handle对于Point的操作了.即Handle::x(int xv)和Handle::(int yv)了.

这里有2种写法.

一种是像指针一样,对于赋值,就直接修改指向的Point里面的值.这种方法有一个问题,即所以都指向这个Point的Handle类获取的x值都会变化.

代码:

1 //point like 2 Handle& Handle::x(int xv) 3 { 4   up->p.x(xv); 5   return *this; 6 } 7 //point like 8 Handle& Handle::y(int yv) 9 {10   up->p.y(yv);11   return *this;12 }

 

还有一种是写时复制技术,即每次对于共享的Point进行修改的时候都复制一份新的Point,然后进行修改.

这种技术在Handle中大量采用.在stl中,string也采用了同样的方法.

其额外开销很小,而效率也不差.

代码:

1 void Handle::allocup() 2 { 3    if (up->u != 1) 4    { 5     --up->u; 6     up = new UPoint(up->p); 7    } 8 } 9 10 Handle& Handle::x(int xv)11 {12    allocup();13    up->p.x(xv);14    return *this;15 }16 17 Handle& Handle::y(int yv)18 {19    allocup();20    up->p.y(yv);21    return *this;22 }
 
 

在本文中,我们将要设计一种新的写法,利用UseCount类来为Handle所指的对象计数,同时减少UPoint类,同时简化Handle的写法。

 

在前文中,我们知道了Handle类需要计数。通过计数和指针来完成减少复制,达到优化的目的。

上文的写法:

1 class Point 2 {
/*{
{
{
*/ 3 public: 4 Point():_x(0),_y(0){} 5 Point(int x,int y):_x(x),_y(y){} 6 int x()const {
return _x;} 7 void x(int xv) { _x = xv;} 8 int y()const { return _y;} 9 void y(int yv) { _y = yv;}10 private:11 int _x;12 int _y;13 };/*}}}*/14 15 16 //all member is private..only assess by Handle17 class UPoint18 {
/*{
{
{
*/19 friend class Handle;20 21 Point p;22 int u;//count23 24 //省略25 };/*}}}*/26 27 class Handle28 {29 public:30 //省略31 32 private:33 UPoint *up;34 };

可以看到,计数是由UPoint来实现的.由Handle来管理.

但做为编写者的我们肯定是不愿意这么写的.这意味着每写一个Handle都得写一个这样的Uxxxx.实在是费事.

因此,我们可以很直观的想到一个写法,即在Handle里面指向的是Point *p和int *count;

这样就没有这个麻烦了.

当然这是一个很好的解决方案.

这里我再提出一种方案,即抽象引用计数,增加一个UseCount类.

通过这个类来计数.其是通用的.所有的Handle都可以使用.其次,这可以减少我们的指针操作.

实例如下:

被Handle管理的类的声明:

1 class Point 2 {
/*{
{
{
*/ 3 public: 4 Point():_x(0),_y(0){} 5 Point(int x,int y):_x(x),_y(y){} 6 int x()const {
return _x;} 7 void x(int xv) { _x = xv;} 8 int y()const { return _y;} 9 void y(int yv) { _y = yv;}10 private:11 int _x;12 int _y;13 };/*}}}*/

这个和原来的是一样的.

再来看看我们的UseCount

1 class UseCount 2 { 3 public: 4     UseCount():count(new int(1)){} 5     UseCount(const UseCount& uc):count(uc.count){ ++*count;} 6     UseCount& operator=(const UseCount &u); 7     ~UseCount(); 8     bool isonly() { return *count == 1;}//判断是否只指向一个计数,用于判断是否要删除 9     bool reattach(const UseCount &u);//重新连接,用于复制10     bool makeonly();//分配一个新的,用于写时复制技术11 private:12     int *count;//计数13 };14 15 UseCount& UseCount::operator=(const UseCount &u)16 {17    reattach(u);18    return *this;19 }20 21 UseCount::~UseCount()22 {23    if (--*count == 0)24     delete count;25 }26 bool UseCount::reattach(const UseCount &u)27 {28   29   ++*u.count;30   if (-- *u.count == 0)31   {32      delete count;33      count = u.count;34      return true;     35   }36   count = u.count;37   return false; 38 }39 40 bool UseCount::makeonly()41 {42   if (*count == 1)43     return false;44   --*count;45   count = new int(1);46   return true;47 }

这个类实现了计数的功能.其内部有一个count的int 指针.在进行UseCount的复制的时候仅仅是++use.

其他的功能函数我也加了注释.大家应该能看懂.

剩下的就是我们的使用UseCount的Handle类了.

1 class Handle 2 { 3 public: 4     Handle():p(new Point){} 5     Handle(int x,int y):p(new Point(x,y)){}//使用了UseCount使得我们的构造函数显得异常简单, 6     Handle(const Point&pp):p(new Point(pp)){}//仅仅是分配了Point的空间而已 7     Handle(const Handle &h):p(h.p),count(h.count){};//复制构造函数也很简单.其实可以省略,也不会出错 8     ~Handle(); 9     Handle& operator=(const Handle &h);10     int x() const{ return p->x(); }11     int y() const{ return p->y(); }12     Handle& x(int);13     Handle& y(int);14 15     16 private:17     Point *p;//被Handle的对象18     UseCount count;//使用UseCount19 };

我们来看看各个函数

析构函数:

1 Handle::~Handle()2 {3     if (count.isonly())4     delete p;5 6 }

这个析构函数判断了p是否只指向了一个对象,如果是的话,因为要析构,所以p也要相应的被删除

=操作符:

1 Handle& Handle::operator=(const Handle &h)2 {3   if (count.reattach(h.count))4       delete p;5   p = h.p;6   return *this;7 }

reattach函数返回bool指,如果count == 1的话,返回值为true,那就要把p给删除.

采用了写时复制技术的x(int),y(int)

1 Handle& Handle::x(int x0) 2 { 3   if (count.makeonly()) 4       p = new Point(*p); 5   p->x(x0); 6   return *this; 7 } 8  9 Handle& Handle::y(int y0)10 {11    if (count.makeonly())12      p = new Point(*p);13    p->y(y0);14    return *this;15 16 }

makeonly()使UseCount分配一个新的计数值,返回true说明需要分配,false则代表其本身就只指向了一个对象,不需要重新分配.

最后是咱们的测试main函数

1 int main() 2 { 3   Handle h(10,10); 4   cout << h.x() << " " << h.y() << endl; 5   h.x(20); 6   cout << h.x() << " " << h.y() << endl;  7   Handle h2(h); 8   cout << h2.x() << " " << h2.y() << endl; 9   return 0;  10 }

总结:

      我们可以看到,通过抽象计数对象,我们的Handle的操作显得特别简单,减少了很多的指针操作.同时也减少了出错的可能.

      而通过这个方法,我们也减少了一个UPoint类的书写.简化了操作.

  Handle类有智能指针一说.其行为有点像指针.同时又省却了我们指针操作的麻烦.而且,减少了很多复制的操作.效率和代码的健壮性得到了提高.

 
 
 
 
 

转载地址:https://blog.csdn.net/yhhwatl/article/details/11882191 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:STL 之 map
下一篇:c++ 基础 之 dynamic_cast (java : instanceof)

发表评论

最新留言

留言是一种美德,欢迎回访!
[***.207.175.100]2024年04月21日 01时07分12秒