来呀,快活呀~

Effective CPP阅读 - Chapter 3 资源管理

C++相信程序员,将内存等底层资源毫无保留地献给程序员使用。然而,做到正确处理资源,写出健壮的代码并不容易,内存泄漏的幽灵始终徘徊在C++程序员身边。遵守本章给出的建议能够使你尽可能地陷入资源泄漏的泥沼,避免奇怪而又毫无头绪的调试。
Pointer是什么?

13 以对象管理资源

书中对“资源”的解释为:一旦使用,将来必须还给系统。最常见的资源是动态分配的内存,此外还有文件描述器,互斥锁,图像界面的笔刷和字型,数据库连接和网络套接字等。

本条款可以(较浅显地)归纳为:

不要使用裸指针!要使用智能指针!

我们不应该指望程序员有多么富有责任心。当获得资源时,不能寄希望于程序员会“良心发现”,在使用完后将其释放。解决这一问题的方法是使用对象管理资源。这样,当离开对象的作用域之后,对象自动析构,资源就会被返回给系统。

许多资源动态分配在堆中。这种情况下,智能指针是一个很好的选择(在C++11中引入了weak_ptrshared_ptr,请使用它们。如果没有C++11,请使用boost)。

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

  • 获得资源后立即放进管理对象内。也就是所谓的RAII(Resource Acquisition Is Initialization)。暴露裸指针是危险的!
  • 管理对象利用析构机制确保资源被释放。不论控制流如何离开区块,一旦对象被销毁,其析构函数自然调用,资源被释放。

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

有时候,资源并非位于堆中(书中所给例子为互斥锁),这时可能需要我们自己建立资源管理类。

如下面的例子,我们将对不同的底层资源使用情景给出不同的解决方案。

1
2
3
4
5
6
7
8
9
10
// Lock是互斥锁的资源管理类
class Lock {
public:
explicit Lock(Mutex* pm):mutexPtr(pm) { // 获得资源
lock(mutexPtr);
}
~Lock() {unlock(mutexPtr); } //释放资源
private:
Mutex* mutexPtr;
};

这样,我们期望能够使用Lock对象实现对互斥锁的自动管理。

1
2
3
4
5
6
Mutex m;   // 互斥锁
// ...
{
Lock ml(&m);
// ...
} // 在区块外,ml自动析构,实现解锁

然而,如何处理Lock对象的拷贝?

  • 情景一,禁止复制。就像上例,很多时候对互斥锁的复制毫无道理。我们可以使用条款6中的trick禁止类的copying行为发生。
  • 情景二,对底层资源进行引用计数。也许我们可以利用shared_ptr,但是需要为其传入参数,指定其析构时并不是要返还资源,而是要解锁。具体请参看shared_ptr部分文档。
  • 情景三,复制底层资源。这时候要注意深度拷贝,例如字符串数组。
  • 情景四,移除底层资源所有权。将所有权移至新的对象。

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

许多API(尤其是和遗留下来的C代码API交互)时,需要获取底层资源的指涉。

对于这种情况,智能指针提供了get()函数用来获取其原始指针的拷贝。同时,它们也重载了->*操作符,允许隐式转换为原始指针。

我们的自定义资源管理类也可以参考它们的实现。其中,隐式转换到类型T可以通过定义operator T()实现。隐式类型转换可能使得代码量更少,客户更方便。但是!请慎用隐式类型转换。

16 成对使用newdelete时采取相同的形式

这项条款是说如果动态分配内存时候使用了new T()得到了单个对象的内存空间,那么销毁时应该使用delete销毁;如果当初使用了new T[]得到了对象数组空间,那么销毁时应该使用delete []。两者不能混用,否则会导致未定义行为。

另外,除非必要,不要使用原始数组。STL中的vectorstring是替代数组的不错选择。

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

以独立语句将newed对象存储于智能指针,否则一旦发生异常,有可能导致难以察觉的内存泄露。

书中给出了一个例子,是由于逗号表达式的执行顺序不定造成的。

如下面的函数声明:

1
2
int priority() { /*some code*/}
void process(shared_ptr<Widget> pw, int priority) { /*some code*/}

在使用时,也许你会这样调用process函数。

1
process(new Widget(), priority());

首先,这样是不能通过编译器的。因为shared_ptr的构造函数是explicit的,不能够隐式将原始指针转换为shared_ptr对象。但是改为下面的代码就没问题了吗?

1
process(shared_ptr<Widget>(new Widget()), priority());

由于C++中函数参数的核算顺序是不确定的,所以可能发生:

  • new出来一个Widget资源
  • 调用priority()函数,注意此时可能引发异常,使得Widget资源无法回收
  • 构造shared_ptr对象

问题已经很明确了。所以我们应该首先确保资源确实被智能指针获取到了,使用下面的独立语句更好。

1
2
auto pw = shared_ptr<Widget>(new Widget());
process(pw, priority());