一直不能理解句柄类究竟有什么作用,尽管看到很多C++书籍中对句柄的使用进行了介绍。直到最近碰到一个问题才让我对句柄类的使用有了较为深入地了解。
这个问题简单来说是这样:设想一个类封装了一种数据结构,当我们声明该类的一个指针对象来指向这种数据结构的一块数据时,有两种情况:
1. 对数据块进行读取操作,并不改变数据的值
2. 对数据块进行改写操作,但要保留原数据块(因为其他地方要使用)
对于第一种情况,我们只需要构造一个类的指针对象,将他指向这个数据块。
对于第二种情况,问题有些不同。首先,需要在类的构造函数中重新申请内存将数据块拷贝过来,然后进行改写。但问题在于什么时候去申请新的数据块我们并不清楚。根据“写时复制”的原则,只有在我们需要改写数据块的时候才应该去重新申请内存。倘若一直没有对数据块进行改写,所有类的指针对象可以共享这个数据块中的内容。
这种内存管理的思想被许多程序框架使用,例如Qt中的隐式共享,在内容有变动的情况下才对的数据结构做复制,否则仅做共享。看一个例子:
QString str1 = "one"; //str1指向"one"
QString str2 = str1; //str2没有改变"one",此时"one"被共享两次
此时,指向"one"的计数器的值为2。现在,倘若我们改变str2指向的内容
str2 = "two"; //改变str2的内容
str2指向了新的数据块,因此"one"的计数减为1,"two"计数变为1。此时在改变str1的值
str1 = str2;
此时,"two"的计数增加到2,"one"计数变为0。这种对内存管理的方式可以用一个新的类来表示,这个类就是句柄。
简单来说,句柄就将指向数据的指针和该数据共享次数封装起来,我们直接看一个例子,假设有一个坐标类Point,建立一个Handle类来管理Point
Point类很简单,有两个成员(x,y)
class Handle;
class Point
{
public:
friend Handle;
Point(int a=0, int b=0):x(a), y(b){};
~Point();
Point& operator =(const Point &rhs);
public:
int x;
int y;
};
Point::~Point()
{
}
Point& Point::operator =(const Point &rhs)
{
x = rhs.x+1;
y = rhs.y+1;
return *this;
}
句柄类
class Handle
{
public:
Handle();
Handle(int x, int y);
Handle(const Point &Data);
~Handle();
Handle &operator =(const Handle &rhs);
private:
Point *data;
int *num; //表示*data被共享的次数
void destroy();
};
Handle::Handle()
{
num = new int(1);
data = new Point(); //缺省复制构造函数将Point初始化为远点坐标
}
Handle::Handle(int x, int y)
{
num = new int(1);
data = new Point(x,y);
}
Handle::Handle(const Point &Data)
{
num = new int(1);
data = new Point(Data);
}
Handle::~Handle()
{
destroy();
}
void Handle::destroy()
{
if(--*num == 0) //如果指向数据的指针只有一个,则直接delete
{ //因为如果改变data指向的数据,则指向这块数据的指针个数为0
delete data;
data = 0;
delete num;
num = 0;
}
}
Handle &Handle::operator =(const Handle &rhs)
{
(*rhs.num)++; //首先将右操作数的计数器加1,
destroy();
data = rhs.data;
num = rhs.num;
return *this;
}
int main(void)
{
Handle h(1,1);
Handle h1;
Handle h2;
std::cout<<"h.num = "<<*h.num<<" h1.num = "<<*h1.num<<" h2.num = "<<*h2.num<<std::endl; //三个对象引用计数都为1
h1 = h; //指向h.data的引用计数变为2
std::cout<<"h.num = "<<*h.num<<" h1.num = "<<*h1.num<<" h2.num = "<<*h2.num<<std::endl;
h1 = h2; //指向h2.data的引用计数变为2,h.num减少为1
std::cout<<"h.num = "<<*h.num<<" h1.num = "<<*h1.num<<" h2.num = "<<*h2.num<<std::endl;
return 0;
}
打印结果
h.num = 1 h1.num = 1 h2.num = 1
h.num = 2 h1.num = 2 h2.num = 1
h.num = 1 h1.num = 2 h2.num = 2
这样就实现了写时复制和对指针的动态管理,节省了复制数据时的开销