条款13:以对象管理资源
Use objects to manage resources
假设有个模拟投资行为的类Investment和对应的工厂函数。
class Investment { ... }
Investment* createInvestment();
createInvestment的调用端使用了函数返回的对象,就有责任删除它。现在考虑有个函数f履行了这个责任。
void f()
{
Investment* pInv = createInvestment();
...
delete pInv;
}
其具有的问题事,无论delete如何被略过去(中间发生return,异常退出或goto等),我们泄露的资源不只是内含投资对象的那块内存,还包括那些投资对象所保存的任何资源。
为确保createInvestment返回的资源总是被释放,我们需要将资源放进对象内。如此,便可倚赖C++的“析构函数自动调用机制”确保资源被释放。
auto_ptr是个“类指针对象”,其析构函数自动对其所指对象调用delete。(auto_ptr现已被抛弃)
void f()
{
std::auto_ptr<Investment> pInv(createInvestment());
}
这个简单的例子示范“以对象管理资源”的两个关键想法:
- 获得资源后立刻放进管理对象createInvestment。返回的资源被当作其管理者auto_ptr的初值。实际上“以对象管理资源”的观念常被称为“资源取得时机便是初始化时机”(Resource Acquisition Is Initialization, RAII),因为我们几乎总是在获得资源后在同一语句内以它初始化某个管理对象。
- 管理对象运用析构函数确保资源被释放。
不要让多个auto_ptr指向同一对象,否则对象会被删除一次以上。同时,若通过copy构造函数或copy assignment操作符复制它们,它们会变成null,复制所得的指针会取得资源的唯一拥有权!(STL容器要求其元素发挥“正常的”复制行为,因此其容不得auto_ptr)
因此,unique_ptr和shared_ptr可能是更好的选择。但其在析构函数内做delete而不是delete[]。这意味着在动态分配的array身上使用unique_ptr和shared_ptr是个馊主意。
请注意:
为防止资源泄露,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。
条款14:在资源管理类中小心copying行为
Think carefully about copying behavior in resource-managing classes
并非所有资源都是heap-based,因此,你可能需要建立自己的资源管理类。
void lock(Mutex* pm); //锁定pm所指的互斥器
void unlock(Mutex* pm); //将互斥其解除锁定
为保证不忘记为每一个锁住的Mutex解锁,你可能希望建立一个“资源在构造期间获得,在析构期间释放”的class。
class Lock{
public:
explicit Lock(Mutex* pm) : mutexPtr(pm) {lock(mutexPtr);}
~Lock() {unlock(mutexPtr);}
private:
Mutex *mutexPtr;
}
Mutex m;
{
Lock m1(&m); //用法
}
但如果Lock对象被复制,会发生什么事。
Lock m11(&m);
Lock m12(m11); //将m11复制到m12身上,会发生什么事?
大多数情况你会选择以下两种可能:
- 禁止复制。将拷贝构造函数和拷贝赋值操作符设置为delete。
- 对底层资源祭出“引用计数法”。有时候我们希望保有资源直到它的最后一个使用者被销毁。使用shared_ptr<Mutex>并加上特殊“删除器(deleter)”,它是一个函数或函数对象,当引用次数为0时被调用。
class Lock{
public:
explicit Lock(Mutex* pm) : mutexPtr(pm, unlock)
{
lock(mutexPtr.get());
}
private:
shared_ptr<Mutex> mutexPtr;
}
- 复制底部资源。
- 转移底部资源所有权。(unique_ptr)
请记住:
- 复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。
- 普遍而常见的RAII class copying行为是:抑制copying、施行引用计数法。不过其他行为也都有可能。
条款15:在资源管理类中提供对原始资源的访问
Provide access to raw resources in resource-managing classes
- 显式转换,提供get成员函数返回智能指针内部的原始指针。
shared_ptr<Investment> pInv;
pInv.get();
- 隐式转换,类型转换运算符重载格式如下。
operator target_type() const;
class Font{
public:
explicit Font(FontHandle fh) : f(fh) {}
operator FontHandle() const
{ return f; }
private:
FontHandle f;
}
FontHandle getFont();
Font f1(getFont());
FontHandle f2 = f1;
请记住:
- API往往要求访问原始资源,所以每一个RAII class应该提供一个“取得其所管理之资源”的方法。
- 对原始资源的访问有显式转换和隐式转换。一般而言显式转换比较安全,隐式转换比较方便。
条款16:成对使用new和delete时要采取相同形式
Use the same form in corresponding uses of new and delete
std::string* stringPtr1 = new std::string;
std::string* stringPtr2 = new std::string[100];
delete stringPtr1;
delete [] stringPtr2;
若delete形式使用错误,则程序行为不明确(未有定义)。
条款17:以独立语句将newed对象置入智能指针
Store newed objects in smart pointers in standalone statements
int priority();
void processWidget(shared_ptr<Widget> pw, int priority);
由于shared_ptr构造函数是explicit,因此,我们可以使用以下形式:
processWidget(shared_ptr<Widget>(new Widget()), priority());
上述语句存在资源泄露的风险,因为上述语句受到编译器的影响,可以有不同顺序。
- 调用priority()
- 执行new Widget()
- 调用shared_ptr构造函数
其中可以确定的是new Widget()肯定在调用shared_ptr构造函数前执行,但priority()的顺位无法确定。
若priority()在new Widget()和shared_ptr构造函数间执行,万一priority()调用导致异常,在此情况下new Widget()指针将会遗失。因为它尚未被置入shared_ptr内。是的,在对processWidget的调用过程中可能引发资源泄露,因为在“资源被创建”和“资源被转换为资源管理对象”这两个时间点之间可能发生异常干扰。
解决方法(编译器对”跨越语句的葛祥操作“没有重新排列):
shared_ptr<Widget> pw(new Widget());
processWidget(pw, priority());
请记住:
- 以独立语句将newed对象存储与智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄露。