在C++中,如果对一个块直接分配资源,而且在释放资源之前发生异常,那么这些资源在栈展开(注1)期间将不会得到释放。例如,一个块可以通过调用new动态分配内存,如果该块因异常退出,编译器将不会删除该指针,已分配的内存也不会得到释放。
比如下面这个函数:
void funtion() { vector<string> str_vec; string s; while(cin >> s) v.push_back(s); string *p = new string[v.size()]; delete [] p; }
这个函数定义了一个局部vector并动态分配了一个string数组。在正常情况下,数组和vector都在退出函数之前被撤销,函数最后一个delete语句释放数组,在函数结束时自动撤销vector。
但是,如果在函数内部发生异常,则将撤销的vector但不会释放数组。问题就在于数组是不会自动释放的。所以在new之后但在delete之前发生的异常使得数组没有被撤销,内存得不到释放。而不管何时发生异常,vector的析构函数都会被保证执行。
即,由类类型对象分配的资源一般都会得到正确的释放。运行局部对象的析构函数,由类类型对象分配的资源通常由它们的析构函数释放。
通过定义一个类来封装资源的分配和释放,可以保证正确的释放资源。这一技术常称为“资源分配即初始化”,简称RAII. 这种技术使程序更加“异常安全(exception safe)"。这就意味着,即使发生异常,程序也能正确操作,保证被分配的资源都得到正确释放。
所以我们应该设计类来管理资源分配,以便构造函数分配资源而系够函数释放资源。下面的类是一个例子:
class Resource { public: Resource(parms p): r(allocate(p)) { } ~Resource() { release(r); } private: resource_type *r; resource_type *allocate(parms p); void release(resource_type*); };
Resource类是分配资源和回收资源的类型,它保存表示该资源的数据成员。Resource的构造函数分配资源,而析构函数释放它。当使用这个类型的时候将自动释放资源。
void funtion2() { Resource res(args); }
如果函数正常终止,就在Resource对象超出作用域时释放资源;如果函数因异常中断,编译器就运行Resource的析构函数作为异常处理过程的一部分。
顺便提一下,STL库中提供了RAII的技术例子:auto_ptr类。它是一个接受一类型形参的模板,它为动态分配的对象提供异常安全。
auto_ptr对象只能保存一个指向对象的指针,并且不能用于指向动态分配的数组,使用auto_ptr对象指向动态分配的数组会导致未定义的运行时行为。
每个auto_ptr对象绑定到一个对象或指向一个对象。当auto_ptr对象指向一个对象的时候,可以说它“拥有”该对象。但每个auto_ptr对象只能“拥有”一个对象。当auto_ptr对象超出作用域或另外撤销的时候,就自动回收auto_ptr所指向的动态分配对象。
void funtion3() { auto_ptr<int> p(new int(100)); }
如上述funtion3()中,无论是否发生异常,编译器都会保证释放p指向的内存。
(全文完)
注1:在抛出异常的时候,当前函数将暂停执行,然后开始查找匹配的catch子句。首先会检查throw本身是否在try块内,如果是,检查与该try相关的catch子句,看是否有与抛出对象相匹配的catch子句。如果有,就处理异常;如果找不到,就退出当前函数(并释放当前函数的内存且撤销局部对象),然后继续在上层调用函数中查找。
如果对抛出异常的函数的调用是在try块中,则检查与该try相关的catch子句。如果找到匹配的catch,就处理异常;如果找不到,调用函数也退出,并继续在上层调用函数中查找。
这个过程就叫栈展开。