所谓资源就是,一旦用了它,将来必须还给系统。如果不这样,糟糕的事情就会发生。
C++程序中最常使用的资源就是动态分配内存(如果你分配内存却从来不曾归还它,会导致内存泄露),
但内存只是你必须管理的众多资源之一。其他常见的资源还包括文件描述符(file description),互斥锁(mutex lock),图形界面中的字型和笔刷,数据库连接,以及网络socket。
不论哪一种资源,最重要的是,当你不再使用它时,必须将它还给系统。
-
条款13:以对象管理资源
-
假设我们使用一个投资行为的程序库,各种股票、债券等,使用一个基类 class Investment:
// “投资类型”集成体系中的root class class Investment { ... };
进一步假设,这个程序库通过一个工厂函数(factory function)供应特定的Investment对象,对象是动态分配的,返回一个指针,那么调用者则必须要管理这个资源,适当的时候要delete,否则会有内存泄漏等风险:
// 返回指针,指向Investment继承体系内的动态分配对象 // 调用者有责任删除它 Investment* createInvestment();
假如现在写一个函数 f() 使用程序库完成任务,需要在最后delate以完成资源管理。
void f(){ // 调用factory函数 Investment* pInv = createInvestment(); ... // 释放pInv所指对象 delete pInv; }
但是,程序运行过程并不如我们想象。首先,会有可能在delete之前的某个程序分支进行了return,这样则跳过了delete。或者,在delete之前的函数抛出了异常,delete也会被跳过。如果delete被跳过,则pInv指向的内存,以及里面保存的资源都会发生泄露。所以这是一种不好的方法。
-
为了确保createInvestment返回的资源被正确的释放:把资源放进对象内,我们便可倚赖C++的“析构函数自动调用机制”确保资源被释放。
-
标准库提供的std::auto_ptr正是针对这种情况而设计的模板。std::auto_ptr是个“类指针(pointer-like)对象”,也就是所谓“智能指针”,其析构函数自动对其所指对象调用delete。
使用std::auto_ptr来重写 f() 函数:
void f(){ // 调用factory函数 std::auto_ptr<Investment> pInv(createInvestment()); // 一如既往的使用pInv ... // 经由auto_ptr的析构函数自动删除pInv }
-
以对象管理资源的两个关键点在于:
1、获得资源后立刻放到管理对象中;
2、管理对象运用析构函数确保资源被释放。 -
auto_ptr 和 shared_ptr
auto_ptr的一个局限性在于,不能让多个指针指向一个对象,会发生多次释放而产生不可预期的错误。(所以auto_ptr的拷贝构造和operator=函数会将源对象变成null,保证只有一个智能指针指向资源)。STL容器也需要用于能够复制的对象。
替代方案是采用引用计数型智能指针(reference-counting smart pointer)。他会支持追踪有多少对象指向了资源,在最后没人指向的时候将其释放。std::tr1::shared_ptr就是一种。
void f(){ // 调用factory函数 std::tr1::shared_ptr<Investment> pInv(createInvestment()); std::tr1::shared_ptr<Investment> pInv2(pInv); // 使用pInv一如既往 ... // 自动销毁pInv和pInv2,不会重复delete }
-
总结
为了防止资源泄露,可采用以对象管理资源的方式;
两个经常被使用的智能指针class是:auto_ptr 和 tr1::shared_ptr。
-
-
条款14:在资源管理类中小心coping行为
-
有些时候会用到自定义的资源管理类,在设计时要考虑到coping行为(即copy构造函数及operator=)。比如下面这个用于管理mutex互斥锁的管理类,为了防止用户获取锁之后忘记释放锁,在析构函数中将mutex锁释放。
class Lock{ public: Lock(Mutex* pm) : mutexPtr(pm) { lock(mutexPtr);} //获得资源 ~Lock(){ unlock(mutexPtr);} //释放资源 private: Mutex *mutexPtr; }
使用时可以这样使用,只需要在代码块开头定义一个lock对象,将锁指针传入,在代码块末尾会调用析构函数完成解锁。
Mutex m; //定义你需要的互斥器 ... { //建立一个区块用来定义critical section Lock ml(&m);//锁定互斥器 ... //执行critical section内的操作 } //在区块最末尾,自动解除互斥器锁定
但是如果发生复制coping行为,默认的coping函数,会发生两次解锁,产生不可预期的后果。
-
解法之一是禁止复制,如条款06中,将coping两个函数声明为private,或者更好的方法设计一个基类,声明他的coping函数为private。
-
解法之二是对底层资源采用“引用计数法”(reference-count),类似上一条中的shared_ptr的原理,可以直接利用share_ptr来实现。一般在class中包含一个shared_ptr则可以让class拥有reference-counting copying的行为。
shared_ptr的功能是在资源数为0的时候才进行删除,但删除并不能作用于mutex,我们需要进行解锁操作,shared_ptr提供了一个功能,能够传入一个函数指针代替原本的删除操作,本例中则将删除delete变成解锁unlock。
class Lock{ public: Lock(Mutex* pm) //以某个Mutex初始化shared_ptr : mutexPtr(pm, unlock) // 并以unlock函数为删除器 { lock(mutexPtr.get()); //get获取到mutexPtr指向的锁 } private: std::tr1::shared_ptr<Mutex> mutexPtr; //使用shared_ptr }
Lock类中拥有了一个private share_ptr对象,在发送coping行为时,编译器自动生成的copy函数直接复制mutexPtr。
而在自动生成的析构函数中,会自动调用mutexPtr的析构函数,而由于shared_ptr的特性,会在最后一个时解锁。
-
在auto_ptr中还给出一种解决方法,就是将资源对象转移到目标对象,将源对象置为空。
-
-
条款15:在资源管理类中提供对原始资源的访问
- 在一些情况下,许多API需要调用资源管理类管理的原始资源,我们需要提供相应的机制。
- 其中有两种方式:显示转换和隐式转换
- 首先看一下显示转换:例如shared_ptr和auto_ptr,都提供了一个get()函数直接返回智能指针内部的原始指针(的复件)
- 对于隐式转换,可利用operator隐式转换函数,参考这个文章:operator隐式转换。同时,例如shared_ptr和auto_ptr也提供了operator-> 和 operator*,使得可以像使用指针那样使用智能指针。
- 一般来说,显示转换看起来麻烦,但是安全性高,而隐式转换虽然用法自然,但可能会发生意料之外的转换。
-
条款16:成对使用new和delete时要采取相同形式
- 对于new分配的内存,要使用delete来释放;对于new[]分配的内存,要用delete[]来释放。
- 对于类中的多个构造函数,要以相同的方式new,否则析构函数中对应就会有错误。
-
条款17:以独立语句将newed对象置于智能指针
- 这是为了保证资源在创建后,立刻进入智能指针或其他资源管理类。
- 假如在这两个操作之间有其他操作,可能会抛出异常等,那么资源就会被泄露,而不会被管理类销毁。