C++ 程序最常使用的就是动态分配内存,并且同时还要管理文件描述符、互斥锁、sockets等等资源。不论哪一种资源,当程序不再使用时,必须归还给系统。
而在繁冗复杂的系统中,难以做到每一步都在意料之中。
因此,需要遵守某些资源管理方法,这样几乎可以消除资源管理可能出现的问题。
class Tree { ... }; // 继承体系的基类
Tree* Tree::createTree(); // 通过工厂函数返回一个派生类对象的指针,
// 一旦调用者拿到了指针,就要负责删除它
void func()
{
Tree* pTree = createTree(); // 调用工厂函数
... // 注意!控制流在这里可能提前return!!!
delete pTree; // 显式释放pTree所指对象
// 这简直是对“内存泄漏”的死亡邀约。。。
}
如上,func()
经过长期的迭代更新,控制流可能根本不会到达 delete pTree;
或者在中间抛出了异常,那么 pTree
所指资源就未被释放。
为此,借助「C++的析构函数调用机制」即可巧妙地确保资源被释放。对象离开其作用域时,析构函数会被自动调用。
// 以对象管理资源,离开func()时,auto_ptr的析构函数被调用,实现自动删除pTree
void func()
{
std::auto_ptr<Tree> pTree(createTree());
...
}
借助对象管理资源有两个核心思想:
在C++中, auto_ptr
即可实现这一功能。然而多个 auto_ptr
并不能指向同一个对象,否则其他 auto_ptr
都会被设为 null
。
采用“引用计数智能指针”(reference-counting smart pointer,RCSP)代替 auto_ptr
,可以多个 shared_ptr
指向同一个对象。
然而 shared_ptr
无法解决循环引用,为此,又可以使用 weak_ptr
代替 shared_ptr
。
很多情况下,需要编程人员自行编写资源管理类(resource-managing classes),不能只依赖于智能指针。
auto_ptr
和 shared_ptr
以及 weak_ptr
。根据RAII对象设计思想,可以简单地写出这样的资源管理类:
// 定义资源管理类 Lock
class Lock {
public:
explict Lock(Mutex* pm) : mutexPtr(pm) { lock(mutexPtr); } // 构造函数,获得互斥锁资源
~Lock() { unlock(mutexPtr); } // 析构函数,释放资源
private:
Mutex *mutexPtr;
};
// 使用资源管理类 Lock 来管理对象
Mutex m;
...
{
Lock m1(&m); // 锁定互斥锁,借助 RAII 对象
... // 执行一些操作
} // 离开作用域(关键代码段、临界区)后,自动解除互斥锁
这样的RAII对象简单而有效,但如果RAII对象被复制该怎么办?
[1].禁止复制
有些RAII对象的复制逻辑并不合理,比如 Lock
, 因此禁用复制是可取的做法。
在前面的条款6已经提到过,让资源管理类继承 class Uncopyable
,class Uncopyable
的拷贝构造函数和赋值运算符都被声明为 private
。
class Lock : private Uncopyable { // 禁止复制的 class
...
}
[2].引用计数法
引用计数都是针对某个对象的,提到引用计数时指的是「指向的对象的引用计数」。
[3].深拷贝
有时,复制成员指针时,同时将指针所指内存被复制,即深拷贝。
[4].转移底部资源拥有权
有时,可能希望永远只有一个RAII对象指向某个原始资源。即使被复制时也如此,复制会使得拥有权从被复制对象转移到目标对象,比如 auto_ptr
。
RAII 对象的存在是为了管理资源,而不是封装资源,因此在资源管理类中提供对原始资源的访问是符合设计逻辑的。
有两种方法提供对原始资源的访问:
get 成员函数
显式转换,简单有效,也是最常见的,只不过使用时需要频繁调用 get()
比较麻烦
隐式转换函数
隐式转换,很巧妙的设计,但这种接口容易被误用
class Font {
public:
operator FontHandle() const // 隐式转换函数
{ return f; } // Font对象作为函数参数时,会隐式转换成FontHandle对象
private:
FontHandle f;
};
使用 delete []
相当于告诉编译器要释放的内存是一块数组内存,那么编译器就会多次调用对应次数的析构函数,逐个释放对象;
而使用 delete
只会删除一个对象,不要混用。
因此,也不建议对数组进行 typedef
,容易让人混淆,请直接使用 STL 库提供的 vector
等等:
typdef std::string AddressLines[4];
std::string* pal = new AddressLines; // 看似用delete就可以删除,而实际上这是个数组,
// 需要用delete []进行删除
new
使用了 []
,那么 delete
也要使用 []
。new
没有使用 []
,那么 delete
也不要使用 []
。考虑一个复合语句,在函数的实参内调用原始资源的 new
操作,并放入智能指针内:
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());
看似天衣无缝,调用步骤如下:
priority()
new Widget
tr1::shared_ptr
构造函数然而实际上并不总是这样,C++ 编译器的执行次序弹性很大,并不能保证总是按照这个顺序执行下去。
因此,假如调用步骤变成这样:
new Widget
priority()
tr1::shared_ptr
构造函数万一 priority
函数内发生了异常,那么 new Widget
返回的指针就遗失了。
我们用以防卫资源泄漏的武器,竟然在函数调用过程中可能发生资源泄漏?!
解决方法很简单,把 new Widget
放入独立语句内,避免C++ 编译器的弹性调用顺序:
std::tr1::shared_ptr<Widget> pw(new Widget); // 放入独立语句内
processWidget(pw, priority());
new
操作和把 newed
对象置入智能指针内的操作,不要用复合语句。因为C++的函数调用次序是弹性的,它并不总是像表面看上去那样。