03.resource-management
本文对应原书第三部分,主要记录
item13 使用对象来管理资源
我们使用系统资源必须遵循申请资源
使用资源
释放资源
这样一个完成的步骤,如果资源使用后没有释放就会造成资源泄漏。在编程中我们通过手动管理资源往往会存在着因为异常或者逻辑不合理跳过了资源释放,或者忘记释放资源等问题。为了避免以上情况出现,我们可以利用
Note:总结
- 为防止资源泄漏,使用
RAII 机制来进行资源管理,通过构造函数获取资源,利用析构函数确保资源被释放。 C++ 中常用的RAII 实例就是智能指针。shared_ptr
使用引用计数来管理资源。
item14 注意资源管理类的拷贝行为
现在假设我们用
class Lock
{
public:
explicit Lock(Mutex* pm):mutexPtr(pm)
{lock(mutexPtr);}
~Lock(){unlock(mutexPtr);}
private:
Mutex* mutexPtr;
}
/* 接下来我们使用这个类*/
Mutex m;
Lock lock1(&m); // 锁定m
Lock lock2(lock1); // 将lock1复制到lock2
在上述代码中,发生了资源的复制。在用
- 禁止拷贝
对于有些资源来说,比如上面提到的互斥锁,它的拷贝是没有意义,我们可以通过将其拷贝构造函数和赋值运算符显式声明为
private 来禁止外部使用其拷贝功能。
class Lock
{
public:
explicit Lock(Mutex* pm):mutexPtr(pm)
{lock(mutexPtr);}
~Lock(){unlock(mutexPtr);}
private:
/* 私有化其拷贝函数 */
Lock(const Lock& lock);
Lock& operator=(const Lock& lock);
Mutex* mutexPtr;
}
- 使用引用计数
有时我们希望保有资源,直到最后一个使用者被销毁。这种情况下复制
RAII 对象时,应该将该资源的引用计数递增。shared_ptr
就是使用的这种机制,shared_ptr
提供了一个特殊的可定义函数——删除器(deleter) ,即在其引用计数为零时调用。
class Lock
{
public:
explicit Lock(Mutex* pm):mutexPtr(pm,unlock) // 将unlock函数绑定到删除器
{lock(mutexPtr.get());} // 锁定原始指针
// 这里不再需要定义析构函数来释放资源 ~Lock(){unlock(mutexPtr);}
private:
std::shared_ptr<Mutex> mutexPtr; // 使用shared_ptr,不再使用原始指针
}
-
深度拷贝 当我们需要的不仅仅是资源的所有权,而是要获取资源的副本的时候,我们就需要拷贝底层资源,即不仅仅是指向资源的指针,在内存上的资源也要进行复制,也就是“深拷贝”。
-
所有权转移 当我们只想要一个
RAII 对象来持有这个资源,在进行拷贝的时候就要进行所有权的转移,即释放原对象对资源的权限,将所有权转移到新的对象上,这也是auto_ptr
的工作原理。
Note:总结
RAII 对象的拷贝要根据其管理的资源具体考虑,资源的拷贝行为决定RAII 对象的拷贝行为。- 常用的
RAII 对象的拷贝行为有:禁止拷贝、使用引用计数、深度拷贝、所有权转移。
item15 在资源管理类中提供对原始资源的访问
资源管理类通过对原始资源的封装可以有效避免资源泄漏,但是在很多情况下外部的
- 显式访问:在
RAII 类中声明一个显式的转换函数,返回原始资源类型的对象 - 隐式访问:利用
operator 操作符声明一个隐式转换函数
- 智能指针获取原始指针
智能指针都有一个成员函数
get()
,来执行显式转换,返回智能指针对象包含的原始指针:
mutexPtr.get(); // 获取指向互斥锁的原始指针
智能指针重载了指针解引操作符
class Investment
{
public:
bool isTaxFree() const;
}
Investment* pInv;
std::shared_ptr<Investment> ptr1(pInv); // 使用shared_ptr管理资源
bool tax1 = ptr1->isTaxFree(); // 使用operator->访问原始资源
std::auto_ptr<Investment> ptr2(pInv); // 使用auto_ptr管理资源
bool tax1 = (*ptr2).isTaxFree(); // 使用operator*访问原始资源
- 自定义
RAII 类获取原始资源 同智能指针类似,在自定义的RAII 类中可以通过定义一个get()
函数来显式的返回原始资源,也可以通过operator 操作符声明一个隐式转换函数。
Note:总结
API 往往需要访问原始资源,因此RAII 类应该提供一个访问原始资源的方法。- 对原始资源的访问有显式和隐式两种。一般而言,显式比较安全,隐式便于使用。
item16 使用new/delete 形式要对应
为了保证内存的释放,我们经常会特别注意
std::string* strArray = new std::string[100];
...
delete strArray;
当使用[]
,
std::string* strPtr1 = new std::string;
std::string* strPtr2 = new std::string[100];
...
delete strPtr1; // 使用delete删除单个对象
delete[] strPtr2;// 使用delete[]删除数组
Note:总结
- 主要针对单个对象,应该使用
new 对应delete ,对于对象数组,new[] 要对应delete[] 。
item17 用单独的语句来创建智能指针
假设有如下函数:
int priority(); // 返回程序的优先级
void processWidget(std::shared_ptr<Widget> pw, int priority);
/* 通过以下语句来调用processWidget */
processWidget(std::shared_ptr<Widget>(new Widget),priority());
上述调用priority()
函数;执行new Widget
;调用shared_ptr构造函数
。其中,new Widget
的执行在调用shared_ptr构造函数
之前,但是调用priority()
函数的次序是不一定的。这是因为在
- 1、执行
new Widget
; - 2、调用
priority()
函数; - 3、调用
shared_ptr构造函数
。 如果调用priority()
函数的过程中抛出异常,new Widget
返回的指针就会遗失,因为它还没有被放入智能指针中,从而造成资源泄漏。 为了避免上述问题,需要用单独的语句来创建智能指针,从而保证两个动作不会被隔离,即将new 的对象立即放入智能指针。
std::shared_ptr<Widget> pw(new Widget);
processWidget(pw,priority());
Note:总结
- 用单独的语句创建智能指针,否则可能导致难以察觉的资源泄漏问题。