c++ std::shared_ptr学习

背景

        c++中智能指针shared_ptr用于自动管理资源,通过引用计数来记录资源被多少出地方使用。在不使用资源时,减少引用计数,如果引用计数为0,表示资源不会再被使用,此时会释放资源。本文记录对c++中std::shared_ptr的源码学习。

为什么需要“智能”指针

       当我们需要在堆上分配对象时,可以通过new来创建,在不需要使用对象时,需要使用delete来释放对象占用的资源,如下所示:

void foo() {
    ...
    int *pi = new int(0);
    ...
    delete pi;
    ...
}

        如果在delete之前,函数提前return,则new出来的内存不会被释放,导致内存泄露。另外,如果一个对象在函数中被new出来,并作为返回值返回,但是使用者如果不清楚函数内部实现,也可能忘记delete调new出来的对象,导致内存泄露,如下所示:

int* foo() {
    return new int(0);
}


void bar() {
    int *pi = foo();
    ...
    return; // 因为不知道foo返回的int*是new出来的,所以没有delete pi
}

        对上面这种场景,还可能会出现一个指针被多处使用。为了确保指针在使用过程中没有被delete,还需要关注指针使用的先后顺序。但在迭代的过程中,这会导致指针的使用变得很难维护。为此,我们需要有一个“智能”的指针,维护我们new出来的对象,并在不需要的时候自动delete,释放资源。

智能指针如何“智能”

        c++智能指针利用class的构造函数和析构函数来对指针管理,构造函数中将new出来的指针传入,在析构函数中判断如果没人再使用,则会释放指针。

        如何判断指针还被使用呢?答案是使用引用计数。只要指针被引用了,引用计数加1,当某一个引用不再使用(析构之后),则引用计数减1,如果指针不再被使用时,引用计数为0,此时即可delete指针,释放资源。

        那如何知道一个指针被引用了呢?答案就是复制构造和赋值构造。如果不知道什么是复制构造和赋值构造,可以先去学习下,以下是个简单的例子:

class A {
public:
    // 构造函数
    A() {
        x = 0;
    }
    // 析构函数
    ~A() {
    }
    // 复制构造
    A(const A &a) {
        x = a.x;
    }
    // 赋值构造
    A& operate=(cosnt A &a) {
        x = a.x;
        return *this;
    }
private:
    int x;
}

int main() {
    A a1; // 构造函数
    A a2(a1); // 复制构造
    A a3 = a1; // 赋值构造
}
// 离开作用域,a1, a2, a3析构

shared_ptr概览

        shared_ptr定义如下:

  /**
   *  @brief  A smart pointer with reference-counted copy semantics.
   *
   *  The object pointed to is deleted when the last shared_ptr pointing to
   *  it is destroyed or reset.
  */
  template<typename _Tp>
    class shared_ptr : public __shared_ptr<_Tp>
    {
      ...
    }

即shared_ptr继承__shared_ptr,__shared_ptr定义如下:

  template<typename _Tp, _Lock_policy _Lp>
    class __shared_ptr
    : public __shared_ptr_access<_Tp, _Lp>
    {
    public:
      using element_type = typename remove_extent<_Tp>::type;
      ...
    private:
      element_type*	   _M_ptr;         // Contained pointer.
      __shared_count<_Lp>  _M_refcount;    // Reference counter.
    }

        从注释中可以看出,__shared_ptr中有两个成员变量:_M_ptr和_M_refcount。_M_ptr是智能指针管理的资源,_M_refcount是引用计数。再看下_M_refcount的类型定义,即__shared_count:

  template<_Lock_policy _Lp>
    class __shared_count
    {
    ...
    private:
      _Sp_counted_base<_Lp>*  _M_pi;
    };



  template<_Lock_policy _Lp = __default_lock_policy>
    class _Sp_counted_base
    : public _Mutex_base<_Lp>
    {
    ...
    private:
      _Atomic_word  _M_use_count;     // #shared
      ...
    };



typedef int _Atomic_word;

        可以看出,引用计数本质是一个int值:_M_use_count。

        shared_ptr简单的类图示意如下:

        根据智能指针实现方式,我们主要考虑构造函数、析构函数、复制构造函数、赋值构造函数。

构造函数

        用以下代码作为示例说明智能指针构造过程:

class A {
public:
    A() {
        cout << "construct A" << endl;
    }

    ~A() {
        cout << "deconstruct A" << endl;
    }
};

void test_init() {
    shared_ptr<A> pa(new A);
}

        当test函数被调用是,创建了一个A指针,该指针作为shared_ptr入参传入构造函数,shared_ptr构造函数调用过程如下:


      // shared_ptr构造函数

      /**
       *  @brief  Construct a %shared_ptr that owns the pointer @a __p.
       *  @param  __p  A pointer that is convertible to element_type*.
       *  @post   use_count() == 1 && get() == __p
       *  @throw  std::bad_alloc, in which case @c delete @a __p is called.
       */
      template<typename _Yp, typename = _Constructible<_Yp*>>
	explicit
	shared_ptr(_Yp* __p) : __shared_ptr<_Tp>(__p) { }


      // __shared_ptr构造函数,本文考虑指针不是数组指针,即is_array<_Tp>::type() == false_type

      template<typename _Yp, typename = _SafeConv<_Yp>>
	explicit
	__shared_ptr(_Yp* __p)
	: _M_ptr(__p), _M_refcount(__p, typename is_array<_Tp>::type())
	{
	  ...
	}

      // 引用计数__shared_ptr::_M_refcount(__shared_count类型)构造函数

      template<typename _Ptr>
	__shared_count(_Ptr __p, /* is_array = */ false_type)
	: __shared_count(__p)
	{ }

      template<typename _Ptr>
        explicit
	__shared_count(_Ptr __p) : _M_pi(0)
	{
	  __try
	    {
	      _M_pi = new _Sp_counted_ptr<_Ptr, _Lp>(__p);
	    }
	  __catch(...)
	    {
	      delete __p;
	      __throw_exception_again;
	    }
	}


  // __shared_count::_M_pi构造函数
  // Counted ptr with no deleter or allocator support
  template<typename _Ptr, _Lock_policy _Lp>
    class _Sp_counted_ptr final : public _Sp_counted_base<_Lp>
    {
    public:
      explicit
      _Sp_counted_ptr(_Ptr __p) noexcept
      : _M_ptr(__p) { }

      ...
    private:
      _Ptr             _M_ptr;
    };


     _Sp_counted_base() noexcept
      : _M_use_count(1), _M_weak_count(1) { }

       可以看到,new创建的指针被存在__shared_ptr::_M_ptr中,同时,__shared_ptr::_M_refcount构造函数中,new创建了_M_refcount::_M_pi,这样,引用计数才不会因为__shared_ptr析构而消失,引用计数的生命周期应该与指针的生命周期一致,才能记录指针被引用的次数。__Sp_counted_base构造函数中,初始化引用计数_M_use_count为1,表示当前只有1处在使用该指针。

        构造函数调用结束之后,shared_ptr数据可简单表示为:

        (为什么内存布局是这样?可查看附录中的测试代码。)

复制构造

        以下代码被调用时,智能指针的复制构造函数被调用:

int test_copy() {
    shared_ptr<A> pa(new A);
    shared_ptr<A> pa_copy(pa);  // 复制构造
}

        复制构造主要实现代码如下:

  template<typename _Tp>
    class shared_ptr : public __shared_ptr<_Tp>
    {
      ...
    public:
      ...
      shared_ptr(const shared_ptr&) noexcept = default;  // 默认复制构造
      ...
    }

  template<typename _Tp, _Lock_policy _Lp>
    class __shared_ptr
    : public __shared_ptr_access<_Tp, _Lp>
    {
    public:
      using element_type = typename remove_extent<_Tp>::type;
      ...
      __shared_ptr(const __shared_ptr&) noexcept = default;  // 默认复制构造
      ...
    private:
      ...
      element_type*	   _M_ptr;         // Contained pointer.
      __shared_count<_Lp>  _M_refcount;    // Reference counter.
    }

        复制构造使用编译器生成的默认复制构造函数,伪代码可以表示成:

template<typename _Tp>
    class shared_ptr : public __shared_ptr<_Tp>
    {
      ...
    public:
      ...
      shared_ptr(const shared_ptr& ptr) noexcept {
          _M_ptr = __ptr._M_ptr;
          _M_refcount.__shared_count::__shared_count(__ptr._M_refcount);
      }
      ...
    }

        继续看__shared_count的复制构造函数:

  template<_Lock_policy _Lp>
    class __shared_count
    {
      ...
    public:
      __shared_count(const __shared_count& __r) noexcept
      : _M_pi(__r._M_pi)
      {
	if (_M_pi != 0)
	  _M_pi->_M_add_ref_copy();
      }
      ...
    private:
      ...
      _Sp_counted_base<_Lp>*  _M_pi;
    }

  template<_Lock_policy _Lp = __default_lock_policy>
    class _Sp_counted_base
    : public _Mutex_base<_Lp>
    {
    public:
      ...
      void
      _M_add_ref_copy()
      { __gnu_cxx::__atomic_add_dispatch(&_M_use_count, 1); }
      ...
    private:
      _Sp_counted_base(_Sp_counted_base const&) = delete;
      _Sp_counted_base& operator=(_Sp_counted_base const&) = delete;

      _Atomic_word  _M_use_count;     // #shared
      _Atomic_word  _M_weak_count;    // #weak + (#shared != 0)
    };

namespace __gnu_cxx _GLIBCXX_VISIBILITY(default)
{
_GLIBCXX_BEGIN_NAMESPACE_VERSION


  // Functions for portable atomic access.
  // To abstract locking primitives across all thread policies, use:
  // __exchange_and_add_dispatch
  // __atomic_add_dispatch
#ifdef _GLIBCXX_ATOMIC_BUILTINS
  static inline _Atomic_word 
  __exchange_and_add(volatile _Atomic_word* __mem, int __val)
  { return __atomic_fetch_add(__mem, __val, __ATOMIC_ACQ_REL); }
  ...
  static inline void
  __attribute__ ((__unused__))
  __atomic_add_dispatch(_Atomic_word* __mem, int __val)
  {
#ifdef __GTHREADS
    if (__gthread_active_p())
      __atomic_add(__mem, __val);
    else
      __atomic_add_single(__mem, __val);
#else
    __atomic_add_single(__mem, __val);
#endif
  }

_GLIBCXX_END_NAMESPACE_VERSION
} // namespace

        可以看到,复制构造函数最终就是将引用计数加1,且是通过原子操作来进行加1的,由此可见:智能指针是线程安全的!至于如何实现的线程安全,本文暂不深究。

赋值构造

        类似地,以下函数调用时,智能指针的赋值构造函数被调用:

int test_assgin() {
    shared_ptr<A> pa(new A);
    shared_ptr<A> pa_assign(new A);
    pa_assign = pa;  // 赋值构造
}

        比复制构造稍微复杂一点,赋值构造的实现如下:

  template<typename _Tp>
    class shared_ptr : public __shared_ptr<_Tp>
    {
      ...
    public:
      ...
      shared_ptr& operator=(const shared_ptr&) noexcept = default;
      ...
    }

  template<typename _Tp, _Lock_policy _Lp>
    class __shared_ptr
    : public __shared_ptr_access<_Tp, _Lp>
    {
    public:
      using element_type = typename remove_extent<_Tp>::type;
      ...
      __shared_ptr& operator=(const __shared_ptr&) noexcept = default;
      ...
    private:
      ...
      element_type*	   _M_ptr;         // Contained pointer.
      __shared_count<_Lp>  _M_refcount;    // Reference counter.
    }

  template<_Lock_policy _Lp>
    class __shared_count
    {
      ...
    public:
      ...
      __shared_count&
      operator=(const __shared_count& __r) noexcept
      {
	_Sp_counted_base<_Lp>* __tmp = __r._M_pi;
	if (__tmp != _M_pi)
	  {
	    if (__tmp != 0)
	      __tmp->_M_add_ref_copy();
	    if (_M_pi != 0)
	      _M_pi->_M_release();
	    _M_pi = __tmp;
	  }
	return *this;
      }
      ...
    private:
      ...
      _Sp_counted_base<_Lp>*  _M_pi;
    }

  template<typename _Ptr, _Lock_policy _Lp>
    class _Sp_counted_ptr final : public _Sp_counted_base<_Lp>
    {
    public:
      ...
      virtual void
      _M_dispose() noexcept
      { delete _M_ptr; }

      virtual void
      _M_destroy() noexcept
      { delete this; }
      ...
    }

  template<_Lock_policy _Lp = __default_lock_policy>
    class _Sp_counted_base
    : public _Mutex_base<_Lp>
    {
    public:
      ...
      void
      _M_add_ref_copy()
      { __gnu_cxx::__atomic_add_dispatch(&_M_use_count, 1); }
      ...
      void
      _M_release() noexcept
      {
        // Be race-detector-friendly.  For more info see bits/c++config.
        _GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_use_count);
	if (__gnu_cxx::__exchange_and_add_dispatch(&_M_use_count, -1) == 1)
	  {
            _GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_use_count);
	    _M_dispose();
	    // There must be a memory barrier between dispose() and destroy()
	    // to ensure that the effects of dispose() are observed in the
	    // thread that runs destroy().
	    // See http://gcc.gnu.org/ml/libstdc++/2005-11/msg00136.html
	    if (_Mutex_base<_Lp>::_S_need_barriers)
	      {
		__atomic_thread_fence (__ATOMIC_ACQ_REL);
	      }

            // Be race-detector-friendly.  For more info see bits/c++config.
            _GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_weak_count);
	    if (__gnu_cxx::__exchange_and_add_dispatch(&_M_weak_count,
						       -1) == 1)
              {
                _GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_weak_count);
	        _M_destroy();
              }
	  }
      }
      ...
    private:
      _Sp_counted_base(_Sp_counted_base const&) = delete;
      _Sp_counted_base& operator=(_Sp_counted_base const&) = delete;

      _Atomic_word  _M_use_count;     // #shared
      _Atomic_word  _M_weak_count;    // #weak + (#shared != 0)
    };


namespace __gnu_cxx _GLIBCXX_VISIBILITY(default)
{
_GLIBCXX_BEGIN_NAMESPACE_VERSION
  ...
  static inline _Atomic_word
  __attribute__ ((__unused__))
  __exchange_and_add_dispatch(_Atomic_word* __mem, int __val)
  {
#ifdef __GTHREADS
    if (__gthread_active_p())
      return __exchange_and_add(__mem, __val);
    else
      return __exchange_and_add_single(__mem, __val);
#else
    return __exchange_and_add_single(__mem, __val);
#endif
  }
  ...

_GLIBCXX_END_NAMESPACE_VERSION
} // namespace

        赋值构造会将被复制的对象引用计数加1,同时,将自身的引用计数减1,如果减1之前,自身的引用计数已经为1,则将释放持有的指针:_M_dispose(),并且weak_count=1时,也会将自身delete掉:_M_destroy()。整个操作也是线程安全的。

析构函数

        当类离开其作用域时,析构函数会被调用:

int test_destroy() {
    {
        shared_ptr<A> pa(new A);
    }
    // 此时, pa的析构函数已经被调用
}

        析构函数的实现如下:

  // shared_ptr没有显式定义析构函数

  template<typename _Tp, _Lock_policy _Lp>
    class __shared_ptr
    : public __shared_ptr_access<_Tp, _Lp>
    {
    public:
      using element_type = typename remove_extent<_Tp>::type;
      ...
      ~__shared_ptr() = default;
      ...
    private:
      ...
      element_type*	   _M_ptr;         // Contained pointer.
      __shared_count<_Lp>  _M_refcount;    // Reference counter.
    }

  template<_Lock_policy _Lp>
    class __shared_count
    {
      ...
    public:
      ...
      ~__shared_count() noexcept
      {
	if (_M_pi != nullptr)
	  _M_pi->_M_release();
      }
      ...
    private:
      ...
      _Sp_counted_base<_Lp>*  _M_pi;
    }

        析构函数调用引用计数的_M_release(),即引用计数减1,且如果资源不再使用,则释放资源。

像使用指针一样

        在使用智能指针时,为了能像使用指针一样,智能指针对操作符*、->进行了重载,我们就能像下面这样使用智能指针了:

struct B {
    int b;
};

void test() {
    B *pb = new B;
    shared_ptr<B> shared_pb(pb);
    shared_pb->b = 1; // 类似于pb->b = 1
    B b = *shared_pb; // 类似于B b = *pb;
}

        重载操作符的实现如下:

  template<typename _Tp, _Lock_policy _Lp>
    class __shared_ptr_access<_Tp, _Lp, true, false>
    {
    public:
      using element_type = typename remove_extent<_Tp>::type;

#if __cplusplus <= 201402L
      [[__deprecated__("shared_ptr<T[]>::operator* is absent from C++17")]]
      element_type&
      operator*() const noexcept
      {
	__glibcxx_assert(_M_get() != nullptr);
	return *_M_get();
      }

      [[__deprecated__("shared_ptr<T[]>::operator-> is absent from C++17")]]
      element_type*
      operator->() const noexcept
      {
	_GLIBCXX_DEBUG_PEDASSERT(_M_get() != nullptr);
	return _M_get();
      }
#endif
      ...
    private:
      element_type*
      _M_get() const noexcept
      { return static_cast<const __shared_ptr<_Tp, _Lp>*>(this)->get(); }
    };

  template<typename _Tp, _Lock_policy _Lp>
    class __shared_ptr
    : public __shared_ptr_access<_Tp, _Lp>
    {
    public:
      using element_type = typename remove_extent<_Tp>::type;
      ...
      element_type*
      get() const noexcept
      { return _M_ptr; }
      ...
    private:
      element_type*	   _M_ptr;         // Contained pointer.
      __shared_count<_Lp>  _M_refcount;    // Reference counter.
    };

总结

        总结一下,智能指针,就是申请资源(本文中的例子是指针)之后,将资源交给智能指针,智能指针复制、赋值时原子增加、减少引用计数,并在引用计数为0,即没有人再使用资源时,释放资源,并将引用计数释放掉(引用计数也是new申请出来的),以此来实现“智能”管理资源。

附录

shared_ptr内存布局测试

        测试代码如下:

# include <memory>
# include <iostream>
# include <stdint.h>


using namespace std;


class A {
public:
    A() {
        cout << "construct A" << endl;
    }

    ~A() {
        cout << "deconstruct A" << endl;
    }
};

/*
    __shared_ptr:
        element_type*	   _M_ptr;         // Contained pointer.
        __shared_count<_Lp>  _M_refcount;    // Reference counter.

     __shared_count:
        _Sp_counted_base<_Lp>*  _M_pi;

    _Sp_counted_ptr:
        void *vptr;
        _Atomic_word  _M_use_count;     // #shared
        _Atomic_word  _M_weak_count;    // #weak + (#shared != 0)
        _Ptr             _M_ptr;
    
*/

int main() {
    {
        A *pa = new A();
        shared_ptr<A> shared_pa(pa);
        char *addr = (char*)&shared_pa;
        void *shared_pa_addr = *((void**)addr);
        void *_M_pi = *((void**)(addr+8));

        printf("pa: 0x%X, shared_pa_addr: 0x%X, _M_pi: 0x%X\n", pa, shared_pa_addr, _M_pi);

        addr = (char*)_M_pi;
        void *vptr = *((void**)addr);
        int _M_use_count = *((int*)(addr+8));
        int _M_weak_count = *((int*)(addr+12));
        void *_M_ptr = *((void**)(addr+16));

        printf("vptr: 0x%X, _M_use_count: %d, _M_weak_count: %d, _M_ptr: 0x%X\n", vptr, _M_use_count, _M_weak_count, _M_ptr);
    }
    cout << "move scope" << endl;
}

面试要求写一个智能指针

        面试中可能会被要求实现一个智能指针,先不考虑线程安全、各种场景的兼容,可以对上面代码进行精简,仅对核心逻辑做实现:

#ifndef M_SMART_POINTER_H
#define M_SMART_POINTER_H

template<typename T>
class MSmartPointer {
public:
    MSmartPointer(T *ptr_) {
        ptr = ptr_;
        ref_count_ptr = new int(1);
    }

    MSmartPointer(const MSmartPointer &r) {
        Copy(r);
    }

    MSmartPointer& operator=(const MSmartPointer &r) {
        if (r.ptr != ptr) {
            Release();
            Copy(r);
        }
        return *this;
    }

    ~MSmartPointer() {
        Release();
    }

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

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

    int ref_count() {
        return *ref_count_ptr;
    }
private:
    void Copy(const MSmartPointer &r) {
        ptr = r.ptr; // 持有指针
        ref_count_ptr = r.ref_count_ptr; // 持有指针对应的引用计数
        if (ref_count_ptr)
            (*ref_count_ptr)++; // 引用计数加1
    }

    void Release() {
        if (!ref_count_ptr) return;
        if (--(*ref_count_ptr) <= 0) { // 引用计数减1,如果引用计数小于等于0,代表没人用了,需要释放资源
            delete ref_count_ptr; // 释放引用计数
            ref_count_ptr = nullptr;
            delete ptr; // 释放持有的指针
            ptr = nullptr;
        }
    }

private:
    T *ptr;
    int *ref_count_ptr;
};

#endif

        附上测试代码:

void test_m_smart_pointer() {
    cout << "test_m_smart_pointer" << endl;
    {
        MSmartPointer<Test> p(new Test);
        cout << "1. ref_count: " << p.ref_count() << endl;
        p->t = 1;
        cout << "1. t: " << p->t << endl;
        {
            MSmartPointer<Test> p1(p);
            cout << "2. ref_count: " << p.ref_count() << endl; 
            (*(p1)).t = 2;
            cout << "2. t: " << p->t << endl;
            MSmartPointer<Test> p2(new Test);
            p2 = p;
            cout << "3. ref_count: " << p.ref_count() << endl; 
        }
        cout << "4. ref_count: " << p.ref_count() << endl; 
    }
    cout << "move scope" << endl;
}

线程安全思考

        在对智能指针的引用计数进行修改时,都使用额原子操作,因此引用计数的加减都是现成安全的。但是,在释放资源时,其整个释放的操作为非原子的。如果在释放资源的过程,发生了复制、赋值,则就可能出现资源被释放,但是还有地方在引用的问题。不考虑多线程场景,以下面的代码举例:

void test_atomic_thread_safe() {
    A *pa = new A;
    shared_ptr<A> shared_pa(pa);
    shared_pa.__shared_ptr<A>::~__shared_ptr<A>();
    shared_ptr<A> shared_pb(pa);
    cout << "test call deconstruct: " << shared_pb.use_count() << endl;
}

         这段代码运行结果如下:

construct A
deconstruct A
test call deconstruct: 1
deconstruct A
deconstruct A
free(): double free detected in tcache 2
[1]    6372 abort (core dumped)  ./output/main

        可以看到,在shared_pa被析构之后,再去对其复制,将导致持有的指针不再有效。但是,正常情况下,谁会主动掉析构函数呢?如果我们不主动调析构函数,则析构函数被调用时,我们也取不到对象了,更别提对其引用。所以总体看来,智能指针还是线程安全的。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值