四:资源管理
不论哪一种资源,重要的是,当你不再使用它时,必须将其归还给系统。
条款十三:以对象管理资源
我们先来看一个例子以说明为什么要这么做:
我们使用一个用来模拟投资行为的程序库,其中有一个基类Investment:
class Investment{...};
我们通过一个工厂函数(factory function)供应某特定的Investment对象:
//返回指针,指向Investment继承体系的动态分配对象,调用者有责任删除它
Investment* creatInvestment();
好,这里把资源回收的责任交给调用者来delete回收,现在我们来用用看,会有什么问题:
void f(){
Investment* pInv = creatInvestment();
...
delete pInv;
}
看着好像挺对的,调用者没有忘记delete,有几种情况会导致delete语句没有执行从而导致资源泄漏:
例如过早的遇到了return语句,或者前面抛出了异常
所以啊,单纯依赖delete语句是行不通的
下面是金玉良言啦:
为确保creatInvestment返回的资源总是被释放,我们需要将资源放进对象内,当控制流离开f,该对象的析构函数会自动释放那些资源,也就是说,我们把资源放进对象,就可以依赖C++ 的析构函数自动调用机制确保资源被释放
void f(){
auto_ptr<Investment> pInv(creatInvestment());
}
由于 auto_ptr 被销毁时会自动删除它所指之物,不能多个 auto_ptr 指向一个对象,所以它有个性质:当通过拷贝函数复制它时,原指针变成null,目标指针将取得资源的唯一拥有权
这个auto_ptr使用起来还是有局限的,它不允许多个指针指向同一个,所以引入了智能指针,带引用计数功能,但它也有问题:环状引用
二者还有一个共同的局限,它们在析构函数中调用的都是delete而不是delete[],所以在动态分配的array上不要使用它们,那么array怎么办呢?别担心,你基本用不到,因为可以用vector和string替代
小结
- 为防止资源泄漏,请使用auto_ptr和智能指针对象管理资源,它们在构造函数中获得资源并在析构函数中释放资源
- 两个常用的指针管理对象分别是引用计数型和auto_ptr,各有优缺点,还有共同局限
条款十四:在资源管理类中小心拷贝行为
我们来直接看例子:
我们使用C API函数处理类型为Mutex的互斥器对象:
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 m1(&m); //构造函数获得资源
}//析构函数释放
这一切都没问题,都是按照之前提款进行,但是现在加这么一句呢:
Lock ml1(&m); //锁定m
Lock ml2(ml1); //复制
这个问题就是:当一个资源管理类对象被复制,会发生什么
一般有两个选择:
- 禁止复制,如果在该类中复制是不合理的,就应该禁止:
Class Lock: private Uncopyable{...};
- 对底层资源使用引用计数
有一个问题是,引用型智能指针在引用次数为0时是删除,这里只想要unlock,幸好我们可以自定义引用型智能指针的删除器(函数或函数对象):class Lock{ public: explicit Lock(Mutex* pm) : mutexPtr(pm, unlock) {lock(mutexPtr.get());} //没有析构是因为这里可以依赖编译器默认生成的去调用删除器 private: shared_ptr<Mutex> mutexPtr; };
小结:
普遍而常见的资源管理类拷贝行为是:禁止拷贝或引用计数法
条款十五:在资源管理类中提供对原始资源的访问
这部分并不是非常符合一贯的设计,只是对现实的屈服,所以我不多写了
小结
- API往往要求对原始资源的访问,所以每个资源管理类应该提供一个去的其所管理资源的方法,智能指针有get()方法返回普通指针,普通指针可以指向原始资源以提供访问
- 对原始资源的访问可以显式转换get(),也可以提供隐式转换函数
显式安全,隐式方便operator FontHandle() const {return f;}
条款十六:成对使用new和delete时要采取相同的形式
小结:
游戏规则很简单:如果你调用new时使用[],delete也要用;new没有[],delete也没有
以独立语句将newed对象置入智能指针
我们来看一个不好的例子:
processWidget(shared_ptr<Widget>(new Widget), priority())
这里的函数参数很复杂,而且在将对象置入智能指针中间可能插入了priority函数,万一该函数报错了,那就gg了,我们一向倡导的以资源管理类来管理没得到实现,于是正确的写法是:
shared_ptr<Widget> pw(new Widget); //单独语句以智能指针存储new的对象
processWidget(pw, priority());
这样priority就插不进去了
小结
以独立语句将new出来的对象存储于智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏