首页 > 其他 > 详细

shared_ptr的一切(本质、make_shared、weak_ptr)

时间:2021-05-23 22:49:30      阅读:34      评论:0      收藏:0      [点我收藏+]

参考

(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.作用

? 用来自动的管理 指针的一个类

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

    1. 可能存在忘记delete,造成内存泄漏

    2. 可能存在重复delete,造成重复释放

    3. 在new 和 手动delete中间,可能存在异常抛出,这样也会是内存泄漏的原因

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

    当只有一个shared_ptr指向这片内存,在他析构的时候就会自动把这片内存释放

2. 常用使用方式

  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());
        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函数

    将当前的指针和计数值清0

3. 本质

? 如何管理的内存呢?看一下shared_ptr内部

? 技术分享图片

? 1. 当use_count 为1的时候的析构被调用 就会析构 _M_ptr

? 否则use_count减一

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

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

    否则weak_count–

4. weak_count是干嘛的 --> 循环引用

#include <iostream>
#include <memory>
using namespace std;

class Test{
public:
    Test() {
        std::cout << "Test()" << std::endl;
    }
    ~Test() {
        std::cout << "~Test()" << std::endl;
    }
};
class Children;
class Father{

public:
    ~Father() {
        std::cout << "~Father()" << std::endl;
    }
    shared_ptr<Children> m_ptr;
};

class Children{
public:
    ~Children() {
        std::cout << "~Children()" << std::endl;
    }
    shared_ptr<Father> m_ptr;
};

int main()
{
    shared_ptr<Father> A(new Father());
    shared_ptr<Children> B(new Children());

    A->m_ptr = B;
    B->m_ptr = A;

    return 0;
}

互相的m_ptr保存了对方的值,那么结果就是析构函数不会被调用,造成内存泄漏

解决办法 weak_ptr,主要作用就是 可以使用lock产生shared_ptr,但是不对内存空间进行管理(shared_ptr指针的作用是管理),weak_ptr仅仅是使用

shared_ptr强引用:管理内存

weak_ptr弱引用:仅仅计数

#include <iostream>
#include <memory>
using namespace std;

class Test{
public:
    Test() {
        std::cout << "Test()" << std::endl;
    }
    ~Test() {
        std::cout << "~Test()" << std::endl;
    }
};
class Children;
class Father{

public:
    ~Father() {
        std::cout << "~Father()" << std::endl;
    }
    shared_ptr<Children> m_ptr;
};

class Children{
public:
    ~Children() {
        std::cout << "~Children()" << std::endl;
    }
    weak_ptr<Father> m_ptr;
};

int main()
{
    shared_ptr<Father> A(new Father());
    shared_ptr<Children> B(new Children());

    A->m_ptr = B;
    B->m_ptr = A;

    return 0;
}

5. weak_ptr的内部

? 其实他和shared_ptr差别不大

? 主要有两点:

  1. 通过shared_ptr来给weak_ptr构造

  2. 析构时的不一样

          void
          _M_weak_release() noexcept
          {
            // Be race-detector-friendly. For more info see bits/c++config.
            _GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_weak_count);
    	if (__gnu_cxx::__exchange_and_add_dispatch(&_M_weak_count, -1) == 1)
    	  {
                _GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_weak_count);
    	    if (_Mutex_base<_Lp>::_S_need_barriers)
    	      {
    	        // See _M_release(),
    	        // destroy() must observe results of dispose()
    	        _GLIBCXX_READ_MEM_BARRIER;
    	        _GLIBCXX_WRITE_MEM_BARRIER;
    	      }
    	    _M_destroy();
    	  }
          }
    

也就是说 weak_ptr会增加weak_count的值

本质里面说过,__shared_count这个指针会在weak_count为1析构时候释放,那么一旦 使用中出现了 weak_ptr,weak_ptr的存在就影响并管理了这个指针

weak_ptr的出现更像是辅助shared_ptr

wpCStateMachine

wpVR_ActionHandle

wpVR_RequestHandle

static std::weak_ptr<VR_SDSIF> s_SDSIF;

void SetStateManager(std::weak_ptr<VR_SDSStateManager> wpStateManager) {m_wpStateManager = wpStateManager;}

std::weak_ptr<VR_SDSStateManager> m_wpStateManager;

typedef std::weak_ptr wpCStateMachine;

typedef std::weak_ptr wpCStateMachine;

6. 工程上weak_ptr场景

? 我们知道 weak_ptr的作用,但是什么时候去使用呢?

? weak_ptr去使用管理的内存的使用应该lock产生shared_ptr

  1. 就是解决循环引用

  2. 观察者模式

       1. 观察者模式是在subject状态发生改变时,通知观察者的一种设计模式。
    
       2. 在多数实现中,每个subject持有指向观察者的指针,这使得当subject状态改变时可以很容易通知观察者。
    
       3. subject不会控制其观察者的生存期,因此应该是持有观察者的weak_ptr指针。同时在subject的使用某个指针时,可以先确定是否空悬。
    
    1. 缓存对象
    	1. 考虑一个工厂函数loadWidget,该函数基于唯一ID来创建一些指向只读对象的智能指针。
    
      2. 假设该只读对象需要被频繁使用,而且经常需要从文件或数据库中加载。那么可以考虑将对象缓存起来。同时为了避免过量缓存,当不再使用时,则将该对象删除。
    
      3. 由于带缓存,工厂函数返回unique_ptr类型显然不合适。因为调用者和缓存管理器均需要一个指向这些对象的指针。
    
      4. 当用户用完工厂函数返回的对象后,该对象会被析构,此时相应的缓存条目将会空悬。因为可以考虑将工厂函数的返回值设定为shared_ptr类型,而缓存类型为weak_ptr类型。
    
    1. enable_shared_from_this

      参见(2条消息) C++11新特性之十:enable_shared_from_this_草上爬的博客-CSDN博客

      用继承enable_shared_from_this的方式来将this指针传递给其他地方

      注意this指针一定要先被shared_ptr管理,才能gp1->getptr()调用,否则就是空值

4. 推荐使用 make_shared

  1. 构建一个shared_ptr需要两次不连续内存分配
    1. 显示new 来 创建需要管理的内存,比如上面的new Test()
    2. 构建 shared_ptr 然后把 需要管理的内存传进来,shared_ptr创建__shared_count
  2. 那么 make_shared呢只需要一次连续的分配,shared_ptr内部计数和指向的内存在连续的块上

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

技术分享图片

技术分享图片

  1. 那么 使用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());

  2. 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 释放的空间才能归还

  3. 题外

    Foo 的构造函数参数要传给 make_shared(),后者再传给 Foo::Foo(),这只有在 C++11 里通过 perfect forwarding 才能完美解决

问题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博客

? 本质就是 在 内部的指针和 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内部的指针值也可能不同

shared_ptr的一切(本质、make_shared、weak_ptr)

原文:https://www.cnblogs.com/zero-waring/p/14802112.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!