Effective C++ 3nd——-资源管理
以对象管理资源
简单来说就是要用类来管理资源,最好使用C++11新标准提供的几种智能指针
请记住:
- 为防止资源泄漏,请使用RAII对象,他们在构造函数中获得资源并在析构函数中释放资源
- 两个常被使用的RAII classes分别是tr1::shared_ptr和auto_ptr。前者通常是较佳选择,因为拷贝行为比较直观。若选择auto_ptr,拷贝动作会使被拷贝的对象指向NULL
RAII:RAII即 “资源获取即初始化”,它是一种方法,定义一个类来封装资源的分配和释放,在类的构造函数中来实现资源的分配和初始化,在类的析构函数中实现资源的释放和清理,是C++中一种管理资源,避免资源泄漏的方法,因为在C++中任何类构造的对象最终都会调用析构函数销毁
在资源管理类中小心 c o p y i n g copying copying 行为
在上述中讲述了基于堆分配的资源管理,然而并非所有资源都是基于堆的,对那种资源而言,像auto_ptr和shared_ptr这样的智能指针往往不适合作为资源掌管着。既然如此,有可能你需要建立自己的资源管理类
假设使用C API函数处理类型为Mutex的互斥器对象,共有lock和unlock两函数可用:
void lock(Mutex &m);
void unlock(Mutex &m);
为确保绝不会忘记将一个被锁住的Mutex解锁,你可能会希望建立一个类用来管理锁。
class Lock{
public:
// 获得资源
explicit Lock(Mutex &pm) : mutexPtr(pm){ lock(mutexPtr); }
// 释放资源
~Lock(){ unlock(mutexPtr); }
private:
Mutex *mutexPtr;
};
// 客户对Lock的用法符合RAII方式
Mutex m;
{ Lock m1(&m); } // 在区块最末尾,自动解锁
这很好,但是如果Lock对象被复制,会发生什么事
Lock m11(&m);
Lock m12(m11);
这是一个一般化问题的特定例子,大多数时候会选择以下两种可能:
- 禁止复制:许多时候允许RAII对象被复制本身不合理,此时可以将cpoying操作声明为私有的
- 使用引用计数法:类似shared_ptr的做法
- 但是shared_ptr指针默认当资源的引用计数为零时删除所指物,并不是删除。但shared_ptr允许指定“删除器”,当引用计数器为零时便被调用,删除器对shared_ptr构造函数而言是第二参数,可有可无。当指定删除器时:
class Lock{ public: // 以某个Mutex初始化shared_ptr,并以unlock函数为删除器 explicit Lock(Mutex &pm) : mutexPtr(pm, unlock){ lock(mutexPtr.get()); } private: std::tr1::shared_ptr<Mutex> mutexPtr;
- 复制底部资源:有时候,只要你喜欢,可以针对一份资源拥有其任意数量的复件。而你需要“资源管理类”的唯一理由是,当你不再需要某个复件时确保它被释放。在此情况下复制资源管理对象,应该同时也复制其所指的资源。也就是说,复制资源管理对象时,进行的是深拷贝
- 转移底部资源的拥有权:在某些场合下你希望确保永远只有一个RAII对象指向一个未加工资源,即使RAII对象被复制依然如此。类似auto_ptr,即资源的所有权会从被复制对象转移到目标对象
请记住:
- 复制RAII对象必须一并复制它所管理的对象,所以资源的copying行为决定RAII对象的copying行为
- 普遍而常见的RAII类copying行为是:抑制cpoying、施行引用计数法。不过其他行为也都可能被实现
在资源管理类中提供对原始资源的访问
简单来说,就是在自己设计的资源管理类中要提供对其所指或所含资源的访问方法,而不是直接处理原始资源。这样你对资源的所有操作就不会离开设计的资源管理类,也会更加安全。
考虑以下代码:
class Investment { };
std::tr1::shared_ptr<Investment> pInv(createInvestment());
// 假设你希望以某个函数处理Investment对象
int daysHeld(const Investment* pi); // 返回投资天数
int days = daysHeld(pInv); // 这样使用会报错,因为函数的形参与实参不匹配
解决方法有两种,分别是显示类型转换和隐式类型转换:
- 显示类型转换可以在类里面定义一个访问资源的成员函数,例如 shared_ptr 中的 get 成员函数
int days = daysHeld(pInv.get()); // 这样就不会报错
- shared_ptr 和 auto_ptr 都重载了 operator -> 运算符和 operator* 运算符,它们允许隐式转换至底部原始指针
由于有时候还是必须取得RAII对象内的原始资源,于是我们可以在类里面提供一个隐式转换函数。考虑以下代码:
FontHandle getFont(); // 这是个C API,用来获取字体
void releaseFont(FontHandle fh); // 释放字体
class Font{
public:
explicit Font(FontHandle fh) : f(fh) { }
~Font() { release(f); }
private:
FontHandle f;
假设有大量与字体相关的C API,它们处理的是FontHandle,那么“将Font对象转换为FontHandle”会是一种很频繁的需求。除了可以在 Font 类里面定义一个显示类型转换函数外,还可以提供一个隐式类型转换函数:
class Font{
public:
...
operator FontHandle() const { return f; } // 隐式类型转换
...
};
// 在调用时我们就可
Font f(getFont());
int newFontSize;
changeFontSize(f, newFontSize); // 将Font隐式转换为FontHandle
但这样的转换会增加错误发生的机会,例如
Font f1(getFont());
FontHanle f2 = f1; // 此时我们希望复制一个Font,但是f1可能隐式地转换为了FontHandle类型
// 此时由f1管理的对象也可以通过f2直接取得,那么如果当f1或f2被销毁时,
// 另外一个就悬空了
是否该提供一个显示转换函数将 RAII 类转换为其底部资源,或是应该提供隐式转换,答案主要取决于 RAII 类被设计执行的特定工作,以及它被使用的情况
最好是使用显示类型转换,因为它将 “非故意的类型转化” 的可能性最小化了,但是有时候隐式类型带来的方便也会更适用
请记住:
- APIs 往往要求访问原始资源,所以每一个 RAII 类应该提供一个 “取得其所管理的资源” 的方法
- 对原始资源的访问可能经由显示转换或隐式转换,一般来说显示转换比较安全,隐式转换比较方便
成对使用 new 和 delete 时要采取相同形式
简单来说就是 new 一个数组的时候,别忘了要采用 delete [] 的形式
delete怎么知道它要删除的是单个对象还是一个对象数组呢。原因在于单一对象和对象数组的内存结构不同,对象数组的内存结构里面还存储了数组的长度(size),以便 delete 知道需要调用多少次析构函数,单一对象的内存则没有这条记录
- 对一个单一对象调用 delete [] 形式的后果是没有定义的,通常会导致发生读异常
- 对一个对象数组调用 delete 形式的后果也是没有定义的,通常会导致调用的析构函数过少
当你使用 typedef 关键字定义对象数组时要注意使用 delete [],例如:
typedef std::string AddressLines[4];
std::string *pal = new AddressLines; // 注意此时new返回的是一个数组
delete pal; // 错误,行为未定义
delete [] pal; // 正确
为避免此类错误,最好不要对数组进行 typedef 动作
请记住:
- 如果你在 new 表达式中使用 [ ] ,必须在相应的 delete 表达式中也使用 [ ] 。如果你在 new 表达式中不使用 [ ] ,一定不要在相应的 delete 表达式中使用 [ ]。
以独立语句将 new 对象置入智能指针
考虑一个函数用来揭示处理程序的优先权,另一个函数用来在某动态分配所得的 Widget 上进行某些带有优先权的处理:
class Widget{
public:
explicit Widget();
};
int priority();
void processWidget(std::tr1::shared_ptr<Widget> pw, int priority());
// 现在我们进行如下调用
processWidget(new Widget, priority());
这样调用会报错,因为 Widget 的构造函数是个 explicit ,不能进行隐式类型转换,此时我们可以
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());
这样可以编译通过,但可能会发生内存泄漏的问题,原因在于 C++ 对函数传递参数的核算顺序并没有明确规定。上面这个函数有两个传入参数,第二个实参只是单纯的调用 priority 函数,但第一个实参由两步组成:
- 第一步是new Widget;第二步是tr1::shared_ptr构造函数
于是编译器可能会采取以下的执行顺序:
- 执行 “new Widget”
- 执行 “priority”
- 执行 “tr1::shared_ptr的构造函数”
如果在第二步执行 priority 的过程中发生错误,则 new 出来的指针将会遗失,因为它并没有被装入智能指针中,因为在 “资源被创建” 和 “资源被转换为资源管理对象” 两个时间点之间有可能发生异常干扰
解决办法就是将这两个实参的核算语句分离开,如:
std::tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());
这样就不会发生错误,因为编译器对于 “跨越语句的各项操作” 没有重新排列的自由。
请记住:
- 以对立语句将 new 对象置入智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏