C++学习之“智能指针”

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/tonglin12138/article/details/85724047

写在前面

1.为什么需要智能指针
2.智能指针的使用及原理
3.各种版本的智能指针了解及实现
auto_ptr: c++ 98 (管理权转移)
unique_ptr: c++11 (防拷贝)
scoped_ptr: boost库 (防拷贝)
shared_ptr: c++11 (共享、引用计数) (重点)
weak_ptr: c++11 (解决循环引用的问题、辅助shared_ptr)
4.C++11和boost中智能指针的关系

1.为什么需要智能指针?

分析下面一段程序:

#include <vector>

//归并排序
void _MergeSort(int* a, int left, int right, int* tmp)
{
  if (left >= right)
   return;
 
  int mid = left + ((right - left) >> 1);

  _MergeSort(a, left, mid, tmp);
  _MergeSort(a, mid + 1, right, tmp);
  int begin1 = left, end1 = mid;
  int begin2 = mid + 1, end2 = right;
  int index = left;

 while (begin1 <= end1 && begin2 <= end2)
 {
    if (a[begin1] < a[begin2])
         tmp[index++] = a[begin1++];
    else
         tmp[index++] = a[begin2++];
 }
    while (begin1 <= end1)
     tmp[index++] = a[begin1++];
    while (begin2 <= end2)
     tmp[index++] = a[begin2++];
    memcpy(a + left, tmp + left, sizeof(int)*(right - left + 1));
}
    

    void MergeSort(int* a, int n)
 {
     int* tmp = (int*)malloc(sizeof(int)*n);
     _MergeSort(a, 0, n - 1, tmp);
 
      ...
      vector<int> v(1000000000, 10);
      ...
 
   
}
  
  int main()
 {
   int a[5] = { 4, 5, 2, 3, 1 };
   MergeSort(a, 5);
   return 0;
}

问题分析:上面的问题分析出来我们发现有以下两个问题?

  1. malloc出来的空间,没有进行释放,存在内存泄漏的问题。
  2. 异常安全问题。如果在malloc和free之间如果存在抛异常,那么还是有内存泄漏。这种问题就叫异常安全。

2.智能指针的使用及原理

2.1 RAII
RAII(Resource Acquisition Is Initialization资源获取即初始化)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
·不需要显式地释放资源;
·采用这种方式,对象所需的资源在其生命期内始终保持有效。

/使用RAII思想设计的SmartPtr类

template<class T>
class SmartPtr {
public:
     SmartPtr(T* ptr = nullptr)
              : _ptr(ptr)
          {}
     ~SmartPtr()
      {
           if(_ptr)
           delete _ptr;
      }
private:
       T* _ptr;
};


2.2 智能指针的原理
上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此:AutoPtr模板类中还得需要将* 、->重载下,才可让其像指针一样去使用。

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;}
private:
    T* _ptr;
};

总结一下智能指针的原理:

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

3.各种版本的智能指针了解及实现

1.auto_ptr

auto_ptr的实现原理:管理权转移的思想。
下面简化模拟实现了一份AutoPtr来了解它的原理:

 模拟实现一份简答的AutoPtr,了解原理

template<class T>
class AutoPtr
{
public:
    AutoPtr(T* ptr = NULL)
            : _ptr(ptr)
     {}
 
    ~AutoPtr()
     {
        if(_ptr)
         delete _ptr;
     }
 
    AutoPtr(AutoPtr<T>& ap)
           : _ptr(ap._ptr)
      {
         ap._ptr = NULL;
      }
 
   AutoPtr<T>& operator=(AutoPtr<T>& ap)
    {
       // 检测是否为自己给自己赋值
         if(this != &ap)
    {
       // 释放当前对象中资源
         if(_ptr)
            delete _ptr;
       _ptr = ap._ptr;
       ap._ptr = NULL;
   }
 
        return *this;
 }
    T& operator*() {return *_ptr;}
    T* operator->() { return _ptr;}
private:
         T* _ptr;
};

 int main() 
 {
    AutoPtr<Date> ap(new Date);
    AutoPtr<Date> copy(ap);
    ap->_year = 2018;
    return 0;
}

一旦发生拷贝,就将ap中资源转移到当前对象中,然后另ap与其所管理资源断开联系,这样就解决了一块空间被多个对象使用而造成程序奔溃问题

现在再从实现原理层来分析会发现,这里拷贝后把ap对象的指针赋空了,导致ap对象悬空,通过ap对象访问资源时就会出现问题。

2.unique_ptr

unique_ptr的实现原理:简单粗暴的防拷贝。
下面简化模拟实现了一份UniquePtr来了解它的原理。


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

private:
     C++98防拷贝的方式:只声明不实现+声明成私有
      UniquePtr(UniquePtr<T> const &);
      UniquePtr & operator=(UniquePtr<T> const &);
 
    C++11防拷贝的方式:delete
      UniquePtr(UniquePtr<T> const &) = delete;
      UniquePtr & operator=(UniquePtr<T> const &) = delete;
 
private:
     T * _ptr;
};

3.scoped ptr

特点:不能共享控制权。scoped_ptr不能通过其他scoped_ptr共享控制权,因为在scoped_ptr类的内部将拷贝构造函数和=运算符重载定义为私有的。我们看下scoped_ptr类的定义就清楚了

 1 namespace boost
 2 {
 3     template<typename T> class scoped_ptr : noncopyable
 4     {
 5     private:
 6 
 7         T *px;
 8 
 9         scoped_ptr(scoped_ptr const &);
10         scoped_ptr &operator=(scoped_ptr const &);
11 
12         typedef scoped_ptr<T> this_type;
13 
14         void operator==( scoped_ptr const & ) const;
15         void operator!=( scoped_ptr const & ) const;
16     public:
17         explicit scoped_ptr(T *p = 0);
18         ~scoped_ptr();
19 
20         explicit scoped_ptr( std::auto_ptr<T> p ): px( p.release() );
21         void reset(T *p = 0);
22 
23         T &operator*() const;
24         T *operator->() const;
25         T *get() const;
26 
27         void swap(scoped_ptr &b);
28     };
29 
30     template<typename T>
31     void swap(scoped_ptr<T> &a, scoped_ptr<T> &b);
32 }

4.shared_ptr

shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。例如:老师放学之前之前都会通知,让最后走的学生记得把门锁好。

  1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
  2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
  3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
  4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指
    针了。

#include <thread>
#include <mutex>
template <class T>
class SharedPtr
{
public:
     SharedPtr(T* ptr = nullptr)
           : _ptr(ptr)
           , _pRefCount(new int(1))
           , _pMutex(new mutex)
    {
  // 如果是一个空指针对象,则引用计数给0
    if (_ptr == nullptr)
       *_pRefCount = 0;
     }
  ~SharedPtr()
   {
      Release();
   }
 //拷贝构造函数
  SharedPtr(const SharedPtr<T>& sp)
              : _ptr(sp._ptr)
              , _pRefCount(sp._pRefCount)
              , _pMutex(sp._pMutex)
  {
       // 如果是一个空指针对象,则不加引用计数,否则才加引用计数
      if (_ptr)
      AddRefCount();
 }
 // sp1 = sp2
   SharedPtr<T>& operator=(const SharedPtr<T>& sp)
   {
       //if (this != &sp)
      if (_ptr != sp._ptr)
   {
         ++(*sp._pRefCount);
      // 释放管理的旧资源
          Release();
      // 共享管理新对象的资源,并增加引用计数
         _ptr = sp._ptr;
         _pRefCount = sp._pRefCount;
         _pMutex = sp._pMutex;
      if (_ptr)
         AddRefCount();
    }
       return *this;
 }
 T& operator*() {return *_ptr;}
 T* operator->() {return _ptr;}

 int UseCount() {return *_pRefCount;}
 T* Get() { return _ptr; }
 
 int AddRefCount()
 {
    // 加锁或者使用加1的原子操作
      _pMutex->lock();
      ++(*_pRefCount);
      _pMutex->unlock();
      return *_pRefCount;
 }
 
 int SubRefCount()
 {
    // 加锁或者使用减1的原子操作
     _pMutex->lock();
     --(*_pRefCount);
     _pMutex->unlock();
     return *_pRefCount;
 }


private:
    void Release()
 {
   // 引用计数减一,如果减到零,则释放资源
   if (_ptr && SubRefCount() == 0)
  {
        delete _ptr;
        delete _pRefCount;
  }
 }
private:
    int* _pRefCount; // 引用计数
    T* _ptr; // 指向管理资源的指针 
    mutex* _pMutex; // 互斥锁
};

std::shared_ptr的线程安全问题
需要注意的是shared_ptr的线程安全分为两方面:

  1. 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或–,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2.这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、–是需要加锁的,也就是说引用计数的操作是线程安全的。
  2. 智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题。

std::shared_ptr的循环引用问题

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,我们不需要手动delete。
  2. node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
  3. node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
  4. 也就是说_next析构了,node2就释放了。
  5. 也就是说_prev析构了,node1就释放了。
  6. 但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,所以这就叫循环引用,谁也不会释放。
    在这里插入图片描述
    解决方案:
 解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
 原理就是,node1->_next = node2;和node2->_prev = node1时, weak_ptr的_next和_prev不会增加
node1和node2的引用计数。
struct ListNode
{
    int _data;
    weak_ptr<ListNode> _prev;
    weak_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;
}

ps:如果不是new出来的对象如何通过智能指针管理呢?其实shared_ptr设计了一个删除器来解决这个问题.具体参见文章“仿函数与定置删除器”。

5.weak_ptr
 weak_ptr是为了配合shared_ptr而引入的一种智能指针,因为它不具有普通指针的行为,没有重载operator*和->,它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。使用weak_ptr的成员函数use_count()可以观测资源的引用计数,另一个成员函数expired()的功能等价于use_count()==0,但更快,表示被观测的资源(也就是shared_ptr的管理的资源)已经不复存在。weak_ptr可以使用一个非常重要的成员函数lock()从被观测的shared_ptr获得一个可用的shared_ptr对象, 从而操作资源。但当expired()==true的时候,lock()函数将返回一个存储空指针的shared_ptr。
简单实现一下吧:

template<class T>
class WeakPtr
{
public:
	WeakPtr()
		:_ptr(NULL)
	{}

	WeakPtr(const SharedPtr<T>& sp)
		:_ptr(sp._ptr)
	{}

	WeakPtr(WeakPtr<T>& wp)
		:_ptr(wp._ptr)
	{}

	WeakPtr<T>& operator=(SharedPtr<T>& sp)
	{
		_ptr = sp._ptr;
		return *this;
	}

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

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

private:
	T* _ptr;
};

4.C++11和boost中智能指针的关系

  1. C++ 98 中产生了第一个智能指针auto_ptr.
  2. C++ boost给出了更实用的scoped_ptr和shared_ptr和weak_ptr.
  3. C++ TR1,引入了shared_ptr等。不过注意的是TR1并不是标准版.
  4. C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的.

猜你喜欢

转载自blog.csdn.net/tonglin12138/article/details/85724047