| | | | | | | [文章信息] | | | 作者: | 刘挺 | | 时间: | 2004-10-25 | | 出处: | csdn | | 责任编辑: | 方舟 | |
| [文章导读] | | | 将资源的概念推广到程序中创建、释放的所有对象也是十分方便的,无论对象是在堆中分配的还是在栈中或者是在全局作用于内生命的 | |
| |
|
| | | |
|
|
|
|
|
Code Inspection(编码检查)
如果你严格遵照资源管理的条款,你就不会再资源泄露或者两次删除的地方遇到麻烦。你也降低了访问野指针的几率。同样的,遵循原有的规则,用delete删除用new申请的德指针,不要两次删除一个指针。你也不会遇到麻烦。但是,那个是更好的注意呢?
这两个方法有一个很大的不同点。就是和寻找传统方法的bug相比,找到违反资源管理的规定要容易的多。后者仅需要一个代码检测或者一个运行测试,而前者则在代码中隐藏得很深,并需要很深的检查。
设想你要做一段传统的代码的内存泄露检查。第一件事,你要做的就是grep所有在代码中出现的new,你需要找出被分配空间地指针都作了什么。你需要确定导致删除这个指针的所有的执行路径。你需要检查break语句,过程返回,异常。原有的指针可能赋给另一个指针,你对这个指针也要做相同的事。
相比之下,对于一段用资源管理技术实现的代码。你也用grep检查所有的new,但是这次你只需要检查邻近的调用:
● 这是一个直接的Strong Pointer转换,还是我们在一个构造函数的函数体中?
● 调用的返回知是否立即保存到对象中,构造函数中是否有可以产生异常的代码。?
● 如果这样的话析构函数中时候有delete?
下一步,你需要用grep查找所有的release方法,并实施相同的检查。
不同点是需要检查、理解单个执行路径和只需要做一些本地的检验。这难道不是提醒你非结构化的和结构化的程序设计的不同吗?原理上,你可以认为你可以应付goto,并且跟踪所有的可能分支。另一方面,你可以将你的怀疑本地化为一段代码。本地化在两种情况下都是关键所在。
在资源管理中的错误模式也比较容易调试。最常见的bug是试图访问一个释放过的strong pointer。这将导致一个错误,并且很容易跟踪。
共享的所有权
为每一个程序中的资源都找出或者指定一个所有者是一件很容易的事情吗?答案是出乎意料的,是!如果你发现了一些问题,这可能说明你的设计上存在问题。还有另一种情况就是共享所有权是最好的甚至是唯一的选择。
共享的责任分配给被共享的对象和它的客户(client)。一个共享资源必须为它的所有者保持一个引用计数。另一方面,所有者再释放资源的时候必须通报共享对象。最后一个释放资源的需要在最后负责free的工作。
最简单的共享的实现是共享对象继承引用计数的类RefCounted:
class RefCounted { public: RefCounted () : _count (1) {} int GetRefCount () const { return _count; } void IncRefCount () { _count++; } int DecRefCount () { return --_count; } private int _count; }; | 按照资源管理,一个引用计数是一种资源。如果你遵守它,你需要释放它。当你意识到这一事实的时候,剩下的就变得简单了。简单的遵循规则--再构造函数中获得引用计数,在析构函数中释放。甚至有一个RefCounted的smart pointer等价物:
template <class T> class RefPtr { public: RefPtr (T * p) : _p (p) {} RefPtr (RefPtr<T> & p) { _p = p._p; _p->IncRefCount (); } ~RefPtr () { if (_p->DecRefCount () == 0) delete _p; } private T * _p; }; | 注意模板中的T不比成为RefCounted的后代,但是它必须有IncRefCount和DecRefCount的方法。当然,一个便于使用的RefPtr需要有一个重载的指针访问操作符。在RefPtr中加入转换语义学(transfer semantics)是读者的工作。
所有权网络
链表是资源管理分析中的一个很有意思的例子。如果你选择表成为链(link)的所有者的话,你会陷入实现递归的所有权。每一个link都是它的继承者的所有者,并且,相应的,余下的链表的所有者。下面是用smart pointer实现的一个表单元:
class Link { // ... private auto_ptr<Link> _next; }; | 最好的方法是,将连接控制封装到一个弄构进行资源转换的类中。
对于双链表呢?安全的做法是指明一个方向,如forward:
class DoubleLink { // ... private DoubleLink *_prev; auto_ptr<DoubleLink> _next; }; | 注意不要创建环形链表。
这给我们带来了另外一个有趣的问题--资源管理可以处理环形的所有权吗?它可以,用一个mark-and-sweep的算法。这里是实现这种方法的一个例子:
template<class T> class CyclPtr { public: CyclPtr (T * p) :_p (p), _isBeingDeleted (false) {} ~CyclPtr () { _isBeingDeleted = true; if (!_p->IsBeingDeleted ()) delete _p;
}
void Set (T * p) { _p = p; } bool IsBeingDeleted () const { return _isBeingDeleted; } private T * _p; bool _isBeingDeleted; }; | 注意我们需要用class T来实现方法IsBeingDeleted,就像从CyclPtr继承。对特殊的所有权网络普通化是十分直接的。
将原有代码转换为资源管理代码
如果你是一个经验丰富的程序员,你一定会知道找资源的bug是一件浪费时间的痛苦的经历。我不必说服你和你的团队花费一点时间来熟悉资源管理是十分值得的。你可以立即开始用这个方法,无论你是在开始一个新项目或者是在一个项目的中期。转换不必立即全部完成。下面是步骤。
首先,在你的工程中建立基本的Strong Pointer。然后通过查找代码中的new来开始封装裸指针。
最先封装的是在过程中定义的临时指针。简单的将它们替换为auto_ptr并且删除相应的delete。如果一个指针在过程中没有被删除而是被返回,用auto_ptr替换并在返回前调用release方法。在你做第二次传递的时候,你需要处理对release的调用。注意,即使是在这点,你的代码也可能更加"精力充沛"--你会移出代码中潜在的资源泄漏问题。
下面是指向资源的裸指针。确保它们被独立的封装到auto_ptr中,或者在构造函数中分配在析构函数中释放。如果你有传递所有权的行为的话,需要调用release方法。如果你有容器所有对象,用Strong Pointers重新实现它们。
接下来,找到所有对release的方法调用并且尽力清除所有,如果一个release调用返回一个指针,将它修改传值返回一个auto_ptr。
重复着一过程,直到最后所有new和release的调用都在构造函数或者资源转换的时候发生。这样,你在你的代码中处理了资源泄漏的问题。对其他资源进行相似的操作。
你会发现资源管理清除了许多错误和异常处理带来的复杂性。不仅仅你的代码会变得精力充沛,它也会变得简单并容易维护。
|
|
|
|
|
|
|
|
|