C++中的健壮指针和资源管理(3)

来源:互联网 发布:尤克里里手机调音软件 编辑:程序博客网 时间:2024/06/05 16:51
 
std::auto_ptr后来我意识到在STL中的auto_ptr模板,就是我的Strong Pointer。在那时候还有许多的实现差异(auto_ptr的Release方法并不将内部的指针清零--你的编译器的库很可能用的就是这种陈旧的实现),但是最后在标准被广泛接受之前都被解决了。Transfer Semantics(转换语义学)目前为止,我们一直在讨论在C++程序中资源管理的方法。宗旨是将资源封装到一些轻量级的类中,并由类负责它们的释放。特别的是,所有用new操作符分配的资源都会被储存并传递进Strong Pointer(标准库中的auto_ptr)的内部。  这里的关键词是传递(passing)。一个容器可以通过传值返回一个Strong Pointer来安全的释放资源。容器的客户只能够通过提供一个相应的Strong Pointer来保存这个资源。任何一个将结果赋给一个"裸"指针的做法都立即会被编译器发现。auto_ptr<Item> item = stack.Pop (); // okItem * p = stack.Pop (); // Error! Type mismatch. 以传值方式被传递的对象有value semantics 或者称为 copy semantics。Strong Pointers是以值方式传递的--但是我们能说它们有copy semantics吗?不是这样的!它们所指向的对象肯定没有被拷贝过。事实上,传递过后,源auto_ptr不在访问原有的对象,并且目标auto_ptr成为了对象的唯一拥有者(但是往往auto_ptr的旧的实现即使在释放后仍然保持着对对象的所有权)。自然而然的我们可以将这种新的行为称作Transfer Semantics。拷贝构造函数(copy construcor)和赋值操作符定义了auto_ptr的Transfer Semantics,它们用了非const的auto_ptr引用作为它们的参数。auto_ptr (auto_ptr<T> & ptr);auto_ptr & operator = (auto_ptr<T> & ptr);这是因为它们确实改变了他们的源--剥夺了对资源的所有权。  通过定义相应的拷贝构造函数和重载赋值操作符,你可以将Transfer Semantics加入到许多对象中。例如,许多Windows中的资源,比如动态建立的菜单或者位图,可以用有Transfer Semantics的类来封装。Strong Vectors标准库只在auto_ptr中支持资源管理。甚至连最简单的容器也不支持ownership semantics。你可能想将auto_ptr和标准容器组合到一起可能会管用,但是并不是这样的。例如,你可能会这样做,但是会发现你不能够用标准的方法来进行索引。vector< auto_ptr<Item> > autoVector; 这种建造不会编译成功;Item * item = autoVector [0];另一方面,这会导致一个从autoVect到auto_ptr的所有权转换auto_ptr<Item> item = autoVector [0];我们没有选择,只能够构造我们自己的Strong Vector。最小的接口应该如下:template <class T>class auto_vector{public:   explicit auto_vector (size_t capacity = 0);   T const * operator [] (size_t i) const;   T * operator [] (size_t i);   void assign (size_t i, auto_ptr<T> & p);   void assign_direct (size_t i, T * p);   void push_back (auto_ptr<T> & p);   auto_ptr<T> pop_back ();};你也许会发现一个非常防御性的设计态度。我决定不提供一个对vector的左值索引的访问,取而代之,如果你想设定(set)一个值的话,你必须用assign或者assign_direct方法。我的观点是,资源管理不应该被忽视,同时,也不应该在所有的地方滥用。在我的经验里,一个strong vector经常被许多push_back方法充斥着。Strong vector最好用一个动态的Strong Pointers的数组来实现:grow方法申请了一个很大的auto_ptr的数组,将所有的东西从老的书组类转移出来,在其中交换,并且删除原来的数组。  auto_vector的其他实现都是十分直接的,因为所有资源管理的复杂度都在auto_ptr中。例如,assign方法简单的利用了重载的赋值操作符来删除原有的对象并转移资源到新的对象:void assign (size_t i, auto_ptr<T> & p){   _arr [i] = p;}我已经讨论了push_back和pop_back方法。push_back方法传值返回一个auto_ptr,因为它将所有权从auto_vector转换到auto_ptr中。对auto_vector的索引访问是借助auto_ptr的get方法来实现的,get简单的返回一个内部指针。代码:T * operator [] (size_t i){   return _arr [i].get ();}没有容器可以没有iterator。我们需要一个iterator让auto_vector看起来更像一个普通的指针向量。特别是,当我们废弃iterator的时候,我们需要的是一个指针而不是auto_ptr。我们不希望一个auto_vector的iterator在无意中进行资源转换。代码:template<class T>class auto_iterator: publiciterator<random_access_iterator_tag, T *>{public:   auto_iterator () : _pp (0) {}   auto_iterator (auto_ptr<T> * pp) : _pp (pp) {}   bool operator != (auto_iterator<T> const & it) const   { return it._pp != _pp; }   auto_iterator const & operator++ (int) { return _pp++; }   auto_iterator operator++ () { return ++_pp; }   T * operator * () { return _pp->get (); }private:   auto_ptr<T> * _pp;};我们给auto_vect提供了标准的begin和end方法来找回iterator:class auto_vector{public:   typedef auto_iterator<T> iterator;   iterator begin () { return _arr; }   iterator end () { return _arr + _end; }};    你也许会问我们是否要利用资源管理重新实现每一个标准的容器?幸运的是,不;事实是strong vector解决了大部分所有权的需求。当你把你的对象都安全的放置到一个strong vector中,你可以用所有其它的容器来重新安排(weak)pointer。  设想,例如,你需要对一些动态分配的对象排序的时候。你将它们的指针保存到一个strong vector中。然后你用一个标准的vector来保存从strong vector中获得的weak指针。你可以用标准的算法对这个vector进行排序。这种中介vector叫做permutation vector。相似的,你也可以用标准的maps, priority queues, heaps, hash tables等等。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;};最好的方法是,将连接控制封装到一个弄构进行资源转换的类中。对于双链表呢?安全的做法是指明一个方向,如forwardclass 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的调用都在构造函数或者资源转换的时候发生。这样,你在你的代码中处理了资源泄漏的问题。对其他资源进行相似的操作。  你会发现资源管理清除了许多错误和异常处理带来的复杂性。不仅仅你的代码会变得精力充沛,它也会变得简单并容易维护。
原创粉丝点击