void func(){
Test* pt = new Test();
...//这里发生了something,just like:异常、return、continue等
delete pt;
}
通过这个例子我们可以发现因为一些奇奇怪怪的情况发生,导致我们最后没有将所使用的资源给系统物归原主。
有时我们为了确保不会忘记将一个锁住的mutex互斥锁解锁,我们可能会建立一个class来管理这个锁,我们会在构造中获得,析构时释放。Just like this:
class Lock{
public:
explicit Lock(Mutex* pm)
:mutexPtr(pm)
{
lock(mutexPtr);
}
~Lock(){unlock(mutexPtr);}
private:
Mutex *mutexPtr;
};
//用户操作:
Mutex m;
void critical_zone(Mutex &m){
Lock m1(&m); //上锁
do something(); //该临界区被独享
//m1作用域结束,自动调用析构函数释放该互斥锁
}
以上代码看似很合理,但是当Lock对象被复制时,则会出现意外:
Lock m11(&m); //上锁
Lock m12(&m11); //将m11复制到m12身上
此时并没有及时解锁,再次调用构造函数时对其进行上锁会因为互斥而导致失败。
因此,从这个特定的例子我们可以一般化为“如何处理当一个RAII对象被复制”这个问题,通常我们的处理方法:
复制底层资源:我们在复制资源管理对象时,应当同时也复制其所管理的资源,也就是说在复制资源管理对象时应当进行“深度拷贝”。即当一个资源管理类中有一个指向heap内存的指针,在复制时,我们不仅需要复制该指针,同时需要复制该指针所指向的内存,从而形成一个复件,实现“深拷贝”。
转移底部资源的拥有权:如果某些场景下你希望确保永远只有一个RAII对象指向raw source,即使在RAII对象被复制时依然如此,那么此时资源的拥有权会从被复制物转移到目标物,即我们在智能指针提到的auto_ptr和unique_ptr的存在意义。
void func(shared_ptr ptT, int process){
...
}
我们假设有这样一个接口的func函数,但当我们用如下方法调用时:
func(new Test, process());
此时会出现编译错误,因为shared_ptr构造函数需要一个原始指针(raw pointer),但该构造函数是一个explicit构造函数,无法进行隐式转换,不可将new Test的原始指针转换为func需要的shared_ptr类型的指针,但如果修改成以下形式则可以通过编译:
func(shared_ptr(new Test), process());
虽然修改后可以通过编译,但会出现严重的泄漏资源的潜在可能。接下来我们详细分析一下该函数调用的流程:
在编译器产生出一个func调用码之前,必须先核算即将被传递的各个实参!在对于第二个实参process而言没什么问题,但对于第一个实参shared_ptr
于是,在调用func函数之前,编译器必须创建代码,做以下三件事:
但c++编译器对于怎样执行以上三件事的次序却不一定!唯一可以确定的是:"new Test"一定在shared_ptr构造函数之前执行,因为执行"new Test"表达式的结果还要传递作为shared_ptr构造函数的一个实参,因此,最终会获得这样的操作序列(对于调用process可以任意行):
以上操作看似正常,实则危机重重。例如一旦process()函数调用异常,此时执行"new Test"所返回的指针则会丢失,因为它尚未置入shared_ptr内,此时则会引发资源泄漏。因此,在“资源被创建(new Test)”和“资源被转换为资源管理对象”两个时间点之间可能发生异常干扰!
为了避免此类问题,我们规定一定要使用分离语句,首先创建出Test对象后,再置入智能指针内,然后再将智能指针传递给func()函数。如下:
shared_ptr ptT(new Test);
func(ptT, process());
通过分离语句使得将“"new Test"表达式””以及“shared_ptr构造函数的调用”这两个动作和“process的调用”所分隔开,所以编译器不得在他们之间任意选择执行次序,从而绝对不会导致泄漏行为的发生。
参考文献
《Effective C++(第三版)》