资源管理

C++程序中最常使用的资源就是动态分配内存,其他的资源还有文件描述符、互斥锁、图形界面中的字形和笔刷、数据库连接、以及网络Socket。

条款13:以对象管理资源

有时候会因为种种原因,略过 delete 语句,这样就会导致内存泄露。

为确保资源总是被释放,需要将资源放进对象内,便可倚赖 C++ 的析构函数自动调用机制确保资源被释放。

  • 获得资源后立刻放进管理对象:“以对象管理资源”的观念常被称为“资源取得时机便是初始化时机”(Resource Acquisition Is Initialization,RAII)。有时候获得资源后立刻初始化某个管理对象,有时候获得的资源被拿来赋值某个管理对象,但不论哪种做法,每一笔资源都在获得的同时立刻被放进管理对象中。
  • 管理对象运用析构函数确保资源被释放。一旦对象被销毁,其析构函数自然会被自动调用,于是资源被释放。

一定要注意别让多个 auto_ptr 同时指向同一对象,如果真是那样,对象会被删除一次以上,会产生未定义行为。为预防这个问题,auto_ptr 有一个不寻常的性质:若通过 copy 构造函数或者 copy assignment 操作符复制它们,它们会变成 NULL,而复制所得的指针将取得资源的唯一拥有权。

auto_ptr 的替代方案是 引用计数型智能指针 shared_ptr,持续追踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源。

小结:

  • 为防止资源泄露,请使用 RAII 对象,它们在构造函数中获得资源并在析构函数中释放资源。
  • 两个常被使用的 RAII classes 分别是 tr1::shared_ptrauto_ptr。前者通常是较佳选择,因为其 copy 行为比较直观。若选择 auto_ptr 复制动作会使它(被复制物)指向 null

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

当一个 RAII 对象被复制,会发生什么事?大多数时候你会选择以下两种可能:

  • 禁止复制。许多时候允许 RAII 对象被复制并不合理,你便应该禁止之。
  • 对底层资源祭出“引用计数法”。通常只要内含一个 tr1::shared_ptr 成员变量,RAII classes 便可实现出 reference-counting copying 行为。

当我们用 Mutex,我们想要做的释放动作是解除锁定而非默认的删除。幸运的是 tr1::shared_ptr 允许指定所谓的“删除器”,那是一个函数或函数对象,当引用次数为0时便被调用(此机能并不存在于 auto_ptr——它总是将其指针删除)。

1
2
3
4
5
6
7
8
9
class Lock {
public:
explicit Lock(Mutex* pm) : mutexPtr(pm, unlock) {
// 以某个Mutex初始化shared_ptr 并以 unlock 函数做为删除器
lock(mutexPtr.get());
}
private:
std::shared_ptr<Mutex> mutexPtr; // 使用 shared_ptr替换raw pointer
};

不需要再声明析构函数,因为没有必要。class 析构函数会自动调用其 non-static 成员变量的析构函数,而 mutexPtr 的析构函数会在互斥器的引用次数为0时自动调用 std::shared_ptr 的删除器(本例为 unlock)。

  • 复制底部资源。可以针对一份资源拥有其任意数量的副本。在此情况下复制资源管理对象,应该同时也复制其所包覆的资源。也就是说,此时进行的是“深度拷贝”。
  • 转移底部资源的控制权。某些罕见场合下你可能希望确保永远只有一个 RAII 对象指向一个未加工资源(raw pointer),即使 RAII 对象被复制依然如此。这是 auto_ptr 奉行的复制意义。

小结:

  • 复制 RAII 对象必须一并复制它所管理的资源,所以资源的 copying 行为决定 RAII 对象的 copying 行为。
  • 普遍而常见的 RAII class copying 行为是:禁止 copying 、施行引用计数法。不过其他行为也都可能被实现。

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

在一个完美世界中你将倚赖这样的资源管理类来处理和资源之间的所有互动,而不是玷污双手直接处理原始资源(raw resource)。

但是在很多场合,许多 API 直接指涉资源,只得绕过资源管理对象,直接访问原始资源。这时候需要一个函数可将 RAII class 对象转换为其所内含之原始资源。

shared_ptrauto_ptr 都提供一个 get 成员函数,用来执行显式转换,也就是返回智能指针内部的原始指针(的复件)。

shared_ptrauto_ptr 也重载了 operator->operator* ,它们允许隐式转换至底部原始指针。
由于有时候还是必须取得 RAII 对象内的原始资源,做法是提供一个隐式转换函数。

小结:

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

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

要注意:即将被删除的那个指针,所指的是单一对象或对象数组?数组所用的内存通常还包括“数组大小”的记录,以便 delete 知道需要调用多少次析构函数。

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

小结:

  • 如果你在 new 表达式中使用 [],必须在相应的 delete 表达式中也是用 []。
  • 如果你在 new 表达式中不使用 [],一定不要在相应的 delete 表达式中使用 []。

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

1
2
3
4
// 函数原型
processWidget(std::shared_ptr<Widget> pw, int priority);

processWidget(new Widget, priority());

这种调用方式不能通过编译,shared_ptr 构造函数是个 explicit 构造函数,无法进行隐式转换。

1
processWidget(std::shared_ptr<widget>(new Widget), priority());

这样写就可以通过编译,但是却可能发生资源泄露。

在调用 processWidget 之前,编译器必须创建代码,做一下三件事:

  • 调用 priority
  • 执行 new Widget
  • 调用 shared_ptr 的构造函数

和其他语言如 Java 不同,C++ 编译器没有特定次序完成这些事情。new 一定发生在 shared_ptr 构造之前,但是调用 priority() 就不确定了。如果第二步调用这个函数,那就是下面的顺序:

  1. 执行 new Widget
  2. 调用 priority()
  3. 调用 shared_ptr 的构造函数
    万一 priority() 的调用导致异常,会发生什么呢?答案是可能引发资源泄露。避免这类问题的方法是:使用分离语句,分别写出 (1)new Widget ,(2)将它置入一个智能指针内,然后再把智能指针传给 processWidget

小结:

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