25.RAII(资源获取及初始化)
RAII(资源获取及初始化):resource acquisition is initialization.PAII基本技术原理很简单,如果希望保持对某个重要资源的跟踪,那么创建一个对象,并将资源的生命期和对象的生命期相关联。这样的话,当你构造一个对象的时候,其构造对象会获得一份资源,而析构函数则释放这份资源。
class Resoruce{…} //一个资源类
class ResourceHandle
{
public:
explicit ResourceHandle(Resource *aResource):r_(aReasource){} //获取资源
~ResourceHandle()
{delete r_;} //释放资源
Resource *get()
{return r_;} //访问资源
private:
ResourceHandle(const ResourceHandle &);
ResourceHandle &operator = (const operator &);
Resource *r_;
};
当ResourceHandle对象被用于一个局部变量,或作为函数的参数,或是一个静态变量,我们都可以保证析构函数会得到调用从而释放对象所引用的资源。所注意的是如果是一个静态变量我们需要显示调用它的析构函数。
26.特定于类的内存管理
如果不喜欢标准的new和delete对某个类类型的处置方式,我们不用去忍受他,我们可以给自己的类型定做operator new和operator delete,以满足其特殊的要求。
示例代码:
class Handle{
public:
//…
void *operator new(size_t);
void operator delete(void *);
//…
};
Handle *h=new Handle;
delete h;
在执行Handle *h=new Handle时,编译器会先到Handle的作用域内查找一个operator new,如果没有的话就会调用全局operator new,delete也是同样。因此,如果我们定义了一个operator new最好也同时定义一个operator delete,反之亦然。
!!注意:成员operator new/delete是静态成员函数。这是有道理的。静态函数不会用到this指针,所以new/delete才可以正常使用(根据这个道理我们可以定义类的静态成员函数,并让该成员函数在不用生成对象之前作用意思的事情)。
需要注意的是如果在基类中定义了成员函数operator new/delete的话,要保证基类的析构函数是虚拟的,如果不是虚函数的话,那么通过一个基类指针来删除一个派生类的对象的行为就是未定义的!
class Handle{
public:
//…
void *operator new(size_t);
void operator delete(void *);
//…
};
class MyHandle:public Handle{
//…
void *operator new(size_t);
void *operator delete(void *,size_t); //注意此处的size_t由编译器自动初始化为第一个参数所指对象的大小
//…
};
Handle *h=new MyHandle;
delete h; //如果基类的析构函数不是虚拟的话,就无法自动调用派生类的析构函数
一个常见的误解是以为使用new/delete操作符就意味着使用堆(或者自由存储区)内存,其实并不如此。标准的、全局的operator new/delete的确是从堆上分配内存,但是成员operator new/delete可以做他们想做的一切,对于分配的内存到底从哪来没有任何的限制:可能来自一个特殊的堆,也可能来自一个静态分配的块,也可能来自一个标准容器内部,也可能来自某个函数范围的局部存储区。
27.new、构造函数和异常
为了编写异常安全的代码,保持对任何分配的资源的跟踪并且时刻准备着当异常发生时释放他们,是必不可少的。我们既可以将代码组织成无需回收的资源的方式(相见24“异常安全的函数”),也可以使用资源句柄来自动回收资源(参见25“RAII”);在某些极端的情况下我们还可以使用“肮脏”的try语句块甚至嵌套的try语句块,记住使用try是最后的选择。
关于new操作符的而是用有一个很明显的问题。new时间上会执行两个不同的操作(参见“placement new”):首先,它调用一个名为operator new的函数来分配一些存储区,然后它调用一个构造函数来将未被初始化的存储区变成一个对象:
String *str=new String(“Kicks”);
其中的一个问题是,如果发生了一个异常,我们说不清楚到底是operator new抛出来的,还是String构造函数抛出来的。区分这儿一点很重要,因为如果是operator new成功了,而构造函数抛出的异常,我们就应该调用operator new来归还内存空间;如果是operator new抛出异常,我们就不用调用operator delete。
一种恐怖的方式是编写try代码:
String *str=static_cast
try{
new(str) String(“Kicks”); //placement new
}
catch(….){
::operator delete(str); //如果构造函数抛出异常,则清理资源
}
这种方式是一种费力不讨好的方式,因为如果String有一个operator new和一个operator delete的成员的话(参见26“特定于类的内存管理”),这段代码将无法正常工作。这是一个很好的反例说明,过于聪明的代码一开始或许可以正常工作,但在将来可能会因为一些细微的改变(例如有人添加了特定于String内存管理机制的代码)而失败。那解决的方法是什么那?
幸运的是编译器会给我们处理这种情况,如果出现string构造异常的话,它会自动调用delete/operator delete。本质上来说,同样的情况也适用于数组分配以及使用了重载版本的operator new[]的其他分配,编译器会试图发现并调用适当的operator delete[]。
28.智能指针
智能指针是一个类类型(通常采用类模板来实现),它乔装成一个指针,但额外提供了内建指针无法提供的能力。通常而言,一个智能指针使用类的构造函数、析构函数和复制操作符所提供的能力。来控制(或跟踪)对它所指向的东西的访问。
所有的智能指针都重载->和*操作符,有的甚至还重载了->*操作符(参见“指向类成员的指针并非指针”)。另外有些智能指针(尤其是STL)还重载了++/—/-/+/+=/-=/[]等。
下面是一个能实现执行一个检查,确保在被使用之前不为空:
template
class CheckPtr{
public:
explicit CheckPtr(T *p):p_(p){}
vritual ~CheckPtr();
T *operator -> (){return get();}
T &operator *() {return *get();}
private:
T *p_; //我们所指向的东西
T *get(){
//返回之前检查指针是否为空
if(!p_)
throw NullCheckedPointer();
return p_;
}
//…
};
29.auto_ptr非同寻常
资源句柄是C++编程中广为使用的技术,因此标准库提供了一个资源句柄模板,以便满足许多需要使用资源句柄的场合,这个模板就是auto_ptr。auto_ptr是一个类模板,用于生成具体的智能指针,它们知道在用完之后怎样清理资源。
using std::auto_ptr;
auto_ptr
aShape->draw();
(*aShape).draw();
我们很难使用基本的指针类型来自己编写一个智能指针,所以尽量使用auto_ptr,并且当auto_ptr离开作用域的时候,其析构函数将会释放它所指向(引用)的任何东西。
对于普通指针而言复制操作不会影响到参与复制的源值,即:
T a;
T b(a); //不会对a产生影响
a=b; //不会对b产生影响
但是,当我们使用auto_ptr时参与赋值的源值和目标值都会受到影响:如果aShape是非空的,那么会先delete掉内容,并且代之以aCircle所指向的东西,并且aCircle也会被置空。所以看起来像是对值的传递。因此,下面两种场合最好不用auto_ptr:
用作容器的元素,因为容器中的元素会常常被赋值;
auto_ptr应该指向单个元素,而不是一个数组,因为它只会调用operator delete,而不是array delete。