?你可能会发现,有时候需要创建自己的资源管理类。例如,假设你正在使用一个C API来操作互斥对象,互斥类型提供了lock和unlock函数:
void lock(Mutex* pm); // 锁住pm指向的互斥量
void unlock(Mutex* pm); // 互斥量解锁
class Lock {
public:
explicit Lock(Mutex* pm)
: mutexPtr(pm)
{
lock(mutexPtr);
} // 获取资源
~Lock() { unlock(mutexPtr); } // 释放资源
private:
Mutex* mutexPtr;
};
Mutex m; // 定义你需要使用的互斥量
...
{ // 创建一个区块
Lock ml(&m); // 锁住互斥量
... // 执行操作
} // 离开块时,自动解锁
目前都没什么问题,但如果复制了一个Lock对象,会发生什么呢?
Lock ml1(&m); // 锁住 m
Lock ml2(ml1); // 将ml1复制到ml2,这里会发生什么?
#include <iostream>
//#include <mutex>
class Mutex {
public:
bool isLocked = false;
};
void lock(Mutex* pm) // 锁住pm指向的互斥量
{
pm->isLocked = true;
puts("给互斥量上锁!");
}
void unlock(Mutex* pm) // 互斥量解锁
{
pm->isLocked = false;
puts("给互斥量开锁!");
}
class Lock {
public:
explicit Lock(Mutex* pm)
: mutexPtr(pm)
{
lock(mutexPtr);
} // 获取资源
~Lock() { unlock(mutexPtr); } // 释放资源
private:
Mutex* mutexPtr;
};
Mutex m; // 定义你需要使用的互斥量
int main()
{
Lock ml1(&m); // 锁住 m
Lock ml2(ml1); // 将ml1复制到ml2,这里会发生什么?
}
这边在析构的时候会进行两次解锁。
复制RAII对象时应该如何处理?大多数时候,你有如下选择:
1、禁止复制:如果允许复制RAII对象没有意义,这时应该禁止它。对于像Lock这样的类来说,这可能是正确的。
2、引用计数底层资源:保留资源,直到使用它的最后一个对象被销毁。std::shared_ptr会调用delete,而我们的Lock类,需要的是解锁。幸运的是,shared_ptr允许指定“删除器”。
3.复制底部资源:有时可以有任意多个资源的副本,需要资源管理类的唯一原因是确保每个副本都能被正确释放。在这种情况下,复制资源管理对象也应该复制它包裹的资源。也就是说,复制资源管理对象时进行“深度拷贝”。
4.转移底部资源的所有权:在极少数情况下,需要确保只有一个RAII对象管理资源,当复制RAII对象时,需要转移资源的所有权。
以下是使用第二种方法解决问题。
class Lock {
public:
explicit Lock(Mutex* pm) // 用互斥量和unlock函数初始化shared_ptr
: mutexPtr(pm, unlock) // 作为删除器
{
lock(mutexPtr.get()); // 获取普通指针
}
private:
std::shared_ptr<Mutex> mutexPtr; // 使用shared_ptr代替普通指针
};