C++| |智能指针

智能指针


# 前言

为什么会有智能指针?

1. 智能指针的原理

RALL(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源的技术

  • 在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源

  • 好处:

  1. 不需要显式的释放资源

  2. 对象所需的资源在生命周期内始终保持有效

简单的实现一个智能指针:

template<class T>
class SmartPtr
{
public:
    SmartPtr(T& ptr = nullptr)
        : _ptr(ptr)
    {
    }
    
    ~SmartPtr()
    {
        if (_ptr)
        {
            delete _ptr;
        }
    }
    
    T& operator*()
    {
        return *_ptr;
    }
    
    T* operator->()
    {
        return _ptr;
    }
   
    SmartPtr(const SmartPtr& sp) = delete;
    SmartPtr& operator=(const SmartPtr& sp) = delete; 
    
private:
    T* _ptr;
};

对于智能指针要遵循以下两点:

  1. RAII特性

  2. 重载operator*和operator->,具有像指针一样的行为

2. 智能指针的实现

2.1 auto_ptr

auto_ptr是c++98版本的库中提供的智能指针

对于auto_ptr存在的问题:管理权转移

  • 也就是不能进行拷贝构造并且不支持赋值运算符重载,如果进行这两项操作的话,就会导致被用来构造和重载的智能指针没有再次管理这份资源的权利了

自我模拟实现C++库中的auto_ptr智能指针

template<class T>
class AutoPtr
{
public:
  AutoPtr(T* ptr_)
    : ptr(ptr_)
  {
  }
​
  ~AutoPtr()
  {
    if(ptr)
    {
      delete ptr;
    }
  }
​
  T& operator*()
  {
    return *ptr;
  }
​
  T* operator->()
  {
    return ptr;
  }
​
  AutoPtr(AutoPtr<T>& t)
    : ptr(t.ptr)
  {
    t.ptr = NULL;
  }
​
  T& operator=( AutoPtr<T>& t)
  {
    if (this != &t)
    {
      if (ptr)
      {
        delete ptr;
      }
        
      ptr = t.ptr;
      t.ptr = NULL;
    }
​
    return *this;
  }
​
private:
  T* ptr;
};

可以看出当当进行拷贝构造以及赋值的时候,就会出现让一个智能指针置为NULL从而导致,管理权转移

2.2 unique_ptr

C++11提供的智能指针,对于该智能指针为了防止管理权转移的情况,直接将拷贝构造以及赋值运算符重载置为delete函数,也就是只声明不实现,并且将其访问权限设置为私有,不让类外进行访问。

自我实现:

template<class T>
class UniquePtr
{
public:
  UniquePtr(T* ptr)
    : _ptr(ptr)
  {
  }
​
  ~UniquePtr()
  {
    if (_ptr)
    {
      delete _ptr;
    }
  }
​
  T& operator*()
  {
    return *_ptr;
  }
​
  T* operator->()
  {
    return _ptr;
  }
​
private:
  //C++98 
  UniquePtr(UniquePtr& t);
  T& operator=(UniquePtr& t);
​
  //C++11
  //UniquePtr(UniquePtr& t) = delete;
  //T& operator=(UniquePtr& t) = delete;
 
private:
  T* _ptr;
};

对于unique_ptr智能指针,直接不让进行拷贝构造和赋值,杜绝了管理权转移的情况

2.3 shared_ptr

C++11提供的可以进行拷贝构造以及赋值的智能指针并且不会出现管理权转移的情况

实现方法:是通过引用计数的方式实现的多个shared_ptr对象之间共同管理一份资源

  • 在shared_ptr维护了一个指针,为该指针在堆上分配一个空间。从而实现对于每一份资源都维护了一个计数,用来记录该资源被几个对象进行共同管理

  • 在对象被销毁的时候,就说明不需要这份资源了,对象的引用计数减一

  • 如果减一之后引用计数变为了0,就说明这是最后一个管理这份资源的对象,直接将这份资源进行释放

  • 如果不是0的话,表示还有其他的对象管理这这份资源,不能释放这份资源,否则其他的资源就会变成了野指针了

自我模拟实现shared_ptr:

template<class T>
class SharedPtr
{
public:
  SharedPtr(T* ptr = nullptr)
    : _ptr(ptr)
    , _RefCount(new int(1))
    , _pMutex(new std::mutex)
  {
    if (_ptr == nullptr)
    {
      *_RefCount = 0;
    }
  }
​
  ~SharedPtr()
  {
    Release();
  }
​
  T* operator->()
  {
    return _ptr;
  }
​
  T& operator*()
  {
    return *_ptr;
  }
​
  SharedPtr(SharedPtr& sp)
    : _ptr(sp._ptr)
    , _RefCount(sp._RefCount)
    , _pMutex(sp._pMutex)
  {
    if (_ptr)
    {
      AddRefCount();
    }
  }
​
  SharedPtr& operator=(SharedPtr& sp)
  {
    //只需要判断是否管理的同一个对象
    if (_ptr != sp._ptr)
    {
      //释放对旧的对象的管理
      Release();
​
      //管理新的对象
      _ptr = sp._ptr;
      _pMutex = sp._pMutex;
      _RefCount = sp._RefCount;
​
      //只有当新的对象不是nullptr猜对其进行引用计数增加1
      if (_ptr)
      {
        AddRefCount();
      }
    }
​
    return *this;
  }
​
  int SubRefCount()
  {
    _pMutex->lock();
    (*_RefCount)--;
    _pMutex->unlock();
​
    return *_RefCount;
  }
​
  int AddRefCount()
  {
    _pMutex->lock();
    (*_RefCount)++;
    _pMutex->unlock();
​
    return *_RefCount;
  }
​
  T* Get()
  {
    return _ptr;
  }
​
  int& UseCount()
  {
    return *_RefCount;
  }
​
private:
  void Release()
  {
    if (_ptr && (SubRefCount() == 0))
    {
      delete _ptr;
      delete _RefCount;
      delete _pMutex;
    }
  }
​
private:
  T* _ptr;
  int*  _RefCount;
  std::mutex* _pMutex;
};

线程安全的问题:

对于shared_ptr可以实现多个对象管理一份资源,那么就要考虑到线程安全的问题

  1. 对于两个线程的智能指针同时对引用计数进行++或--的操作

  2. 对于指针两个线程的智能指针管理的对象放在堆上,两个线程同时去访问就会导致线程安全的问题

循环引用:

struct ListNode
{
    int _data;
    shared_ptr<ListNode> _prev;
    shared_ptr<ListNode> _next;
    ~ListNode(){ cout << "~ListNode()" << endl; }
};

int main()
{
    shared_ptr<ListNode> node1(new ListNode);
    shared_ptr<ListNode> node2(new ListNode);
    cout << node1.use_count() << endl;
    cout << node2.use_count() << endl;
    node1->_next = node2;
    node2->_prev = node1;
    cout << node1.use_count() << endl;
    cout << node2.use_count() << endl;
    return 0;
}

循环引用分析:

  1. node1和node2两个智能指针对象指向两个节点,引用计数变为1

  2. node1的_next指向node2,node2的 _prev指向node1,引用计数变为2

  3. node1和node2析构,引用计数减为1,但是_next指向下一个节点, _prev指向上一个结点、

  4. 引用计数为1,node1和node2不会进行资源的释放

  5. 但是_next属于node1的成员,node1释放了 _next才会释放,但是 _prev属于node2,所以两个就都不会释放。就是在循环引用,和死锁一样。

解决方案:

  • 把结点中的_prev和 _next类型变成 weak_ptr就行了

  • 原理就是,node1-> _next = node2和node2-> _prev = node1时weak_ptr的 _next和 _prev不会增加node1和node2的引用计数

3. RAII扩展学习

守卫锁:防止异常安全的死锁问题

自我实现:

template<class Mutex>
class LockGuard
{
public:
  LockGuard(Mutex& mutex)
    : _mutex(mutex)
  {
    _mutex.lock();
  }
​
  ~LockGuard()
  {
    _mutex.unlock();
  }
​
  LockGuard(const LockGuard& lg) = delete;
  LockGuard& operator=(const LockGuard& lg) = delete;
​
private:
  Mutex& _mutex;
};

最后将我写的代码放在这里大家可以参考一下,里面有测试:

https://github.com/YKitty/LinuxDir/tree/master/C%2B%2BCode/RALL

猜你喜欢

转载自blog.csdn.net/qq_40399012/article/details/86441797