资源一旦用了它,将来必须还给系统。

条款 13: 以对象管理资源

把资源放进对象内,便可倚赖 C++的“析构函数自动调用机制”确保资源被释放。

createInvestment() 的调用端使用了函数返回的对象后, 有责任删除它。 f 函数履行了这个责任:

1
2
3
4
5
6
7
Investment* createlnvestment(); //返回指针,指向Investment 继承体系内的动态分配对象。调用者有责任删除它。 

void f () {
Investment* pInv = createInvestment(); //调用 factory 函数
...
delete pInv; //释放pInv 所指对象
}

但一些情况下 f 可能无法删除它得自 createinvestment 的对象, 如“…”区域内的一个过早的 return语句。为确保createInvestment() 返回的资源总是被释放,我们需要将资源放进对 内,当控制流离开 f, 该对象的析构函数会自动释放那些资源。

标准程序库提供的 auto_ptr 正是针对这种形势而设计的特制产品。 auto_ptr 是个“类指针 (pointer-like) 对象”,也就是所谓智能指针,其析构函数自动对其所指对象调用 delete。

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

以对象管理资源的两个关键想法:

  • 获得资源后立刻放进管理对象 (managing object) 内。“以对象管理资源” 的观念常被称为 “资源取得时机便是初始化时机” (Resource Acquisition Is Initialization; RAII) ,因为我们几乎总是在获得一笔资源后于同一语句内以它初始化某个管理对象。有时候获得的资源被拿来赋值某个管理对象。但不论哪一种做法,每一笔资源都在获得的同时立刻被放进管理对象中。
  • 管理对象 (managing object) 运用析构函数确保资源被释放。不论控制流如何离开区块,一旦对象被销毁,其析构函数自然会被自动调用,于是资源被释放。

由千 auto_ptr 被销毁时会自动删除它所指之物,所以一定要注意别让多个auto_ptr同时指向同一对象。如果这样,对象会被删除一次以上,这是一个未定义的行为。

为了预防这个问题, auto_ptr 有一个不寻常的性质:若通过copy 构造函数或 copy 赋值操作符复制它, 它本身 (被复制者) 会变成 null, 而复制所得的指针将取得资源的唯一拥有权。

1
2
3
4
std::auto_ptr<Investment> pInv1(createInvestment()); 
//pInv1 指向 createInvestment 返回物
std::auto_ptr<Investment> pInv2(pInv1); //现在 pInv2 指向对象, pInv1 被设为 null
pInv1 = pInv2; //现在 pInv1 指向对象,pInv2 被设为 null

auto_ptr 的替代方案是 “引用计数型智慧指针” (reference-counting smart pointer; RCSP)。RCSP 持续追踪共有多少对象指向某笔资源,在无人指向它时自动删除该资源。 RCSP 提供的行为类似垃圾回收 (garbage collection),不同的是 RCSP 无法打破环状引用 (cycles of references, 如两个其实已经没被使用的对象彼此互指,因而好像还处在被使用状态)。

TR1 的 tr1::shared_ptr (见条款 54) 就是个 RCSP.

auto_ptrtr1::shared_ptr 两者都在其析构函数内做 delete 而不是 delete[]动作。那意味在动态分配而得的 array 身上使用 auto_ptrtr1::shared_ptr 是个馈主意。 vector 和 string 几乎总是可以取代动态分配而得的数组。

但有时候你所使用的资源是目前这些预制式 classes 无法妥善管理的。这样就需要精巧制作你自己的资源管理类。这需要涉及若干你需要考虑的细节。(条款14, 15)。

最后,必须指出, createInvestment() 返回的“未加工指针”(raw
pointer) 是很危险的。即使使用 auto_ptrtr1::shared_ptr 来执行 delete, 首先必须记得将 createinvestment 的返回值存储于智能指针对象内。需要对 createinvestment 进行接口修改(条款 18)。

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

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

有时候会发现需要建立自己的资源管理类。

假设我们使用 C API 函数处理类型为 Mutex 的互斥器对象, lock和 unlock两函数可用:

1
2
void lock(Mutex* pm);  //锁定 pm所指的互斥器
void unlock(Mutex* pm);//将互斥器解除锁定

为确保不会忘记将一个被锁住的 Mutex 解锁,希望建立一个 class 用来管理机锁。这样的 class 的基本结构由 RAII 守则支配,也就是“资源在构造期间获得,在析构期间释放”:

1
2
3
4
5
6
7
8
class Lock { 
public:
explicit Lock (Mutex* pm) : mutexPtr(pm)
{ lock (mutexPtr) ; } //获得资源
~Lock() { unlock (mutexPtr); } //释放资源
private:
Mutex *mutexPtr;
}

我们还需要面对一个问题: “当一个 RAII 对象被复制,会发生什么事?” 大多数时候会选择以下两种可能:

  • 禁止复制。许多时候允许 RAII 对象被复制并不合理。如果复制动作对 RAII class 并不合理,应该禁止复制。条款 6 告诉我们可以将 copying操作声明为 private。

  • 对底层资源运用 “引用计数法”(reference-count) 。有时候我们希望保有资源, 直到它的最后一个使用者被销毁。这种情况下复制 RAII 对象时,应该将资源的 “被引用数” 递增。tr1::shared_ptr 便是如此。

    通常只要内含一个 tr1::shared_ptr 成员变量, RAII 类便可实现引用计数。如果前述的 Lock 打算使用引用计数, 可以改变 mutexPtr 的类型,将它从 Mutex* 改为 tr1::shared_ptr<Mutex>。 然而 tr1::shared_ptr 析构函数的缺省行为是 “当引用次数为 0 时删除其所指物“, 那不是我们所要的 unlock。为此,tr1::shared_ptr 允许指定特定的删除器——一个函数或函数对象 (function object) ,当引用次数为 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 ()); //条款 15 谈到 “get"
    }
    private:
    std::tr1::shared_ptr<Muex> mutexPtr; //使用 shared_ptr 替换 raw point
    };

    请注意,本例的 Lock 类不再声明析构函数。因为没有必要。 class 析构函数会自动调用其 non-static 成员变量的析构函数。

  • 复制底部资源。可以针对一份资源拥有其任意数量的副本。需要“资源管理类”的唯一理由是,当你不再需要某个复件时确保它被释放。在此情况下复制资源管理对象,应该同时复制其所包覆的 资源。也就是说,复制资源管理对象时进行“深度拷贝”。

  • 转移底部资源的拥有权。某些罕见场合下你可能希望确保永远只有一个 RAII 对象指向一个未加工资源,即使 RAII 对象被复制依然如此。此时资源的拥有权会从被复制物转移到目标物。这是 auto_ptr 奉行的复制行为。

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

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

需要一个函数可将 RAII 类的对象转换为其所内含之原始资源 。有两个做法可以达成目标: 显式转换和隐式转换。

auto_ptrtr1::shared_ptr 都提供一个 get 成员函数,用来执行显式转换, 也就是它会返回智能指针内部的原始指针。

auto_ptrtr1::shared_ptr 也重载了指针取值操作符 (operator->operator*) ,它们允许隐式转换至底部原始指针。

对于自己设计的 RAII 类,可以提供显式转换函数,也可以提供隐式转换函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void changeFontSize(FontHandle f, int newSize);
void releaseFont(FontHandle fh);
class Font {
public:
explicit Font(FontHandle fh) : f(fh)
{ }
~Font() { releaseFont(f) ; }
operator FontHandle() const //隐式转换函数
{ return f; }
private:
FontHandle f;
}

Font f(getFont());
int newFontSize;
...
changeFontSize(f, newFontSize); //将 Font 隐式转换为 FontHandle

这使得客户调用 C API 时比较轻松且自然。但是隐式转换会增加错误发生机会。

operator用于隐式类型转换

C++中的 operator 主要有两个作用,一是操作符的重载,一是操作符的转换。这里用到操作符的转换。

语法很简单: operator Type() {}。函数没有参数,没有返回类型,但是有return 语句,在return语句中返回目标类型数据。

1
2
3
4
5
6
7
class A {
public:
......
operator int(){return num;} //在需要情况下, A对象可以转成int类型对象。
private:
int num;
};

是否该提供一个显式转换函数将 RAII 类转换为其底部资源,或是应该提供隐式转换,答案主要取决于 RAII 类被设计执行的特定工 作以及它被使用的情况。最佳设计很可能是坚待条款 18 的忠告: **”让接口容易被正确使用,不易被误用”**。

你的内心也可能认为, RAIl 类内的返回原始资源的函数与“封装” 发生矛盾。但 RAII 类并不是为了封装某物而存在:它们的存在是为了确保资源释放会发生。

也有某些 RAIl 类结合底层资源封装以获得真正的封装实现。例如 tr1::shared_ptr 将它的所有引用计数机构封装了起来,但还是让外界很容易访问其所内含的原始指针。设计良好的 classes隐藏了客户不需要看的部分,但备妥客户需要的所有东西。

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

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

当使用 new 时,有两件事发生。第一, 内存被分配出来。第二, 针对此内存会有一个(或更多)构造函数被调用。当你使用 delete, 也有两件事发生:针对此内存会有一个(或更多)析构函数被调用,然后内存才被释放。 delete 的最大问题 在于:即将被删除的内存之内存有多少对象?这个问题的答案决定了有多少个析构函数需要被调用。

这个问题可以更简单些:将被删除的那个指针所指的是单一对象或对象数组?数组所用的内存通常还包括 “数组大小” 的记录,以便 delete 知道需要调用多少次析构函数。单一对象的内存则没有这笔记录。当你对着一个指针使用 delete, 唯一能够让 delete 知道内存中是否存在一个 “数组大小记录”的办法就是:由你来告诉它。

规则很简单:如果你调用 new 时使用 [], 你必须在对应调用 delete 时也使用 []。如果你调用 new 时没有使用 [], 那么也不该在对应调用 delete 时使用 []。

这个规则对于喜欢使用 typedef 的人也很重要。

1
2
3
4
5
6
typedef std::string AddressLines[4]; //没个人的地址有 4 行,每行是一个 string

std::string* pal = new AddressLines; //"new AddressLines" 返回一个 string*,就像 "new string [4]" 一样。

delete pal; //行为未定义
delete [] pal; //正确

为避免诸如此类的错误,最好不要对数组形式做 typedef 动作。 C++标准程序库 含有 string,vector 等 templates, 可将数组的需求几乎降至为零。

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

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

1
2
3
4
int priority(); 
void processWidget(std::tr1::shared_ptr<Widget> pw, int priority);

processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());//可能造成资源泄露

编译器产出一个 processWidget 调用码之前,必须首先核算即将被传递的各个实参。上述第二实参只是一个单纯的对 priority 函数的调用,但第一实参 std::tr1::shared_ptr<Widget>(new Widget) 由两部分组成.

在调用 processWidget 之前,编译器必须做以下三件事:

  • 调用 priority()
  • 执行 new Widget 表达式
  • 调用 tr1::shared_ptr 构造函数

c++ 编译器可能以不同的次序完成这些事情:对 priority 的调用可以在第一或第二或第三执行。可能获得这样的操作序列:

  1. 执行 new Widget 表达式
  2. 调用 priority()
  3. 调用 tr1::shared_ptr 构造函数

万一对 priority 的调用导致异常, “new Widget” 返回的指针将会遗失。

避免这类问题的办法很简单:使用分离语句:

1
2
std::tr1::shared_ptr<Widget> pw(new Widget); //在单独语旬内以智能指针存储 newed 所得对象.
processWidget(pw, priority());

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


读书笔记,来自 《Effective C++》第 3 版