shared_ptr的一些尴尬

http://blog.csdn.net/henan_lujun/article/details/8984543

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

智能指针是万能良药?

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

[cpp] view plain copy
  1. //header file  
  2. void func( shared_ptr<T1> ptr1, shared ptr<T2> ptr2 );  
  3.    
  4. //call func like this  
  5. 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对象所分配内存必然泄漏。

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

[cpp] view plain copy
  1. template<class T>  
  2. shared_ptr<T> shared_ptr_new()  
  3. {  
  4.     return shared_ptr<T>( new T );  
  5. }  
  6.    
  7. //call like this  
  8. func( shared_ptr_new<T1>(), shared_ptr_new<T2>() );  

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

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

[cpp] view plain copy
  1. //header file  
  2. void func( shared_ptr<T1> ptr1, shared_ptr<T2> ptr2 );  
  3.    
  4. //call func like this  
  5. shared_ptr<T1> ptr1( new T1() );  
  6. shared_ptr<T2> ptr2( new T2() );  
  7. func(ptr1, ptr2  );  

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

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

 

shared_ptr 交叉引用导致的泄漏

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

[cpp] view plain copy
  1. class CLeader;  
  2. class CMember;  
  3.    
  4. class CLeader  
  5. {  
  6. public:  
  7.       CLeader() { cout << "CLeader::CLeader()" << endl; }  
  8.       ~CLeader() { cout << "CLeader:;~CLeader() " << endl; }  
  9.    
  10.       std::shared_ptr<CMember> member;  
  11. };  
  12.    
  13. class CMember  
  14. {  
  15. public:  
  16.       CMember()  { cout << "CMember::CMember()" << endl; }  
  17.       ~CMember() { cout << "CMember::~CMember() " << endl; }  
  18.    
  19.       std::shared_ptr<CLeader> leader;     
  20. };  
  21.    
  22. void TestSharedPtrCrossReference()  
  23. {  
  24.       cout << "TestCrossReference<<<" << endl;  
  25.       boost::shared_ptr<CLeader> ptrleader( new CLeader );  
  26.       boost::shared_ptr<CMember> ptrmember( new CMember );  
  27.    
  28.       ptrleader->member = ptrmember;  
  29.       ptrmember->leader = ptrleader;  
  30.    
  31.       cout <<"  ptrleader.use_count: " << ptrleader.use_count() << endl;  
  32.       cout <<"  ptrmember.use_count: " << ptrmember.use_count() << endl;  
  33. }  
  34. //output:  
  35. CLeader::CLeader()  
  36. CMember::CMember()  
  37.   ptrleader.use_count: 2  
  38.   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的声明改成如下形式,就可以解除这种循环,从而每个资源都可以顺利释放。

[cpp] view plain copy
  1. class CMember  
  2. {  
  3. public:  
  4.       CMember()  { cout << "CMember::CMember()" << endl; }  
  5.       ~CMember() { cout << "CMember::~CMember() " << endl; }  
  6.    
  7.       boost::weak_ptr<CLeader> leader;     
  8. };  

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

 

类向外传递this与shared_ptr

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

[cpp] view plain copy
  1. class Point1  
  2. {  
  3. public:  
  4.     Point1() :  X(0), Y(0) { cout << "Point1::Point1(), (" << X << "," << Y << ")" << endl; }  
  5.     Point1(int x, int y) :  X(x), Y(y) { cout << "Point1::Point1(int x, int y), (" << X << "," << Y << ")" << endl; }  
  6.     ~Point1() { cout << "Point1::~Point1(), (" << X << "," << Y << ")" << endl; }  
  7.        
  8. public:  
  9.     Point1* Add(const Point1* rhs) { X += rhs->X; Y += rhs->Y; return this;}  
  10.    
  11. private:  
  12.     int X;  
  13.     int Y;  
  14. };  
  15.    
  16. void TestPoint1Add()  
  17. {  
  18.     cout << "TestPoint1Add() >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" << endl;  
  19.     shared_ptr<Point1> p1( new Point1(2,2) );  
  20.     shared_ptr<Point1> p2( new Point1(3,3) );  
  21.        
  22.     p2.reset( p1->Add(p2.get()) );  
  23. }  
  24.    
  25. 输出为:  
  26. TestPoint1Add() >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>  
  27. Point1::Point1(int x, int y), (2,2)  
  28. Point1::Point1(int x, int y), (3,3)  
  29. Point1::~Point1(), (3,3)  
  30. Point1::~Point1(), (5,5)  
  31. Point1::~Point1(), (5411568,5243076)  

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

 

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

[cpp] view plain copy
  1. class Point2  
  2. {  
  3. public:  
  4.     Point2() :  X(0), Y(0) { cout << "Point2::Point2(), (" << X << "," << Y << ")" << endl; }  
  5.     Point2(int x, int y) :  X(x), Y(y) { cout << "Point2::Point2(int x, int y), (" << X << "," << Y << ")" << endl; }  
  6.     ~Point2() { cout << "Point2::~Point2(), (" << X << "," << Y << ")" << endl; }  
  7.        
  8. public:  
  9.     shared_ptr<Point2> Add(const Point2* rhs) { X += rhs->X; Y += rhs->Y; return shared_ptr<Point2>(this);}  
  10.    
  11. private:  
  12.     int X;  
  13.     int Y;  
  14. };  
  15.    
  16. void TestPoint2Add()  
  17. {  
  18.     cout << endl << "TestPoint2Add() >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" << endl;  
  19.     shared_ptr<Point2> p1( new Point2(2,2) );  
  20.     shared_ptr<Point2> p2( new Point2(3,3) );  
  21.        
  22.     p2.swap( p1->Add(p2.get()) );  
  23. }  
  24.    
  25. 输出为:  
  26. TestPoint2Add() >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>  
  27. Point2::Point2(int x, int y), (2,2)  
  28. Point2::Point2(int x, int y), (3,3)  
  29. Point2::~Point2(), (3,3)  
  30. Point2::~Point2(), (5,5)  
  31. 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:

[cpp] view plain copy
  1. class Point3 : public enable_shared_from_this<Point3>  
  2. {  
  3. public:  
  4.     Point3() :  X(0), Y(0) { cout << "Point3::Point3(), (" << X << "," << Y << ")" << endl; }  
  5.     Point3(int x, int y) :  X(x), Y(y) { cout << "Point3::Point3(int x, int y), (" << X << "," << Y << ")" << endl; }  
  6.     ~Point3() { cout << "Point3::~Point3(), (" << X << "," << Y << ")" << endl; }  
  7.        
  8. public:  
  9.     shared_ptr<Point3> Add(const Point3* rhs) { X += rhs->X; Y += rhs->Y; return shared_from_this();}  
  10.    
  11. private:  
  12.     int X;  
  13.     int Y;  
  14. };  
  15.    
  16. void TestPoint3Add()  
  17. {  
  18.     cout << endl << "TestPoint3Add() >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" << endl;  
  19.     shared_ptr<Point3> p1( new Point3(2,2) );  
  20.     shared_ptr<Point3> p2( new Point3(3,3) );  
  21.        
  22.     p2.swap( p1->Add(p2.get()) );  
  23. }  
  24. 输出为:  
  25. TestPoint3Add() >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>  
  26. Point3::Point3(int x, int y), (2,2)  
  27. Point3::Point3(int x, int y), (3,3)  
  28. Point3::~Point3(), (3,3)  
  29. Point3::~Point3(), (5,5)  

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

[cpp] view plain copy
  1. Point3 p1(10, 10);  
  2. Point3 p2(20, 20);  
  3.    
  4. 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从类内部传递出来;
  • 资源管理保持统一风格,要么使用智能指针,要么就全部自己管理裸指针;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/383787.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C++转换构造函数和类型转换函数

参考博客&#xff1a;https://blog.csdn.net/feiyanaffection/article/details/79183340 隐式类型转换 如果不同类型的数据在一起操作的时候编译器会自动进行一个数据类型转换。例如常用的基本数据类型有如下类型转换关系&#xff1a; 转换构造函数 构造函数有且仅有一个参数…

C++析构函数执行顺序

今天发现主程序中有多个对象时析构函数的执行顺序不是对象定义的顺序&#xff0c;而是对象定义顺序反过来。 思考了一下&#xff0c;结合之前继承、成员对象等的析构函数执行的顺序&#xff0c;我觉得析构函数执行的顺序为&#xff1a;构造函数的顺序反过来&#xff0c;可能是…

c++写时拷贝1

http://blog.csdn.net/SuLiJuan66/article/details/48882303 Copy On Write Copy On Write(写时复制)使用了“引用计数”&#xff08;reference counting&#xff09;&#xff0c;会有一个变量用于保存引用的数量。当第一个类构造时&#xff0c;string的构造函数会根据传入的参…

【Java学习笔记十】输入输出流

在Java.io包中提供了一系列用于处理输入/输出的流类。从功能上分为两类&#xff1a;输入流和输出流。从六结构上可分为&#xff1a;字节流&#xff08;以字节为处理单位&#xff09;和字符流&#xff08;以字符为处理单位&#xff09;。 字符是由字节组成。在Java中所有字符用…

C++ 写时拷贝 2

什么情况下会用到c中的拷贝构造函数】&#xff1a; 1&#xff09;用已经存在的同类的对象去构造出另一个新的对象 2&#xff09;当函数的形参是类的对象时&#xff0c;这时调用此函数&#xff0c;使用的是值的拷贝&#xff0c;也会调用拷贝构造函数 3&#xff09;当函数的返…

【Java学习笔记十一】图形用户界面

图形用户界面或图形用户接口(Graphical User Interface&#xff0c;GUI)是指采用图形方式,借助菜单、按钮等标准界面元素&#xff0c;用户可以通过鼠标等外设向计算机系统发出指令、启动操作&#xff0c;并将系统运行的结果同样以图形方式显示给用户的技术。 GUI是事件驱动的&…

C++ 写时拷贝 3

http://blog.csdn.net/ljianhui/article/details/22895505 字符串一种在程序中经常要使用到的数据结构&#xff0c;然而在C中却没有字符串这种类型。在C中&#xff0c;为了方便字符串的使用&#xff0c;在STL中提供了一个string类。该类维护一个char指针&#xff0c;并封装和提…

C++ String类写时拷贝 4

http://blog.51cto.com/zgw285763054/1839752 维基百科&#xff1a; 写入时复制&#xff08;英语&#xff1a;Copy-on-write&#xff0c;简称COW&#xff09;是一种计算机程序设计领域的优化策略。其核心思想是&#xff0c;如果有多个调用者&#xff08;callers&#xff09;同时…

C++笔试复习

基础知识点 C中对象数组在定义的时候全部进行实例化&#xff08;与Java不同&#xff0c;Java相当于只是定义了一个指针数组&#xff0c;没有进行实例化&#xff09; 程序的三种基本控制结构是&#xff1a;顺序结构、循环结构、选择结构 一个C程序开发步骤通常包括&#xff1a…

Java笔试复习

Java程序运行 Java程序的执行必须经过编辑、编译和运行三个步骤 编辑指编写代码&#xff0c;最终形成后缀名为.java的Java源文件编译指使用Java编译器&#xff08;javac指令&#xff09;将源文件翻译为二进制代码&#xff0c;编译后生成后缀名为.class的字节码文件&#xff0c…

Java类名与包名不区分大小写

刚才写了一个简单的Java程序&#xff0c;经过测试得到一个令人震惊的结论&#xff1a;Java类名和包名是不区分大小写的 可以看一下这个例子&#xff1a; package Test;class aBcdEfG {}class AbCdefg {}public class TTT {public static void main(String[] args){AbCdefg tm…

处理大并发之一 对epoll的理解,epoll客户端服务端代码

http://blog.csdn.net/zhuxiaoping54532/article/details/56494313处理大并发之一对epoll的理解&#xff0c;epoll客户端服务端代码序言&#xff1a;该博客是一系列的博客&#xff0c;首先从最基础的epoll说起&#xff0c;然后研究libevent源码及使用方法&#xff0c;最后研究n…

链栈基本操作

http://blog.csdn.net/jwentao01/article/details/46765517###;栈基本概念&#xff1a; 栈&#xff08;stack&#xff09;是限定在表尾进行插入和删除操作的线性表&#xff08;或单链表&#xff09;。 //只能在一段进行插入和删除&#xff0c;因此不存在&#xff0c;在中间进行…

Linux网络编程---I/O复用模型之select

https://blog.csdn.net/men_wen/article/details/53456435Linux网络编程—I/O复用模型之select 1. IO复用模型 IO复用能够预先告知内核&#xff0c;一旦发现进程指定的一个或者多个IO条件就绪&#xff0c;它就通知进程。IO复用阻塞在select或poll系统调用上&#xff0c;而不是阻…

Linux网络编程---I/O复用模型之poll

https://blog.csdn.net/men_wen/article/details/53456474Linux网络编程—I/O复用模型之poll 1.函数poll poll系统调用和select类似&#xff0c;也是在指定时间内轮询一定数量的文件描述符&#xff0c;以测试其中是否有就绪者。 #include <poll.h>int poll(struct pollfd…

Linux网络编程---I/O复用模型之epoll

https://blog.csdn.net/men_wen/article/details/53456474 Linux网络编程—I/O复用模型之epoll 1. epoll模型简介 epoll是Linux多路服用IO接口select/poll的加强版&#xff0c;e对应的英文单词就是enhancement&#xff0c;中文翻译为增强&#xff0c;加强&#xff0c;提高&…

POJ 1741tree-点分治入门

学习了一下点分治&#xff0c;如果理解有误还请不吝赐教。 为了快速求得树上任意两点之间距离满足某种关系的点对数&#xff0c;我们需要用到这种算法。 点分治是树上的一种分治算法&#xff0c;依靠树和子树之间的关系进行分治从而降低复杂度。 和其他树上的算法有一些区别…

基于单链表的生产者消费者问题

『生产者与消费者问题分析』「原理」生产者生产产品&#xff0c;消费者消费产品。产品如果被消费者消费完了&#xff0c;同时生产者又没有生产出产品&#xff0c;消费者 就必须等待。同样的&#xff0c;如果生产者生产了产品&#xff0c;而消费者没有去消费&#x…

C++智能指针(二)模拟实现三种智能指针

https://blog.csdn.net/nou_camp/article/details/70186721在上一篇博客中提到了Auto_ptr(C智能指针&#xff08;一&#xff09;)&#xff0c;下面进行模拟实现Auto_ptr 采用类模板实现 #include<iostream> using namespace std; template<class T> class Autoptr …

C++智能指针(三)总结

https://blog.csdn.net/nou_camp/article/details/70195795 在上一篇博客中&#xff08;C智能指针&#xff08;二&#xff09;&#xff09;模拟实现了三种智能指针。 其中最好的就是shared_ptr,但是这并不代表它就是最完美的&#xff0c;它也有问题&#xff0c;这个问题就是循环…