Shared_ptr详解

来源:互联网 发布:山东临沂外包淘宝客服 编辑:程序博客网 时间:2024/06/07 13:09

shared_ptr在boost库中已经有多年了,C++11又为其正名,把他引入了STL库,放到了std的下面,可见其颇有用武之地;但是shared_ptr是万能的吗?有没有什么样的问题呢?本文并不说明shared_ptr的设计原理,也不是为了说明如何使用,只说一下在使用过程中的几点注意事项。

用法

shared_ptr<int> sp(new int(10));                //一个指向整数的shared_ptr  assert(sp.unique());                            //现在shared_ptr是指针的唯一持有者   shared_ptr<int> sp2 = sp;                       //第二个shared_ptr,拷贝构造函数   assert(sp == sp2 && sp.use_count() == 2);       //两个shared_ptr相等,指向同一个对象,引用计数为2  *sp2 = 100;                                     //使用解引用操作符修改被指对象  assert(*sp == 100);                             //另一个shared_ptr也同时被修改   sp.reset();                                     //停止shared_ptr的使用  assert(!sp);                                    //sp不再持有任何指针(空指针)

shared_ptr的线程安全性

shared_ptr本身不是100%线程安全的,它的引用计数本身是安全且无锁的,但对象的读写则不是,因为shared_ptr有两个数据成员,读写操作不能原子化。根据文档,shared_ptr的线程安全级别和内建类型、标准容器、string一样,即(参考智能指针线程相关):

  • 一个shared_ptr 实体可被多个线程同时读取;
  • 两个shared_ptr 实体可以被两个线程同时写入,“析构”算写操作;
  • 如果要从多个线程读写同一个shared_ptr对象,那么需要加锁;

智能指针是万能良药?

智能指针为解决资源泄漏,编写异常安全代码提供了一种解决方案,但它是万能良药吗?使用智能指针,就能保证不会再有资源泄漏吗?来看下面的代码:

//header filevoid func( shared_ptr<T1> ptr1, shared ptr<T2> ptr2 );//call func like thisfunc( shared_ptr<T1>( new T1() ), shared_ptr<T2>( new T2() ) );

上面的函数调用,看起来是安全的,但在现实世界中,其实不然:由于C++并未定义一个表达式的求值顺序,因此上述函数调用除了func在最后得到调用之外可以确定,其他的执行序列则很可能被拆分成如下步骤

a.分配内存给T1b.构造T1对象c.分配内存给T2   d.构造T2对象    e.构造T1的智能指针对象   f.构造T2的智能指针对象   g.调用func

或者:

a1.分配内存给T1b1.分配内存给T2c1.构造T1对象       d1.构造T2对象   e1.构造T1的智能指针对象  f1.构造T2的智能指针对象  g1.调用func

上述无论哪种形式的构造序列,如果在 c 或者 d / c1 或者d1失败,则T1对象分配内存必然泄漏。

为了解决这个问题,有一个依然使用智能指针的笨办法:

template<class T>shared_ptr<T> shared_ptr_new(){    return shared_ptr<T>( new T );}//call like thisfunc( shared_ptr_new<T1>(), shared_ptr_new<T2>() );

使用这种办法,可以解决因为产生异常导致资源泄漏的问题,然而另一个问题又出现了,如果T1或者T2的构造函数需要提供参数该怎么办呢?难道提供很多个重载的版本?——>可以倒是可以,只要你不嫌累,而且有足够的先见性。

其实,最完美的方案,其实也是最简单的,就是尽量简单的书写代码,像这样:

//header filevoid func( shared_ptr<T1> ptr1, shared_ptr<T2> ptr2 );//call func like thisshared_ptr<T1> ptr1( new T1() );shared_ptr<T2> ptr2( new T2() );func(ptr1, ptr2  );

这样简简单单的代码,避免了异常导致的泄漏。又应了那句话:简单就是美。其实,在一个表达式中,分配多个资源或者需要求多个值等操作都是不安全的。

归总为一句话:抛弃临时对象,让所有的智能指针都有名字,就可以避免此类问题的发生。

shared_ptr交叉引用导致的泄漏

是否让每个智能指针都有了名字,就不会再有内存泄漏?不一定,看下面的代码的输出,是否会感到惊讶。。。

class CLeader;class CMember;class CLeader{public:      CLeader() { cout << "CLeader::CLeader()" << endl; }      ~CLeader() { cout << "CLeader:;~CLeader() " << endl; }      std::shared_ptr<CMember> member;};class CMember{public:      CMember()  { cout << "CMember::CMember()" << endl; }      ~CMember() { cout << "CMember::~CMember() " << endl; }      std::shared_ptr<CLeader> leader;   };void TestSharedPtrCrossReference(){      cout << "TestCrossReference<<<" << endl;      boost::shared_ptr<CLeader> ptrleader( new CLeader );      boost::shared_ptr<CMember> ptrmember( new CMember );      ptrleader->member = ptrmember;      ptrmember->leader = ptrleader;      cout <<"  ptrleader.use_count: " << ptrleader.use_count() << endl;      cout <<"  ptrmember.use_count: " << ptrmember.use_count() << endl;}//output:CLeader::CLeader()CMember::CMember()  ptrleader.use_count: 2  ptrmember.use_count: 2

从运行结果来看,两个对象的析构函数都没有调用,也就是出现了内存泄漏—–>原因在于:TestSharedPtrCrossReference() 函数退出时,两个shared_ptr对象的引用计数都是2,所以不会释放对象;

这里写图片描述

这里出现了常见的交叉引用的问题,这个问题,即使用原生指针互相记录时也需要格外小心。shared_ptr在这里栽了跟头,ptrleader和ptrmember在离开作用域的时候,由于引用计数不为1,所以最后一次的release操作(shared_ptr析构函数里面调用)也无法destroy掉所托管的资源。

为了解决这个问题,可以采用weak_ptr来隔断交叉引用的回路,所谓的weak_ptr,是一种弱引用,表示只是对某个对象的一个引用和使用,而不做管理工作。weak_ptr 和 shared_ptr对比:

shared_ptr weak_ptr 强引用 弱引用 强引用存在,则引用的对象必定存在;
只要有一个强引用存在,强引用对象就不能释放; 是对象存在的一个引用;
即使有弱引用存在,对象仍然可以释放; 增加对象的引用计数 不增加对象的引用计数 负责资源管理,在引用计数为0时释放资源 不负责资源管理 有多个构造函数,可以从任意类型初始化 只能从一个shared_ptr或者weak_ptr对象上进行初始化 zebra stripes 行为类似原生指针,不过可以用expired()判断对象是否已经释放

由于weak_ptr具有上述的一些性质,所以如果把CMember的声明改成如下形式,就可以解除这种循环,从而每个资源都可以顺利释放。

class CMember{public:      CMember()  { cout << "CMember::CMember()" << endl; }      ~CMember() { cout << "CMember::~CMember() " << endl; }      boost::weak_ptr<CLeader> leader;   };

这种使用weak_ptr的方式,是基于已暴露问题的修正方案,在做设计的时候,一般很难注意到这一点,总之,C++缺少垃圾收集机制,虽然智能指针提供了一个解决方案,但它也难以到达完美,因此,C++的资源管理必须慎之又慎。

类向外传递this 与 shared_ptr

可以说,shared_ptr着力解决类对象一级的资源管理,至于类对象内部,shared_ptr暂时还无法管理,那么这是否会出现问题呢?看如下代码:

class Point1{public:    Point1() :  X(0), Y(0) { cout << "Point1::Point1(), (" << X << "," << Y << ")" << endl; }    Point1(int x, int y) :  X(x), Y(y) { cout << "Point1::Point1(int x, int y), (" << X << "," << Y << ")" << endl; }    ~Point1() { cout << "Point1::~Point1(), (" << X << "," << Y << ")" << endl; }public:    Point1* Add(const Point1* rhs) { X += rhs->X; Y += rhs->Y; return this;}private:    int X;    int Y;};void TestPoint1Add(){    cout << "TestPoint1Add() >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" << endl;    shared_ptr<Point1> p1( new Point1(2,2) );    shared_ptr<Point1> p2( new Point1(3,3) );    p2.reset( p1->Add(p2.get()) );}输出为:TestPoint1Add() >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>Point1::Point1(int x, int y), (2,2)Point1::Point1(int x, int y), (3,3)Point1::~Point1(), (3,3)Point1::~Point1(), (5,5)Point1::~Point1(), (5411568,5243076)

为了使类似Point::Add()可以连续进行Add操作成为可能,Point1定义了Add方法,并返回了this指针(从Effective C++的条款看,这里最好该以传值形式返回临时变量,在此为了说明问题,暂且不考虑这种设计是否合理,但他就这样存在了)。在TestPoint1Add()函数中,使用此返回的指针重置了p2,这样p2和p1就同时管理了同一个对象,但是他们却互相不知道这事儿,于是悲剧发生了。在作用域结束的时候,他们两个都去对所管理的资源进行析构,从而出现了上述的输出。从最后一行输出也可以看出,所管理的资源,已经处于“无效”的状态了。

那么,我们是否可以改变一下呢,让Add返回一个shared_ptr了呢。我们来看看Point2:

class Point2{public:    Point2() :  X(0), Y(0) { cout << "Point2::Point2(), (" << X << "," << Y << ")" << endl; }    Point2(int x, int y) :  X(x), Y(y) { cout << "Point2::Point2(int x, int y), (" << X << "," << Y << ")" << endl; }    ~Point2() { cout << "Point2::~Point2(), (" << X << "," << Y << ")" << endl; }public:    shared_ptr<Point2> Add(const Point2* rhs) { X += rhs->X; Y += rhs->Y; return shared_ptr<Point2>(this);}private:    int X;    int Y;};void TestPoint2Add(){    cout << endl << "TestPoint2Add() >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" << endl;    shared_ptr<Point2> p1( new Point2(2,2) );    shared_ptr<Point2> p2( new Point2(3,3) );    p2.swap( p1->Add(p2.get()) );}输出为:TestPoint2Add() >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>Point2::Point2(int x, int y), (2,2)Point2::Point2(int x, int y), (3,3)Point2::~Point2(), (3,3)Point2::~Point2(), (5,5)Point2::~Point2(), (3379952,3211460)

从输出来看,哪怕使用shared_ptr来作为Add函数的返回值,仍然无济于事;对象仍然被删除了两次;

针对这种情况,shared_ptr的解决方案是:enable_shared_from_this这个模板类,所有需要在内部传递this指针的类,都从enable_shared_from_this继承,在需要传递this的时候,使用其成员函数shared_from_this() 来返回一个shared_ptr,运用这种方案,我们改良我们的Point类,得到如下的Point3:

class Point3 : public enable_shared_from_this<Point3>{public:    Point3() :  X(0), Y(0) { cout << "Point3::Point3(), (" << X << "," << Y << ")" << endl; }    Point3(int x, int y) :  X(x), Y(y) { cout << "Point3::Point3(int x, int y), (" << X << "," << Y << ")" << endl; }    ~Point3() { cout << "Point3::~Point3(), (" << X << "," << Y << ")" << endl; }public:    shared_ptr<Point3> Add(const Point3* rhs) { X += rhs->X; Y += rhs->Y; return shared_from_this();}private:    int X;    int Y;};void TestPoint3Add(){    cout << endl << "TestPoint3Add() >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" << endl;    shared_ptr<Point3> p1( new Point3(2,2) );    shared_ptr<Point3> p2( new Point3(3,3) );    p2.swap( p1->Add(p2.get()) );}输出为:TestPoint3Add() >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>Point3::Point3(int x, int y), (2,2)Point3::Point3(int x, int y), (3,3)Point3::~Point3(), (3,3)Point3::~Point3(), (5,5)

从这个输出可以看出,在这里的对象析构已经变得正常。因此,在类内部需要传递this的场景下,enable_shared_from_this是一个比较靠谱的方案,只不过,要谨慎的脊柱,使用该方案的一个前提,就是类的对象已经被shared_ptr管理,否则就等着抛异常吧。例如:

Point3 p1(10, 10);Point3 p2(20, 20);p1.Add( &p2 ); //此处抛异常

上面的代码会导致crash。那是因为p1没有被shared_ptr管理。之所以这样,是由于shared_ptr的构造函数才会去初始化enable_shared_from_this相关的引用计数(具体可以参考代码),所以如果对象没有被shared_ptr管理,shared_from_this()函数就会出错。

于是,shared_ptr又引入了注意事项:

  • 若要在内部传递this,请考虑从enable_shared_from_this继承;
  • 若从enable_shared_from_this继承,则类对象必须让shared_ptr接管;
  • 如果要使用智能指针,那么就要保持一致,统统使用智能智能,尽量减少raw pointer裸指针的使用;

总结

  • C++没有垃圾收集,资源管理需要自己来做;
  • 智能指针可以部分解决资源管理的工作,但是不是万能的;
  • 使用智能指针的时候,每个shared_ptr 对象都应该有一个名字,也就是避免在一个表达式内做多个资源的初始化;
  • 避免shared_ptr的交叉引用,使用weak_ptr打破交叉引用;
  • 使用enable_shared_from_this 机制来把this 从类内部传递出来;
  • 资源管理保持统一风格,要么使用智能指针,要么就全部自己管理裸指针;
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 我要贷款5万怎么办 拍拍贷一千不还怎么办 牙龈肿里面有脓怎么办 爱奇艺会员账号忘了怎么办 被私立医院坑了怎么办 在医院被坑了怎么办 流产后子宫内膜薄怎么办 人流后内膜过厚怎么办 子宫内膜薄月经量少怎么办 子宫内膜很薄该怎么办 月经量少子宫内膜薄怎么办 子宫内薄没月经怎么办 感冒20多天不好怎么办 皮肤干燥又痒怎么办了 眼周皮肤很干怎么办 产后掉头发很厉害怎么办 班上学生很吵怎么办 进了网贷黑名单怎么办 预约了挂号没去怎么办 吃完米索手心痒怎么办 三岁宝宝湿疹了怎么办 割完剥皮后水肿怎么办 微医预约挂号后怎么办 人流后出现腰疼怎么办 生育服务单丢了怎么办 客厅地面砖坏了怎么办 门锁很涩不好开怎么办 胶水粘到手机上怎么办 沾鞋胶水粘到手怎么办 轮胎内衬板坏了怎么办 轿车后减震异响怎么办 租的房间隔音差怎么办 彩钢房顶下雨响怎么办 酷派手机开不开机怎么办 酷派手机无法开机怎么办 酷派手机丢了怎么办 实木家具掉漆了怎么办 木桌子上有划痕怎么办 烤漆家具掉漆怎么办 洗碗帕沾了油污怎么办 手被棍子打肿了怎么办