shared_ptr的一些尴尬

Posted tgww88

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了shared_ptr的一些尴尬相关的知识,希望对你有一定的参考价值。

        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的智能指针对象

或者:

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的构造函数需要提供参数怎么办呢?难道提供很多个重载版本?

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

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()" << ednl;
    

    ~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;
    shared_ptr<CLeader> ptrleader( new CLeader );
    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;


输出结果如下:

CLeader::CLeader()
CMember::CMember()
ptrleader.use_count: 2
ptrmember.use_count: 2

          从运行输出来看,两个对象的析构函数都没有调用,也就是出现了内存泄漏——原因在于:

       TestSharedPtrCrossReference()函数退出时,两个shared_ptr对象的引用计数都是2,因此不会释放对象。

       这里出现了常见的交叉引用问题,这个问题,即使用原生指针互相记录时也需要格外小心;shared_ptr在这里也跌了跟头,pthread和ptrmember

在离开作用域的时候,由于引用计数不为1,所以最后一次的release操作也无法destroy掉锁托管的资源。

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

shared_ptrweak_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) 
	 
		std::cout << "Point1::Point1(), (" << X << "," << Y << ")" << std::endl; 
	

	Point1(int x, int y) : X(x), Y(y) 
	 
		std::cout << "Point1::Point1(int x, int y), (" << X << "," << Y << ")" << std::endl; 
	

	~Point1() 
	 
		std::cout << "Point1::~Point1(), (" << X << "," << Y << ")" << std::endl; 
	

public:
	Point1* Add(const Point1* rhs) 
	 
		X += rhs->X; 
		Y += rhs->Y; 
		return this; 
	

private:
	int X;
	int Y;
;

输出为:

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)


           在TestPoint1Add()函数中,使用add返回的指针重置了p2,这样p2和p1就同时管理了同一个对象,但是他们却互相不知道这事儿,于是悲剧发生了。在作用域结束时,他们两个都去对所管理的资源进行析构,从而出现了上述的输出。从最后一行输出也可以看出,所管理的资源,已经处于“无效”的状态了(reset()函数新建一个shared_ptr对象,然后执行swap操作)。

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

class Point2

public:
	Point2() : X(0), Y(0) 
	 
		std::cout << "Point2::Point2(), (" << X << "," << Y << ")" << std::endl; 
	

	Point2(int x, int y) : X(x), Y(y) 
	 
		std::cout << "Point2::Point2(int x, int y), (" << X << "," << Y << ")" << std::endl; 
	

	~Point2() 
	 
		std::cout << "Point2::~Point2(), (" << X << "," << Y << ")" << std::endl; 
	

public:
	std::shared_ptr<Point2> Add(const Point2* rhs) 
	 
		X += rhs->X; 
		Y += rhs->Y; 
		return std::shared_ptr<Point2>(this); 
	

private:
	int X;
	int Y;
;

void TestPoint2Add()

	std::cout << endl << "TestPoint2Add() >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" << std::endl;
	std::shared_ptr<Point2> p1(new Point2(2, 2));
	std::shared_ptr<Point2> p2(new Point2(3, 3));

	p2.swap(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)

        从输出来看,哪怕使用了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 std::enable_shared_from_this<Point3>

public:
	Point3() : X(0), Y(0) 
	 
		std::cout << "Point3::Point3(), (" << X << "," << Y << ")" << std::endl; 
	

	Point3(int x, int y) : X(x), Y(y) 
	 
		std::cout << "Point3::Point3(int x, int y), (" << X << "," << Y << ")" << std::endl; 
	

	~Point3() 
	 
		std::cout << "Point3::~Point3(), (" << X << "," << Y << ")" << std::endl; 
	

public:
	std::shared_ptr<Point3> Add(const Point3* rhs) 
	 
		X += rhs->X; 
		Y += rhs->Y; 
		return shared_from_this(); 
	

private:
	int X;
	int Y;
;

void TestPoint3Add()

	std::cout << endl << "TestPoint3Add() >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" << std::endl;
	std::shared_ptr<Point3> p1(new Point3(2, 2));
	std::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_ptr_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又引入了注意事项:

        (1)若要在内部传递this,请考虑从enable_shared_from_this继承。

        (2)若从enable_shared_from_this继承,则类对象必须要shared_ptr接管。

        (3)如果要使用智能指针,那么就要保持一致,统统使用智能指针,尽量较少raw pointer裸指针的使用。

 

        最后,再做一个总结:

       (1)C++没有垃圾收集,资源管理需要自己来做

       (2)智能指针可以部分解决资源管理的工作,但是不是万能的

       (3)使用智能指针的时候,每个shared_ptr对象都应该有一个名字;也就是避免在一个表达式内做多个资源的初始化

       (4)避免使用shared_ptr的交叉引用;使用weak_ptr打破交叉

       (5)使用enable_shared_from_this机制来把this从类内部传递出来

       (6)资源管理保持统一风格,要么使用智能指针,要么就全部自己管理裸指针


以上是关于shared_ptr的一些尴尬的主要内容,如果未能解决你的问题,请参考以下文章

智能指针不是解决内存泄漏的万能良药

智能指针不是解决内存泄漏的万能良药

机票搭售暴露了OTA们的无奈,服务费或是一剂良药

Xpath—解决这个问题的良药

shared_ptr的作用

每天一剂开发良药