Item 14: Think carefully about copying behavior in resource-managing classes.
这一节是承接Item13来接着讲关于资源管理类中的拷贝行为。说实话这一节还有很多地方我没想的很清楚,这里先做个记录。
回顾前面,item13介绍了RAII的资源管理法则,同时提到了智能指针对于堆资源的应用。
但是很多情况智能指针就不太合适,需要自己来写资源管理类。
接着作者给出了互斥锁mutex的例子,互斥锁有lock和unlock两个函数可以使用。为了保证正确对锁加锁解锁,一般会建立一个类来管理锁的机制,管理锁的类遵循RAII的思想,在构造函数中对mutex加锁,析构函数中解锁 代码如下
class Lock {
public:
explicit Lock(Mutex *pm)
: mutexPtr(pm)
{ lock(mutexPtr); } // acquire resource
~Lock() { unlock(mutexPtr); } // release resource
private:
Mutex *mutexPtr;
};
这些内容都是上一节里讲的,现在的问题是如果对RAII对象进行复制,会怎么样。比如对这个Lock类的对象用如下代码:
Lock m11(&m);
Lock ml2(ml1);
想一下,lock类是用于对mutex管理的,而mutex是一个互斥锁,复制一个对互斥锁进行管理的对象,好像是没有什么意义的。那既然没有意义,就应该对这个类禁止拷贝,这也是本节对资源管理类的拷贝操作处理的第一种情况。至于禁止拷贝的方法,在item6里讲的很详细。
另外一种情况,就是拷贝有意义的情况,这时我们的处理办法就是对管理对象所管理的资源进行引用计数,即shared_ptr的做法。比如还是上面的lock类,可以把它的mutexPtr装进智能指针,即shared_ptr< Mutex >。
但是问题就是智能指针会在引用计数为0的时候释放资源,然而这里的本意只是希望在析构的时候解锁。
解决这个问题的办法就是shared_ptr提供了删除器,可以在对mutexPtr初始化的时候,把它的删除器初始化为解锁函数,这样当引用为0的时候,智能指针就不是释放资源而是调用解锁函数,代码如下:
class Lock {
public:
explicit Lock(Mutex *pm) // init shared_ptr with the Mutex
: mutexPtr(pm, unlock ) // to point to and the unlock func as the deleter
{
lock(mutexPtr.get() ); // see Item 15 for info on "get"
}
private:
std::tr1::shared_ptr<Mutex> mutexPtr; // use shared_ptr
}; // instead of raw pointe
而且值得注意的是,这个类是无需再声明析构函数的。原因就是没有意义,它只有一个智能指针成员,这个成员前面说到,在引用数为0的时候自动解锁便完成了它的任务,所以不需要额外声明析构函数。
讲到这里,好像本节的内容也应该是说完了。但是作者继续提到了两个跟管理资源的类的拷贝有关系的点。这里也记录一下:
- Copy the underlying resource(我理解成深拷贝)
作者讲到,对一份资源,我们是可以拷贝任意份的,但是资源管理的意义,是在不需要资源的时候保证释放。这就意味着我们在拷贝资源管理的对象的时候,必须同时拷贝它管理的资源,即深拷贝
这个点我认为还是很重要的,来看一个例子就明白了。
#include<iostream>
#include<string>
using namespace std;
class MyStr
{
private:
char *name;
int id;
public:
};
int main()
{
MyStr str2(1, "hhxx");
MyStr str3;
str3 = str2;
return 0;
}
上面这个类中,我们没有显式重载拷贝赋值运算符,编译器会默认构造一个,然而执行的是浅拷贝,也就意味着str2的name成员和str1的name成员指向同一个字符串 就像这样
这就会带来致命的错误,比如我们更改了str3的name指针的值,那么同时就修改了str2的name,再比如对str2和str3都进行析构,就会对一块内存释放两次,显然这都是错误。
回到书本,这就是作者强调的除了复制资源管理对象,还应该同时复制它管理的资源,也就是深拷贝。部分代码如下:
MyStr(const MyStr& str)
{
id = str.id;
if (name != NULL)
delete[] name;
name = new char[strlen(str.name) + 1];
strcpy_s(name, strlen(str.name) + 1, str.name);
}
MyStr& operator =(const MyStr& str)
{
if (this != &str)
{
if (name != NULL)
delete[] name;
this->id = str.id;
int len = strlen(str.name);
name = new char[len + 1];
strcpy_s(name, strlen(str.name) + 1, str.name);
}
return *this;
}
这样才能实现对对象的正确拷贝。
-Transfer ownership of the underlying resource (转移资源的拥有权)
某些情况下,我们可能只希望有一个RAII对象指向底层资源,那么解决办法可以参考auto_pty的处理方法,也就是转移它的占有者。
作者在这一节末尾,作者强调了一下有些时候必须要自己来写拷贝函数,除非编译器生成的版本能解决问题。写到这里,关于这一节的内容就写完了。我的感受其实是讲的看上去很简单的话题,但实际包含的东西很多,也有很多地方我还没有彻底想清楚,需要以后多加实践来理解。
由于比较杂,总结一下这一节: