参考
(2条消息) 为什么多线程读写 shared_ptr 要加锁?_陈硕的Blog-CSDN博客
(2条消息) C++11使用make_shared的优势和劣势_yagerfgcs的博客-CSDN博客_makeshared
(2条消息) C++11新特性之十:enable_shared_from_this_草上爬的博客-CSDN博客
第22课 weak_ptr弱引用智能指针 - 浅墨浓香 - 博客园 (cnblogs.com)
(2条消息) C++:智能指针(5)——enable_shared_from_this工作原理、源码分析_cocoa0409的博客-CSDN博客
(2条消息) C++11新特性之十:enable_shared_from_this_草上爬的博客-CSDN博客
1.作用
作为可以共享的智能指针来管理堆内存,当最后一个智能指针进行析构的时候,内部引用计数归零,也就是没有智能指向指向这片堆内存,进行内存释放
最大的好处是在对象不被需要的时候进行释放
2.明显的好处
-
以前没有shared_ptr的时候需要,手动 delete,这样带来三个明显问题:
- 可能存在忘记delete,造成内存泄漏
- 可能存在重复delete,造成重复释放
-
在new 和 手动delete中间,可能存在异常抛出,这样也会是内存泄漏的原因
-
有个shared_ptr,就可以在对象当前作用域结束后,自动 释放 内部管理的空间(RAII技术)
-
当只有一个shared_ptr指向这片内存,在他析构的时候就会自动把这片内存释放,这是线程安全的释放!!!!,内部是原子的对引用计数的增加和减少,因而也影响了性能
3. 常用使用方式
-
头文件
#include <memory>
-
常用使用方式
#include <iostream> #include <memory> using namespace std; class Test{ public: Test() { std::cout << "Test()" << std::endl; } ~Test() { std::cout << "~Test()" << std::endl; } }; int main() { shared_ptr<Test> test(new Test()); shared_ptr<Test> test(make_shared<Test>());//推荐这种 return 0; }
输出:
Test() ~Test()
在main的这个{}作用域内结束后,自动释放Test空间
必须用explicit构建,因为内部构造函数使用explicit的方式
都是使用智能指针类值的方式管理 其他内存,一般不会使用智能指针还是 new出来的这种方式
-
使用的方式就和指针一样
test->xx函数
等同于Test的指针->xx函数
-
可以使用 *test,得到原始指针的引用值
-
test.get()得到内部裸漏的指针,多用于兼容其他 c版本的函数
-
默认的可以直接 shared_ptr
test创建对象 shared_ptr<Test> test; test.reset(new Test());
先把对象创建之后再把管理的对象 移入
-
reset函数
减少一个引用计数
-
use_count函数
查看一个对象的引用计数
4. 本质
-
当use_count 为1的时候的析构被调用 就会析构 _M_ptr否则use_count减一
-
那么__shared_count这个指针什么时候析构呢?
当weak_count为1的时候的析构被调用 就会析构 _M_ptr
否则weak_count–
-
shared_ptr中自定义析构器不会影响大小,这和unique_ptr不同,这里面的图会发现,一个shared_ptr包含好多东西哇
动态分布的Control Block
任意大的删除器和分配器
虚函数Function
原子的use_count和weak_count
5. 注意点
1. shared_ptr可以由unique_ptr创建,但是绝不可以unique_ptr由shared_ptr创建,因为shared_ptr内部的use_count即使为1也不会因为赋值给unique_ptr改变的
2. shared_ptr仅仅只针对单一的对象,他和unique_ptr不同,没有shared_ptr<T[]>, 也不应该有,因为shared_ptr允许子类赋值父类,参见 **问题3 :shared_ptr 派生类和基本赋值问题**,当出现数组那么就非常不正确了;因而因为这一点在 unique_ptr<T[]>中禁用了这种赋值
5. shared_ptr Vs make_shared
-
构建一个shared_ptr需要两次不连续内存分配
显示new 来 创建需要管理的内存,比如上面的new Test()
构建 shared_ptr 然后把 需要管理的内存传进来,shared_ptr堆上动态创建use_count
带来的就是两次 不连续的 内存创建
-
那么 make_shared呢只需要一次连续的分配,shared_ptr内部计数和指向的内存在连续的块
-
借用 http://bitdewy.github.io/blog/2014/01/12/why-make-shared/的图
-
那么 使用make_shared的好处有哪些
- 效率更好,因为只需要一次内存分配,并且不需要用户显示new
- 异常安全
f(shared_ptr<Test>(new Test()), getMemory());
我们以为的顺序:
1. new Test 2. std::shared_ptr 3. getMemory()
不同的语言对于函数参数的顺序是不确定的, 但是 new Test肯定在 std::shared_ptr之前
那么
1. new Test 2. getMemory 3. std::shared_ptr
当getMemory发生异常,内存泄漏就出来了
解决办法:
-
独立的语句
shared_ptr
a = shared_ptr (new Test()); f(a, getMemory());
-
make_shared
f(make_shared
(), getMemory());
-
make_shared的坏处
-
可能造成部分内存锁定
因为上面的分析我们知道 weak_count管理哪些引用的个数,当这个为0释放那个count结构
但是make_shared 将管理的对象内存和count结构的内存绑定在一起,尽管use_count计数为0释放了空间
由于count结构可能存在,只有当 weak_count释放count结构的时候,这整个的由make_shared 释放的空间才能归还
-
题外
Foo 的构造函数参数要传给 make_shared(),后者再传给 Foo::Foo(),这只有在 C++11 里通过 perfect forwarding 才能完美解决
6. 合适的时机使用移动构造shared_ptr
我们需要知道当我们每次创建同一shared_ptr的拷贝,带来的是引用计数的增加并且还是原子的,那么使用移动构造后,直接把old的内部全都转移到新的shared_ptr里面,这样就不需要原来的那些开销
7. 有趣的事情 : shared_ptr中自定义析构器不会影响大小,这和unique_ptr不同
其实这个话题需要知道shared_ptr内部的结构,我们平常总说 shared_ptr内部有一个裸指针 + 引用计数count,但是这其实是不准确的
看图:
- Control Block是从堆内存创建的空间,所以自定义析构器不会影响shared_ptr本身的大小
- Control Block里面为啥还有指针指向 T Object请看 问题4 为啥__shared_count里面还有指向ptr的指针
- 该Control Block在裸指针创建shared_ptr或者unique_ptr移动构造或weak_ptr构造才会被创建,这也暗示我们用相同的裸指针初始化两个shared_ptr就会创建两个Control Block导致重复释放
8. 合理使用enable_shared_from_this
enable_shared_from_this,这个是一个类用于在函数内部将this指针封装为shared_ptr返回
-
为啥需要这个我直接this返回不行么
#include <iostream> // std::streambuf, std::cout #include <memory> // std::ofstream #include <functional> // std::ofstream using namespace std; class Test { public: shared_ptr<Test> get() { return shared_ptr<Test>(this); } ~Test() { std::cout << "~Test()" << std::endl; } }; int main () { shared_ptr<Test> test(make_shared<Test>()); shared_ptr<Test> test1 = test->get(); return 0; }
我想你猜到了,注意我们之前说的那个Control Block, 这里分别使用this裸指针和make_shared来创建shared_ptr,结果就是创建了两个不同的Control Block啊,最终导致重复释放了啊啊啊
- 怎么使用enable_shared_from_this
#include <iostream> // std::streambuf, std::cout #include <memory> // std::ofstream #include <functional> // std::ofstream using namespace std; class Test : public enable_shared_from_this<Test>{ public: shared_ptr<Test> get() { return shared_from_this(); } ~Test() { std::cout << "~Test()" << std::endl; } }; int main () { shared_ptr<Test> test(make_shared<Test>()); shared_ptr<Test> test1 = test->get(); return 0; }
需要注意的是,必须Test是由shared_ptr管理的之后才能调用shared_from_this,其实你可能猜到enable_shared_from_this的实现就是依赖于shared_ptr的,shared_from_this不能在构造函数中调用,因为对象还没有构建(shared_ptr还没赋值呢)
问题1:当多个线程执行shared_ptr析构是否出现重复释放?
shared_ptr对管理的内存是线程安全的
因为源码中
_Atomic_word _M_use_count; // #shared
_Atomic_word _M_weak_count;
析构的时候
if (__gnu_cxx::__exchange_and_add_dispatch(&_M_use_count, -1) == 1)
{
类型都是原子类型,对他们的操作都是采用原子操作来实现了
即使多个线程来减少,他仅仅判断_M_use_count为1的时候进行 -1才会释放内存
问题2: 多线程读写shared_ptr需要加锁吗
多线程引用计数是安全的,但是对象的读写不是
**如果要从多个线程读写同一个 shared_ptr 对象,那么需要加锁**
参见[(2条消息) 为什么多线程读写 shared_ptr 要加锁?_陈硕的Blog-CSDN博客](https://blog.csdn.net/Solstice/article/details/8547547)
本质就是 在 内部的指针和 count计数对象赋值这两个步骤整体不是原子的问题
问题3 :shared_ptr 派生类和基本赋值问题
-
默认的指针隐式转换 到了 模板中该怎么办?
比如 子类指针可以直接赋值给 父类指针
子类对象可以直接赋值给 父类对象
class Top { //..... }; class Middle : public Top { //... }; class Bottom : public Top { //... }; int main(int argc, char**argv) { Top* middle = new Middle(); //子类赋值给父类 Top* bottom = new Bottom(); //子类赋值给父类 const Top* top = middle; //父类隐式给const父类 return 0; }
上面肯定没问题,那么当和智能指针碰撞呢,我们希望有这样的实现
Shared_ptr<Top> pt1 = Shared_ptr<Middle>(new Middle()); Shared_ptr<Top> pt2 = Shared_ptr<Middle>(new Bottom());
但是!!!,如果不自己处理,默认是不可能的的!!,因为 Shared_ptr
Shared_ptr (new Middle()) 没有任何关系 解决办法:
编写转换构造函数,这个转换构造函数的参数肯定不能是某一个固定类型的参数,因为 Top会有很多子类,一个一个写累死了
template<typename T> class SmartPtr { public: template<typename U> SmartPtr(const SmartPtr<U>& user) : m_ptr(user.m_ptr) { } T* get() const { return m_ptr; } private: T* m_ptr; };
变成上面这样就解决了
-
不加 explicit的原因是 原生的就支持 隐式转换
-
并且 m_ptr(user.m_ptr),编译器会判断这两个内部指针能否进行赋值,这也就是实现了 相关的类赋值以及 子类赋值给父类的强制要求
只有在 U指针可以隐式转换为 T指针才能被编译
-
另一个成员函数模板的用处: 赋值操作
比如标准 shared_ptr之类的
template<class T> class shared_ptr { public: template<class Y> explicit shared_ptr(Y *p); //构造,可以进行任何和T兼容的指针的赋值 template<class Y> shared_ptr(shared_ptr<Y>const &p); //对shared_ptr进行隐式拷贝构造 template<class Y> explicit shared_ptr(weak_ptr<Y>const &p); //对weak_ptr进行隐式拷贝构造 template<class Y> explicit shared_ptr(auto_ptr<Y>const &p); //对auto_ptr进行隐式拷贝构造 template<class Y> shared_ptr& operator=(shared_ptr<Y>const &p); //对shared_ptr进行赋值拷贝 template<class Y> shared_ptr& operator=(auto_ptr<Y>const &p); //对auto_ptr进行隐式拷贝构造 };
- 只有泛化构造是 explicit的,所以 只有这个是隐式转换
- 对于 auto_ptr 本来就改动了,不需要 const
-
-
当泛化构造和 默认的拷贝构造 ?
member template并不改变语言规则--》如果你没有主动实现拷贝构造,编译器默认给你生成
这二者不冲突,如果你想要控制copy构造的每一个细节,必须同时声明实现泛化和拷贝构造
问题4 为啥__shared_count里面还有指向ptr的指针
基于问题3,为了精准调用对应的析构函数,即使没有虚析构的存在下
上面例子中
Shared_ptr<Top> pt1 = Shared_ptr<Middle>(new Middle());
pt1在析构的时候调用的是 delete Middle, 而不是 Top
看源码:
template<typename _Tp1>
shared_ptr(const shared_ptr<_Tp1>& __r, _Tp* __p) noexcept
: __shared_ptr<_Tp>(__r, __p) { }
然后 __shared_ptr:
template<typename _Tp1, typename = typename
std::enable_if<std::is_convertible<_Tp1*, _Tp*>::value>::type>
__shared_ptr(const __shared_ptr<_Tp1, _Lp>& __r) noexcept
: _M_ptr(__r._M_ptr), _M_refcount(__r._M_refcount)
{ }
_Tp1是参数类型推导,类的类型是 _Tp
调用->的使用使用的类型是 _ Tp(top)类型,在 __shared_ptr参数转换构造的时候,_M_ptr参数内部包含的是 _Tp1(Middle)类型
这样之后不管你 shared_ptr指向什么类型,即使是void, 那么析构的时候 Middle仍然被正常析构
当出现虚析构的时候 shared_ptr内部的指针和 __shared_count内部的指针值也可能不同