c++RAII资源管理

1.简介及应用背景

  • 我们在编程时会管理许多资源,比如:内存、文件描述符,互斥锁等,我们对于资源的态度是有借有还,一旦使用了它就必须要归还给系统。在c++程序中我们会经常使用的资源就是进行动态分配内存,如果分配了内存却没有归还它,就会导致我们常说的内存泄漏
  • the simple example:
void func(){
    Test* pt = new Test();
    ...//这里发生了something,just like:异常、return、continue等
    delete pt;
}

通过这个例子我们可以发现因为一些奇奇怪怪的情况发生,导致我们最后没有将所使用的资源给系统物归原主。

2.以对象管理资源

  • 资源管理类:把资源放进对象内,便可以依赖c++的析构函数“自动调用机制”来确保资源被释放。可以这么说,排除资源泄漏是良好设计系统的根本性质,而资源管理类是对抗资源泄漏的堡垒。
  • 构建资源管理类的唯一理由:当你不再需要某个复件时确保它被释放。
  • RAII(Resource Acquisition Is Initialization):资源管理类的核心观念即“资源取得时便是初始化时。因为我们总是在获得某一资源后就在同一语句内以它来初始化某个管理对象,或者拿该资源去赋值某个管理对象。无论哪一种做法,每一笔资源都需要在获得的同时立刻被放进管理对象中。
  • 综上所述:RAII对象在构造函数中获得资源,在析构函数中释放资源

3.资源管理类的copying行为

有时我们为了确保不会忘记将一个锁住的mutex互斥锁解锁,我们可能会建立一个class来管理这个锁,我们会在构造中获得,析构时释放。Just like this:

class Lock{
public:
    explicit Lock(Mutex* pm)
        :mutexPtr(pm)
{
    lock(mutexPtr);
}
    ~Lock(){unlock(mutexPtr);}
private:
    Mutex *mutexPtr;
};
//用户操作:
Mutex m;
void critical_zone(Mutex &m){
    Lock m1(&m); //上锁
    do something(); //该临界区被独享
                    //m1作用域结束,自动调用析构函数释放该互斥锁
}

以上代码看似很合理,但是当Lock对象被复制时,则会出现意外:

Lock m11(&m); //上锁
Lock m12(&m11); //将m11复制到m12身上

此时并没有及时解锁,再次调用构造函数时对其进行上锁会因为互斥而导致失败。

因此,从这个特定的例子我们可以一般化为“如何处理当一个RAII对象被复制”这个问题,通常我们的处理方法:

  • 禁止复制:当我们遇到类似上述Lock这样的class时,再去允许RAII对象被复制将并不合理,因此我们可以通过将copying操作声明为private来禁止复制行为的发生。
  • 引用计数法:有时候我们希望保有资源直到它的最后一个使用者被销毁,此时我们只需为使用者进行计数,直到最后一个被销毁此时的计数器为0,我们再将资源释放。这个本质也是智能指针shared_ptr的底层原理,因此我们可以在上述的Lock类中加入该智能指针,修改为shared_ptr<Mutex>。(warning:shared_ptr的默认行为是在引用次数为0时删除其所指物,但在我们互斥锁的情景下显然不能直接把锁删除,而是将锁接解除,因此我们需要使用其deleter的功能。并且此时的Lock不需要再声明析构函数,因为在引用此时为0时会自动调用shared_ptr的删除器) 

复制底层资源:我们在复制资源管理对象时,应当同时也复制其所管理的资源,也就是说在复制资源管理对象时应当进行“深度拷贝”。即当一个资源管理类中有一个指向heap内存的指针,在复制时,我们不仅需要复制该指针,同时需要复制该指针所指向的内存,从而形成一个复件,实现“深拷贝”。

转移底部资源的拥有权:如果某些场景下你希望确保永远只有一个RAII对象指向raw source,即使在RAII对象被复制时依然如此,那么此时资源的拥有权会从被复制物转移到目标物,即我们在智能指针提到的auto_ptr和unique_ptr的存在意义。

4.在资源管理类中提供对原始资源的访问

  • 一些APIs往往要求访问raw resources(原始资源),所以每一个RAII class应该提供一个能够取得其所管理的资源的方法。
  • 对于原始资源的访问可能是经过显示转换或者隐式转换的。一般而言显式转换比较安全,但隐式转换对客户比较方便。

5.以独立语句将new的对象置入类似智能指针的资源管理类

void func(shared_ptr<Test> ptT, int process){
    ...
}

我们假设有这样一个接口的func函数,但当我们用如下方法调用时:

func(new Test, process());

此时会出现编译错误,因为shared_ptr构造函数需要一个原始指针(raw pointer),但该构造函数是一个explicit构造函数,无法进行隐式转换,不可将new Test的原始指针转换为func需要的shared_ptr类型的指针,但如果修改成以下形式则可以通过编译:

func(shared_ptr<Test>(new Test), process());

虽然修改后可以通过编译,但会出现严重的泄漏资源的潜在可能。接下来我们详细分析一下该函数调用的流程:

在编译器产生出一个func调用码之前,必须先核算即将被传递的各个实参!在对于第二个实参process而言没什么问题,但对于第一个实参shared_ptr<Test>(new Test)而言却有两部分组成:

  • 执行"new Test"表达式
  • 调用shared_ptr构造函数

于是,在调用func函数之前,编译器必须创建代码,做以下三件事:

  • 调用process()函数
  • 执行"new Test"
  • 调用shared_ptr构造函数

但c++编译器对于怎样执行以上三件事的次序却不一定!唯一可以确定的是:"new Test"一定在shared_ptr构造函数之前执行,因为执行"new Test"表达式的结果还要传递作为shared_ptr构造函数的一个实参,因此,最终会获得这样的操作序列(对于调用process可以任意行):

  1. 执行"new Test"
  2. 调用process()函数
  3. 调用shared_ptr构造函数

以上操作看似正常,实则危机重重。例如一旦process()函数调用异常,此时执行"new Test"所返回的指针则会丢失,因为它尚未置入shared_ptr内,此时则会引发资源泄漏。因此,在“资源被创建(new Test)”和“资源被转换为资源管理对象”两个时间点之间可能发生异常干扰!

        为了避免此类问题,我们规定一定要使用分离语句,首先创建出Test对象后,再置入智能指针内,然后再将智能指针传递给func()函数。如下:

shared_ptr<Test> ptT(new Test);
func(ptT, process()); 

通过分离语句使得将“"new Test"表达式””以及“shared_ptr构造函数的调用”这两个动作和“process的调用”所分隔开,所以编译器不得在他们之间任意选择执行次序,从而绝对不会导致泄漏行为的发生。

参考文献

《Effective C++(第三版)》

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值