用于大型程序的工具
--异常处理[续2]
八、自动资源释放
考虑下面函数:
void f() { vector<string> v; string s; while (cin >> s) { v.push_back(s); } string *p = new string[v.size()]; //... delete p; }
在正常情况下,数组和vector都在退出函数之前被撤销,函数中最后一个语句释放数组,在函数结束时自动撤销vector。
但是,如果在函数内部发生异常,则将撤销vector而不会释放数组。问题在于数组不是自动释放的。在new之后但在delete之前发生的异常使得数组没有被撤销。不管何时发生异常,都保证运行vector析构函数。
用类管理资源分配
对析构函数的运行导致一个重要的编程技术的出现:异常安全的---即使发生异常,程序也能正常工作。在这种情况下,“安全”来自于保证“如果发生异常,被分配的任何资源都适当地释放”!
通过定义一个类来封装资源的分配和释放,可以保证正确释放资源---“资源分配即初始化”(RAII)
具体:设计资源管理类,以便构造函数分配资源而析构函数释放资源。想要分配资源的时候,就定义该类类型的对象。如果不发生异常,就在获得资源的对象超出作用域的时候释放资源。更为重要的是,如果在创建了对象之后但在它超出作用域之前发生异常,那么,编译器保证撤销该对象,作为展开定义对象的作用域的一部分。如:
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 fcn() { Resource res(args); //申请资源 //... }//自动释放资源
如果函数正常终止,就在Resource对象超出作用域时释放资源;如果函数因异常而提早退出,编译器就运行Resource的析构函数作为异常处理过程的一部分。
【最佳实践】
可能存在异常的程序以及分配资源的程序应该使用类来管理那些资源---使用资源管理类来分配和回收可以保证如果发生异常就释放资源。
//P591 习题17.7 //1 void exercise(int *b,int *e) { vector<int> v(b,e); int *p = new int[v.size()]; try { ifstream in("ints"); //... } catch(...) { delete p; //... } }
//2 template <typename resource_type> class Resource { public: Resource(size_t sz):r(new resource_type[sz]) {} ~Resource() { release(r); } private: resource_type *r; void release(resource_type *); }; void exercise(int *b,int *e) { vector<int> v(b,e); Resource<int> res(v.size()); //... ifstream in("ints"); //... }
九、auto_ptr类
标准库auto_ptr类是上一节中介绍的异常安全的“资源分配即初始化”技术例子。auto_ptr类是接受一个类型形参的模板,它为动态分配的对象提供异常安全,auto_ptr类在头文件memory中定义。
auto_ptr类 |
|
---|---|
auto_ptr<T>ap; |
创建名为ap的未绑定的auto_ptr对象 |
auto_ptr<T>ap(p); |
创建名为ap的auto_ptr对象,ap拥有指针p指向的对象。该构造函数为explicit。 |
auto_ptr<T>ap1(ap2); |
创建名为ap1的auto_ptr对象,ap1保存原来存储在ap2中的指针。将所有权转给ap1,ap2成为未绑定的auto_ptr对象 |
ap1= ap2 |
将所有权ap2转给ap1。删除ap1指向的对象并且使ap1指向ap2指向的对象,使ap2成为未绑定的 |
~ap |
析构函数。删除ap指向的对象 |
*ap |
返回对ap所绑定的对象的引用 |
ap-> |
返回ap保存的指针 |
ap.reset(p) |
如果p与ap的值不同,则删除ap指向的对象并且将ap绑定到p |
ap.release() |
返回ap所保存的指针并且使ap成为未绑定的 |
ap.get() |
返回ap保存的指针 |
【小心地雷】
auto_ptr只能用于管理从new返回的一个对象,它不能管理动态分配的数组[会导致未定义的运行时行为]。
当auto_ptr被复制或复制时,有不寻常的行为,因此,不能将auto_ptr存储在标准库容器类型中。
每个auto_ptr对象绑定到一个对象或者指向一个对象。当auto_ptr对象指向一个对象的时候,可以说它“拥有”该对象。当auto_ptr对象超出作用域或者另外撤销的时候,就自动回收auto_ptr所指向的动态分配对象。
class Text { public: Text() { cout << "Text" << endl; } ~Text() { cout << "~Text" << endl; } }; int main() { //对比! auto_ptr<Text> ap(new Text); Text *p = new Text; }
1、为异常安全的内存分配使用auto_ptr
如果通过常规指针分配内存,而且在执行delete之前发生异常,就不会自动释放内存:
void f() { int *ip = new int(42); //如果在此处抛出异常,并且不被局部捕获,则内存没法释放 delete ip; }
如果使用auto_ptr对象来代替,将会自动释放内存,即使提早退出这个块也是这样:
void f() { auto_ptr<Text> ap(new Text); throw runtime_error("TEXT"); }
在这个例子中,编译器保证在展开超过了f之前运行ap的析构函数。
2、auto_ptr是可以保存在任何类型指针的模板
auto_ptr类是可以接受单个类型形参的模板:
auto_ptr<string> strPtr(new string("Brontosaurus")); cout << *strPtr << endl;
3、将auto_ptr绑定到指针
在最常见的情况下,将auto_ptr对象初始化为由new表达式返回的对象的地址:
auto_ptr<int> intPtr(new int(1024));
接受指针的构造函数为explicit构造函数,所以必须使用初始化的直接形式来创建auto_ptr对象:
auto_ptr<int> iP = new int(1024); //Error auto_ptr<int> pI(new int(1024)); //OK
iP所指的由 new表达式创建的对象在超出作用域时自动删除。如果iP是局部对象,iP所指对象在定义pi的块的末尾删除;如果发生异常,则iP也超出作用域,析构函数将自动运行iP的析构函数作为异常处理的一部分;如果iP是全局对象,就在程序末尾删除iP引用的对象。
4、使用auto_ptr对象
auto_ptr类定义了解引用操作符(*)和箭头操作符(->)的重载版本,因为auto_ptr定义了这些操作符,所以可以用类似于使用内置指针的方式使用auto_ptr对象:
auto_ptr<string> strPtr(new string("HELLO!")); cout << *strPtr << endl; *strPtr = "TRex"; string s = *strPtr; cout << s << endl; if (strPtr -> empty()) { cout << "Empty!" << endl; } else { cout << "Not Empty!" << endl; }
auto_ptr的主要目的,在保证自动删除auto_ptr对象引用的对象的同时,支持普通指针式行为。正如我们所见,自动删除该对象这一事实导致在怎样复制和访问它们的地址值方面,auto_ptrs与普通指针明显不同。