句柄类解决了什么问题?
- 跟代理类相同,解决了保存不同类型对象,同时保持多态性质的问题。
- 改进了代理类每次都需要进行内存拷贝的问题,采用引用计数和写时拷贝提升代码效率。
句柄类重点知识
-
句柄类的成员中,需要保存一个指向持有对象的指针和一个指向引用计数的指针。
句柄类 - 句柄类应当和所持有的对象保持一致的行为。
例子
- 首先我们需要一个类,它可以是存在继承体系结构中的任意类,为了方便我们定义一个简单的类进行描述。
class Point
{
public:
Point():x(0),y(0) {}
Point(int x, int y): x(x), y(y){}
Point& x(int x) {this->x = x; return *this;}
Point& y(int y){this->y = y; return *this;}
int x() const {return this->x;}
int y() const {return this->y;}
private:
int x, y;
};
- 绑定到句柄类中
假设我们已经有了一个句柄类-Handle,则绑定到句柄类的操作:Point p; Handle h(p);
正确的解释应该是h绑定到一个跟局部变量p一样的对象,而不是绑定到局部变量p。否则,会出现对象p被销毁两次的情况。
所以我们的handle类应该提供跟Point类类似的构造函数:可以通过Handle h0(x0, y0);
,Handle h(p);
和Handle h1();
来控制创建Point对象。 - 声明Handle类的成员函数
class Handle {
public:
Handle();
Handle(int, int);
Handle(const Point&);
Handle(const Handle&); // 因为持有对象,所以需要定义拷贝构造函数和赋值操作符
Handle& operator=(const Handle&);
~Handle(); // 因为持有对象,需要在析构函数中进行资源回收,所以需要定义
/** 这里开始需要实现Point的行为 **/
int x() const;
Handle& x(int x); // 这里返回值类型为 Handle& 防止暴露持有的对象地址
int y() const;
Handle& y(int y);
private:
/** 私有成员应该是什么呢? **/
};
- 这里的私有成员应该是什么呢?
首先肯定需要一个持有对象的指针Point* p;
。
因为我们是要解决不必要的拷贝问题,所以应该存在一个引用计数的属性。但是引用计数如果当作int
类型直接放在句柄类中,会导致当我们更新引用计数时,必须找到指向同一个对象的所有句柄类对象,同时更新他们的引用计数才行。如果将引用计数这个属性加入到所持有的对象中呢?这样就会要求我们修改要持有的类,某些场景下这是不可能实现的。所以我们还需要另外一个间接层,将引用计数脱离Handle和Point类,单独存放。
class Handle {
public:
Handle();
Handle(int, int);
Handle(const Point&);
Handle(const Handle&); // 因为持有对象,所以需要定义拷贝构造函数和赋值操作符
Handle& operator=(const Handle&);
~Handle(); // 因为持有对象,需要在析构函数中进行资源回收,所以需要定义
/** 这里开始需要实现Point的行为 **/
int x() const;
Handle& x(int x); // 这里返回值类型为 Handle& 防止暴露持有的对象地址
int y() const;
Handle& y(int y);
private:
Point* p; // 指向所持有的对象
int* use_count_ptr; // 指向引用计数区域,这里用int*来表示,也可以单独作为一个类来使用。
};
- 我们来实现它吧!
Handle::Handle():p(new Point()),u(new int(1)) {} // 新建一个Point对象,同时引用计数初始值设置为1
Handle::Handle(int x, int y): p(new Point(x,y)), u(new int(1)) {}
Handle::Handle(const Point& point) : p(new Point(point)), u(new int(1)) {}
Handle::Handle(const Handle& h): p(h.p), u(h.u) { ++(*u); } // 复制构造函数,将p,u指向h的p,u,同时引用计数加1
Handle& Handle::operator=(const Handle& h)
{
++ *h.use_count_ptr; // 先将右侧的引用计数加一,方式自己赋值给自己时先减到0导致资源释放
if (-- *use_count_ptr == 0) // 引用计数减1如果是0则清理资源
{
delete use_count_ptr;
delete p;
}
use_count_ptr = h.use_count_ptr;
p = h.p;
return *this;
}
Handle::~Handle()
{
if (-- *use_count_ptr == 0) // -- *use_count_ptr 等价于 ((*use_count_ptr)-- - 1)
{
delete use_count_ptr;
delete p;
}
}
- 现在开始考虑实现Point的行为
int Handle::x() const
{
return p->x();
}
int Handle::y() const
{
return p->y();
}
针对赋值的两个行为,我们可以考虑实现两种语义:值语义和引用语义。
采用引用语义则不存在复制Point对象的问题,但是会出现修改一个Handle对象会影响所有指向同一个Point对象的Handle对象。
引用语义的实现:
Handle& Handle::x(int x)
{
p->x(x);
return *this;
}
Handle& Handle::y(int y)
{
p->y(y);
return *this;
}
值语义的实现:
Handle& Handle::x(int x)
{
// 检查use_count_ptr是否为1,不是1表示该对象不是该handle唯一持有的,需要复制一份数据提供修改。
if (*use_count_ptr != 1) {
-- *use_count_ptr; // 先调整引用计数
use_count_ptr = new int(1); // 新建一个引用计数并初始化为1
p = new Point(*p); // 复制一个Point对象唯一持有
}
p->x(x);
return *this;
}
- 不想在Handle里面玩弄这个引用计数的指针应该怎么优化?
上面值语义的实现还是比较复杂的,对于句柄类来讲维护引用计数的行为不是主要行为,所以我们考虑将引用计数封装。同时也方便我们后续的使用。
class UseCount
{
public:
UseCount():count(new int(1)) {}
UseCount(const UseCount& other): count(other.count) { ++ *count; }
~UseCount() { if (-- *count == 0) { delete count; } }
UseCount& operator=(const UseCount& other) = delete; // 不支持赋值操作符* 后续说明为什么
private:
int* count;
};
class Handle {
public:
Handle();
Handle(int, int);
Handle(const Point&);
Handle(const Handle&); // 因为持有对象,所以需要定义拷贝构造函数和赋值操作符
Handle& operator=(const Handle&);
~Handle(); // 因为持有对象,需要在析构函数中进行资源回收,所以需要定义
/** 这里开始需要实现Point的行为 **/
int x() const;
Handle& x(int x); // 这里返回值类型为 Handle& 防止暴露持有的对象地址
int y() const;
Handle& y(int y);
private:
Point* p; // 指向所持有的对象
UseCount uc;
};
我们没有定义operator=
操作,因为如果实现如下:
UseCount& UseCount::operator=(const UseCount& other)
{
++ *other.count;
if (-- *count == 0)
{
delete count; // 此处外面的用户也应该删除自己持有的资源,本例中应该删除Point对象才对。但是Handle无从得知这件事情。
}
count = other.count;
return *this;
}
所以我们提供另外一个函数来实现这种操作,书中作者也表明该操作的名字不是很好取,这里遵循书中的名称。
class UseCount
{
public:
UseCount():count(new int(1)) {}
UseCount(const UseCount& other): count(other.count) { ++ *count; }
~UseCount() { if (-- *count == 0) { delete count; } }
UseCount& operator=(const UseCount& other) = delete; // 不支持赋值操作符* 后续说明为什么
bool reattach(const UseCount& other);
private:
int* count;
};
bool UseCount::reattach(const UseCount& other)
{
++ *other.count;
if (-- *count == 0) {
delete count;
count = other.count;
return true;
}
count = other.count;
return false;
}
则Handle的operator=
可以实现为:
Handle& Handle::operator=(const Handle& other)
{
if (uc.reattach(other.uc)) {
delete p; // 引用计数到0了,所以应该释放掉持有的Point对象
}
p = other.p;
return *this;
}
进一步实现Handle的构造函数
Handle::Handle(): p(new Point()) {} // 因为Handle使用的是UseCount对象不是指针,所以可以依赖UseCount的默认构造函数初始化引用计数为1
Handle::Handle(int x, int y) : p(new Point(x, y)) {} // 注意Handle的uc的初始化在构造函数执行之前就已经初始化好了。
Handle::Handle(const Point& point): p(new Point(point)) {} // 注意这里是新建一个对象,防止对局部变量取地址
Handle::Handle(const Handle& other): p(other.p), uc(other.uc) {} // 指向同一个所持对象,同时修改引用计数的操作交给UseCount的拷贝构造函数处理
Handle::~Handle()
{
if (/*唯一持有对象的handle?*/) {
delete p; // 销毁持有的对象,uc则在Handle的析构函数执行完成后执行自己的析构函数
}
}
所以我们需要一种判断是不是唯一持有对象的Handle,这里我们需要UseCount提供一个public的方法。
bool UseCount::only() { return *count == 1; } // 此处省略了该方法的声明,需要补充在类定义中。
// 然后实现Handle的析构方法
Handle::~Handle()
{
if (uc.only())
{
delete p;
}
}
- 写时复制应该怎么实现呢?
跟本身持有引用计数指针类似,只有当唯一持有某个对象的时候才直接修改,否则都创建一个新的Point副本,同时将引用计数强制设置为1.
UseCount提供的only方法只能判断是不是唯一持有对象的handle,不能实现强制设置引用计数为1,所以我们需要UseCount提供一个新的方法:makeOnly()
bool UseCount::makeOnly()
{
if (*count == 1) {
// 唯一持有对象,外部使用方不需要进行复制,可以直接修改。
return false;
}
// 先将引用计数减1,表明不再指向该对象。
-- *count;
// 新建一个引用计数,设置为1,表示将要唯一持有一个新对象,外部使用方需要进行复制操作。
count = new int(1);
return true;
}
Handle& Handle::x(int x)
{
if (uc.makeOnly()) {
// 进行复制操作
p = new Point(*p);
}
p->x(x);
return *this;
}
至此完全实现了基于引用计数的句柄类。
总结
通过引入引用计数我们可以减少不必要的复制;
通过将引用计数单独封装为类,我们的Handle类减少了针对引用计数的操作,同时可以将封装的引用计数进行复用。