C++相信程序员,将内存等底层资源毫无保留地献给程序员使用。然而,做到正确处理资源,写出健壮的代码并不容易,内存泄漏的幽灵始终徘徊在C++程序员身边。遵守本章给出的建议能够使你尽可能地陷入资源泄漏的泥沼,避免奇怪而又毫无头绪的调试。
13 以对象管理资源
书中对“资源”的解释为:一旦使用,将来必须还给系统。最常见的资源是动态分配的内存,此外还有文件描述器,互斥锁,图像界面的笔刷和字型,数据库连接和网络套接字等。
本条款可以(较浅显地)归纳为:
不要使用裸指针!要使用智能指针!
我们不应该指望程序员有多么富有责任心。当获得资源时,不能寄希望于程序员会“良心发现”,在使用完后将其释放。解决这一问题的方法是使用对象管理资源。这样,当离开对象的作用域之后,对象自动析构,资源就会被返回给系统。
许多资源动态分配在堆中。这种情况下,智能指针是一个很好的选择(在C++11中引入了weak_ptr
和shared_ptr
,请使用它们。如果没有C++11,请使用boost)。
以对象管理资源的两个关键想法:
- 获得资源后立即放进管理对象内。也就是所谓的RAII(Resource Acquisition Is Initialization)。暴露裸指针是危险的!
- 管理对象利用析构机制确保资源被释放。不论控制流如何离开区块,一旦对象被销毁,其析构函数自然调用,资源被释放。
14 在资源管理类中小心coping行为
有时候,资源并非位于堆中(书中所给例子为互斥锁),这时可能需要我们自己建立资源管理类。
如下面的例子,我们将对不同的底层资源使用情景给出不同的解决方案。
1 | // Lock是互斥锁的资源管理类 |
这样,我们期望能够使用Lock
对象实现对互斥锁的自动管理。
1 | Mutex m; // 互斥锁 |
然而,如何处理Lock
对象的拷贝?
- 情景一,禁止复制。就像上例,很多时候对互斥锁的复制毫无道理。我们可以使用条款6中的trick禁止类的copying行为发生。
- 情景二,对底层资源进行引用计数。也许我们可以利用
shared_ptr
,但是需要为其传入参数,指定其析构时并不是要返还资源,而是要解锁。具体请参看shared_ptr
部分文档。 - 情景三,复制底层资源。这时候要注意深度拷贝,例如字符串数组。
- 情景四,移除底层资源所有权。将所有权移至新的对象。
15 在资源管理类中提供对原始资源的访问
许多API(尤其是和遗留下来的C代码API交互)时,需要获取底层资源的指涉。
对于这种情况,智能指针提供了get()
函数用来获取其原始指针的拷贝。同时,它们也重载了->
和*
操作符,允许隐式转换为原始指针。
我们的自定义资源管理类也可以参考它们的实现。其中,隐式转换到类型T
可以通过定义operator T()
实现。隐式类型转换可能使得代码量更少,客户更方便。但是!请慎用隐式类型转换。
16 成对使用new
和delete
时采取相同的形式
这项条款是说如果动态分配内存时候使用了new T()
得到了单个对象的内存空间,那么销毁时应该使用delete
销毁;如果当初使用了new T[]
得到了对象数组空间,那么销毁时应该使用delete []
。两者不能混用,否则会导致未定义行为。
另外,除非必要,不要使用原始数组。STL中的vector
和string
是替代数组的不错选择。
17 以独立语句将newed对象置于智能指针
以独立语句将newed对象存储于智能指针,否则一旦发生异常,有可能导致难以察觉的内存泄露。
书中给出了一个例子,是由于逗号表达式的执行顺序不定造成的。
如下面的函数声明:
1 | 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 | auto pw = shared_ptr<Widget>(new Widget()); |