C++11(1)-智能指针

2019-06-23  本文已影响0人  WalkeR_ZG

C++裸指针的内存问题有:
1、空悬指针/野指针
2、重复释放
3、内存泄漏
4、不配对的申请与释放

使用智能指针可以有效的避免以上问题,智能指针是对裸指针进行包装,行为类似于裸指针的指针,但是却避免了使用裸指针时会遇到的问题。智能指针使用RAII技术,RAII即“Resource Acquisition is Initialization”,即在构造函数中申请分配资源,在析构函数中释放资源,将资源和对象的生命周期绑定。C++11中共有三种智能指针:std::unique_ptr,std::shared_ptr,以及std::weak_ptr。而理解智能指针的重要一环是所有权,std::unique_ptr为所有权唯一的智能指针,std::shared_ptr是所有权可以共享的智能指针,std::weak_ptr是为了解决std::shared_ptr循环引用而生的。

智能指针的经典实践:

  1. 优先使用std::unique_ptr指针来管理对象的生命周期
  2. 只在所有权需共享时使用std::shared_ptr
  3. 使用std::weak_ptr来打破循环引用
  4. 尽量不要裸指针和智能指针混合使用
  5. 优先使用std::make_unique和std::make_shared,而非直接使用new
  6. 将std::unique_ptr作为工厂方法的返回类型
  7. std::shared_ptr为线程非安全的,多线程时需考虑加锁
  8. 使用std::unique_ptr可以管理单个对象和对象数组,
  9. 使用std::shared_ptr只用来管理单个对象,如需管理数组可以考虑std::shared_ptr<std::vector<>>或std::shared_ptr<std::array<,>>形式。
  10. STL容器如需存放指针,不要使用裸指针,应根据具体情况选择合适的智能指针代替。

std::unique_ptr:唯一所有权

std::unique_ptr:是所有权唯一的智能指针,即有且只有该指针管理对应资源,从源码中也可以看出,对于std::unique_ptr,其复制构造及赋值操作都是被delete的,但是所有权可以转移(源智能指针释放所有权,目的智能智能获得所有权),所以其move操作是保留的,所以std::unique_ptr是可以作为函数的返回值使用的。关于copy、move的知识牵扯到左值、右值及RVO(return value optimization)将在后续的文章中描述。

管理单个对象

unique_ptr的模板定义如下:从模板定义可以看出,对于unique_ptr可以指定删除器,默认使用default_delete删除,而default_delete重载了operator()(_Tp* __ptr) const函数,在该函数中使用delete进行删除。

  template <typename _Tp, typename _Dp = default_delete<_Tp> >
    class unique_ptr
      void
      operator()(_Tp* __ptr) const
      {
    static_assert(!is_void<_Tp>::value,
              "can't delete pointer to incomplete type");
    static_assert(sizeof(_Tp)>0,
              "can't delete pointer to incomplete type");
    delete __ptr;
      }
管理数组对象

unique_ptr针对指针类型的偏特化模板代码如下:从其定义自然可知,该智能指针是管理数组类型的资源,它的删除器是靠default_delete的偏特化版本default_delete<_Tp[]>进行资源删除的,最终使用的是delete[]删除资源。

  template<typename _Tp, typename _Dp>
    class unique_ptr<_Tp[], _Dp>
      template<typename _Up>
      typename enable_if<is_convertible<_Up(*)[], _Tp(*)[]>::value>::type
    operator()(_Up* __ptr) const
      {
    static_assert(sizeof(_Tp)>0,
              "can't delete pointer to incomplete type");
    delete [] __ptr;
      }

(Tips:关于模板全特化、偏特化、Traits(类型萃取)的知识后面也会专门写博客)

自定义删除器

从上面的代码可以看到,std::unique_ptr不仅可以管理单个对象,同样可以管理对象数组。并且删除器是作为类型的一部分。我们也可以自定义删除器,让std::unique_ptr在释放资源时调用自定义的删除器,代码如下:

template<typename T>
class MyDeleter{
public:
    void operator()(T * x){
        std::cout<<"MyDeleter"<<std::endl;;
        delete x;
        x = nullptr;
    }
};

unique_ptr<int, MyDeleter<int>> uq(new int);
unique_ptr<A, MyDeleter<A>> uq(new A);//A is a class 
异类型move构造

在源码中有以下代码,它的出现是为什么呢?首先看到std::unique_ptr的类型定义的两个类模板分别为_Tp和_Dp,其中_Tp为std::unique_ptr所管理的对象类型,而_Dp是删除器类型,下面的代码中又出现了_Up和_Ep,其实_Up是另一个std::unique_ptr的对象类型,而_Ep是其对应的删除器类型。这里unique_ptr(unique_ptr<_Up, _Ep>&& __u)是一个move构造,也即可以使用不同类型的unique_ptr来构造unique_ptr,那自然这涉及到安全的问题,新的unique_ptr能否管理传入的unique_ptr智能指针指向的对象呢?正因为安全性的考虑,所以引入了__safe_conversion_up ,用于在编译器进行转换的检查,如果这种转换是非法的,则会发生编译错误。如果转换是合法的,则没有问题。比如有两个类A,B,其中B是A的子类,传入的智能指针是unique_ptr<B>,要构造的类型是unique_ptr<A>则自然是没有问题的。如果传入的智能指针是unique_ptr<A>,要构造的类型是unique_ptr<B>,由于A类型转换不了B类型,则自然会发生编译期错误。

      template<typename _Up, typename _Ep>
    using __safe_conversion_up = __and_<
            is_convertible<typename unique_ptr<_Up, _Ep>::pointer, pointer>,
                __not_<is_array<_Up>>,
                __or_<__and_<is_reference<deleter_type>,
                             is_same<deleter_type, _Ep>>,
                      __and_<__not_<is_reference<deleter_type>>,
                             is_convertible<_Ep, deleter_type>>
                >
              >;

      template<typename _Up, typename _Ep, typename = _Require<
               __safe_conversion_up<_Up, _Ep>,
           typename conditional<is_reference<_Dp>::value,
                    is_same<_Ep, _Dp>,
                    is_convertible<_Ep, _Dp>>::type>>
    unique_ptr(unique_ptr<_Up, _Ep>&& __u) noexcept
    : _M_t(__u.release(), std::forward<_Ep>(__u.get_deleter()))
    { }
将std::unique_ptr作为工厂方法的返回类型

std::unique_ptr作为工厂方法的返回值有其很大优势,工厂方法通常的做法是申请一个堆空间对象作为返回值,但是这需要使用者需要注意在使用完成后释放堆空间,否则会造成内存泄漏。如果使用std::unique_ptr作为返回值,则对于使用者来说,返回对象的生命周期不需要进行管理,由系统自行释放,无疑增强了代码的健壮性。由于std::unique_ptr可以用来构造std::shared_ptr智能指针,这一特性使得std::unique_ptr更加的适合工厂函数的返回类型。根据使用者自身的使用场景灵活选择即可。

std::shared_ptr:共享所有权

std::shared_ptr是通过对资源的引用计数来确定是否需要释放资源的。其内部有两个计数器:_M_use_count和_M_weak_count,其中_M_use_count是std::shared_ptr的指针指向该资源的个数,_M_weak_count是std::weak_ptr指针指向该资源的个数+1(对std::shared_ptr初始化时的初始值)。

尽量使用std::unique_ptr管理资源

在std::shared_ptr章节的开始,首先要强调的是在能使用std::unique_ptr即可的地方使用std::unique_ptr而非std::shared_ptr来管理资源,原因如下:

  1. std::unique_ptr是唯一所有权,对资源的所有权清晰,而std::shared_ptr使用不当容易造成内存无法回收问题。
  2. std::shared_ptr占用的内存更大,因为其内部不但包含了指向资源的裸指针,还包括指向_M_use_count和_M_weak_count信息的指针,
    并且由于_M_use_count和_M_weak_count(两者都是_Atomic_word类型)的更新在内部实现有原子操作,所以std::shared_ptr的效率相比std::unique_ptr较差。
std::shared_ptr与std::unique_ptr的删除器差异

前面已经阐述对于std::unique_ptr,其删除器是template <typename _Tp, typename _Dp = default_delete<_Tp> >,删除器是作为类型参数传入的,而看std::shared_ptr的类型是template<typename _Tp>,删除器是作为入参,为什么有这种设计的差异呢?因为效率和内存,unique_ptr的设计目标之一是尽可能的高效, 析构也尽量高效,而shared_ptr要分配一个控制块,对于shared_ptr判断是否需要析构时,总要访问控制块(控制块与所管理的资源的内存地址不连续,所以对于CPU读取效率不高),所以将其设计为类型参数没有太大的意义,并且还会增加设计的复杂度。

std::shared_ptr与std::unique_ptr的另一差异

std::unique_ptr有一个特化版本用来管理数组对象,而std::shared_ptr没有特化版本,对于数组对象不建议使用std::shared_ptr来管理,可以选用std::shared_ptr<vector<T>>、std::shared_ptr<array<T>>等替代,如果一定使用std::shared_ptr来管理动态数组,一定传入删除器作为参数,否则会造成内存泄漏。
当然在项目开发中,如果std::unique_ptr不满足需求时,如何正确地使用std::shared_ptr呢?

std::shared_ptr设计思想

std::shared_ptr的设计思想其实就是在对象拷贝构造、赋值构造时对引用计数进行累加,在move构造时,将原智能指针release,然后将新智能指针管理对应资源。源码中对于引用计数采用了原子操作,可以保证引用计数的更新是原子性的,那std::shared_ptr是线程安全的吗?答案不是。

     __shared_count(const __shared_count& __r) noexcept
      : _M_pi(__r._M_pi)
      {
    if (_M_pi != 0)
      _M_pi->_M_add_ref_copy();
      }

      __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;
      }
std::shared_ptr构造

std::shared_ptr提供了很多的构造函数,可以使用裸指针、unique_ptr、weak_ptr、share_ptr构造,还有make_share<Type>(Args...)进行构造,在这里特别要阐述的是裸指针构造和make_share<Type>(Args...)构造。
裸指针构造:
1、优势:可以指定自定义的删除器
2、劣势:存在内存泄漏风险,错误书写可能会引起重复析构
裸指针与智能指针混用容易会出现问题,而只使用智能指针基本能满足日常需求。
下面的代码:

Foo* foo = new Foo();
std::shared_ptr<Foo> sf1(foo);
......
std::shared_ptr<Foo> sf2(foo);//裸指针被多个智能指针构造会引起多次析构

使用make_share<Type>(Args...)构造智能指针的优劣与裸指针相反,所以在不需要自定义删除器时,优先使用make_share<Type>(Args...)。如果需要自定义删除器,则可以使用裸指针来初始化智能指针。

std::shared_ptr的线程安全性

std::shared_ptr不是线程安全性的,线程安全性 有详细的阐述。

std::weak_ptr:衍生智能指针

std::shared_ptr是通过引用计数进行判断是否需要对资源进行释放的,如果发生了循环引用,则两个对象的引用计数都不会降为0,则两个对象永远不会被析构,而std::weak_ptr的引入就是为了解决此类问题的,std::weak_ptr不是一种独立的智能指针,它不会影响资源的引用计数,同时没有重载operator*和->,其必须转为std::shared_ptr才能访问资源。
扯远些,在JAVA中有四种引用类型,强引用、弱引用、软引用、虚引用。这与JAVA的GC策略有关系,不同的引用类型在GC时表现不同,比如强引用不会释放,弱引用当内存不足时会被释放,而软引用只要是GC就会被释放。他们都有不同的使用场景。而C++智能指针的内存释放其实使用的RAII机制,std::weak_ptr它只能使用std::shared_ptr和std::weak_ptr进行构造,而不能使用裸指针。这个也很自然,它本身不会影响资源的生命周期。
std::weak_ptr的计数代码如下:

      __weak_count(const __shared_count<_Lp>& __r) noexcept
      : _M_pi(__r._M_pi)
      {
    if (_M_pi != nullptr)
      _M_pi->_M_weak_add_ref();
      }

      __weak_count(const __weak_count& __r) noexcept
      : _M_pi(__r._M_pi)
      {
    if (_M_pi != nullptr)
      _M_pi->_M_weak_add_ref();
      }

      __weak_count(__weak_count&& __r) noexcept
      : _M_pi(__r._M_pi)
      { __r._M_pi = nullptr; }

      ~__weak_count() noexcept
      {
    if (_M_pi != nullptr)
      _M_pi->_M_weak_release();
      }

      __weak_count&
      operator=(const __shared_count<_Lp>& __r) noexcept
      {
    _Sp_counted_base<_Lp>* __tmp = __r._M_pi;
    if (__tmp != nullptr)
      __tmp->_M_weak_add_ref();
    if (_M_pi != nullptr)
      _M_pi->_M_weak_release();
    _M_pi = __tmp;
    return *this;
      }

      __weak_count&
      operator=(const __weak_count& __r) noexcept
      {
    _Sp_counted_base<_Lp>* __tmp = __r._M_pi;
    if (__tmp != nullptr)
      __tmp->_M_weak_add_ref();
    if (_M_pi != nullptr)
      _M_pi->_M_weak_release();
    _M_pi = __tmp;
    return *this;
      }

      __weak_count&
      operator=(__weak_count&& __r) noexcept
      {
    if (_M_pi != nullptr)
      _M_pi->_M_weak_release();
    _M_pi = __r._M_pi;
        __r._M_pi = nullptr;
    return *this;
      }

由于std::weak_ptr不会影响资源的声明周期,所以根据这个特性在合适的时刻选用该智能指针。

STL容器与指针

STL容器中存放指针一般不是好的实践,很容易造成空悬指针、重复释放的问题,程序设计者需要考虑其生命周期问题,使用智能指针可以解决。

std::unique_ptr与容器

std::unique_ptr<std::vector<T>>与std::vector<std::unique_ptr<T>>的差异,std::unique_ptr<std::vector<T>>是智能指针管理一个容器,这个容器可以保存T类的对象,而std::vector<std::unique_ptr<T>>是一个容器,容器里存着的是所有权唯一的智能指针,注意:std::unique_ptr不能被拷贝及赋值,所以一些STL的方法不能使用。

    vector<unique_ptr<T>> vec;
    vec.push_back(make_unique<T>());
    for(auto& t : vec){//如果没有&,编译错误,不能被拷贝
        ......
    }
std::shared_ptr与容器

std::shared_ptr放入容器中使用时一定注意其生命周期是否合理,是否存在延长对象生命周期的情况。因为容器不被析构,里面的所有对象都不会被析构。如果容器与所存的生命周期不一致,则应该使用weak_ptr。

一切技术都不是万能的,智能指针使用也有很多的坑,用modern C++的方式书写代码,注意其生命周期是否合适。
WalkeR_ZG

上一篇下一篇

猜你喜欢

热点阅读