【C++】share_ptr详解
芒果甜汤圆 2024-08-02 15:35:01 阅读 75
一、share_ptr 的简单使用
1.1、基本用法
从较浅的层面看,智能指针是利用了一种叫做RAII(资源获取即初始化)的技术对普通的指针进行封装,这使得智能指针实质是一个对象,行为表现的却像一个指针。
智能指针的作用是防止忘记调用delete释放内存和程序异常的进入catch块忘记释放内存。另外指针的释放时机也是非常有考究的,多次释放同一个指针会造成程序崩溃,这些都可以通过智能指针来解决。
智能指针的行为类似于一个常规指针,与常规指针之间重要的区别就是它负责自动释放所管理的资源,share_ptr 使用引用计数,允许多个 share_ptr 指向同一资源,每多一个 share_ptr 指向该资源,share_ptr 的引用计数就 +1 ,减为0时表示没有 share_ptr 对该资源进行引用了,就会释放所指向的资源。share_ptr 内部中的引用计数是线程安全的,但是引用的资源不是线程安全的。
1.2、初始化
裸指针直接初始化,但不能通过隐式转换来构造,因为 share_ptr 构造函数被声明为 explicit允许移动构造与拷贝构造通过 make_share 构造
#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、控制块的创建规则
std::make_shared 总是创建一个控制块从具备所有权的指针触发构造一个 share_ptr 的时候,会创建一个控制块(比如 unique_ptr 在转换成 share_ptr 的时候会创建控制块,因为 unique_ptr 本身不使用控制块,同时 unique_ptr 置空)当 share_ptr 构造函数使用裸指针作为实参时,会创建一个控制块(也就是说,从同一个裸指针出发构造多个 share_ptr 的时候会创建多个控制块,这也就意味着裸指针可能会被释放多次。如果想从一个已经拥有控制块的对象出发创建一个 share_ptr ,此时我们就可以传递一个 share_ptr 或 weak_ptr 而非裸指针作为构造函数的参数,这样就不会创建新的控制器)
因此,尽可能避免将裸指针传递给 share_ptr 的一个有效的办法是 make_shared。如果必须将一个裸指针作为参数传入到 share_ptr 的构造函数,就直接传递 new 运算符运算的结果而非传递一个裸指针。
2.3、尽量使用 make 函数
make_shared 内部是通过调用 allocate_shared 来进行实现的,与 new 相比,make 系列函数的优势:避免代码冗余,创建智能指针的时候,被创建的对象只需要写一次,如 make_shared,而用 new 来进行创建智能指针的时候需要写两次。异常安全性,make 系列函数可编写异常安全代码,增强了安全性
使用 make 函数与使用 new 进行构造的 share_ptr 内存布局如下:
make 系列函数的局限性:
所有的 make 系列函数都不允许自定义删除器make 系列函数创建对象时,不能接受{}初始化列表(这是因为完美转发的转发函数是一个模板函数,利用模板类型进行推导。因此无法把{}推导为 initializer_list)也就是说,make 系列只能将圆括号内的参数进行转发自定义内存管理的类(如重载了 operator new 和 operator delete)不建议使用 make_shared 来创建,因为重载 operator new 和 operator delete 时,往往用来分配和释放该类精确尺寸的内存块,而 make_shared 创建的 shared_ptr,是一个自定义了分配器 (allocate_shared) 和删除器的智能指针,由 allocate_shared 分配的内存大小也不等于上述的尺寸,而是在此基础上加上控制块的大小对象的内存可能无法及时回收。因为:make_shared 只分配一次内存,减少了内存分配的开销,使得控制块和托管对象在同一内存块上分配。而控制块是由 shared_ptr 和 weak_ptr 共享的,因此两者共同管理着这个内存块(托管对象 + 控制块)。当强引用计数为 0 时,托管对象被析构(即析构函数被调用),但内存块并未被回收,只有等到最后一个 weak_ptr 离开作用域时,弱引用也减为 0 才会释放这块内存块。原本强引用减为 0 时就可以释放的内存, 现在变为了强引用和弱引用都减为 0 时才能释放, 意外的延迟了内存释放的时间。这对于内存要求高的场景来说,是一个需要注意的问题。
2.4、引用计数
shared_ptr 中的引用计数直接关系到何时是否进行对象的析构,因此它的变动变得尤为重要shared_ptr 的构造函数会使该引用计数递增,而析构函数则会使得引用计数递减。但移动构造表示从一个已有的 shared_ptr 移动构造到另一个新的 shared_ptr ,这意味着一旦新的 shared_ptr 产生后,原有的 shared_ptr 就会被置空,结果就是引用计数没有变化。拷贝赋值构造同时执行两种操作如(例如 p1 和 p2 是指向不同对象的 shared_ptr ,则执行 p1 = p1 时,将修改 p1 使得其指向 p2 所值的对象,而最初 p1 所指向的对象的引用计数递减,同时 p2 所指向的对象引用计数递增 )reset 函数,如果不带参数时,则引用计数减 1。如果带参数时,如 sp.reset(p) 则 sp 原来指向的对象引用计数减 1 ,同时 sp 指向新的对象 p。如果实施一次递减后,最后的引用计数变为 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 <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。
<code>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博客
上一篇: 在cmd/powershell中使用java/javac -cp/--class-path命令链接多个jar包
下一篇: 还不知道怎么卸载Python?一篇文章讲清楚如何将Python彻底卸载干净!
本文标签
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。