取自《Effective C++:改善程序与设计的55个具体做法》中文版第三版
三、资源管理
13:以对象管理资源
假设一个基类:
1 | class Investment { ... }; |
该类通过一个工厂函数供应特定的对象:
1 | Investment* createInvestment(); //返回指针,指向动态分配内存 |
在调用端使用了该函数返回的对象后,如果忽略了 delete 语句,则会造成内存泄漏。
为确保 createInvestment 返回的资源总是被释放,可将资源放至对象内,以期通过“析构函数自动调用机制”来确保资源释放。对此标准库提供 auto_ptr ,即“类指针对象”,也叫“智能指针”,使用如下:
1 | void f() |
这是实例用来示范“以对象管理资源”的两个关键想法:
- 获得资源后立即放进管理对象内。
- 管理对象运用析构函数确保资源被释放。
注意,auto_ptr 为防止多个 auto_ptr 指向同一个对象,有一个不寻常的性质:若复制它们,它们会变成 null,复制所得的指针取得资源的唯一拥有权。auto_ptr 的替代方案是“引用技术型智慧指针”(RCSP),RCSP 也是智能指针,持续追踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源,但是无法打破环状引用。
TRI的 tr1::shared_ptr 就是个 RCSP,使用如下:
1 | { |
auto_ptr 和 tr1::shared_ptr 两者在析构函数内做的是 delete 而不是 delete[],因此不适合用在动态分配的数组上。
为防止资源泄露,请使用 RAII 对象,它们在构造函数中获得资源并在析构函数中释放资源。
两个常被使用的 RAII classes 分别是 auto_ptr 和 tr1::shared_ptr,后者通常是较佳选择。若选择 auto_ptr,复制动作会使它指向 null。
14:在资源管理类中小心copying行为
假设需要建立一个类用来管理机锁,基本结构由 RAII 守则支配(资源在构造期间获得,在析构期间释放):
1 | class Lock |
用户的用法符合 RAII 方式:
1 | Mutex m; |
如果 Lock 对象被复制,如何处理?
- 禁止复制。参照条款6,将 copying 操作申明为 private:
1 | class Lock: private Uncopyable { |
- 对底层资源使用“引用计数法”。利用 tr1::shared_ptr 允许指定所谓的“删除器”实现:
1 | class Lock |
- 复制底部资源。进行深度拷贝。
- 转移底部资源的拥有权。例如 auto_ptr 将资源的拥有权从被复制物转移到目标物。
复制 RAII 对象必须一并复制它所管理的资源,所以资源的 copying 行为决定 RAII 的 copying 行为。
常见的 RAII class copying 行为是:抑制 copying、施行引用计数。
15:在资源管理类中提供对原始资源的访问
对于条款13的例子,假设添加一个函数用来处理 Investment 对象,如果传的是 tr1::shared_ptr
- 显式转换:使用 tr1::shared_ptr 和 auto_ptr 的 get 成员函数来执行显示转换,即返回智能指针内部的原始指针。
- 隐式转换:使用其重载的 (operator->和operator*)操作符。
对于需要获取 RAII 对象内的原始资源,可通过添加转换函数的方式实现,见实例:
1 | FontHandle getFont(); //C API |
使用如下:
1 | void changeFontSize(FontHandle f, int newSize); //C API |
注意,使用隐式转换会增加错误发生机会。
APIs 往往要求原始资源,所以每一个 RAII class 应该提供一个“取得其所管理之资源”的方法;
对原始资源的访问可以经由显式转换或隐式转换。一般而言,显式转换安全些,但隐式转换使用比较方便。
16:成对使用 new 和 delete 时要采用相同形式
1 | std::string* stringPtr1 = new std::string; |
对于上述例子,如果对 stringPtr1 采用 “delete []” 形式,可能会多次调用析构函数,释放多余的内存;如果对 stringPtr2 采用 “delete”,可能导致内存释放不完全。
因此调用 new 时使用 [],那么对应的调用 delete 时使用 [];如果调用 new 时没有使用 [],那么对应的调用 delete 时也不该使用 []。对于创建 typedef 类型对象则尤为注意。
如果在 new 表达式中使用 [],必须在相应的 delete 表达式中也使用 [];如果在 new 表达式中不使用 [],不要再相应的 delete 表达式中使用 []。
17:以独立语句将 newed 对象置入智能指针
用一个实例来描述,现假设有以下函数:
1 | int priority(); |
此时调用 processWidget(new Widget, priority());
函数是无法通过编译的,因为构造函数是个 explicit 构造哈桑农户,无法隐式转换,现改写如下:
1 | processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority()); |
此时,编译器在编译 processWidget 前创建代码,完成下述三件事:
- 调用 priority
- 执行
new Widget
- 调用 tr1::shared_ptr 构造函数
然而,C++编译器的工作次序弹性很大,若以下列次序依次完成:
- 执行
new Widget
- 调用 priority
- 调用 tr1::shared_ptr 构造函数
则会出现一种情况带来的资源泄露:编译器在调用 priority 阶段出现异常,从而导致 “new Widget” 返回的指针遗失。该类问题的规避很简单,使用分离语句即可:
1 | std::tr1::shared_ptr<Widget> pw(new Widget); |
以独立语句将 newed 对象存储于智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄露。