欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

C++智能指针 shared_ptr + weak_ptr

程序员文章站 2022-06-02 13:56:51
...

前面所讲解的scoped_ptr(unique_ptr)对于拷贝构造函数和赋值运算符的重载解决方法是直接防拷贝,禁止使用这两个函数,但是不能避免的在有些场景中,我们不仅需要资源管理即初始化,资源退出即释放,我们还需要对这个对象进行拷贝或者赋值,在这种车场景下,就诞生了shared_ptr

shared_ptr


原理: 引用计数

C++智能指针 shared_ptr + weak_ptr

当p1起始被创建出来以后,引用计数的值为1,当进行了赋值或者拷贝以后,p1和p2的引用计数都称为2,并且指向同意块空间

在析构的时候,首先检查 --count 是不是为0,如果是0,就释放掉对应的空间,如果非0,就只进行引用计数的减减操作

  • 拷贝构造

 sharedPtr(const sharedPtr<T>& p)
        :_ptr(p._ptr)
         ,_pCount(p._pCount)
    {
        ++*_pCount;
    }

拷贝构造很简单,就是使被拷贝的对象的引用计数+1,使拷贝的对象的成员指向被拷贝对象的成员

  • 赋值运算符的重载

赋值运算符所涉及到的情况比较多,我们需要分析一下以下的四个情况:

p1 = p2

  • 当前对象(p1)独享空间

C++智能指针 shared_ptr + weak_ptr

这种情况下,p1独占资源,当进行了赋值运算的时候,p1指向的资源引用计数就会进行减减操作,减减过后,p1的引用计数为0,所以自己的空间随机被释放掉,p1指向p2的资源,同时p2指向资源的引用计数进行加加

  • 当前对象(p1)和别人共享空间

C++智能指针 shared_ptr + weak_ptr

当p1的引用计数大于1的时候,证明p1所指向的资源不是独享的,还有其他的对象管理着这块资源,当进行赋值以后,p1原本指向的资源引用计数进行减减(称为1),p1再指向p2所管理的资源,再讲引用计数进行加加

  • p2._ptr == nullptr

C++智能指针 shared_ptr + weak_ptr

当p2位nullptr的时候,进行赋值运算以后,p1也会指向nullptr,并且原有的资源数进行减减,如果为0,就释放

  • p1._ptr == nullptr

C++智能指针 shared_ptr + weak_ptr

当p1位nullptr时,p2指向了一块资源,在赋值运算以后,p1和p2都只想p2原有资源,并且引用计数加加

赋值运算符的重载:

   sharedPtr<T>& operator=(const sharedPtr<T>& p)
    {
        if(_ptr != p._ptr)
        {
            Release();
            _ptr = p._ptr;
            _pCount = p._pCount;
            if(_pCount != nullptr)
            {
                ++*_pCount;
            }
        }
        return *this;
    }

完整代码:

template<class T>
class sharedPtr
{
public:
    sharedPtr(T* ptr = nullptr)
        :_ptr(ptr)
         ,_pCount(nullptr)
    {
        if(_ptr)
        {
            _pCount = new int(1);
        }
    }
    ~sharedPtr()
    {
        Release();
    }

    sharedPtr(const sharedPtr<T>& p)
        :_ptr(p._ptr)
         ,_pCount(p._pCount)
    {
        ++*_pCount;
    }

    // 4种情况: p1 = p2
    //  1) 当前对象(p1)独享空间
    //  2) 当前对象(p1)和别人共享空间
    //  3) p2._ptr == nullptr
    //  4) p1._ptr == nullptr
    sharedPtr<T>& operator=(const sharedPtr<T>& p)
    {
        if(_ptr != p._ptr)
        {
            Release();
            _ptr = p._ptr;
            _pCount = p._pCount;
            if(_pCount != nullptr)
            {
                ++*_pCount;
            }
        }
        return *this;
    }

    T& operator*()
    {
        return *_ptr;
    }
    T* operator->()
    {
        return _ptr;
    }

    void Show_pCount()
    {
        if(_pCount)
        {
            cout<<"_pCount = "<<*_pCount<<endl;
            return;
        }
        cout<<"_pCount = 0"<<endl;
    }

private:
    void Release()
    {
        if(_ptr != nullptr)
        {
            if(--(*_pCount) == 0)
            {
                _Dptr(_ptr);
                delete _pCount;
                _ptr = nullptr;
                _pCount = nullptr;
            }
        }
    }

private:
    T* _ptr;
    int* _pCount;
};

定制删除器


上面的代码中,析构函数是用delete来释放资源的,但是我们很可能有以下几种方式申请的资源需要释放

    sharedPtr<int,Delete<int>> np(new int);

    sharedPtr<FILE,Close> fp(fopen("./test","r"));

    sharedPtr<int,Free<int>> mp = ((int*)malloc(sizeof(int)));

如果我们malloc申请的空间却使用delete来释放,很可能会出错

所以就引出了下面的内容,我们可以为对象定制删除器,实现 new的对象使用delete来释放, malloc的对象使用free来释放, fopen的文件使用fclose来关闭

代码如下:

template<class T>
class Delete
{
public:
    void operator()(T*& p)
    {
        if(p != nullptr)
        {
            cout<<"Delete"<<endl;
            delete p;
            p = nullptr;
        }
    }
};

template<class T>
class Free
{
public:
    void operator()(T*& p)
    {
        if(p != nullptr)
        {
            cout<<"Free"<<endl;
            free(p);
            p == nullptr;
        }
    }
};

class Close
{
public:
    void operator()(FILE*& p)
    {
        if(p != nullptr)
        {
            cout<<"Close"<<endl;
            fclose(p);
            p = nullptr;
        }
    }
};

template<class T,class Dx = Delete<T>>
class sharedPtr
{
public:
    sharedPtr(T* ptr = nullptr)
        :_ptr(ptr)
         ,_pCount(nullptr)
    {
        if(_ptr)
        {
            _pCount = new int(1);
        }
    }
    ~sharedPtr()
    {
        Release();
    }

    sharedPtr(const sharedPtr<T>& p)
        :_ptr(p._ptr)
         ,_pCount(p._pCount)
    {
        ++*_pCount;
    }
    sharedPtr<T>& operator=(const sharedPtr<T>& p)
    {
        if(_ptr != p._ptr)
        {
            Release();
            _ptr = p._ptr;
            _pCount = p._pCount;
            if(_pCount != nullptr)
            {
                ++*_pCount;
            }
        }
        return *this;
    }

    T& operator*()
    {
        return *_ptr;
    }
    T* operator->()
    {
        return _ptr;
    }

    void Show_pCount()
    {
        if(_pCount)
        {
            cout<<"_pCount = "<<*_pCount<<endl;
            return;
        }
        cout<<"_pCount = 0"<<endl;
    }

private:
    void Release()
    {
        if(_ptr != nullptr)
        {
            if(--(*_pCount) == 0)
            {
                Dx()(_ptr); // 和下面两句的效果一样
                // Dx dx;
                // dx(_ptr); //dx.operator()(_ptr);
                delete _pCount;
                _ptr = nullptr;
                _pCount = nullptr;
            }
        }
    }

private:
    T* _ptr;
    int* _pCount;
};

我们使用的是函数对象,也就是仿函数的方法来实现定制删除器的,使用方法如下

    sharedPtr<int,Delete<int>> np(new int);

    sharedPtr<FILE,Close> fp(fopen("./test","r"));

    sharedPtr<int,Free<int>> mp = ((int*)malloc(sizeof(int)));

shared_ptr的循环引用问题


使用双向链表来举例说明

链表节点的类定义如下:

template<class T>
class Node
{
public:
    shared_ptr<Node<T>> _next;
    shared_ptr<Node<T>> _prev;
    T _data;
    Node(const T& a)
        :_data(a)
    {
        cout<<"Node() "<<this<<endl;
    }
    ~Node()
    {
        cout<<"~Node() "<<this<<endl;
    }
};

为了保证节点内部的指针可以正确的初始化和释放,我们使用了shared_ptr来定义

但是当我们执行了这两句代码以后,就会发生错误的现象

node1->_prev = node2;

node2->_next = node1;

如图:

C++智能指针 shared_ptr + weak_ptr

当我们执行node1->_prev=node2时,编译器会根绝赋值符两边的类型确定调用shared_ptr的赋值运算,于是顺理成章的node2的引用计数进行了++; node2->_next = node1也同理;

这时我们发现,命名每个资源都只有一个对象来管理,却又两个引用计数,这也表示这,在析构的时候会因为引用计数没有减到0而不释放其资源,造成内存泄漏!!!

当然,C++11库中也给出了响应的解决方法,就是weak_ptr

weak_ptr

weak_ptr是一个"弱"指针,是和shared_ptr搭配使用,在进行如上的赋值时,并不进行引用计数的加加操作,这也保证了在释放的时候不会因为引用计数不为0而没有正确释放,造成内存泄漏。

具体使用,只要将_next和_prev定义为weak_ptr即可:

template<class T>
class Node
{
public:
    // shared_ptr<Node<T>> _next;
    // shared_ptr<Node<T>> _prev;
    weak_ptr<Node<T>> _next;
    weak_ptr<Node<T>> _prev;
    T _data;
    Node(const T& a)
        :_data(a)
    {
        cout<<"Node() "<<this<<endl;
    }
    ~Node()
    {
        cout<<"~Node() "<<this<<endl;
    }
};

这里我们使用如下代码进行验证,在这里我们不做实际的操作,观察引用计数的变化即可

void Test()
{
    shared_ptr<Node<int>> p1(new Node<int>(10));
    shared_ptr<Node<int>> p2(new Node<int>(20));
    cout<<p1.use_count()<<endl;
    cout<<p2.use_count()<<endl;
    p1->_next = p2;
    p2->_prev = p1;
    cout<<p1.use_count()<<endl;
    cout<<p2.use_count()<<endl;
}
  • 使用shared_ptr产生循环引用

C++智能指针 shared_ptr + weak_ptr

  • 使用weak_ptr避免循环引用

C++智能指针 shared_ptr + weak_ptr

 

总结一下


  • shared_ptr可以通过有效的方法保证智能指针的安全性,但是有时会存在循环引用的问题,建议配合weak_ptr一同使用(注意: weak_ptr是弱指针,他不可以单独使用,必须和shared_ptr一同使用

  • 我们可以为智能指针定制删除器,使对象可以按照搭配的方式进行内存的释放或者是文件的关闭