shared_ptr详解

最近遇到了智能指针shared_ptr,发现对其并不是很了解,所以参考了几篇博客,特此新写一篇进行总结一下。由于C++标准库已经将std::shared_ptr引入,而且和boost::shared_ptr区别不大,所以并没有对参考的博客进行区分。

介绍

boost库中对shared_ptr说明:An enhanced relative of scoped_ptr with reference counted copy semantics. The object pointed to is deleted when the last shared_ptr pointing to it is destroyed or reset.

先来看例程:

#include <boost/shared_ptr.hpp>
#include <iostream>
using namespace std;

class X
{
public:
    X()
    {
        cout << "X ..." << endl;
    }
    ~X()
    {
        cout << "~X ..." << endl;
    }
};

int main(void)
{
    cout << "Entering main ..." << endl;
    boost::shared_ptr<X> p1(new X);
    cout << p1.use_count() << endl;
    boost::shared_ptr<X> p2 = p1;
    //boost::shared_ptr<X> p3;
    //p3 = p1;

    cout << p2.use_count() << endl;
    p1.reset();
    cout << p2.use_count() << endl;
    p2.reset();
    cout << "Exiting main ..." << endl;
    return 0;
}

运行结果:
shared_ptr详解_第1张图片
图示上述程序的过程也就是:

再深入一点,看源码,shared_ptr 的实现 比 scoped_ptr 要复杂许多,涉及到多个类,下面就不贴完整源码,看下面的类图:

执行 boost::shared_ptr p1(new X); 这一行之后:

而执行 p1.use_count(); 先是 pn.use_count(); 接着 pi_ != 0? pi_->use_count(): 0; return use_count_; 即返回1.

接着执行 boost::shared_ptr p2 = p1;

本想跟踪shared_ptr 的拷贝构造函数,在当行设置断点后F11直接跳过了,说明是shared_ptr类没有实现拷贝构造函数,使用的是编译器默认的拷

贝构造函数,那如何跟踪呢?如果你的C++基础比较好,可以想到拷贝构造函数跟构造函数一样,如果有对象成员是需要先构造对象成员的(这一点也可以从调用堆栈上看出),故可以在shared_count 类的拷贝构造函数设置断点,然后就可以跟踪进去,如下的代码:

// shared_count
shared_count(shared_count const &r): pi_(r.pi_)  // nothrow
{
    if( pi_ != 0 ) pi_->add_ref_copy();
}
// sp_counted_base
void add_ref_copy()
{
    BOOST_INTERLOCKED_INCREMENT( &use_count_ );
}

故p2.pn.pi_ 也指向 唯一的一个 sp_counted_impl_p 对象,且use_count_ 增1.再者,shared_ptr 类的默认拷贝构造函数是浅拷贝,故现在p2.px 也指向 X.由于p2 和 p1 共享一个sp_counted_impl_p 对象,所以此时无论打印p2.use_count(); 还是 p1.use_count(); 都是2。

接着执行p1.reset();

// shared_ptr
void reset() // never throws in 1.30+
{
    this_type().swap(*this);
}
void swap(shared_ptr<T> &other)  // never throws
{
    std::swap(px, other.px);
    pn.swap(other.pn);
}

this_type() 构造一个临时对象,px = 0, pn.pi_ = 0; 然后swap交换p1 与 临时对象的成员,即现在p1.px = 0; p1.pn.p1_ = 0; 如上图。

reset 函数返回,临时对象需要析构,但跟踪时却发现直接返回了,原因跟上面的一样,因为shared_ptr 没有实现析构函数,调用的是默认的析构函数,与上面拷贝函数同样的道理,可以在shared_count 类析构函数设置断点,因为pn 是对象成员,故析构函数也会被调用。如下代码:

//shared_count
~shared_count() // nothrow
{
    if( pi_ != 0 ) pi_->release();
}

// sp_counted_base
void release() // nothrow
{
    if( BOOST_INTERLOCKED_DECREMENT( &use_count_ ) == 0 )
    {
        dispose();
        weak_release();
    }
}

现在use_count_ 减为1,但还不为0,故 dispose(); 和 weak_release(); 两个函数没有被调用。当然此时打印 p2.use_count() 就为1 了。

最后 p2.reset(); 跟p1.reset(); 同样的流程,只不过现在执行到release 时,use_count_ 减1 为0;需要继续执行dispose(); 和

weak_release(); 如下代码:

//sp_counted_impl_p
virtual void dispose() // nothrow
{
boost::checked_delete( px_ );
}
//sp_counted_base
void weak_release() // nothrow
{
if( BOOST_INTERLOCKED_DECREMENT( &weak_count_ ) == 0 )
{
destroy();
}
}

virtual void destroy() // nothrow
{
delete this;
}

在check_delete 中会 delete px_; 也就是析构 X。接着因为weak_count_ 减1 为0, 故执行destroy(); 函数里面delete this; 即析构自身(sp_counted_impl_p 对象是在堆上分配的)。

说到这里,我们也可以明白,即使最后没有调用p2.reset(); 当p2 栈上对象生存期到, 需要调用shared_ptr 类析构函数,进而调用shared_count 类析构函数,所以执行的结果也是跟reset() 一样的,只不过少了临时对象this_type()的构造。

总结一下:
和前面介绍的boost::scoped_ptr相比,boost::shared_ptr可以共享对象的所有权,因此其使用范围基本上没有什么限制(还是有一些需要遵循的使用规则,下文中介绍),自然也可以使用在stl的容器中。另外它还是线程安全的,这点在多线程程序中也非常重要。
boost::shared_ptr并不是绝对安全,下面几条规则能使我们更加安全的使用boost::shared_ptr:

  1. 避免对shared_ptr所管理的对象的直接内存管理操作,以免造成该对象的重释放
  2. shared_ptr并不能对循环引用的对象内存自动管理(这点是其它各种引用计数管理内存方式的通病)。
  3. 不要构造一个临时的shared_ptr作为函数的参数。

详见 http://www.boost.org/doc/libs/1_52_0/libs/smart_ptr/shared_ptr.htm

如下列bad 函数内 的代码则可能导致内存泄漏:

void f(shared_ptr<int>, int); int g();

    void ok() {
        shared_ptr<int> p(new int(2));
        f(p, g()); }

    void bad() {
        f(shared_ptr<int>(new int(2)), g()); }

如bad 函数内,假设先构造了堆对象,接着执行g(), 在g 函数内抛出了异常,那么由于裸指针还没有被智能指针接管,就会出现内存泄漏。

缺点

1.环状的链式结构可能会形成内存泄露

如上总结shared_ptr 时说到引用计数是一种便利的内存管理机制,但它有一个很大的缺点,那就是不能管理循环

#include <boost/shared_ptr.hpp>
#include <iostream>
using namespace std;

class Parent;
class Child;
typedef boost::shared_ptr<Parent> parent_ptr;
typedef boost::shared_ptr<Child> child_ptr;

class Child
{
public:
    Child()
    {
        cout << "Child ..." << endl;
    }
    ~Child()
    {
        cout << "~Child ..." << endl;
    }
    parent_ptr parent_;
};

class Parent
{
public:
    Parent()
    {
        cout << "Parent ..." << endl;
    }
    ~Parent()
    {
        cout << "~Parent ..." << endl;
    }
    child_ptr child_;
};

int main(void)
{
    parent_ptr parent(new Parent);
    child_ptr child(new Child);
    parent->child_ = child;
    child->parent_ = parent;

    return 0;
}

例子,运行程序可以发现Child 和 Parent 构造函数各被调用一次,但析构函数都没有被调用。由于Parent和Child对象互相引用,
shared_ptr详解_第2张图片

它们的引用计数最后都是1,不能自动释放,并且此时这两个对象再无法访问到。这就引起了内存泄漏

其中一种解决循环引用问题的办法是 手动打破循环引用,如在return 0; 之前加上一句 parent->child_.reset(); 此时
shared_ptr详解_第3张图片
当栈上智能指针对象child 析构,Child 对象引用计数为0,析构Chlid 对象,它的成员parent_ 被析构,则Parent 对象引用计数减为1,故当栈上智能指针对象parent 析构时,Parent 对象引用计数为0,被析构。

但手动释放不仅麻烦而且容易出错,这里主要介绍一下弱引用智能指针 weak_ptr 的用法,下面是简单的

namespace boost
{
    template<typename T> class weak_ptr
    {
    public:
        template <typename Y>
        weak_ptr(const shared_ptr<Y> &r);
        weak_ptr(const weak_ptr &r);
        template<class Y>
        weak_ptr &operator=( weak_ptr<Y> && r );
        template<class Y>
        weak_ptr &operator=(shared_ptr<Y> const &r);
        ~weak_ptr();
        bool expired() const;
        shared_ptr<T> lock() const;
    };
  }

上面出现了 && 的用法,在这里并不是逻辑与的意思,而是C++ 11中的新语法,如下解释:

&& is new in C++11, and it signifies that the function accepts an RValue-Reference – that is, a reference to an argument that is about to be destroyed.

两个常用的功能函数:expired()用于检测所管理的对象是否已经释放;lock()用于获取所管理的对象的强引用智能指针。

强引用与弱引用:

  • 强引用,只要有一个引用存在,对象就不能释放
  • 弱引用,并不增加对象的引用计数(实际上是不增加use_count_,会增加weak_count_);但它能知道对象是否存在 通过weak_ptr访问对象的成员的时候,要提升为shared_ptr。如果存在,提升为shared_ptr(强引用)成功 ;如果不存在,提升失败

对于上述的例子,只需要将Parent 类里面的成员定义改为如下,即可解决循环引用问题:

class Parent
{
public:
    boost::weak_ptr<parent> child_;
};

shared_ptr详解_第4张图片
因为此例子涉及到循环引用,而且是类成员引用着另一个类,涉及到两种智能指针,跟踪起来难度很大,我也没什么心情像分析
shared_ptr 一样画多个图来解释流程,这个例子需要解释的代码远远比shared_ptr 多,这里只是解释怎样使用,有兴趣的朋友自
己去分析一下。

下面再举个例子说明lock() 和 expired() 函数的用法:

#include <boost/shared_ptr.hpp>
#include <boost/weak_ptr.hpp>
#include <boost/scoped_array.hpp>
#include <boost/scoped_ptr.hpp>
#include <iostream>
using namespace std;

class X
{
public:
    X()
    {
        cout << "X ..." << endl;
    }
    ~X()
    {
        cout << "~X ..." << endl;
    }

    void Fun()
    {
        cout << "Fun ..." << endl;
    }
};
int main(void)
{
    boost::weak_ptr<X> p;
    boost::shared_ptr<X> p3;
    {
        boost::shared_ptr<X> p2(new X);
        cout << p2.use_count() << endl;
        p = p2;
        cout << p2.use_count() << endl;

        /*boost::shared_ptr<X> */
        p3 = p.lock();
        cout << p3.use_count() << endl;
        if (!p3)
            cout << "object is destroyed" << endl;
        else
            p3->Fun();
    }
    /*boost::shared_ptr<X> p4 = p.lock(); if (!p4) cout<<"object is destroyed"<<endl; else p4->Fun();*/

    if (p.expired())
        cout << "object is destroyed" << endl;
    else
        cout << "object is alived" << endl;

    return 0;
}

从输出可以看出,当p = p2; 时并未增加use_count_,所以p2.use_count() 还是返回1,而从p 提升为 p3,增加了use_count_, p3.use_count() 返回2;出了大括号,p2 被析构,use_count_ 减为1,程序末尾结束,p3 被析构,use_count_ 减为0,X 就被析构了。

2.多线程环境下使用代价更大。

因为share_ptr内部有两个数据成员,一个是指向对象的指针 ptr,另一个是 ref_count 指针,指向堆上的 ref_count 对象,读写操作不能原子化,(具体的结构图可以查看 陈硕的文章《为什么多线程读写 shared_ptr 要加锁?》)所以多线程下要么加锁,要么小心翼翼使用share_ptr。

class Test
{
public:
    Test() {}
    ~Test() {}
    // ...
protected:
private:
};

void func(std::shared_ptr test_ptr)
{
    // 大量使用test_ptr
    std::shared_ptr temp_ptr = test_ptr;
}

int _tmain(int argc, _TCHAR* argv[])
{
    std::shared_ptr sp(new Test());
    boost::thread th1(std::bind(&func, sp));
    boost::thread th2(std::bind(&func, sp));
    th1.join();
    th2.join();
    return 0;
}

代码不知道什么时候可能就宕了,而且不容易找到问题,这个时候你就得硬看代码了。

你也可以通过使用weak_ptr来解决这个问题,例如上述例子可以修改为:

class Test
{
public:
    Test() {}
    ~Test() {}
    // ...
protected:
private:
};

void func(std::weak_ptr test_ptr)
{
    // 大量使用test_ptr
    std::weak_ptr temp_ptr = test_ptr;
}
int _tmain(int argc, _TCHAR* argv[])
{
    std::shared_ptr sp(new Test());
    std::weak_ptr wp(sp);
    boost::thread th1(std::bind(&func, wp));
    boost::thread th2(std::bind(&func, wp));
    th1.join();
    th2.join();

   return 0;
}

weak_ptr是一种可构造可赋值的不增加引用计数来管理share_ptr的智能指针,它可以非常方便的通过weak_ptr.lock()转为share_ptr,通过weak_ptr.expired()来判断智能指针是否被释放,还是非常方便的。

3.share_ptr包装this的时候使用enable_shared_from_this

class Test
{
public:
    Test() {}

    ~Test() {}

    std::shared_ptr get_ptr()
    {
        return std::shared_ptr(this);
    }

    // ...
protected:
private:
};

int _tmain(int argc, _TCHAR* argv[])
{
    Test t;
    std::shared_ptr t_ptr(t.get_ptr());
    return 0;
}

这样就会发生析构两次的问题,可以使用enable_shared_from_this来做共享,上面例子修改为

class Test : public std::enable_shared_from_this<Test>
{
public:
    Test() {}

    ~Test() {}

    std::shared_ptr get_ptr()
    {
        return shared_from_this();
    }

    // ...
protected:
private:
};

int _tmain(int argc, _TCHAR* argv[])
{
    std::shared_ptr t_ptr(new Test());
    t_ptr->get_ptr();

    return 0;
}

4.share_ptr多次引用同一数据会导致内存多次释放

int _tmain(int argc, _TCHAR* argv[])
{
    int* int_ptr = new int[100];
    std::shared_ptr s_int_ptr1(int_ptr);

    // do something

    std::shared_ptr s_int_ptr2(int_ptr);

    return 0;
}

而且C++之父对share_ptr的初衷是:“shared_ptr用于表示共享拥有权。然而共享拥有权并不是我的初衷。在我看来,一个更好的办法是为对象指明拥有者并且为对象定义一个可以预测的生存范围。”

为什么shared_ptr 中的 ref_count 也有指向 Foo 的指针?

shared_ptr详解_第5张图片
shrared_ptr数据结构

shared_ptr sp(new Foo) 在构造 sp 的时候捕获了 Foo 的析构行为。实际上 shared_ptr.ptr 和 ref_count.ptr 可以是不同的类型(只要它们之间存在隐式转换),这是 shared_ptr 的一大功能。分 3 点来说:

1. 无需虚析构

假设 Bar 是 Foo 的基类,但是 Bar 和 Foo 都没有虚析构。

shared_ptr<Foo> sp1(new Foo); // ref_count.ptr 的类型是 Foo*

shared_ptr<Bar> sp2 = sp1; // 可以赋值,自动向上转型(up-cast)

sp1.reset(); // 这时 Foo 对象的引用计数降为 1

此后 sp2 仍然能安全地管理 Foo 对象的生命期,并安全完整地释放 Foo,因为其 ref_count 记住了 Foo 的实际类型。

2. shared_ptr 可以指向并安全地管理(析构或防止析构)任何对象

muduo::net::Channel class 的 tie() 函数就使用了这一特性,防止对象过早析构,见书 7.15.3 节。

shared_ptr<Foo> sp1(new Foo); // ref_count.ptr 的类型是 Foo*

shared_ptr<void> sp2 = sp1; // 可以赋值,Foo* 向 void* 自动转型

sp1.reset(); // 这时 Foo 对象的引用计数降为 1

此后 sp2 仍然能安全地管理 Foo 对象的生命期,并安全完整地释放 Foo,不会出现 delete void* 的情况,因为 delete 的是 ref_count.ptr,不是 sp2.ptr。

3. 多继承。

假设 Bar 是 Foo 的多个基类之一,那么:

shared_ptr<Foo> sp1(new Foo);

shared_ptr<Bar> sp2 = sp1; // 这时 sp1.ptr 和 sp2.ptr 可能指向不同的地址,因为 Bar subobject 在 Foo object 中的 offset 可能不为0。

sp1.reset(); // 此时 Foo 对象的引用计数降为 1

但是 sp2 仍然能安全地管理 Foo 对象的生命期,并安全完整地释放 Foo,因为 delete 的不是 Bar*,而是原来的 Foo*。换句话说,sp2.ptr 和 ref_count.ptr 可能具有不同的值(当然它们的类型也不同)。

参考

从零开始学C++之boost库(一):详解 boost 库智能指针(scoped_ptr 、shared_ptr 、weak_ptr 源码分析)

shared_ptr智能指针源码剖析

为什么多线程读写 shared_ptr 要加锁?

share_ptr的几个注意点

你可能感兴趣的:(C++,智能指针)