C++ 智能指针——简单实现以及循环引用问题

来源:互联网 发布:手机mac过滤怎么设置 编辑:程序博客网 时间:2024/06/11 10:22

   智能指针 smart pointer


一.智能指针及其发展史


什么是智能指针呢?


       现阶段的智能指针(smart pointer)的一种通用实现技术,是使用引用计数(reference count)。智能指针类将一个

计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象的指针指向同一对象。


        智能指针的原理就是管理资源的RALL机制(RALL机制便是通过利用对象的自动销毁,使得资源也具有了生命周

期,有了自动销毁,自动回收的功能)。RAII全称为Resource Acquisition Is Initialization,它是在一些面向对象语

言中的一种惯用法。资源分配即初始化,定义一个类来封装资源的分配和释放,在构造函数完成资源的分配和初始

化,在析构函数完成资源的清理,可以保证资源的正确初始化和释放。RAII要求,资源的有效期与持有资源的对象的

生命期严格绑定,即由对象的构造函数完成资源的分配,同时由析构函数完成资源的释放。在这种要求下,只要

对象能正确地析构,就不会出现资源泄露问题。


        从另一个角度看,智能指针是存储指向动态分配(堆)对象指针的类。除了能够在适当的时间自动删除指向的对

象外,他们的工作机制很像C++的内置指针。智能指针在面对异常的时候格外有用,因为他们能够确保正确的销毁动

态分配的对象。他们也可以用于跟踪被多用户共享的动态分配对象。


        事实上,智能指针能够做的还有很多事情,例如处理线程安全,提供写时复制,确保协议,并且提供远程交互服

务。有能够为这些ESP (Extremely Smart Pointers)创建一般智能指针的方法,但是并没有涵盖进来。智能指针的大部

分使用是用于生存期控制,阶段控制。它们使用operator->和operator*来生成原始指针,这样智能指针看上去就像一

个普通指针。


智能指针的发展史:


第一阶段(C++98):

                                  auto_ptr  —— 自动指针

      auto_ptr 的主要思想是 管理权转移

      但其缺陷很大,当通过拷贝构造函数,通过操作符=赋值后,原来的那个智能指针对象就失效了。只有新的智能 

      指针对象可以有效使用了。被包装的指针指向的内存块就像是一份独享占用的财产,当通过复制构造,通过=赋 

      值传给别的智能指针对象时,原有的对象就失去了对那块内存区域的拥有权。

      简单的说,也就是任何时候只能有一个智能指针对象指向那块内存区域,不能有两个对象同时指向那块内存区域。


第二阶段(C++03):

即在Boost中的智能指针:

                               scoped_ptr —— 守卫指针

        scoped_ptr的主要思想是 防拷贝


                               share_ptr —— 共享指针

                               weak_ptr —— 用于解决share_ptr的循环引用缺陷问题

        share_ptr的主要思想是 引用计数

        boost库中提供了一种新型的智能指针shared_ptr,它解决了在多个指针间共享对象所有权的问题,同时也满足容

        器对元素的要求,因而可以安全地放入容器中。


第三阶段(C++11):

                               unique_ptr

                               share_ptr

                               weak_ptr

         unique_ptr还是运用了 防拷贝 的思想

         unique_ptr是一种定义在<memory>中的智能指针。它持有对对象的独有权——两个unique_ptr不能指向一个对

         象,不能进行复制操作只能进行移动操作。



二.智能指针的简单实现

(1)auto_ptr 的实现

template <class T>class AutoPtr{public:AutoPtr(T* ptr):_ptr(ptr){}AutoPtr(AutoPtr<T>& ap):_ptr(ap._ptr){_ptr = NULL;}AutoPtr<T>& operator=(AutoPtr<T>& ap){if (_ptr != ap._ptr){if (_ptr){delete _ptr;_ptr = ap._ptr;ap._ptr = NULL;}}return *this;}~AutoPtr(){if (_ptr){delete _ptr;}}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}protected:T* _ptr;};


根据auto_ptr的原理我们能明白,如果按以下的写法:

AutoPtr<int>ap1(new int);AutoPtr<int>ap2(ap1);

我们会发现同一块内存释放了两次,程序会奔溃。如图:


      这里是因为 ap2 完全的夺取了 ap1 的管理权。然后导致访问 ap1 的时候程序就会崩溃。如果在这里调用 ap2 = 
    ap1 程序一样会崩溃,原因还是 ap1 被夺走管理权,所以这种编程思想及其不符合C++思想,所以它的设计思想就是有一定的缺陷。
    因此,一般不推荐使用 auto_ptr 智能指针。


(2)scoped_ptr 的实现
template<class T>class ScopedPtr{public:ScopedPtr(T*_ptr):_ptr(ptr){}~ScopedPtr(){delete _ptr;}T &operator*(){return *_ptr;}T*operator->(){return _ptr;}protected:ScopedPtr(ScopedPtr<T>& s);ScopedPtr<T> operator=(ScopedPtr<T>& s);protected:T*_ptr;};


scoped_ptr 属于 boost 库,定义在 namespace boost 中,使用时要包含头文件#include<boost/smart_ptr.hpp> scoped_ptr 独享所有权,不会提供拷贝构造 和 "="的功能。即它将拷贝构造和赋值运算符的重载定义为protected,
并且只声明不实现,这样就实现了 防拷贝 的原理。
protected:ScopedPtr(ScopedPtr<T>& s);ScopedPtr<T> operator=(ScopedPtr<T>& s);


      由于 scoped_ptr 独享所有权,当我们真真需要复制智能指针时,需求便满足不了了,如此我们再引入一个智能指针,专门用于处理复制,参数传递的情况,这便是如下的 shared_ptr 和 weak_ptr 。


(3)share_ptr 的实现
template<class T>class SharePtr{public:SharePtr(T* ptr):_ptr(ptr), _refCount(new int(1)){}~SharePtr(){Release(); }inline void Release(){if (--*_refCount == 0){delete _ptr;delete _refCount;_ptr = NULL;_refCount = NULL;}}T &operator*(){return *_ptr;}T*operator->(){return _ptr;}SharePtr(const SharePtr<T>& sp):_ptr(sp._ptr), _refCount(sp._refCount){++(*_refCount);}SharePtr<T>&operator=(const SharePtr<T>&sp){if (_ptr != sp._ptr){Release();_ptr = sp._ptr;_refCount = sp._refCount;++(*_refCount);}return *this;}protected:T* _ptr;int* _refCount;};


share_ptr 使用时应该注意,在实例化后delete时,SharePtr<int>sp1<new int> 与 SharePtr<string>sp1<newstring[20]>的不同。前者是正确的,而后者就会使程序奔溃,因为后者会多开4个字节的空间,导致释放发生错误。

如图所示,若使用delete,则会造成内存泄露。因此,要使用 share_array 中的 operator[ ],用delete[ ] 来释放。


share_ptr 因为它采用引用计数的思想,所有它是功能较为完善的,但是 share_ptr 还是有缺陷的,例如在解决 循环引用 问题上。



三.循环引用

什么是循环引用问题?我们通过 双向链表 来举例说明:
struct Node{shared_ptr<Node> _prev;shared_ptr<Node> _next;~Node(){cout << "~Node():" << this << endl;}int data;};void FunTest(){shared_ptr<Node> cur(new Node);shared_ptr<Node> next(new Node);cur->_next = next;next->_prev = cur;cout << "cur.use_count:"<<cur.use_count() << endl;cout << "next.use_count:"<<next.use_count() << endl;}int main(){FunTest();return 0;}

运行结果:


可以看到 shared_ptr 的使用使得一块空间有两个对象管理,即头个结点的_next域和下一个指针共同管理,或者又头一个指针和第二个结点的_ptrv域共同管理所以其_refCount=2,这就是我们所谓的 循环引用 问题。

       如图所示,这里导致的问题就是内存泄漏,这段空间一直都没有释放,现在很明显引用计数在这里就不是很合适
了,但是shared_ptr除了这里不够完善,其他功能还是很不错的,所以这里补充一个week_ptr,来解决其循环引用问题,接下来我们看最后一个智能指针week_ptr。




weak_ptr
weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作,它只可以从一个 
shared_ptr 或另一个 weak_ptr 对象构造,它的构造和析构不会引起引用记数的增加或减少。

它更像是shared_ptr的一个助手而不是智能指针,因为它不具有普通指针的行为,没有重载operator*和->,它的最大作用在于协助shared_ptr工作。

现在我们并不能根据内部的引用计数。weak_ptr 是 boost::shared_ptr 的观察者对象,观察者意味着weak_ptr 只对shared_ptr 进行引用而不改变其引用计数,当被观察的shared_ptr 失效后,相应的weak_ptr 也相应失效,然后它就什么都不管光是个删 , 也就是这里的cur和next在析构的时候 , 不用引用计数减一 , 直接删除结点就好。这样也就间接地解决了循环引用的问题。








原创粉丝点击