《Effective C++》笔记(三)

取自《Effective C++:改善程序与设计的55个具体做法》中文版第三版

三、资源管理

13:以对象管理资源

假设一个基类:

1
class Investment { ... };

该类通过一个工厂函数供应特定的对象:

1
Investment* createInvestment(); //返回指针,指向动态分配内存

在调用端使用了该函数返回的对象后,如果忽略了 delete 语句,则会造成内存泄漏。

为确保 createInvestment 返回的资源总是被释放,可将资源放至对象内,以期通过“析构函数自动调用机制”来确保资源释放。对此标准库提供 auto_ptr ,即“类指针对象”,也叫“智能指针”,使用如下:

1
2
3
4
5
void f()
{
std::auto_ptr<Investment> pInv(createInvestment());
//调用工厂函数,经由auto_ptr的析构函数自动删除pInv
}

这是实例用来示范“以对象管理资源”的两个关键想法:

  1. 获得资源后立即放进管理对象内。
  2. 管理对象运用析构函数确保资源被释放。

注意,auto_ptr 为防止多个 auto_ptr 指向同一个对象,有一个不寻常的性质:若复制它们,它们会变成 null,复制所得的指针取得资源的唯一拥有权。auto_ptr 的替代方案是“引用技术型智慧指针”(RCSP),RCSP 也是智能指针,持续追踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源,但是无法打破环状引用。

TRI的 tr1::shared_ptr 就是个 RCSP,使用如下:

1
2
3
4
{
std::tr1::shared_pt<Investment> pInv(createInvestment());
//和auto_ptr使用相似
}

auto_ptr 和 tr1::shared_ptr 两者在析构函数内做的是 delete 而不是 delete[],因此不适合用在动态分配的数组上。

为防止资源泄露,请使用 RAII 对象,它们在构造函数中获得资源并在析构函数中释放资源。
两个常被使用的 RAII classes 分别是 auto_ptr 和 tr1::shared_ptr,后者通常是较佳选择。若选择 auto_ptr,复制动作会使它指向 null。

14:在资源管理类中小心copying行为

假设需要建立一个类用来管理机锁,基本结构由 RAII 守则支配(资源在构造期间获得,在析构期间释放):

1
2
3
4
5
6
7
8
9
10
11
class Lock
{
public:
explicit Lock(Mutex* pm): mutexPtr(pm)
{ lock(mutexPtr); } //获得资源

~Lock()
{ unlock(mutexPtr); } //释放资源
private:
Mutex *mutexPtr;
};

用户的用法符合 RAII 方式:

1
2
3
4
5
Mutex m;
...
{
Lock m1(&m);
}

如果 Lock 对象被复制,如何处理?

  1. 禁止复制。参照条款6,将 copying 操作申明为 private:
1
2
3
4
class Lock: private Uncopyable {
public:
...
};
  1. 对底层资源使用“引用计数法”。利用 tr1::shared_ptr 允许指定所谓的“删除器”实现:
1
2
3
4
5
6
7
8
9
class Lock
{
public:
explicit Lock(Mutex* pm): mutexPtr(pm, unlock)
{ lock(mutexPtr).get; } //以unlock为删除器,不再需要申明析构函数

private:
std::tr1::shared_ptr<Mutex> *mutexPtr;
};
  1. 复制底部资源。进行深度拷贝。
  2. 转移底部资源的拥有权。例如 auto_ptr 将资源的拥有权从被复制物转移到目标物。

复制 RAII 对象必须一并复制它所管理的资源,所以资源的 copying 行为决定 RAII 的 copying 行为。
常见的 RAII class copying 行为是:抑制 copying、施行引用计数。

15:在资源管理类中提供对原始资源的访问

对于条款13的例子,假设添加一个函数用来处理 Investment 对象,如果传的是 tr1::shared_ptr 对象则会出错,有两种做法可以实现该目标:显式转换和隐式转换。

  1. 显式转换:使用 tr1::shared_ptr 和 auto_ptr 的 get 成员函数来执行显示转换,即返回智能指针内部的原始指针。
  2. 隐式转换:使用其重载的 (operator->和operator*)操作符。

对于需要获取 RAII 对象内的原始资源,可通过添加转换函数的方式实现,见实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
FontHandle getFont();   //C API
void releaseFont(FontHandle fh);

class Font {
public:
explicit Font(FontHandle fh): f(fh) {}
~Font() { releaseFont(f); }

FontHandle get() const { return f; } //显式转换函数
operator FontHandle() condt { return f; } //隐式转换函数
private:
FontHandle f;
}

使用如下:

1
2
3
4
5
6
void changeFontSize(FontHandle f, int newSize); //C API
Font f(getFont());
int newFontSize;
...
changeFontSize(f.get(), newFontSize); //使用显式转换函数
changeFontSize(f, newFontSize); //使用显式转换函数

注意,使用隐式转换会增加错误发生机会。

APIs 往往要求原始资源,所以每一个 RAII class 应该提供一个“取得其所管理之资源”的方法;
对原始资源的访问可以经由显式转换或隐式转换。一般而言,显式转换安全些,但隐式转换使用比较方便。

16:成对使用 new 和 delete 时要采用相同形式

1
2
3
4
5
std::string* stringPtr1 = new std::string; 
std::string* stringPtr2 = new std::string[100];
...
delete stringPtr1;
delete [] stringPtr2;

对于上述例子,如果对 stringPtr1 采用 “delete []” 形式,可能会多次调用析构函数,释放多余的内存;如果对 stringPtr2 采用 “delete”,可能导致内存释放不完全。

因此调用 new 时使用 [],那么对应的调用 delete 时使用 [];如果调用 new 时没有使用 [],那么对应的调用 delete 时也不该使用 []。对于创建 typedef 类型对象则尤为注意。

如果在 new 表达式中使用 [],必须在相应的 delete 表达式中也使用 [];如果在 new 表达式中不使用 [],不要再相应的 delete 表达式中使用 []。

17:以独立语句将 newed 对象置入智能指针

用一个实例来描述,现假设有以下函数:

1
2
int priority();
void processWidget(std::tr1::shared_ptr<Widget> pw, 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++编译器的工作次序弹性很大,若以下列次序依次完成:

  1. 执行 new Widget
  2. 调用 priority
  3. 调用 tr1::shared_ptr 构造函数

则会出现一种情况带来的资源泄露:编译器在调用 priority 阶段出现异常,从而导致 “new Widget” 返回的指针遗失。该类问题的规避很简单,使用分离语句即可:

1
2
std::tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());

以独立语句将 newed 对象存储于智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄露。