条款13:以对象管理资源
假设一个类层次,基类是Investment,然后通过一个工厂函数产生某个特定的invertment对象(可能是某个继承类对象),如下:
//基类
class Investment
{...}
//程序库通过一个工厂函数产生某个特定的inverstment对象,返回指针
Investment* createInvestment();
//这导致调用者需要对这个返回的指针进行删除
void f()
{
Investment* pInv = createInvestment();
//这里中间可能过早返回,或抛出异常,则都不会执行delete,也就会发生资源泄漏
...
delete pInv;
}
为了解决上述问题,可以使用对象来管理这个对象,当退出f时,自动调用对象的析构函数。
C++有两个类可以提供:str::auto_ptr, TR1的tr1::shared_ptr。这两个都可以当ptr被销毁时,对象被销毁。为了防止资源泄漏,应使用RAII对象,它们在构造函数汇总获得资源并在析够函数中释放资源。
注意不能让多个auto_ptr指向同一对象,否则一个对象会被删除多次,严重错误。所以auto为了防止这个问题,有以下不同寻常的性质:若通过copy构造函数或拷贝赋值操作符复制它们,它们会变成null, 而复制所得的指针将取得资源的唯一所有权。由于它的异常拷贝行为,所以它并不是管理动态分配资源的利器。而且不能在STL容器中使用auto_ptr,因为STL容器要求其元素发挥“正常的复制行为”。auto_ptr的行为:
//解决方法1:使用auto_ptr管理对象资源
void f()
{
//这里是RAII,获得资源后立刻放进管理对象
std::auto_ptr<Investment> pInv(createInvestment);
//auto_ptr的复制行为
std::auto_ptr<Investment> pInv2(pInv); //pInv2指向对象,pInv=null
pInv = pInv2; //pInv指向对象,pInv2=null
}//离开函数的时候,调用auto_ptr的析构函数确保资源被释放
auto_ptr的替代方案是“引用计数型智能指针”,它维护一个变量表示共有多少对象指向某笔资源,并在变量变为0时删除对象。类似垃圾回首,但无法打破环状引用。它的拷贝行为正常:
//解决方法2:使用tr1::shared_ptr管理对象资源
void f()
{
//这里是RAII,获得资源后立刻放进管理对象
std::tr1::shared_ptr<Investment> pInv(createInvestment);
//shared_ptr的复制行为
std::tr1::shared_ptr<Investment> pInv2(pInv); //pInv2, pInv指向同一对象
pInv = pInv2; //pInv,pInv2指向同一对象
}//离开函数的时候,调用shared_ptr的析构函数确保pInv,pInv2被释放
令auto_ptr与shared_ptr都在其析构函数内做delete而不是delete[],这意味着在动态分配而得的array上使用它们是不应该的,如:
//以下行为虽然可以通过编译器,但是会有资源泄漏,不应该使用
std::auto_ptr<std::string> aps(new std::string[10]);
std::tr1::shared_ptr<std::string> api(new int[1024]);
条款14:在资源管理类中小心copying行为
对于某些资源,并不适合使用auto_ptr和shared_ptr管理资源,有可能需要自己写管理资源的类,并遵守RAII,例如以下,Mutex表示互斥对象,lock, unlock对资源管理,可以建一个类来管理这个资源,并且对于这个类的拷贝行为,可以有以下四种行为,如下:
//资源管理类的copying行为
class Lock
{
public:
explicit Lock(Mutex* pm) : mutexPtr(pm)
{
lock(mutexPtr); //构造函数锁住资源
}
~Lock()
{
unlock(mutexPtr); //析构函数释放资源
}
};
//客户端的用法
Mutex m;
Lock ml(&m); //锁定,执行关键区域内的操作
...
//如果进行拷贝,可以发生以下情况:
Lock ml1(&m);
Lock ml2(ml1);
//情况1:禁止复制,参考条款6的做法,
//1声明为私有,不定义,如果复制发生链接错误
//2建一个不能拷贝的基类,并私有继承于它,这将错误移到编译期
class Lock : private Uncopyable
{
};
//情况2:引用计数法,例如shared_ptr的行为,但当资源变为0时,它的默认行为是
//删除所指物,而如果我们只是释放锁定,这种情况下可以利用shared_ptr中可以指定“删除器”
//的一个函数或函数对象,当引用计数为0时,调用此函数,这个参数对它的构造函数是可有可无的第二个参数
class Lock
{
public:
explicit Lock(Mutex* pm) : mutexPtr(pm, unlock) //unlock即为指定的删除器
{
lock(mutexPtr.get());
}
private:
std::tr1::shared_ptr<Mutex> mutexPtr; //用shared_ptr管理这个对象,并且指定删除器,自定义行为
};
//而且以上不再需要析够函数,因为析构函数会在引用计数为0时自动调用shared_ptr的删除器
//情况3:复制底部资源,进行深度拷贝,例如string类,
//情况4:转移底部所有权,如auto_ptr的行为,资源所有权从被复制物转移到目标物
总结:1 赋值RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的拷贝行为 2 普遍而常见的RAII类拷贝行为是:抑制拷贝,施行引用 计数法。不过其他行为也都可能实现。
条款15:在资源管理类中提供对原始资源的访问
APIs往往要求访问原始资源,所以每个RAII类应该提供一个“取得其所管理之资源”的方法。
对原始资源的访问可能经由显示转换或隐式转换。一般而言显示转换比较安全,但隐式转换对客户比较方便。例如shared_ptr与auto_ptr都提供了一个get函数,来返回指向的内部资源。再如下面的例子:
//资源管理类中提供对原始资源的访问
class Font
{
public:
explicit Font(FontHandle fh) : f(fh)
{}
//可以提供两种访问原始资源的方式:
//1 显示转换函数,优点是安全,缺点是每次调用都需要访问该函数
FontHandle get() const
{
return f;
}
//2 隐式转换,重载转换操作符,优点不需显示调用,会隐式执行,缺点:容易出错
operator FontHandle() const
{
return f;
}
~Font()
{
releaseFont(f);
}
private:
FontHandle f; //管理的原始字体资源
};
对于隐式的转换会增加错误发生的机会,如下:
Font f1(getFont());
FontHandle f2 = f1; //本来是想拷贝Font对象,却隐式转换f1为FontHandle,然后进行了复制
以上一个潜在的问题是,f1拥有的资源被f2取得,这不会有好下场,例如f1被销毁,字体释放,f2成为虚吊的。
所以获得RAII底部资源,是使用显示调用还是隐式转换,取决于RAII类被设计的特定工作,以及它被使用的情况。条款18有,让接口容易被正确使用,不易被勿用,所以显示的get通常比较受欢迎。
条款16:成对的使用new和delete时采取相同的形式
如果在new表达式中使用[ ], 必须在相应的delete表示式中也使用 [ ], 如果new不使用,delete也不要使用。
这其实就是数组与单个对象的形式。因为单一对象的内存布局一般而言不同于数组内存布局。数组内存通常还包括“数组大小”,以便知道delete需要调用多少次析构函数。单一对象没有这笔记录。
当使用new时,有两件事发生:1 内存会被分配出来(通过operator new) 2针对此内存调用一个(或多个)构造函数;
当使用delete时,有两件事发生:1 析构函数调用 2 内存被释放(通过operator delete)
当你写的一个类中含有一个指针指向动态分配的内存,并提供多个构造函数时,这时要使所有构造函数中使用相同的形式的new将指针成员初始化,因为析构函数中只能有一种delete形式。
最好不要对数组形式做typedefs动作,不然容易发生错误:
typedef std::string AddressLines[4]; //定义了一个AddressLines类型,执行string [4]
std::string* pal = new AddressLines; //这里分配的是数组,相当于new string[4]
delete pal; //错误,但是可能会发生
delete [] pal; //正确的形式,但与以上new不对称
条款17:以独立语句将newed对象置如智能对象。
如以下例子会有隐式的资源泄漏:
//以独立语句将newed对象置入智能指针
//有以下函数
int priority();
void processWidget(std::tr1::shared_ptr<Widget> pw, int priority);
//以下调用形式,不会通过编译,因为tr1::shared_ptr的构造函数是explicit函数
//虽然它接受一个widget的指针,但是不能进行隐式转换,
processWidget(new Widget, priority());
//所以如果使用以下强制转化,可以通过,但这可能有隐式的资源泄漏
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());
对于std::tr1::shared_ptr<Widget>(new Widget)的调用有两部分组成:
1 执行“new Widget”表达式
2 调用tr1::shared_ptr构造函数
于是在调用processWidget之前,必须做以下三件事:
* 调用priority
* 执行“new Widget”表达式
* 调用tr1::shared_ptr构造函数
但编译器完成以上事情的次序并不一定,但是new Widget一定在shared_ptr构造函数之前,它有可能是以下序列
1 执行“new Widget”表达式
2调用priority
3调用tr1::shared_ptr构造函数
这样可能会有资源泄漏,即如果priority调用过程中发生异常,则new出的指针会丢失!!太隐蔽了。。。为了避免以上事情的发生:使用分离语句 1 创建Widge,构造一个智能指针,2 传给processWidget
//解决方案:
std::tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());
总结:以独立语句将newed对象存储与智能指针内,如果不这么做,一旦异常被抛出,可能导致难以察觉的资源泄漏。