一种优雅的资源管理技术——RAII

      RAII技术,很有意思,与其说是一个技术,不如说是一个编程上的窍门。我们平常编程时也可能会用到,在stl模板库里也有体现,但可能我们并不知道它的名字。

      RAII,即Resource acquisition is initialization”,也就是“资源获取就是初始化”。恩,就是这样。啥意思?不知道(个人感觉这个名字确实取得不怎么样)。

      好,让我们抛开名字,直接用它的简写RAII。RAII简单的说,是为了防止诸如内存泄露、资源泄露等情况产生的一种编程技巧。我们平常在编程时,经常会遇到申请堆内存、获取windows系统资源handle的情况。而这些情况下,均需要我们手动的显示释放你之前申请的内存或资源。我们可以理解为有“借”必须有“还”(在运用锁的情况下,可以看做有“关”必须有“开”,不然就会死锁)。

     但是,你能够保证做到你编写的代码有“借”了,一定就会“还”吗?比如我们获取了windows文件资源的句柄,接下来我们执行了若干操作,中间可能有某些条件下的return语句,甚至还会抛出异常,你能足够细心在每个return及异常的catch中释放句柄资源吗?好,即使我们都释放了,那么代码中会有多处重复的释放资源代码,总是不够那么优雅。

     不用慌张,RAII能让我们优雅的做完释放资源这件事。C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终一定会被调用。我们前面说过,“借”了,一定要“还”。注意到那两个红色的“一定”了吗?一个是C++中一定会调用的函数,一个是我们一定要做的事。那我们把一定要做的事情放到一定会调用的函数中可以吗?这样我们就保证了一定要做的事最终一定会发生!

     没错,RAII就是利用析构函数一定会被调用的特性(不管是在return还是在异常退出的情况下),将释放资源的代码写到类的析构函数中。在类对象初始化的时候,将资源传入该类对象中“托管”,并利用类对象最终调用析构函数将托管的资源释放掉。

     这样,资源的生命周期就等同于类对象的生命周期。

     我们提炼出RAII的三个关键词:

  • 构造函数 (将资源传入类对象,实现“托管”)
  • 生命周期(在类对象的生命周期内,托管的资源不会释放)
  • 析构函数(当类对象生命周期结束(或异常退出时,在进入catch语句前,会自动调用对象析构函数),调用其析构函数,托管的资源在析构函数中同时也释放掉)

显然,要实现RAII,我们需要设计一个符合上述关键词的类,而对于这种类的对象,我们称之为 RAII对象(RAII Object)
那么就上面管理文件资源句柄的例子而言,我们可以设计下面一个RAII类。
// HandleMgr.h
class CHandleMgr
{
public:
	CHandleMgr(const HANDLE& khandle);
	virtual ~CHandleMgr();
        void ReleaseHandle();
        HANDLE& GetHandle() { return m_handle; }
private:
       HANDLE m_handle;
       bool  m_bReleased;
};

// HandleMgr.cpp
CHandleMgr::CHandleMgr(const HANDLE& khandle) : m_handle(khandle),  m_bReleased(false)
{}
CHandleMgr::ReleaseHandle()
{
	if (m_handle != nullptr)
        {
		CloseHandle(m_handle);
		m_handle = nullptr;
        }
	m_bReleased = true;
}
CHandleMgr::~CHandleMgr
{
   try
{	if (!m_bReleased)
	{
		if (m_handle != nullptr)
        	{
			CloseHandle(m_handle);
			m_handle = nullptr;
        	}
	}
}
catch(...) { // do sty }
}

  注意,1、在CHandleMgr类中,我添加了一个ReleaseHandle方法,这样,我们通过显示调用 ReleaseHandle来释放资源,不必非要等到CHandleMgr类生命周期结束。
2、在CHandleMgr析构函数中,有一个try/catch语句,它会捕获所有的C++异常。放在这里的作用是“决不让异常跑出析构函数”(《Effective C++》)。由于对于资源的释放,有时候是危险的,在某些情况下会抛出异常。而让异常逃出析构函数会导致对象析构不完整,同时,请注意,由于C++中try语句只能够捕获一个异常,那么下面的代码片段
CTestClass 
{
public:
       ….
	~CTestClass()
	{
		….
		//may throw exception 2
	}
  	…..
};

int main()
{
	try
	{
	     ……….
		CTestClass  a;
             ………
             // may throw exception 1
	}catch(…)
	{
             // do sht
	}
}
    在main函数的代码中,有可能会抛出异常1。好,当异常1抛出后,被main中的try捕获到,在进入catch前,调用CTestClass的析构函数,但不幸的是,这时候析构函数也抛出了异常2,由于这里只有一个try,并且被异常1用掉了,那么异常2则不会被捕获,导致程序的崩溃。
上面大致是对RAII的描述。其实ReleaseHandle方法并不是必须的,我们可以完全利用对象的生命周期来释放资源。如果要灵活的控制对象的生命周期,我们只需在适当的地方加上{}即可。当程序执行过{},那么{}里面的对象则超出了作用域,生命周期结束,资源释放。
RAII在锁的应用中也是常见的,比如有时候我加了锁,但却忘记解锁,导致死锁。而在C++ 11 中的lock_guard和unique_lock中,则利用了RAII来实现了对锁的自动解锁。下面是stl中lock_guard的实现,在其类内部实现了对锁的管理,在其析构函数中,释放了锁资源。
template <class _Mutex>
class _LIBCPP_TYPE_VIS lock_guard
{
public:
    typedef _Mutex mutex_type;

private:
    mutex_type& __m_;
public:

    _LIBCPP_INLINE_VISIBILITY
    explicit lock_guard(mutex_type& __m)  // 构造函数,接收锁资源,并自动上锁
        : __m_(__m) {__m_.lock();}
    _LIBCPP_INLINE_VISIBILITY
    lock_guard(mutex_type& __m, adopt_lock_t)
        : __m_(__m) {}
    _LIBCPP_INLINE_VISIBILITY
    ~lock_guard() {__m_.unlock();}   // 析构函数, 释放锁资源

private:
    lock_guard(lock_guard const&);// = delete;
    lock_guard& operator=(lock_guard const&);// = delete;
};
     而在std::ofstream中,则实现了对文件handle的自动管理,类似我上面的代码,有兴趣的话大家可以看一下stl源码是怎样实现的。

    

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值