1 为什么要使用句柄类?
句柄?windows中的句柄吗?那不是一个整数吗?不是,当然不是。句柄类应该算是C++中的一种技术,一种管理指针的技术,一种实现面向对象编程的技术。
为什么要使用句柄呢?
多态性是C++中的一个重要概念,指的是程序在运行过程中可以根据对象的实际类型来调用相应的函数,当然,这种方式的实现要满足两个条件:一是函数是虚函数,二是以对象指针或者引用来调用。因此,要想实现多态性,就要用指针或者引用来调用函数。假设你需要保存一系列的“对象”,但是,你不知道对象的实际类型,该如何实现呢?定义对象的容器是不行的,不管定义基类类型的容器还是派生类型的容器,那样的话,在调用函数的时候就不能应用多态,调用实际类型相应的函数。那么,用指针呢?可以,但是管理指针是件很麻烦的事,用户必须保证,指针指的对象有效,删除容器时,对象自动撤销。于是,就有了“句柄”。
句柄可以用来管理指针,以句柄代表的语义可以将句柄分为:指针型句柄和值型句柄。这里只讨论指针型句柄。
这里的内容并非全部原创,部分内容来自C++ Primer,加上自己的一些理解,就当作是做笔记吧。
2 一个句柄类
首先,给出一个句柄类的实现的例子:
class handle {
public:
handle() : p(0), use(new size_t(1)) { }
handle(const base& b) : p(b.clone()), use(new size_t(1)) { }
handle(const handle& h) : p(h.p), use(h.use) { ++*use; }
~handle() { decr_use(); }
handle& operator=(const handle&);
const base* operator->() const
{
if(p)
return p;
else
throw logic_error("unbound handle");
}
const base& operator*() const
{
if(p)
return *p;
else
throw logic_error("unbound handle");
}
private:
base *p;
size_t *use;
void decr_use()
{
if(--*use == 0) {
delete p;
delete use;
}
}
};
handle& handle::operator=(const handle& h)
{
++*h.use;
decr_use();
p = h.p;
use = h.use;
return *this;
}
上面是一个句柄类,主要是用指针指向基类的对象,用计数控制对象的复制和销毁。
句柄类定义了三个构造函数:
默认构造函数:没有形参,将指针置0,将计数置1,既然指针是0,也就是不指向任何对象,为什么不将计数置0呢?首先,如果采用默认构造函数创建句柄,这时是不能够使用的,因为句柄必然要和某个对象相关联,那么,接下来就必然要用operator=将另一个句柄对它进行赋值,看operator=的代码,如果此时*use已经等于0了,再减就是-1,不会调用delete p和delete use,之后p和use就指向新的内容,就会造成“内存泄漏”,因此,在这里将计数置1,接下来赋值时就可以调用decr_use进行销毁对象的内存。
以基类引用为参数的构造函数:将基类的对象或者派生类的对象与句柄关联,这里调用了引用的clone函数,它的功能之后介绍。
拷贝构造函数:将指针复制,计数值加1。
由于这是一个“指针型句柄”,也就是说,在使用它的时候,要将它当作指针来使用,因此,就要定义operator->和operator*,分别返回保存的指针和指向的对象的引用,通过这两个操作符的定义,就能够将句柄当作指针来使用。其实,从两个函数的返回值就可以看到,一个返回指针,一个返回引用,这不正是多态性的一个条件吗?
因为有指针操作,于是就需要定义operator=,它将左操作数保存的指针也指向右操作数保存的指针指向的对象,先将右操作数的计数加1,左操作数的计数减1,然后将左操作数的指针进行赋值,并返回引用。operator=通常要注意的问题是自身的赋值,如果这里有自身的赋值呢?计数先加1,再减1,保持不变,安全!!!
3 类层次
接下来,看看类层次的代码,还要看看刚在句柄的以基类引用为参数的构造函数所使用的clone函数。
class base {
public:
virtual base* clone() const
{
return new base(*this);
}
virtual void func() const
{
cout << "base function is called" << endl;
}
};
class derived : public base {
public:
derived* clone() const
{
return new derived(*this);
}
void func() const
{
cout << "derived function is called" << endl;
}
};
类的代码很简单,基类中定义了两个虚函数(通常来说,基类中应该定义虚析构函数,这里为了简单省略),func()作测试用,这里的clone函数是关键。
当要将一个基类的引用(可能是基类,也可能是派生类)与句柄关联时,该如何做呢?这里采用的方法是用基类的引用的对象创建一个对象,然后使句柄的指针指向该对象。在基类中定义一个名为clone()的虚函数,返回类型是基类的指针,在派生类中定义名为clone()的函数,返回类型是派生类的指针。等等?这样可以吗?虚函数的返回类型不同也可以吗?事实上,这样做是可以的,一般来说,虚函数的原型在类层次中应该是一样的,但是例外情况是当基类中的虚函数的返回类型是基类的指针或者引用时,派生类中对应的虚函数的返回类型可以是派生类的指针或者应用。懂了吗?好吧,返回到刚才的地方,在类层次中定义了名为clone()的函数,它生成一个对象的副本,并返回一个指向对象的指针,这个指针就给句柄中的指针。这就是在句柄中的构造函数的clone()的功能,而且,由于构造函数的参数是引用,clone()是虚函数,它具有多态性,可以根据引用的实际对象创建相应对象的副本。
4 句柄类的使用
vector<handle> hvec;
base b;
handle h1(b);
derived d;
handle h2(d);
hvec.push_back(h1);
hvec.push_back(h2);
hvec[0]->func();
hvec[1]->func();
上面是一个句柄类使用的例子,创建一个句柄类的vector,然后创建两个对象,一个是基类对象,一个是派生类的对象,分别将它们关联一个句柄,接下来,将两个句柄加入到vector中,然后对vector的两个成员调用func()函数。结果是:
base function is called
derived function is called
也就是说它们具有多态性,而且具有类似指针的使用方法。
使用句柄,可以降低管理指针的复杂性,还能够获取多态性。