以对象管理资源、在资源管理类中小心coping行为、在资源管理类中提供对原始资源的访问

本文探讨了资源管理的重要性,特别是如何使用对象管理资源以避免内存泄漏等问题。通过具体实例介绍了资源获取即初始化(RAII)的概念,并展示了如何设计资源管理类以确保资源正确释放。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在系统中,资源是有限的,一旦用完必须归还给系统,否则可能会造成资源耗尽或其他问题。例如,动态分配的内存如果用完不释放会造成内存泄漏。

这里说的资源不仅仅是指内存,还包括其他,例如文件描述符、网络连接、数据库连接、互斥锁等。

在任何情况下都要把不使用的资源归还系统是一件非常困难的事情。尤其是考虑到异常、函数内多重回传路径等。

基于对象的资源管理办法几乎可以消除资源管理的问题。下面介绍的方法也可以弥补一些不足。

条款13:以对象管理资源

现在假设用一个类来模拟投资行为,各种投资类都继承自一个基类Investment。

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. class Investment//基类  
  2. {  
  3.     ……  
  4. };  

用工厂模式来创建特定的投资对象

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. Investment* CreateInvestment()  
  2. {  
  3.     ……  
  4. }  
在一个作用域内使用
[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. void fun()  
  2. {  
  3.     Investment* pInv=CreateInvestment();  
  4.     ……  
  5.     delete pInv;//释放资源  
  6. }  

这样看起来没有问题,但是任何情况下,都能确保执行“delete pInv”吗?

显然不是,如果在“……”提前返回,或者出现异常,都不会执行到“delete pInv”。当然,谨慎编写程序可以防止这些想到的问题,但是我们编写的代码可能被后人维护和修改。如果他人在中间加上一个return或者其他空余语句都有可能改变执行的流程。所以,单纯依靠delete来删除远远不够。

为了确保资源总是在出了作用域内被释放,可以用对象来控制资源。把对象建到栈上,当对象出了作用域时自然会析构,在这个析构函数释放资源。这就是对象管理资源的机制。

标准库里提供了一些指针,正是针对上面这种情况的。例如,使用auto_ptr来实现资源管理

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. void fun()  
  2. {  
  3.     Investment* pInv=CreateInvestment();  
  4.     ……  
  5.     delete pInv;//释放资源  
  6. }  
上面以资源管理对象的关键在于两点

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行为

在条款13中,讲述了资源获取即初始化(resource acquisition is initialization;RAII)的资源管理方法,并介绍了智能指针auto_prt和shared_ptr指针。上述方法管理的资源都是开辟在堆上(heap-based),但是并不是所有资源都是开辟在堆上,有时候我们需要自己建立资源管理类。
例如现在使用C语言API函数处理类型为Mutex的互斥器对象(mutex object),有lock和unlock两个函数可以用。
[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. void lock(Mutex* mu);//加锁  
  2. void unlock(Mutex* mu);//解锁  
为了确保给上锁的Mutex变量解锁,我们需要建立一个类来管理Mutex锁。这个类按照RAII方法来建立。
[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. class Lock{  
  2. public:  
  3.     explicit Lock(Mutex* mu):mutexPtr(mu)  
  4.     {  
  5.         lock(mutexPtr);  
  6.     }  
  7.     ~Lock()  
  8.     {  
  9.         unlock(mutexPtr);  
  10.     }  
  11. private:  
  12.     Mutex* mutexPtr;  
  13. };  

这样客户对Lock的使用方法符合RAII方式:
[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. Mutex m;//定义互斥器  
  2. ……  
  3. {//建立区块来定义critical section  
  4.     Lock(&m);  
  5.     ……//执行critical section 内的操作  
  6. }//在区块末尾,自动解除互斥器的锁  

上面是个一般性的例子。由此可以引出一个问题:当一个RAII对象被复制,会发生什么?
[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. Lock m1(&m);  
  2. Lock m2(m1);  
这是个一般性的问题。当RAII对象复制时,应该怎么做?通常有以下几种做法:
1、禁止复制
许多情况下,复制RAII对象并不合理。例如Lock类,这时候便可以禁止复制,只需将coping函数设为私有,条款6有讲述。
[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. class Lock:private Uncopyable{  
  2. ……  

2、对管理资源使用引用计数法。
有时候我们希望保持资源直到最后一个使用者。这时RAII对象复制时,应该将持有资源的引用计数器加一。例如shared_ptr。
通常只需要在RAII class中包含一个shared_ptr成员变量便可实现引用计数的方法。如果上述Lock类使用引用计数器的话,只需把mutexPrt变为类型从Mutex*变为shared<Mutex>即可。但shared_ptr默认是当引用计数为0时,删除多指向对象,这不是我们想要的,我们想要的是调用unlock函数。幸运的是在shared_ptr中允许指定“删除器”,即引用计数为0时调用的函数。所以修改后如下:
[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. class Lock:private Uncopyable{  
  2. public:  
  3.     explicit Lock(Mutex* mu):mutexPtr(mu,unlock)//以某个Mutex初始化,unlock作为删除其  
  4.     {  
  5.         lock(mutexPtr);  
  6.     }  
  7. private:  
  8.     shared_prt<Mutex> mutexPtr;  
  9. };  
需要注意的是在这个类中并没有自己编写析构函数。因为mutexPtr是类中的普通成员变量,编译器会自动生成析构函数类析构这样的变量。这个在条款5中有说明。

3、复制底部资源。
使用资源管理类的目的是保证不需要这个资源时能正确释放。如果这种资源可以任意复制,我们只需编写好适当的copying函数即可。确保复制时是深度复制。关于深复制,参考 这里
C++中的string类,内部是指向heap的指针。当string复制时,底层的指针指向的内容都会多出一份拷贝。

4、转移底层资源的拥有权。
有时候资源的拥有权只能给一个对象,这时候当资源复制时,就需要剥夺原RAII类对该资源的拥有权。像auto_ptr。在C++11新标准中的std::move便是这个功能。可以把一个左值转换为一个右值。

copying函数如果你不编写,编译器会帮你合成,其合成版本行为可参考条款5。要记住的是不论是自己编写还是编译器合成,都要符合自己资源管理类的需要。

条款15:在资源管理类中提供对原始资源的访问

使用资源管理类来屏蔽原始资源,对抗内存泄露等问题,避免使用原始资源(raw resources)。但是在现实中并不是这样,许多APIs要求使用原始资源。例如在条款13中,使用shared_ptr保存factory函数的返回值。
[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. shared_prt<Investment> pInv=(createInvestment());  
但是如果一个API是这样的
[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. int dayHeld(const Investment* pi);  
显然是无法使用shared_ptr对象的。这时候资源管理类需要一个函数,将管理的原始资源暴露出来(即返回原始资源的直接访问方法)。
杂shared_ptr和auto_ptr都提供一个get函数,用于执行这样的显示转换。这时如果在调用上面的API时:
[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. dayHeld(pInv.get());  

为了使智能指针使用起来像普通指针一样,它们要重载指针取值(pointer dereferencing)操作符(operator->和operator*),它们允许转换至底部原始指针。
例如Investment类中有个public函数isTaxFree:
[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. class Investment{  
  2. public:  
  3.     bool isTaxFree() const;  
  4.     ……  
  5. };  

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. shared_prt<Investment> pi(CreateInvestment());  
  2. pi->isTaxFree();//通过operator->访问  
  3.   
  4. (*pi).isTaxFree();//通过operator*访问  

有时候我们必须使用RAII class内的原始资源。通常有以下两种做法。考虑下面一个例子:
[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. FontHandle getFont();//C的API。为求简化省略参数  
  2. void releaseFont(FontHandle fh);  
  3.   
  4. class Font{//RAII class  
  5. public:  
  6.     explicit Font(FontHandle fh):f(fh)  
  7.     {}  
  8.     ~Font(){releaseFont(f);}  
  9. private:  
  10.     FontHandle f;  
  11. };  

如果有大量的API,它们要求处理的是FontHandle,那么将Font转换为FontHandle是一个比较频繁的需求。这时可以提供一个显示的转换函数,像shared_ptr中get那样
[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. class Font{  
  2. public:  
  3.     FontHandle get()const{return f;}  
  4.     ……  
  5. };  
这时,如果需要使用FontHandle,直接调用get函数即可。
如果到处都要使用get函数,这样看起来很难受,也增加了内存泄露的可能,因为我们把原始资源返回给其他API了。

还有一种方法是提供隐式转换
[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. class Font{  
  2. public:  
  3.     operator FontHandle ()const{return f;}//隐式转换  
  4.     ……  
  5. };  
这样使用起来就比较自然。但是隐式转换可能在我们不希望发生的时候发生,例如
[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. Font f1(getFont());  
  2. FontHandle f2=f1;//发生了隐式转换  
在隐式转换中,把f1隐式转换为FontHandle,把底部资源给了f2。这样把f1的底层资源给了f2,如果f2释放,那么f1还不知情,这样就失去了资源管理的意义。

那么在实际中,是使用显示get这样的转换呢,还是使用隐式转换?
答案是取决于RAII class被设计用来执行那么工作,在哪些情况下使用。通常来说,get比较受欢迎,因为它避免了隐式转换带了的问题。
现在看来,RAII class内的返回资源的函数和封装资源之间有矛盾。的确是这样,但这样不是什么灾难。RAII class不是为了封装资源,而是为确保资源释放。当然可以在这个基本功能之上再加上一层封装,但是通常不是必要的。但是也有些RAII class结合十分松散的底层资源封装,以获得真正的封装实现。例如shared_ptr将引用计数器封装起来,但是外界很容易访问其所内含的原始指针。它隐藏了客户看不到的部分,但是具备客户需要的东西。良好的class就应该这样。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值