c/c++开发分享C++智能指针之shared_ptr的具体使用

std::shared_ptr概念unique_ptr因为其局限性(独享所有权),一般很少用于多线程操作。在多线程操作的时候,既可以共享资源,又可以自动释放资源,这就引入了shared_ptr。sha

std::shared_ptr概念

unique_ptr因为其局限性(独享所有权),一般很少用于多线程操作。在多线程操作的时候,既可以共享资源,又可以自动释放资源,这就引入了shared_ptr。

shared_ptr为了支持跨线程访问,其内部有一个引用计数(线程安全),用来记录当前使用该资源的shared_ptr个数,在结束使用的时候,引用计数为-1,当引用计数为0时,会自动释放其关联的资源。

特点 相对于unique_ptr的独享所有权,shared_ptr可以共享所有权。其内部有一个引用计数,用来记录共享该资源的shared_ptr个数,当共享数为0的时候,会自动释放其关联的资源。

对比unique_ptr,shared_ptr不支持数组,所以,如果用shared_ptr指向一个数组的话,需要自己手动实现deleter,如下所示:

std::shared_ptr<int> p(new int[8], [](int *ptr){delete []ptr;});  

shared_ptr模板类

template<class t> class shared_ptr {    public:      using element_type = remove_extent_t<t>;      using weak_type    = weak_ptr<t>;         // 构造函数      constexpr shared_ptr() noexcept;      constexpr shared_ptr(nullptr_t) noexcept : shared_ptr() { }      template<class y> explicit shared_ptr(y* p);      template<class y, class d> shared_ptr(y* p, d d);      template<class y, class d, class a> shared_ptr(y* p, d d, a a);      template<class d> shared_ptr(nullptr_t p, d d);      template<class d, class a> shared_ptr(nullptr_t p, d d, a a);      template<class y>      shared_ptr(const shared_ptr<y>& r, element_type* p) noexcept;      template<class y>      shared_ptr(shared_ptr<y>&& r, element_type* p) noexcept;      shared_ptr(const shared_ptr& r) noexcept;      template<class y> shared_ptr(const shared_ptr<y>& r) noexcept;      shared_ptr(shared_ptr&& r) noexcept;      template<class y> shared_ptr(shared_ptr<y>&& r) noexcept;      template<class y> explicit shared_ptr(const weak_ptr<y>& r);      template<class y, class d> shared_ptr(unique_ptr<y, d>&& r);         // 析构函数      ~shared_ptr();         // 赋值      shared_ptr& operator=(const shared_ptr& r) noexcept;      template<class y>      shared_ptr& operator=(const shared_ptr<y>& r) noexcept;      shared_ptr& operator=(shared_ptr&& r) noexcept;      template<class y>      shared_ptr& operator=(shared_ptr<y>&& r) noexcept;      template<class y, class d>      shared_ptr& operator=(unique_ptr<y, d>&& r);         // 修改函数      void swap(shared_ptr& r) noexcept;      void reset() noexcept;      template<class y> void reset(y* p);      template<class y, class d> void reset(y* p, d d);      template<class y, class d, class a> void reset(y* p, d d, a a);         // 探察函数      element_type* get() const noexcept;      t& operator*() const noexcept;      t* operator->() const noexcept;      element_type& operator[](ptrdiff_t i) const;      long use_count() const noexcept;      explicit operator bool() const noexcept;      template<class u>      bool owner_before(const shared_ptr<u>& b) const noexcept;      template<class u>      bool owner_before(const weak_ptr<u>& b) const noexcept;    };  

shared_ptr多个指针指向相同的对象。shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。

  • 初始化。智能指针是个模板类,可以指定类型,传入指针通过构造函数初始化。也可以使用make_shared函数初始化。不能将指针直接赋值给一个智能指针,一个是类,一个是指针。例如std::shared_ptr<int> p4 = new int(1);的写法是错误的,是不能隐式转换。
  • 拷贝和赋值。拷贝使得对象的引用计数增加1,赋值使得原对象引用计数减1,当计数为0时,自动释放内存。后来指向的对象引用计数加1,指向后来的对象。
  • get函数获取原始指针。
  • 注意不要用一个原始指针初始化多个shared_ptr,否则会造成二次释放同一内存
  • 注意避免循环引用,shared_ptr的一个最大的陷阱是循环引用,循环,循环引用会导致堆内存无法正确释放,导致内存泄漏。循环引用我们在后面的weak_ptr中介绍。

所有智能指针类都有一个explicit构造函数,该构造函数将指针作为参数。因此不需要自动将指针转换为智能指针对象:

std::shared_ptr<int> pi;  int* p_reg = new int;  //pi = p_reg;  // not allowed(implicit conversion)  pi = std::shared_ptr<int>(p_reg);  // allowed(explicit conversion)  //std::shared_ptr<int> pshared = p_reg;  // not allowed(implicit conversion)  //std::shared_ptr<int> pshared(g_reg);  // allowed(explicit conversion)  

下面我们看一个简单的例子:

#include &lt;iostream&gt;  #include &lt;memory&gt;  using namespace std;    int main()  {      std::shared_ptr&lt;int&gt; sp = std::make_shared&lt;int&gt;(10);      cout &lt;&lt; sp.use_count() &lt;&lt; endl;//1      std::shared_ptr&lt;int&gt; sp1(sp);//再次被引用则计数+1      cout &lt;&lt; sp1.use_count() &lt;&lt; endl;//2  }  

从上面可以看到,多次被引用则会增加计数,我们可以通过使用use_count方法打印具体的计数。

shared_ptr的构造和析构

#include <iostream>  #include <memory>    struct c {int* data;};    int main () {   auto deleter = [](int* ptr){      std::cout << "custom deleter calledn";      delete ptr;    };//labmbda表达式      //默认构造,没有获取任何指针的所有权,引用计数为0    std::shared_ptr<int> sp1;    std::shared_ptr<int> sp2 (nullptr);//同1    //拥有指向int的指针所有权,引用计数为1    std::shared_ptr<int> sp3 (new int);    //同3,但是拥有自己的析构方法,如果指针所指向对象为复杂结构c    //结构c里有指针,默认析构函数不会将结构c里的指针data所指向的内存释放,    //这时需要自己使用自己的析构函数(删除器)    std::shared_ptr<int> sp4 (new int, deleter);    //同4,但拥有自己的分配器(构造函数),    //如成员中有指针,可以为指针分配内存,原理跟浅拷贝和深拷贝类似                             std::shared_ptr<int> sp5 (new int, [](int* p){delete p;}, std::allocator<int>());    //如果p5引用计数不为0,则引用计数加1,否则同样为0, p6为0    std::shared_ptr<int> sp6 (sp5);    //p6的所有权全部移交给p7,p6引用计数变为为0    std::shared_ptr<int> sp7 (std::move(sp6));    //p8获取所有权,引用计数设置为1    std::shared_ptr<int> sp8 (std::unique_ptr<int>(new int));    std::shared_ptr<c> obj (new c);    //同6一样,只不过拥有自己的删除器与4一样    std::shared_ptr<int> sp9 (obj, obj->data);      std::cout << "use_count:n";    std::cout << "p1: " << sp1.use_count() << 'n'; //0    std::cout << "p2: " << sp2.use_count() << 'n'; //0    std::cout << "p3: " << sp3.use_count() << 'n'; //1    std::cout << "p4: " << sp4.use_count() << 'n'; //1    std::cout << "p5: " << sp5.use_count() << 'n'; //2    std::cout << "p6: " << sp6.use_count() << 'n'; //0    std::cout << "p7: " << sp7.use_count() << 'n'; //2    std::cout << "p8: " << sp8.use_count() << 'n'; //1    std::cout << "p9: " << sp9.use_count() << 'n'; //2    return 0;  }  

shared_ptr赋值

给shared_ptr赋值有三种方式,如下

#include <iostream>  #include <memory>    int main () {    std::shared_ptr<int> foo;    std::shared_ptr<int> bar (new int(10));    //右边是左值,拷贝赋值,引用计数加1    foo = bar;     //右边是右值,所以是移动赋值    bar = std::make_shared<int> (20);     //unique_ptr 不共享它的指针。它无法复制到其他 unique_ptr,    //无法通过值传递到函数,也无法用于需要副本的任何标准模板库 (stl) 算法。只能移动unique_ptr    std::unique_ptr<int> unique (new int(30));    // move from unique_ptr,引用计数转移    foo = std::move(unique);       std::cout << "*foo: " << *foo << 'n';    std::cout << "*bar: " << *bar << 'n';      return 0;  }  

make_shared

看下面make_shared的用法:

#include <iostream>  #include <memory>    int main () {      std::shared_ptr<int> foo = std::make_shared<int> (10);    // same as:    std::shared_ptr<int> foo2 (new int(10));    //创建内存,并返回共享指针,只创建一次内存    auto bar = std::make_shared<int> (20);      auto baz = std::make_shared<std::pair<int,int>> (30,40);      std::cout << "*foo: " << *foo << 'n';    std::cout << "*bar: " << *bar << 'n';    std::cout << "*baz: " << baz->first << ' ' << baz->second << 'n';      return 0;  }  

效率提升 std::make_shared(比起直接使用new)的一个特性是能提升效率。使用std::make_shared允许编译器产生更小,更快的代码,产生的代码使用更简洁的数据结构。考虑下面直接使用new的代码:

std::shared_ptr<test> sp(new test);  

很明显这段代码需要分配内存,但是它实际上要分配两次。每个std::shared_ptr都指向一个控制块,控制块包含被指向对象的引用计数以及其他东西。这个控制块的内存是在std::shared_ptr的构造函数中分配的。因此直接使用new,需要一块内存分配给widget,还要一块内存分配给控制块。如果使用std::make_shared来替换:

auto sp = std::make_shared<test>();  

一次分配就足够了。这是因为std::make_shared申请一个单独的内存块来同时存放widget对象和控制块。这个优化减少了程序的静态大小,因为代码只包含一次内存分配的调用,并且这会加快代码的执行速度,因为内存只分配了一次。另外,使用std::make_shared消除了一些控制块需要记录的信息,这样潜在地减少了程序的总内存占用。

对std::make_shared的效率分析可以同样地应用在std::allocate_shared上,所以std::make_shared的性能优点也可以扩展到这个函数上。

异常安全

另外一个std::make_shared的好处是异常安全,我们看下面一句简单的代码:

calltest(std::shared_ptr<test>(new test), secondfun());  

简单说,上面这个代码可能会发生内存泄漏,我们先来看下上面这个调用中几个语句的执行顺序,可能是顺序如下:

new test()  secondfun()  std::shared_ptr<test>()  

如果真是按照上面这样的代码顺序执行,那么在运行期,如果secondfun()中产生了一个异常,程序就会直接返回了,则第一步new test分配的内存就泄露了,因为它永远不会被存放到在第三步才开始管理它的std::shared_ptr中。但是如果使用std::make_shared则可以避免这样的问题。调用代码将看起来像这样:

calltest(std::make_shared<test>(), secondfun());             

在运行期,不管std::make_shared或secondfun哪一个先被调用。如果std::make_shared先被调用,则在secondfun调用前,指向动态分配出来的test的原始指针能安全地被存放到std::shared_ptr中。如果secondfun之后产生一个异常,std::shared_ptr的析构函数将发现它持有的test需要被销毁。并且如果secondfun先被调用并产生一个异常,std::make_shared就不会被调用,因此这里就不需要考虑动态分配的test了。

计数线程安全?

我们上面一直说shared_ptr中的计数是线程安全的,其实shared_ptr中的计数是使用了我们前面文章介绍的std::atomic特性,引用计数加一减一操作是原子性的,所以线程安全的。引用计数器的使用等价于用 std::memory_order_relaxed 的 std::atomic::fetch_add 自增(自减要求更强的顺序,以安全销毁控制块)。

#include <iostream>  #include <memory>  #include <thread>  #include <chrono>  #include <mutex>     struct test  {      test() { std::cout << " test::test()n"; }      ~test() { std::cout << " test::~test()n"; }  };     //线程函数  void thr(std::shared_ptr<test> p)  {      //线程暂停1s      std::this_thread::sleep_for(std::chrono::seconds(1));        //赋值操作, shared_ptr引用计数use_cont加1(c++11中是原子操作)      std::shared_ptr<test> lp = p;      {          //static变量(单例模式),多线程同步用          static std::mutex io_mutex;            //std::lock_guard加锁          std::lock_guard<std::mutex> lk(io_mutex);          std::cout << "local pointer in a thread:n"              << " lp.get() = " << lp.get()              << ", lp.use_count() = " << lp.use_count() << 'n';      }  }     int main()  {      //使用make_shared一次分配好需要内存      std::shared_ptr<test> p = std::make_shared<test>();      //std::shared_ptr<test> p(new test);        std::cout << "created a shared testn"          << " p.get() = " << p.get()          << ", p.use_count() = " << p.use_count() << 'n';        //创建三个线程,t1,t2,t3      //形参作为拷贝, 引用计数也会加1      std::thread t1(thr, p), t2(thr, p), t3(thr, p);      std::cout << "shared ownership between 3 threads and releasedn"          << "ownership from main:n"          << " p.get() = " << p.get()          << ", p.use_count() = " << p.use_count() << 'n';      //等待结束      t1.join(); t2.join(); t3.join();      std::cout << "all threads completed, the last one deletedn";        return 0;  }  

输出:

test::test()
created a shared test
 p.get() = 0xa7cec0, p.use_count() = 1
shared ownership between 3 threads and released
ownership from main:
 p.get() = 0xa7cec0, p.use_count() = 4
local pointer in a thread:
 lp.get() = 0xa7cec0, lp.use_count() = 5
local pointer in a thread:
 lp.get() = 0xa7cec0, lp.use_count() = 4
local pointer in a thread:
 lp.get() = 0xa7cec0, lp.use_count() = 3
all threads completed, the last one deleted
 test::~test()

enable_shared_from_this

在某些场合下,会遇到一种情况,如何安全的获取对象的this指针,一般来说我们不建议直接返回this指针,可以想象下有这么一种情况,返回的this指针保存在外部一个局部或全局变量,当对象已经被析构了,但是外部变量并不知道指针指向的对象已经被析构了,如果此时外部继续使用了这个指针就会发生程序奔溃。既要像指针操作对象一样,又能安全的析构对象,很自然就想到,智能指针就很合适!我们来看下面这段程序:

#include <iostream>  #include <memory>    class test{  public:      test(){          std::cout << "test::test()" << std::endl;      }      ~test(){          std::cout << "test::~test()" << std::endl;      }        std::shared_ptr<test> getthis(){          return std::shared_ptr<test>(this);      }  };    int main()  {      std::shared_ptr<test> p(new test());      std::shared_ptr<test> p_this = p->getthis();        std::cout << p.use_count() << std::endl;      std::cout << p_this.use_count() << std::endl;        return 0;  }  

编译运行后程序输出如下:

free(): double free detected in tcache 2
test::test()
1
1
test::~test()
test::~test()

从上面的输出可以看到,构造函数调用了一次,析构函数却调用了两次,很明显这是不正确的。而std::enable_shared_from_this正是为了解决这个问题而存在。

std::enable_shared_from_this 能让一个对象(假设其名为 t ,且已被一个 std::shared_ptr 对象 pt 管理)安全地生成其他额外的 std::shared_ptr 实例(假设名为 pt1, pt2, … ) ,它们与 pt 共享对象 t 的所有权(这个是关键,直接使用this无法达到该效果)。

std::enable_shared_from_this是模板类,内部有个_tp类型weak_ptr指针,std::enable_shared_from_this的构造函数都是protected,因此不能直接创建std::enable_from_shared_from_this类的实例变量,只能作为基类使用,通过调用shared_from_this成员函数,将会返回一个新的 std::shared_ptr<t> 对象,它与 pt 共享 t 的所有权。因此使用方法如下代码所示:

#include <iostream>  #include <memory>    // 这里必须要 public继承,除非用struct  class test : public std::enable_shared_from_this<test> {  public:      test(){          std::cout << "test::test()" << std::endl;      }      ~test(){          std::cout << "test::~test()" << std::endl;      }        std::shared_ptr<test> getthis(){          std::cout << "shared_from_this()" << std::endl;          return shared_from_this();      }  };    int main()  {      std::shared_ptr<test> p(new test());      std::shared_ptr<test> p_this = p->getthis();        std::cout << p.use_count() << std::endl;      std::cout << p_this.use_count() << std::endl;        return 0;  }  

在类内部通过 enable_shared_from_this 定义的 shared_from_this() 函数构造一个 shared_ptr<test>对象, 能和其他 shared_ptr 共享 test 对象。一般我们使用在异步线程中,在异步调用中,存在一个保活机制,异步函数执行的时间点我们是无法确定的,然而异步函数可能会使用到异步调用之前就存在的变量。为了保证该变量在异步函数执期间一直有效,我们可以传递一个指向自身的share_ptr给异步函数,这样在异步函数执行期间share_ptr所管理的对象就不会析构,所使用的变量也会一直有效了(保活)。

shared_ptr使用注意事项:

  • 不要把一个原生指针给多个shared_ptr管理;不要主动删除 shared_ptr 所管理的裸指针;

    bigobj *p = new bigobj();  std::shared_ptr<bigobj> sp(p);  std::shared_ptr<bigobj> sp1(p);  delete p;  
  • 不要把this指针给shared_ptr,像上面一样使用enable_shared_from_this;

  • 不要不加思考地把指针替换为shared_ptr来防止内存泄漏,shared_ptr并不是万能的,而且使用它们的话也是需要一定的开销的;

  • 共享拥有权的对象一般比限定作用域的对象生存更久,从而将导致更高的平均资源使用时间;

  • 在多线程环境中使用共享指针的代价非常大,这是因为你需要避免关于引用计数的数据竞争;

  • 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器。

总结

智能指针是模板类而不是指针。创建一个智能指针时,必须指针可以指向的类型,<int>,<string> ……等。 智能指针实质就是重载了->*操作符的类,由类来实现对内存的管理,确保即使有异常产生,也可以通过智能指针类的析构函数完成内存的释放。具体来说它利用了引用计数技术和 c++ 的 raii(资源获取就是初始化)特性。 

到此这篇关于c++智能指针之shared_ptr的具体使用的文章就介绍到这了,更多相关c++ shared_ptr内容请搜索<猴子技术宅>以前的文章或继续浏览下面的相关文章希望大家以后多多支持<猴子技术宅>!

需要了解更多c/c++开发分享C++智能指针之shared_ptr的具体使用,都可以关注C/C++技术分享栏目—猴子技术宅(www.ssfiction.com)

本文来自网络收集,不代表猴子技术宅立场,如涉及侵权请点击右边联系管理员删除。

如若转载,请注明出处:https://www.ssfiction.com/c-cyuyankaifa/1240187.html

(0)
上一篇 4天前
下一篇 4天前

精彩推荐

发表回复

您的电子邮箱地址不会被公开。