C++ 智能指针

一、为什么使用智能指针

  在C++中,堆上申请内存之后,需要手动对内存进行释放,随着代码逐渐复杂,很难保证所有的内存都被释放。

  智能指针设计的初衷就是可以帮助我们管理堆上申请的内存,可以理解为开发者只需要申请,而释放交给智能指针。


  

二、unique_ptr,shared_ptr,weak_ptr

  

unique_ptr

  unique_ptr 拥有对 持有对象的唯一所有权,即两个 unique_ptr 不能同时指向同一个对象
  1、unique_ptr不能被复制到另外一个unique_ptr
  2、unique_ptr所持有的对象只能通过转移语义将所有权转移到另外一个unique_ptr
  
  智能指针有一个通用的规则,就是->表示用于调用指针原有的方法,而 . 则表示调用智能指针本身的方法。
  

unique_ptr本身拥有的方法主要包括:

1、get() 获取其保存的原生指针,尽量不要使用

2、bool() 判断是否拥有指针

3、release() 释放所管理指针的所有权,返回原生指针。但并不销毁原生指针。

4、reset() 释放并销毁原生指针。如果参数为一个新指针,将管理这个新指针

unique_ptr 一般在不需要多个指向同一个对象的指针时使用,但这个条件本身就很难判断,在我看来可以简单的理解:这个对象 / 在 /对象或方法内部使用时优先使用 unique_ptr

1、对象内部使用

class TestUnique
{
private:
    std::unique_ptr<A> a_ = std::unique_ptr<A>(new A());
public:
    void process1()
    {
        a_->do_something();
    }

    void process2()
    {
        a_->do_something();
    }

    ~TestUnique()
    {
        //此处不再需要手动删除a_
    }
};

2、方法内部使用

void test_unique_ptr()
{
    std::unique_ptr<A> a(new A());
    a->do_something();
}

  
  

shared_ptr

  与unique_ptr的唯一所有权所不同的是,shared_ptr强调的是共享所有权。也就是说多个shared_ptr可以拥有同一个原生指针的所有权。

std::shared_ptr<A> a1(new A());
std::shared_ptr<A> a2 = a1;//编译正常,允许所有权的共享

  shared_ptr 是通过引用计数的方式管理指针,当引用计数为 0 时会销毁拥有的原生对象。

shared_ptr本身拥有的方法主要包括:

1、get() 获取其保存的原生指针,尽量不要使用

2、bool() 判断是否拥有指针

3、reset() 释放并销毁原生指针。如果参数为一个新指针,将管理这个新指针

4、unique() 如果引用计数为 1,则返回 true,否则返回 false

5、use_count() 返回引用计数的大小

std::shared_ptr<A> a1(new A());
std::shared_ptr<A> a2 = a1;//编译正常,允许所有权的共享

A *origin_a = a1.get();//尽量不要暴露原生指针

if(a1)
{
    // a1 拥有指针
}

if(a1.unique())
{
    // 如果返回true,引用计数为1
}

long a1_use_count = a1.use_count();//引用计数数量

  shared_ptr 的内存占用是裸指针的两倍,因为除了要管理一个裸指针外,还需要维护一个 引用计数 ,因此相比于unique_ptr,shared_ptr的内存占用更高。原子操作性能低。考虑到线程安全问题,引用计数的增减必须是原子操作。而原子操作一般情况下都比非原子操作慢。

使用场景:1、shared_ptr 通常使用在共享权不明的场景。有可能多个对象同时管理同一个内存时。
     2、对象的延迟销毁。陈硕在《Linux 多线程服务器端编程》中提到,当一个对象的析构非常耗时,甚至影响到了关键线程的速度。可以使用 BlockingQueue<std::shared_ptr> 将对象转移到另外一个线程中释放,从而解放关键线程。

  
  
shared_ptr的使用场景

shared_ptr一般在需要多个执行同一个对象的指针使用,在我看来可以简单的理解:这个对象需要被多个class 同时使用的时候

class B
{
private:
    std::shared_ptr<A> a_;

public:
    B(std::shared_ptr<A>& a): a_(a) {}
};

class C
{
private:
    std::shared_ptr<A> a_;

public:
    C(std::shared_ptr<A>& a): a_(a) {}
};

std::shared_ptr<B> b_;
std::shared_ptr<C> c_;

void test_A_B_C()
{
    std::shared_ptr<A> a = std::make_shared<A>();
    b_ = std::make_shared<B>(a);
    c_ = std::make_shared<C>(a);
}

  
  

weak_ptr

  weak_ptr 比较特殊,它主要是为了配合shared_ptr而存在的。就像它的名字一样,它本身是一个弱指针,因为它本身是不能直接调用原生指针的方法的。如果想要使用原生指针的方法,需要将其先转换为一个shared_ptr。那weak_ptr存在的意义到底是什么呢?
   由于shared_ptr是通过引用计数来管理原生指针的,那么最大的问题就是循环引用(比如 a 对象持有 b 对象,b 对象持有 a 对象),这样必然会导致内存泄露。而weak_ptr不会增加引用计数,因此将循环引用的一方修改为弱引用,可以避免内存泄露。

weak_ptr可以通过一个shared_ptr创建。

std::shared_ptr<A> a1(new A());
std::weak_ptr<A> weak_a1 = a1;//不增加引用计数

weak_ptr本身拥有的方法主要包括:

1、expired() 判断所指向的原生指针是否被释放,如果被释放了返回 true,否则返回 false

2、use_count() 返回原生指针的引用计数

3、lock() 返回 shared_ptr,如果原生指针没有被释放,则返回一个非空的 shared_ptr,否则返回一个空的 shared_ptr

std::shared_ptr<A> a1(new A());
std::wead_ptr<A> weak_a1 = a1;  //并不增加引用计数

if(weak_a1.expired()){
	 //如果为true,weak_a1对应的原生指针已经被释放了
}
long a1_use_count = weak_a1.use_count();  // 引用计数量

if(std::shared_ptr<A> shared_a = weak_a1.lock()){
	//此时可以通过 shared_a 进行原生指针的方法调用
}
weak_a1.reset();  // 将 weak_a1 置空

  
  

三、一些关于智能指针的点

  
shared_ptr对象在内部指向两个内存位置
1、指向对象的指针
2、用于控制引用计数数据的指针

创建 shared_ptr 对象

  • 使用原始指针创建shared_ptr 对象
  • 带有参数的shared_ptr 构造函数是 explicit 类型的,所以不能像 std::shared_ptr p1 = new int() 创建,应该
std::shared_ptr<int> ptr2(new int());

此方法在堆上创建了两块内存:1:存储int。2:控制块上用于引用计数的内存,管理附加此内存的 shared_ptr 对象的计数,最初计数将为1。

使用make_shared

std::shared_ptr<int> ptr = std::make_shared<int>();

  std::make_shared 一次性为 int对象 和 用于引用计数 的数据都分配了内存,而 new 操作符只是为int分配了内存。
  
  因为使用new的方式创建shared_ptr会导致出现两次内存申请,而std::make_shared在内部实现时只会申请一个内存。因此建议后续均使用std::make_shared。

  
  
  
  

可以给 shared_ptr 添加自定义删除器

   当 shared_ptr 对象调用其析构函数,它将引用计数减1,如果引用计数的新值为0,则删除关联的原始指针。析构函数中删除内部原始指针,默认调用的是delete()函数。 当shared_ptr指向数组时,该函数针对数组类型创建的指针是不起作用的,需要自己来自定义一个删除器来delete[].

//删除器
void deleter(Sample * x)
{
	std::cout << "delete\n";
	delete[] x;
}
//第二个参数传递自定义删除器指针
std::shared_ptr<Sample> p(new Sample[2], deleter);

  
  

四、源码解析

  

uniptr_ptr

有两个模板参数,分别为 _Tp 和 _Dp
  • _Tp表示原生指针的类型
  • _Dp则表示析构器,开发者则可以自定义指针 销毁的代码,其拥有一个默认值 default_delete<_Tp> ,其实就是标准的 delete 函数

函数声明中 typename __pointer_type<_Tp, deleter_type>::type 可以简单理解为 _Tp*,即原生指针类型

template <class _Tp, class _Dp = default_delete<_Tp> >
class _LIBCPP_TEMPLATE_VIS unique_ptr {
public:
  typedef _Tp element_type;
  typedef _Dp deleter_type;
  typedef typename __pointer_type<_Tp, deleter_type>::type pointer;
  //...
}

unique_ptr中唯一的数据成员就是原生指针和析构器的 pair。

private:
  __compressed_pair<pointer, deleter_type> __ptr_;

下面看下unique_ptr的构造函数。

template <class _Tp, class _Dp = default_delete<_Tp> >
class _LIBCPP_TEMPLATE_VIS unique_ptr {

public:
  // 默认构造函数,用pointer的默认构造函数初始化__ptr_
  constexpr unique_ptr() noexcept : __ptr_(pointer()) {}

  // 空指针的构造函数,同上
  constexpr unique_ptr(nullptr_t) noexcept : __ptr_(pointer()) {}

  // 原生指针的构造函数,用原生指针初始化__ptr_
  explicit unique_ptr(pointer __p) noexcept : __ptr_(__p) {}

  // 原生指针和析构器的构造函数,用这两个参数初始化__ptr_,当前析构器为左值引用
  unique_ptr(pointer __p, _LValRefType<_Dummy> __d) noexcept
      : __ptr_(__p, __d) {}

  // 原生指针和析构器的构造函数,析构器使用转移语义进行转移
  unique_ptr(pointer __p, _GoodRValRefType<_Dummy> __d) noexcept
      : __ptr_(__p, _VSTD::move(__d)) {
    static_assert(!is_reference<deleter_type>::value,
                  "rvalue deleter bound to reference");
  }

  // 移动构造函数,取出原有unique_ptr的指针和析构器进行构造
  unique_ptr(unique_ptr&& __u) noexcept
      : __ptr_(__u.release(), _VSTD::forward<deleter_type>(__u.get_deleter())) {
  }

  // 移动赋值函数,取出原有unique_ptr的指针和析构器进行构造
  unique_ptr& operator=(unique_ptr&& __u) _NOEXCEPT {
    reset(__u.release());
    __ptr_.second() = _VSTD::forward<deleter_type>(__u.get_deleter());
    return *this;
  }

}

再看下unique_ptr几个常用函数的实现。

template <class _Tp, class _Dp = default_delete<_Tp> >
class _LIBCPP_TEMPLATE_VIS unique_ptr {

// 返回原生指针
pointer get() const _NOEXCEPT {
  return __ptr_.first();
}

// 判断原生指针是否为空
_LIBCPP_EXPLICIT operator bool() const _NOEXCEPT {
  return __ptr_.first() != nullptr;
}

// 将__ptr置空,并返回原有的指针
pointer release() _NOEXCEPT {
  pointer __t = __ptr_.first();
  __ptr_.first() = pointer();
  return __t;
}

// 重置原有的指针为新的指针,如果原有指针不为空,对原有指针所指对象进行销毁
void reset(pointer __p = pointer()) _NOEXCEPT {
  pointer __tmp = __ptr_.first();
  __ptr_.first() = __p;
  if (__tmp)
    __ptr_.second()(__tmp);
}
}

再看下unique_ptr指针特性的两个方法。

// 返回原生指针的引用
typename add_lvalue_reference<_Tp>::type
operator*() const {
  return *__ptr_.first();
}
// 返回原生指针
pointer operator->() const _NOEXCEPT {
  return __ptr_.first();
}

最后再看下unique_ptr的析构函数。

// 通过reset()方法进行对象的销毁
~unique_ptr() { reset(); }



shared_ptr

  shared_ptr 与unique_ptr最核心的区别就是比unique_ptr多了一个引用计数,并由于引用计数的加入,可以支持拷贝。

  先看下shared_ptr的声明。shared_ptr主要有两个成员变量,一个是原生指针,一个是控制块的指针,用来存储这个原生指针的shared_ptr和weak_ptr的数量。

template<class _Tp>
class shared_ptr
{
public:
    typedef _Tp element_type;

private:
    element_type*      __ptr_;
    __shared_weak_count* __cntrl_;
    //...
}

我们重点看下__shared_weak_count的定义。

// 共享计数类
class __shared_count
{
    __shared_count(const __shared_count&);
    __shared_count& operator=(const __shared_count&);

protected:
    // 共享计数
    long __shared_owners_;
    virtual ~__shared_count();
private:
    // 引用计数变为0的回调,一般是进行内存释放
    virtual void __on_zero_shared() _NOEXCEPT = 0;

public:
    // 构造函数,需要注意内部存储的引用计数是从0开始,外部看到的引用计数其实为1
    explicit __shared_count(long __refs = 0) _NOEXCEPT
        : __shared_owners_(__refs) {}

    // 增加共享计数
    void __add_shared() _NOEXCEPT {
      __libcpp_atomic_refcount_increment(__shared_owners_);
    }

    // 释放共享计数,如果共享计数为0(内部为-1),则调用__on_zero_shared进行内存释放
    bool __release_shared() _NOEXCEPT {
      if (__libcpp_atomic_refcount_decrement(__shared_owners_) == -1) {
        __on_zero_shared();
        return true;
      }
      return false;
    }

    // 返回引用计数,需要对内部存储的引用计数+1处理
    long use_count() const _NOEXCEPT {
        return __libcpp_relaxed_load(&amp;__shared_owners_) + 1;
    }
};

class __shared_weak_count
    : private __shared_count
{
    // weak ptr计数
    long __shared_weak_owners_;

public:
    // 内部共享计数和weak计数都为0
    explicit __shared_weak_count(long __refs = 0) _NOEXCEPT
        : __shared_count(__refs),
          __shared_weak_owners_(__refs) {}
protected:
    virtual ~__shared_weak_count();

public:
    // 调用通过父类的__add_shared,增加共享引用计数
    void __add_shared() _NOEXCEPT {
      __shared_count::__add_shared();
    }
    // 增加weak引用计数
    void __add_weak() _NOEXCEPT {
      __libcpp_atomic_refcount_increment(__shared_weak_owners_);
    }
    // 调用父类的__release_shared,如果释放了原生指针的内存,还需要调用__release_weak,因为内部weak计数默认为0
    void __release_shared() _NOEXCEPT {
      if (__shared_count::__release_shared())
        __release_weak();
    }
    // weak引用计数减1
    void __release_weak() _NOEXCEPT;
    // 获取共享计数
    long use_count() const _NOEXCEPT {return __shared_count::use_count();}
    __shared_weak_count* lock() _NOEXCEPT;

private:
    // weak计数为0的处理
    virtual void __on_zero_shared_weak() _NOEXCEPT = 0;
};

  其实__shared_weak_count也是虚类,具体使用的是__shared_ptr_pointer。_shared_ptr_pointer 中有一个成员变量__data,用于存储原生指针、析构器、分配器。__shared_ptr_pointer继承了__shared_weak_count,因此它就主要负责内存的分配、销毁,引用计数。

class __shared_ptr_pointer
    : public __shared_weak_count
{
    __compressed_pair<__compressed_pair<_Tp, _Dp>, _Alloc> __data_;
public:
    _LIBCPP_INLINE_VISIBILITY
    __shared_ptr_pointer(_Tp __p, _Dp __d, _Alloc __a)
        :  __data_(__compressed_pair<_Tp, _Dp>(__p, _VSTD::move(__d)), _VSTD::move(__a)) {}

#ifndef _LIBCPP_NO_RTTI
    virtual const void* __get_deleter(const type_info&) const _NOEXCEPT;
#endif

private:
    virtual void __on_zero_shared() _NOEXCEPT;
    virtual void __on_zero_shared_weak() _NOEXCEPT;
};

了解了引用计数的基本原理后,再看下shared_ptr的实现。

// 使用原生指针构造shared_ptr时,会构建__shared_ptr_pointer的控制块
shared_ptr<_Tp>::shared_ptr(_Yp* __p,
                            typename enable_if<is_convertible<_Yp*, element_type*>::value, __nat>::type)
    : __ptr_(__p)
{
    unique_ptr<_Yp> __hold(__p);
    typedef typename __shared_ptr_default_allocator<_Yp>::type _AllocT;
    typedef __shared_ptr_pointer<_Yp*, default_delete<_Yp>, _AllocT > _CntrlBlk;
    __cntrl_ = new _CntrlBlk(__p, default_delete<_Yp>(), _AllocT());
    __hold.release();
    __enable_weak_this(__p, __p);
}

// 如果进行shared_ptr的拷贝,会增加引用计数
template<class _Tp>
inline
shared_ptr<_Tp>::shared_ptr(const shared_ptr& __r) _NOEXCEPT
    : __ptr_(__r.__ptr_),
      __cntrl_(__r.__cntrl_)
{
    if (__cntrl_)
        __cntrl_->__add_shared();
}

// 销毁shared_ptr时,会使共享引用计数减1,如果减到0会销毁内存
template<class _Tp>
shared_ptr<_Tp>::~shared_ptr()
{
    if (__cntrl_)
        __cntrl_->__release_shared();
}

weak_ptr

  了解完shared_ptr,weak_ptr也就比较简单了。weak_ptr也包括两个对象,一个是原生指针,一个是控制块。虽然weak_ptr内存储了原生指针,不过由于未实现operator->因此不能直接使用。

class _LIBCPP_TEMPLATE_VIS weak_ptr
{
public:
    typedef _Tp element_type;
private:
    element_type*        __ptr_;
    __shared_weak_count* __cntrl_;

}

// 通过shared_ptr构造weak_ptr。会将shared_ptr的成员变量地址进行复制。增加weak引用计数
weak_ptr<_Tp>::weak_ptr(shared_ptr<_Yp> const&amp; __r,
                        typename enable_if<is_convertible<_Yp*, _Tp*>::value, __nat*>::type)
                         _NOEXCEPT
    : __ptr_(__r.__ptr_),
      __cntrl_(__r.__cntrl_)
{
    if (__cntrl_)
        __cntrl_->__add_weak();
}

// weak_ptr析构器
template<class _Tp>
weak_ptr<_Tp>::~weak_ptr()
{
    if (__cntrl_)
        __cntrl_->__release_weak();
}

参考链接

C++ 智能指针最佳实践&源码分析

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值