在系统中,资源是有限的,一旦用完必须归还给系统,否则可能会造成资源耗尽或其他问题。例如,动态分配的内存如果用完不释放会造成内存泄漏。
这里说的资源不仅仅是指内存,还包括其他,例如文件描述符、网络连接、数据库连接、互斥锁等。
在任何情况下都要把不使用的资源归还系统是一件非常困难的事情。尤其是考虑到异常、函数内多重回传路径等。
基于对象的资源管理办法几乎可以消除资源管理的问题。下面介绍的方法也可以弥补一些不足。
条款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);//解锁
- 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);
- class Lock:private Uncopyable{
- ……
2、对管理资源使用引用计数法。
- class Lock:private Uncopyable{
- public:
- explicit Lock(Mutex* mu):mutexPtr(mu,unlock)//以某个Mutex初始化,unlock作为删除其
- {
- lock(mutexPtr);
- }
- private:
- shared_prt<Mutex> mutexPtr;
- };
C++中的string类,内部是指向heap的指针。当string复制时,底层的指针指向的内容都会多出一份拷贝。
条款15:在资源管理类中提供对原始资源的访问
- shared_prt<Investment> pInv=(createInvestment());
- int dayHeld(const Investment* pi);
杂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;}
- ……
- };
如果到处都要使用get函数,这样看起来很难受,也增加了内存泄露的可能,因为我们把原始资源返回给其他API了。
还有一种方法是提供隐式转换
- class Font{
- public:
- operator FontHandle ()const{return f;}//隐式转换
- ……
- };
- Font f1(getFont());
- FontHandle f2=f1;//发生了隐式转换
那么在实际中,是使用显示get这样的转换呢,还是使用隐式转换?
答案是取决于RAII class被设计用来执行那么工作,在哪些情况下使用。通常来说,get比较受欢迎,因为它避免了隐式转换带了的问题。
现在看来,RAII class内的返回资源的函数和封装资源之间有矛盾。的确是这样,但这样不是什么灾难。RAII class不是为了封装资源,而是为确保资源释放。当然可以在这个基本功能之上再加上一层封装,但是通常不是必要的。但是也有些RAII class结合十分松散的底层资源封装,以获得真正的封装实现。例如shared_ptr将引用计数器封装起来,但是外界很容易访问其所内含的原始指针。它隐藏了客户看不到的部分,但是具备客户需要的东西。良好的class就应该这样。