【C++】share_ptr详解

一、share_ptr 的简单使用

1.1、基本用法

从较浅的层面看,智能指针是利用了一种叫做RAII(资源获取即初始化)的技术对普通的指针进行封装,这使得智能指针实质是一个对象,行为表现的却像一个指针。

智能指针的作用是防止忘记调用delete释放内存和程序异常的进入catch块忘记释放内存。另外指针的释放时机也是非常有考究的,多次释放同一个指针会造成程序崩溃,这些都可以通过智能指针来解决。

智能指针的行为类似于一个常规指针,与常规指针之间重要的区别就是它负责自动释放所管理的资源,share_ptr 使用引用计数,允许多个 share_ptr 指向同一资源,每多一个 share_ptr 指向该资源,share_ptr 的引用计数就 +1 ,减为0时表示没有 share_ptr 对该资源进行引用了,就会释放所指向的资源。share_ptr 内部中的引用计数是线程安全的,但是引用的资源不是线程安全的。

1.2、初始化 

  1. 裸指针直接初始化,但不能通过隐式转换来构造,因为 share_ptr 构造函数被声明为 explicit
  2. 允许移动构造与拷贝构造
  3. 通过 make_share 构造
#include <iostream>
#include <memory>

class test {};

int main()
{
  std::shared_ptr<test> f(new test());              // 裸指针直接初始化
  //std::shared_ptr<test> f1 = new test();            // Error,explicit禁止隐式初始化
  std::shared_ptr<test> f2(f);                       // 拷贝构造函数
  std::shared_ptr<test> f3 = f;                      // 拷贝构造函数
  f2 = f;                                             // copy赋值运算符重载
  std::cout << f3.use_count() << " " << f3.unique() << std::endl;

  std::shared_ptr<test> f4(std::move(new test()));        // 移动构造函数
  //std::shared_ptr<test> f5 = std::move(new test());       // Error,explicit禁止隐式初始化
  std::shared_ptr<test> f6(std::move(f4));                 // 移动构造函数
  std::shared_ptr<test> f7 = std::move(f6);                // 移动构造函数
  std::cout << f7.use_count() << " " << f7.unique() << std::endl;

  std::shared_ptr<test[]> f8(new test[10]());             // Error,管理动态数组时,需要指定删除器
  std::shared_ptr<test> f9(new test[10](), std::default_delete<test[]>());

  auto f10 = std::make_shared<test>();               // std::make_shared来创建

  return 0;
}

在初始化上 share_ptr 与 unique_ptr 在初始化上的方式就有区别,二者都不支持隐式初始化,但是 unque_ptr 不支持拷贝构造和拷贝赋值,而 share_ptr 则都支持。

1.3、删除器

删除器可以是普通函数、函数对象和 lambda 表达式等,默认的删除器为 std::default_delete ,内部是使用 delete 来实现的,与 unique_ptr 不同,删除器不是 share_ptr 类型的组成部分,也就是说两个 sptr1,sptr2 有不同的删除器,但只要它们的类型是相同的都可以被放入同一容器中。此外,在动态管理数组的时候,share_ptr 需要指定删除器。智能指针的大小与被引用的资源的大小是无关的,因为智能指针是也是通过指针来对该资源进行访问,而不是存储在智能指针内部。

#include <iostream>
#include <memory>
#include <vector>

class Frame {};

int main()
{
  auto del1 = [](Frame* f){
    std::cout << "delete1" << std::endl;
    delete f;
  };
  auto del2 = [](Frame* f){
    std::cout << "delete2" << std::endl;
    delete f;
  };

  std::shared_ptr<Frame> f1(new Frame(), del1);
  std::shared_ptr<Frame> f2(new Frame(), del2);
  std::unique_ptr<Frame, decltype(del)> f3(new Frame(), del);

  std::vector<std::shared_ptr<Frame> > v;
  v.push_back(f1);
  v.push_back(f2);

  return 0;
}

二、剖析 share_ptr

2.1、share_ptr 的内存模型

share_ptr 的内存模型长这样(直接使用的陈硕大神的图)

将其简化只剩下引用计数与原始指针后长这样:

从图中我们可以看出,share_ptr 包含了一个指向对象的指针和一个指向控制块的指针。每一个由share_ptr 管理的对象都有一个控制块,除了包含强引用计数、弱引用技术之外,还包含了自定义删除器的副本和分配器的副本以及其他附加数据。

2.2、控制块的创建规则

  1. std::make_shared 总是创建一个控制块
  2. 从具备所有权的指针触发构造一个 share_ptr 的时候,会创建一个控制块(比如 unique_ptr 在转换成 share_ptr 的时候会创建控制块,因为 unique_ptr 本身不使用控制块,同时 unique_ptr 置空)
  3. 当 share_ptr 构造函数使用裸指针作为实参时,会创建一个控制块(也就是说,从同一个裸指针出发构造多个 share_ptr 的时候会创建多个控制块,这也就意味着裸指针可能会被释放多次。如果想从一个已经拥有控制块的对象出发创建一个 share_ptr ,此时我们就可以传递一个 share_ptr 或 weak_ptr 而非裸指针作为构造函数的参数,这样就不会创建新的控制器)

因此,尽可能避免将裸指针传递给 share_ptr 的一个有效的办法是 make_shared。如果必须将一个裸指针作为参数传入到 share_ptr 的构造函数,就直接传递 new 运算符运算的结果而非传递一个裸指针。

2.3、尽量使用 make 函数

  1. make_shared 内部是通过调用 allocate_shared 来进行实现的,与 new 相比,make 系列函数的优势:
  2. 避免代码冗余,创建智能指针的时候,被创建的对象只需要写一次,如 make_shared,而用 new 来进行创建智能指针的时候需要写两次。
  3. 异常安全性,make 系列函数可编写异常安全代码,增强了安全性

使用 make 函数与使用 new 进行构造的 share_ptr 内存布局如下:

make 系列函数的局限性:

  1. 所有的 make 系列函数都不允许自定义删除器
  2. make 系列函数创建对象时,不能接受{}初始化列表(这是因为完美转发的转发函数是一个模板函数,利用模板类型进行推导。因此无法把{}推导为 initializer_list)也就是说,make 系列只能将圆括号内的参数进行转发
  3. 自定义内存管理的类(如重载了 operator new 和 operator delete)不建议使用 make_shared 来创建,因为重载 operator new 和 operator delete 时,往往用来分配和释放该类精确尺寸的内存块,而 make_shared 创建的 shared_ptr,是一个自定义了分配器 (allocate_shared) 和删除器的智能指针,由 allocate_shared 分配的内存大小也不等于上述的尺寸,而是在此基础上加上控制块的大小
  4. 对象的内存可能无法及时回收。因为:make_shared 只分配一次内存,减少了内存分配的开销,使得控制块和托管对象在同一内存块上分配。而控制块是由 shared_ptr 和 weak_ptr 共享的,因此两者共同管理着这个内存块(托管对象 + 控制块)。当强引用计数为 0 时,托管对象被析构(即析构函数被调用),但内存块并未被回收,只有等到最后一个 weak_ptr 离开作用域时,弱引用也减为 0 才会释放这块内存块。原本强引用减为 0 时就可以释放的内存, 现在变为了强引用和弱引用都减为 0 时才能释放, 意外的延迟了内存释放的时间。这对于内存要求高的场景来说,是一个需要注意的问题。

2.4、引用计数

  1. shared_ptr 中的引用计数直接关系到何时是否进行对象的析构,因此它的变动变得尤为重要
  2. shared_ptr 的构造函数会使该引用计数递增,而析构函数则会使得引用计数递减。但移动构造表示从一个已有的 shared_ptr 移动构造到另一个新的 shared_ptr ,这意味着一旦新的 shared_ptr 产生后,原有的 shared_ptr 就会被置空,结果就是引用计数没有变化。
  3. 拷贝赋值构造同时执行两种操作如(例如 p1 和 p2 是指向不同对象的 shared_ptr ,则执行         p1 = p1 时,将修改 p1 使得其指向 p2 所值的对象,而最初 p1 所指向的对象的引用计数递减,同时 p2 所指向的对象引用计数递增 )
  4. reset 函数,如果不带参数时,则引用计数减 1。如果带参数时,如 sp.reset(p) 则 sp 原来指向的对象引用计数减 1 ,同时 sp 指向新的对象 p。
  5. 如果实施一次递减后,最后的引用计数变为 0 ,即不再有 shared_ptr 指向该对象,则会被 shared_ptr 析构掉。

需要注意的是:引用计数本身是安全且无锁的,但对象的读写则不是。

2.5、this 返回 shared_ptr 

不要将 this 指针返回给 shared_ptr。当希望将 this 指针托管给 shared_ptr 时,类需要继承自          std::enable_shared_from_this ,并且从 shared_from_this() 中获得 shared_ptr 指针。

#include <iostream>
#include <memory>

class Frame {
  public:
    std::shared_ptr<Frame> GetThis() {
      return std::shared_ptr<Frame>(this);
    }
};

int main()
{
  std::shared_ptr<Frame> f1(new Frame());
  std::shared_ptr<Frame> f2 = f1->GetThis();
  std::cout << f1.use_count() << " " << f2.use_count() << std::endl;

  std::shared_ptr<Frame> f3(new Frame());
  std::shared_ptr<Frame> f4 = f3;
  std::cout << f3.use_count() << " " << f4.use_count() << std::endl;
  
  return 0;
}

运行结果:

此时就很奇怪,GetThis() 返回的不是 f1 本身嘛?而为什么 f2 和 f4 的结果就有这样的区别呢?

其实,直接从 this 指针创建,会为 this 对象创建新的控制块,也就相当于从裸指针重新创建一个新的控制块。

为了解决这个问题,标准库提供了解决方法:让类派生于一个模板类:enable_shared_from_this<T>,然后调用 shared_from_this 函数即可

通过调用 shared_from_this 成员函数获得一个和this指针指向相同对象的shared_ptr。

class Frame : public std::enable_shared_from_this<Frame> {
  public:
    std::shared_ptr<Frame> GetThis() {
      return shared_from_this();
    }
};

原理:

template<typename _Tp>
class enable_shared_from_this
{
  protected:
    constexpr enable_shared_from_this() noexcept { }
    enable_shared_from_this(const enable_shared_from_this&) noexcept { }
    enable_shared_from_this& operator=(const enable_shared_from_this&) noexcept { return *this; }
    ~enable_shared_from_this() { }

  public:
    shared_ptr<_Tp> shared_from_this()
    { return shared_ptr<_Tp>(this->_M_weak_this); }

    shared_ptr<const _Tp> shared_from_this() const
    { return shared_ptr<const _Tp>(this->_M_weak_this); }

  private:
    template<typename _Tp1>
    void _M_weak_assign(_Tp1* __p, const __shared_count<>& __n) const noexcept
    { _M_weak_this._M_assign(__p, __n); }

    template<typename _Tp1, typename _Tp2>
    friend void __enable_shared_from_this_helper(const __shared_count<>&,
            const enable_shared_from_this<_Tp1>*,
            const _Tp2*) noexcept;

    mutable weak_ptr<_Tp>  _M_weak_this;
};

可以看到 enable_shared_from_this 模板类提供两个 public 属性的 shared_from_this 成员函数。这两个成员函数内部会通过 _M_weak_this 成员来创建 shared_ptr 。其中 _M_weak_assign 函数不能手动调用,这个函数会被 shared_ptr 自动调用,用处是来初始化唯一的成员变量 _M_weak_this 。

分析:根据对象生成顺序,先初始化基类 enable_shared_from_this ,再初始化派生类 Frame 对象本身,这时 Frame 对象已经生成,但 _M_weak_this 成员还未被初始化,最后应通过 shared_ptr<T> sp(new T()) 等方式调用 shared_ptr 构造函数(内部会调用 _M_weak_assign 成员函数来初始化 _M_weak_this 成员)。而如果在调用 shared_from_this 函数之前 weak_this_ 成员未被初始化,则会通过 ASSERT 报错显示。

更深层次:这个 enable_shared_from_this 中有一个弱指针 weak_ptr ,这个弱指针能够监视 this 指针,在调用 shared_from_this 这个函数时,这个函数内部实际上是调用 weak_ptr 的 lock 方法,lock() 会让 shared_ptr 指针计数 +1,同时返回这个 shared_ptr。

参考:

第21课 shared_ptr共享型智能指针 - 浅墨浓香 - 博客园 (cnblogs.com)

【C++】shared_ptr共享型智能指针详解-CSDN博客

  • 26
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

芒果甜汤圆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值