shared_ptr的一切(本质、make_shared)

参考

(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.明显的好处

  1. 以前没有shared_ptr的时候需要,手动 delete,这样带来三个明显问题:

    1. 可能存在忘记delete,造成内存泄漏
    2. 可能存在重复delete,造成重复释放
  2. 在new 和 手动delete中间,可能存在异常抛出,这样也会是内存泄漏的原因

  3. 有个shared_ptr,就可以在对象当前作用域结束后,自动 释放 内部管理的空间(RAII技术)

  4. 当只有一个shared_ptr指向这片内存,在他析构的时候就会自动把这片内存释放,这是线程安全的释放!!!!,内部是原子的对引用计数的增加和减少,因而也影响了性能

3. 常用使用方式

  1. 头文件

    #include <memory>
    
  2. 常用使用方式

    #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出来的这种方式

  3. 使用的方式就和指针一样

    test->xx函数

    等同于Test的指针->xx函数

  4. 可以使用 *test,得到原始指针的引用值

  5. test.get()得到内部裸漏的指针,多用于兼容其他 c版本的函数

  6. 默认的可以直接 shared_ptr test创建对象

    shared_ptr<Test> test;
    test.reset(new Test());
    

    先把对象创建之后再把管理的对象 移入

  7. reset函数

    减少一个引用计数

  8. use_count函数

    查看一个对象的引用计数

4. 本质

  1. 当use_count 为1的时候的析构被调用 就会析构 _M_ptr否则use_count减一

  2. 那么__shared_count这个指针什么时候析构呢?

    当weak_count为1的时候的析构被调用 就会析构 _M_ptr

    否则weak_count–

  3. 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

  1. 构建一个shared_ptr需要两次不连续内存分配

    显示new 来 创建需要管理的内存,比如上面的new Test()

    构建 shared_ptr 然后把 需要管理的内存传进来,shared_ptr堆上动态创建use_count

    带来的就是两次 不连续的 内存创建

  2. 那么 make_shared呢只需要一次连续的分配,shared_ptr内部计数和指向的内存在连续的块

  3. 借用 http://bitdewy.github.io/blog/2014/01/12/why-make-shared/的图

  4. 那么 使用make_shared的好处有哪些

    1. 效率更好,因为只需要一次内存分配,并且不需要用户显示new
    2. 异常安全
    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发生异常,内存泄漏就出来了

    解决办法:

    1. 独立的语句

      shared_ptr a = shared_ptr(new Test());

      f(a, getMemory());

    2. make_shared

      f(make_shared(), getMemory());

  5. make_shared的坏处

    1. 需要保证构造函数c++ - How do I call ::std::make_shared on a class with only protected or private constructors? - Stack Overflow

    2. 可能造成部分内存锁定

      因为上面的分析我们知道 weak_count管理哪些引用的个数,当这个为0释放那个count结构

      但是make_shared 将管理的对象内存和count结构的内存绑定在一起,尽管use_count计数为0释放了空间

      由于count结构可能存在,只有当 weak_count释放count结构的时候,这整个的由make_shared 释放的空间才能归还

  6. 题外

    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,但是这其实是不准确的

​ 看图:

绘图1

  1. Control Block是从堆内存创建的空间,所以自定义析构器不会影响shared_ptr本身的大小
  2. Control Block里面为啥还有指针指向 T Object请看 问题4 为啥__shared_count里面还有指向ptr的指针
  3. 该Control Block在裸指针创建shared_ptr或者unique_ptr移动构造或weak_ptr构造才会被创建,这也暗示我们用相同的裸指针初始化两个shared_ptr就会创建两个Control Block导致重复释放

8. 合理使用enable_shared_from_this

​ enable_shared_from_this,这个是一个类用于在函数内部将this指针封装为shared_ptr返回

  1. 为啥需要这个我直接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啊,最终导致重复释放了啊啊啊

    1. 怎么使用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 派生类和基本赋值问题

  1. 默认的指针隐式转换 到了 模板中该怎么办?

    比如 子类指针可以直接赋值给 父类指针

    子类对象可以直接赋值给 父类对象

    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;
            
    };
    
    变成上面这样就解决了
    
    1. 不加 explicit的原因是 原生的就支持 隐式转换

    2. 并且 m_ptr(user.m_ptr),编译器会判断这两个内部指针能否进行赋值,这也就是实现了 相关的类赋值以及 子类赋值给父类的强制要求

      只有在 U指针可以隐式转换为 T指针才能被编译

    3. 另一个成员函数模板的用处: 赋值操作

    比如标准 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进行隐式拷贝构造  
    };
    
    1. 只有泛化构造是 explicit的,所以 只有这个是隐式转换
    2. 对于 auto_ptr 本来就改动了,不需要 const
  2. 当泛化构造和 默认的拷贝构造 ?

    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内部的指针值也可能不同

posted @ 2021-05-23 20:44  make_wheels  阅读(3372)  评论(0编辑  收藏  举报