3 资源管理

文章目录

  • 0. 前言
  • 13. 以对象管理资源
        • 13.1 借助C++的析构函数调用机制
        • 13.2 智能指针
        • 13.3 小结
  • 14. 在资源管理类中小心 coping 行为
        • 14.1 小心拷贝(coping)行为
        • 14.2 小结
  • 15. 在资源管理类中提供对原始资源的访问
        • 15.1 两种方法提供对原始资源的访问
        • 15.2 小结
  • 16. 成对使用 new 和 delete 要采取相同形式
        • 16.1 new 对 delete,new [] 对 delete []
        • 16.2 小结
  • 17. 以独立语句将 newed 对象放入智能指针
        • 17.1 C++ 函数调用的弹性次序
        • 17.2 小结

0. 前言

C++ 程序最常使用的就是动态分配内存,并且同时还要管理文件描述符互斥锁sockets等等资源。不论哪一种资源,当程序不再使用时,必须归还给系统。
而在繁冗复杂的系统中,难以做到每一步都在意料之中。
因此,需要遵守某些资源管理方法,这样几乎可以消除资源管理可能出现的问题。

13. 以对象管理资源

13.1 借助C++的析构函数调用机制

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());
	...
}

借助对象管理资源有两个核心思想:

  1. 获得资源后立刻放入一个管理对象内。也就是“获取资源时机就是初始化时机”(Resource Acquisition Is Initialization,RAII)
  2. 管理对象运用析构函数确保资源释放。无需关心控制流,只要对象离开作用域,析构函数就会被调用,于是资源被释放。

13.2 智能指针

在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),不能只依赖于智能指针。

13.3 小结

  • 为防止资源泄漏,请使用 RAII 对象,它们在构造函数获得资源,并在析构函数释放资源。
  • C++ 内置的RAII对象auto_ptrshared_ptr 以及 weak_ptr

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

14.1 小心拷贝(coping)行为

根据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 Uncopyableclass Uncopyable 的拷贝构造函数和赋值运算符都被声明为 private

class Lock : private Uncopyable {	// 禁止复制的 class
	...
}

[2].引用计数法
引用计数都是针对某个对象的,提到引用计数时指的是「指向的对象的引用计数」。

  1. 创建新对象时,引用计数置1
  2. 拷贝构造函数中,当对象作为另一个对象的副本时,副本指针的引用计数加1
  3. 赋值运算符中,左操作数所指对象引用计数减1,如果减1后引用计数为0,则释放指针所指对象内存;然后右操作数所指对象的引用计数加1
  4. 调用析构函数时,引用计数减1,如果减至0则delete对象

[3].深拷贝
有时,复制成员指针时,同时将指针所指内存被复制,即深拷贝

[4].转移底部资源拥有权
有时,可能希望永远只有一个RAII对象指向某个原始资源。即使被复制时也如此,复制会使得拥有权从被复制对象转移到目标对象,比如 auto_ptr

14.2 小结

  • 复制 RAII 对象时必须同时复制它所管理的资源,所以资源的 copying 行为决定了 RAII 对象的 copying 行为。
  • 常见的 copying 行为有:禁止复制引用计数法,另外深拷贝转移底部资源拥有权也有可能需要实现。

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

15.1 两种方法提供对原始资源的访问

RAII 对象的存在是为了管理资源,而不是封装资源,因此在资源管理类中提供对原始资源的访问是符合设计逻辑的。
有两种方法提供对原始资源的访问:

  1. get 成员函数
    显式转换,简单有效,也是最常见的,只不过使用时需要频繁调用 get() 比较麻烦

  2. 隐式转换函数
    隐式转换,很巧妙的设计,但这种接口容易被误用

class Font {
public:
	operator FontHandle() const		// 隐式转换函数
	{ return f; }					// Font对象作为函数参数时,会隐式转换成FontHandle对象
private:
	FontHandle f;
};

15.2 小结

  • API 往往要求访问原始资源,每个 RAII 类都应提供对原始资源的访问
  • 显式转换隐式转换都可以实现对原始资源的访问,显式转换比较安全,但隐式转换比较方便。

16. 成对使用 new 和 delete 要采取相同形式

16.1 new 对 delete,new [] 对 delete []

使用 delete [] 相当于告诉编译器要释放的内存是一块数组内存,那么编译器就会多次调用对应次数的析构函数,逐个释放对象;
而使用 delete 只会删除一个对象,不要混用。
因此,也不建议对数组进行 typedef,容易让人混淆,请直接使用 STL 库提供的 vector 等等:

typdef std::string AddressLines[4];
std::string* pal = new AddressLines;	// 看似用delete就可以删除,而实际上这是个数组,
										// 需要用delete []进行删除

16.2 小结

  • 如果 new 使用了 [],那么 delete 也要使用 []
    如果 new 没有使用 [],那么 delete 也不要使用 []

17. 以独立语句将 newed 对象放入智能指针

17.1 C++ 函数调用的弹性次序

考虑一个复合语句,在函数的实参内调用原始资源的 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());

17.2 小结

  • 独立语句执行原始资源的 new 操作和把 newed 对象置入智能指针内的操作,不要用复合语句。因为C++的函数调用次序是弹性的,它并不总是像表面看上去那样。

你可能感兴趣的:(资源管理,C++,Effective,C++,Effective,C++)