首页 > 代码库 > 《Effective C++》资源管理:条款13-条款15
《Effective C++》资源管理:条款13-条款15
在系统中,资源是有限的,一旦用完必须归还给系统,否则可能会造成资源耗尽或其他问题。例如,动态分配的内存如果用完不释放会造成内存泄漏。
这里说的资源不仅仅是指内存,还包括其他,例如文件描述符、网络连接、数据库连接、互斥锁等。
在任何情况下都要把不使用的资源归还系统是一件非常困难的事情。尤其是考虑到异常、函数内多重回传路径等。
基于对象的资源管理办法几乎可以消除资源管理的问题。下面介绍的方法也可以弥补一些不足。
条款13:以对象管理资源
class Investment//基类 { …… };
用工厂模式来创建特定的投资对象
Investment* CreateInvestment() { …… }在一个作用域内使用
void fun() { Investment* pInv=CreateInvestment(); …… delete pInv;//释放资源 }
这样看起来没有问题,但是任何情况下,都能确保执行“delete pInv”吗?
显然不是,如果在“……”提前返回,或者出现异常,都不会执行到“delete pInv”。当然,谨慎编写程序可以防止这些想到的问题,但是我们编写的代码可能被后人维护和修改。如果他人在中间加上一个return或者其他空余语句都有可能改变执行的流程。所以,单纯依靠delete来删除远远不够。
为了确保资源总是在出了作用域内被释放,可以用对象来控制资源。把对象建到栈上,当对象出了作用域时自然会析构,在这个析构函数释放资源。这就是对象管理资源的机制。
标准库里提供了一些指针,正是针对上面这种情况的。例如,使用auto_ptr来实现资源管理
void fun() { Investment* pInv=CreateInvestment(); …… delete pInv;//释放资源 }上面以资源管理对象的关键在于两点
1、RAII:资源获取即初始化(resource acquisition is initialization)。获取资源后立即放进对象内进行管理。
2、管理对象运用析构函数确保资源释放。管理对象是开辟在栈上面的,离开作用域系统会自动释放管理对象,自然会调用管理对象的析构函数。
auto_ptr指针会有资源的唯一使用权。当auto_ptr指针给其他指针赋值时,对资源的使用权将被剥夺。更多关于auto_ptr内容请参考这里。
由于对资源的唯一使用权的这个行为使得auto_ptr使用比较受限。例如STL容器中的元素不能使用auto_ptr。
还有一种指针是引用计数器型智能指针(refercence-counting smart pointer;RCSP)。它会记录有多少个对象在使用资源,当使用资源的计数器为零时,就会释放资源。可以参考这里。在标准库里面是shared_ptr。
需要注意的时,auto_ptr和shared_ptr释放资源用的都是delete,而不是delete[],所以不能用于数组。关于数组方面的指针,有shared_array来对应。类似的还有scope_array,可以参考这里。
最后还要说明一点:createInvestment返回的是未加工指针(raw pointer),调用者极易忘记释放,即便是使用智能指针,也要首先把CreateInvestment()返回的指针存储于智能指针对象内。后面的条款18将会就这个问题进行讨论。
条款14:在资源管理类中小心coping行为
void lock(Mutex* mu);//加锁 void unlock(Mutex* mu);//解锁为了确保给上锁的Mutex变量解锁,我们需要建立一个类来管理Mutex锁。这个类按照RAII方法来建立。
class Lock{ public: explicit Lock(Mutex* mu):mutexPtr(mu) { lock(mutexPtr); } ~Lock() { unlock(mutexPtr); } private: Mutex* mutexPtr; };
这样客户对Lock的使用方法符合RAII方式:
Mutex m;//定义互斥器 …… {//建立区块来定义critical section Lock(&m); ……//执行critical section 内的操作 }//在区块末尾,自动解除互斥器的锁
上面是个一般性的例子。由此可以引出一个问题:当一个RAII对象被复制,会发生什么?
Lock m1(&m); Lock m2(m1);这是个一般性的问题。当RAII对象复制时,应该怎么做?通常有以下几种做法:
class Lock:private Uncopyable{ ……
2、对管理资源使用引用计数法。
class Lock:private { public: explicit Lock(Mutex* mu):mutexPtr(mu,unlock)//以某个Mutex初始化,unlock作为删除其 { lock(mutexPtr); } private: shared_prt<Mutex> mutexPtr; };需要注意的是在这个类中并没有自己编写析构函数。因为mutexPtr是类中的普通成员变量,编译器会自动生成析构函数类析构这样的变量。这个在条款5中有说明。
C++中的string类,内部是指向heap的指针。当string复制时,底层的指针指向的内容都会多出一份拷贝。
条款15:在资源管理类中提供对原始资源的访问
shared_prt<Investment> pInv=(createInvestment());但是如果一个API是这样的
int dayHeld(const Investment* pi);显然是无法使用shared_ptr对象的。这时候资源管理类需要一个函数,将管理的原始资源暴露出来(即返回原始资源的直接访问方法)。
杂shared_ptr和auto_ptr都提供一个get函数,用于执行这样的显示转换。这时如果在调用上面的API时:
dayHeld(pInv.get());
为了使智能指针使用起来像普通指针一样,它们要重载指针取值(pointer dereferencing)操作符(operator->和operator*),它们允许转换至底部原始指针。
例如Investment类中有个public函数isTaxFree:
class Investment{ public: bool isTaxFree() const; …… };
shared_prt<Investment> pi(CreateInvestment()); pi->isTaxFree();//通过operator->访问 (*pi).isTaxFree();//通过operator*访问
有时候我们必须使用RAII class内的原始资源。通常有以下两种做法。考虑下面一个例子:
FontHandle getFont();//C的API。为求简化省略参数 void releaseFont(FontHandle fh); class Font{//RAII class public: explicit Font(FontHandle fh):f(fh) {} ~Font(){releaseFont(f);} private: FontHandle f; };
如果有大量的API,它们要求处理的是FontHandle,那么将Font转换为FontHandle是一个比较频繁的需求。这时可以提供一个显示的转换函数,像shared_ptr中get那样
class Font{ public: FontHandle get()const{return f;} …… };这时,如果需要使用FontHandle,直接调用get函数即可。
如果到处都要使用get函数,这样看起来很难受,也增加了内存泄露的可能,因为我们把原始资源返回给其他API了。
还有一种方法是提供隐式转换
class Font{ public: operator FontHandle ()const{return f;}//隐式转换 …… };这样使用起来就比较自然。但是隐式转换可能在我们不希望发生的时候发生,例如
Font f1(getFont()); FontHandle f2=f1;//发生了隐式转换在隐式转换中,把f1隐式转换为FontHandle,把底部资源给了f2。这样把f1的底层资源给了f2,如果f2释放,那么f1还不知情,这样就失去了资源管理的意义。
那么在实际中,是使用显示get这样的转换呢,还是使用隐式转换?
答案是取决于RAII class被设计用来执行那么工作,在哪些情况下使用。通常来说,get比较受欢迎,因为它避免了隐式转换带了的问题。
现在看来,RAII class内的返回资源的函数和封装资源之间有矛盾。的确是这样,但这样不是什么灾难。RAII class不是为了封装资源,而是为确保资源释放。当然可以在这个基本功能之上再加上一层封装,但是通常不是必要的。但是也有些RAII class结合十分松散的底层资源封装,以获得真正的封装实现。例如shared_ptr将引用计数器封装起来,但是外界很容易访问其所内含的原始指针。它隐藏了客户看不到的部分,但是具备客户需要的东西。良好的class就应该这样。
《Effective C++》资源管理:条款13-条款15