shared_ptr的一些尴尬
http://blog.csdn.net/henan_lujun/article/details/8984543
shared_ptr在boost库中已经有多年了,C++11又为其正名,把他引入了STL库,放到了std的下面,可见其颇有用武之地;但是shared_ptr是万能的吗?有没有什么样的问题呢?本文并不说明shared_ptr的设计原理,也不是为了说明如何使用,只说一下在使用过程中的几点注意事项。
智能指针是万能良药?
智能指针为解决资源泄漏,编写异常安全代码提供了一种解决方案,那么他是万能的良药吗?使用智能指针,就不会再有资源泄漏了吗?来看下面的代码:
- //header file
- void func( shared_ptr<T1> ptr1, shared ptr<T2> ptr2 );
- //call func like this
- func( shared_ptr<T1>( new T1() ), shared_ptr<T2>( new T2() ) );
上面的函数调用,看起来是安全的,但在现实世界中,其实不然:由于C++并未定义一个表达式的求值顺序,因此上述函数调用除了func在最后得到调用之外是可以确定,其他的执行序列则很可能被拆分成如下步骤:
a. 分配内存给T1
b. 构造T1对象
c. 分配内存给T2
d. 构造T2对象
e. 构造T1的智能指针对象
f. 构造T2的智能指针对象
g. 调用func
或者:
a’. 分配内存给T1
b’. 分配内存给T2
c’. 构造T1对象
d’. 构造T2对象
e’. 构造T1的智能指针对象
f’. 构造T2的智能指针对象
g’. 调用func
上述无论哪种形式的构造序列,如果在c或者d / c’或者d’失败,则T1对象所分配内存必然泄漏。
为解决这个问题,有一个依然使用智能智能的笨重办法:
- template<class T>
- shared_ptr<T> shared_ptr_new()
- {
- return shared_ptr<T>( new T );
- }
- //call like this
- func( shared_ptr_new<T1>(), shared_ptr_new<T2>() );
使用这种方法,可以解决因为产生异常导致资源泄漏的问题;然而另外一个问题出现了,如果T1或者T2的构造函数需要提供参数怎么办呢?难道提供很多个重载版本?——可以倒是可以,只要你不嫌累,而且有足够的先见性。
其实,最最完美的方案,其实是最简单的——就是尽量简单的书写代码,像这样:
- //header file
- void func( shared_ptr<T1> ptr1, shared_ptr<T2> ptr2 );
- //call func like this
- shared_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,是一种弱引用,表示只是对某个对象的一个引用和使用,而不做管理工作;我们把他和shared_ptr来做一下对比:
shared_ptr |
weak_ptr |
强引用 |
弱引用 |
强引用存在,则引用的对象必定存在; 只要有一个强引用存在,强引用对象就不能释放 |
是对象存在时的一个引用; 及时有弱引用存在,对象仍然可以释放 |
增加对象的引用计数 |
不增加对象的引用计数 |
负责资源管理,在引用计数为0时释放资源 |
不负责资源管理 |
有多个构造函数,可以从任意类型初始化 |
只能从一个shared_ptr或者weak_ptr对象上进行初始化 |
行为类似原生指针,不过可以用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()可以连续进行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从类内部传递出来;
- 资源管理保持统一风格,要么使用智能指针,要么就全部自己管理裸指针;
shared_ptr的一些尴尬相关推荐
- get方法报空指针_智能指针shared_ptr踩坑笔记
平时写代码一直避免使用指针,但在某些场景下指针的使用还是有必要的.最近在项目中简单使用了一下智能指针(shared_ptr),结果踩了不少坑,差点就爬不出来了.痛定思痛抱着<Cpp Primer ...
- c++【深度剖析shared_ptr】
shared_ptr解决了scoped_ptr管理单个对象的缺陷,且解决了防拷贝的问题.shared_ptr可以管理多个对象,并且实现了资源共享. 但是仍然存在一些问题,比如,我们熟悉的双向链表: s ...
- c++——智能指针学习(shared_ptr和weak_ptr)
先看一个例子:Stark和Targaryen家族你中有我,我中有你.我们设计以下类企图避免内存泄漏,使得析构函数都能调用到: #include<iostream> #include< ...
- 自动驾驶公司尴尬处境
自动驾驶公司尴尬处境 小马智行(Pony.ai)成立于2016年底, 以中国和美国为起点,分别在硅谷.广州.北京.上海设立研发中心,并获得中美多地自动驾驶测试.运营资质与牌照. 凭借人工智能技术领域的 ...
- C++ 智能指针(unique_ptr / shared_ptr)代码实现
文章目录 unique_ptr 智能指针的实现 shared_ptr 智能指针的实现 指针类型转换 unique_ptr 智能指针的实现 一个对象只能被单个unique_ptr 所拥有. #inclu ...
- C++智能指针: shared_ptr 实现详解
文章目录 shared_ptr描述 声明 作用 原理实现 函数使用 关于shared_ptr循环引用问题 shared_ptr描述 声明 shared_ptr属于C++11特性中新加的一种智能指针,它 ...
- 动态内存管理和智能指针 2.0 -- shared_ptr
shared_ptr出现原因 通过第一章的学习,我们知道不管是auto_ptr合适scoped_ptr都是存在缺陷的,于是我们必须想出一个方法既能很好的管理我们的内存,而且在使用的时候,可以多个指针指 ...
- 【Smart_Point】unique_ptr与shared_ptr使用实例
shared_ptr使用实例 文章目录 shared_ptr使用实例 unique_ptr使用实例 cv::fitLine中斜率为正无穷的情况,需要特殊考虑 std::string path = &q ...
- 【Smart_Point】C/C++ 中共享指针 shared_ptr
1. 共享指针 shared_ptr 目录 1. 共享指针 shared_ptr 1.1 共享指针解决的问题? 1.2 创建 shared_ptr 对象 1.3 分离关联的原始指针 1.4 自定义删除 ...
最新文章
- 经验风险、期望风险、结构风险
- pycharm更改为系统python3_今天配置好python3 装好了pycharm 就新建一个django一直改不好...
- 有规律的进行效果最好--打扫有感
- AI发展“风口”在哪里?陪伴才是“蓝海”
- 航天金税502设置模拟环境步骤
- 少儿Python编程教程
- 使用arecord、aplay 实现录音和播放
- 百度网盘客户端终于解除限速啦
- matlab高斯消元法列主元,matlab用列主元高斯消去法解方程组,
- 2022-2028年全球与中国便携式茶包市场现状及未来发展趋势分析报告
- 计算彩色图像的直方图
- 如何从电脑内转播声音
- 把EXCEL表格导入到MYSQL中_将EXCEL表格中的数据导入mysql数据库表中(两种方法)...
- 变现 起航篇! 手把手交你用chatgpt快速生成视频!
- 系统平台新店铺运营思路
- 北大2021年计算机分数分数线,北京大学2021年考研复试基本分数线已发布
- C语言-函数案例-计算圆周长
- cmd怎么切换mysql目录_mysql 移除服务,并在cmd下切换目录
- 电容器和电池有什么不同?
- 一个笔记本能连WiFi 通过网线共享网络给另一个电脑上网(笔记本对台式/笔记本对笔记本)
热门文章
- poj2115C Looooops
- 40种Javascript中常用的使用小技巧【转】
- Acer Travelmate T3290笔记本拆机指南
- 新风系统风速推荐表_新风系统风速标准及与噪音的关系
- 微型计算机接口与技术的交通灯,微机原理及接口技术课程设计交通灯
- android媒体播放框架,Android 使用超简单的多媒体播放器JiaoZiVideoPlayer
- 我的python学习笔记全集_我的python学习笔记
- oracle 日志切换太频繁,诊断一次Oracle日志切换频繁的问题
- h5 php js实验总结,H5学习_番外篇_PHP数据库操作
- ice库c语言例子,很不多的ICE架构入门学习例子