软件开发中,会用到各种各样的资源。狭义的资源指内存,而广义的资源包括文件、网络连接、数据库连接、信号量、事件、线程、内存等,甚至可以是状态。资源获取后由于种种原因导致***不能释放的资源称为资源泄漏。针对资源泄漏,提出了各种各样的软件机制和程序设计惯用法,如垃圾收集、RRID[1]、RAII、确定性资源清理等。
RAII是C++语言的一种管理资源、避免泄漏的惯用法。C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。简单的说,RAII 的做法是使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,***在对象析构的时候释放资源。
本文简单介绍RAII的分类以及如何使用RAII,以使代码安全地管理资源。
RAII的分类
根据RAII对资源的所有权可分为常性类型和变性类型,代表者分别是boost:shared_ptr<>[2]和std::auto_ptr<>;从所管资源的初始化位置上可分为外部初始化类型和内部初始化类型。
常性类型是指获取资源的地点是构造函数,释放点是析构函数,并且在这两点之间的一段时间里,任何对该RAII类型实例的操纵都不应该从它手里夺走资源的所有权。变性类型是指可以中途被设置为接管另一个资源,或者干脆被置为不拥有任何资源。外部初始化类型是指资源在外部被创建,并被传给RAII实例的构造函数,后者进而接管了其所有权。boost:shared_ptr<>和std::auto_ptr<>都是此类型。与之相对的是内部初始化类型。
其中,常性且内部初始化的类型是最为纯粹的RAII形式,最容易理解,最容易编码。
RAII实际应用
每当处理需要配对的获取/释放函数调用的资源时,都应该将资源封装在一个对象中,实现自动资源释放。例如,我们无需直接调用一对非成员函数OpenPort/ClosePort,而是可以考虑定义常性且内部初始化的RAII概念的“端口”操作类:
- class Port{
- public:
- Port(const string& destination);//调用OpenPort
- ~Port();//调用ClosePort
- };
- void DoSomething(){
- Port port1(“server1:80”);
- …
- }
- shared_ptr<Port> post2 = /*…*/; //port2在***一个引用它的
- //shared_ptr离开作用域后关闭
通过使用上述RAII类型,可以避免程序员忘记关闭端口而引起的泄漏,还可以确保异常发生时栈展开过程中自动释放端口资源。
RAII与STL容器
STL容器是基于值语义的,在容器内部,对象是常被复制的。如果RAII类型需要存入STL容器,需要作一些处理。
- class Resource
- {
- public:
- Resource() {/*分配资源*/}
- ~ Resource() {/*释放资源*/}
- private:
- int handle;
- };
- std::map< Identifier, Resource > resourceMap;
以上代码中STL容器对Resource的复制将导致运行期错误。***的方法是让RAII类型继承于boost::noncopyable[2],而后在容器中使用引用计数的指针:
- class Resource : public boost::noncopyable
- {
- public:
- Resource() {/*分配资源*/}
- ~ Resource() {/*释放资源*/}
- private:
- int handle;
- };
- typedef boost::shared_ptr<Resource> PointerToResourceType;
- typedef std::map< Identifier, PointerToResourceType> ResourceMapType;
- ResourceMapType resourceMap;
作为替代,还可以使用非拷贝行为的容器:boost::ptr_map<Identifier,Resource> map;
域守卫类
广义的资源可代表状态。这时,域守卫类(scoping classes)所带来的安全价值是无法衡量的。例如:对于在多线程应用中用于同步线程的Mutex,ScopedLock类用于实现锁/解锁的操作:
- class ScopedLock {
- public:
- explicit ScopedLock (Mutex& m) : mutex(m) { mutex.lock(); locked = true; }
- ~ScopedLock () { if (locked) mutex.unlock(); }
- void unlock() { locked = false; mutex.unlock(); }
- private:
- ScopedLock (const ScopedLock&);
- ScopedLock& operator= (const ScopedLock&);
- Mutex& mutex;
- bool locked;
- };
当ScopedLock实例对象被创建时,mutex就被锁定了,而当实例作用域生命期结束时mutex隐式释放。通过这种方法避免了忘记释放的锁,从而避免了此原因所引起的死锁和崩溃。
- {
- ScopedLock locker(mtx);
- …
- } // 自动释放
为每一种资源建立一个RAII类型会使代码显得冗长且容易出错。使用ScopeGuard模板类能够写出简单、异常安全和避免资源泄漏的代码。
- {
- void *buffer = std::malloc(1024);
- ScopeGuard freeIt = MakeGuard(std::free, buffer);
- FILE *fp = std::fopen("afile.txt");
- ScopeGuard closeIt = MakeGuard(std::fclose, fp);
- …
- }
总结
RAII的核心思想是使用对象管理资源,对象“消亡”则自动释放资源。理解和使用RAII能使软件设计更清晰,代码更健壮。与大名鼎鼎的垃圾收集(GC)不同的是,RAII可管理广义的资源,而垃圾收集只关注“内存泄漏”,不关心诸如文件句柄、同步对象等一些系统资源的泄漏问题。RAII能使程序员确定资源释放的时机,这也正是C++/CLI引入确定性资源清理的原因。
【编辑推荐】