C++ Primer 学习笔记_56_STL剖析(十一)(原boost库):详解智能指针(unique_ptr(原scoped_ptr) 、shared_ptr 、weak_ptr源码分析)

注意:现在boot库已经归入STL库,用法基本上还和boost类似

在C++11中,引入了智能指针。主要有:unique_ptr, shared_ptr, weak_ptr。

这3种指针组件就是采用了boost里的智能指针方案。很多有用过boost智能指针的朋友,很容易地就能发现它们之间的关间:

std boost 功能说明
unique_ptr scoped_ptr 独占指针对象,并保证指针所指对象生命周期与其一致
shared_ptr shared_ptr 可共享指针对象,可以赋值给shared_ptr或weak_ptr。

指针所指对象在所有的相关联的shared_ptr生命周期结束时结束,是强引用。

weak_ptr weak_ptr 它不能决定所指对象的生命周期,引用所指对象时,需要lock()成shared_ptr才能使用。

C++11将boost里的这一套纳入了标准。

一、boost 智能指针

智能指针是利用RAII(Resource Acquisition Is Initialization:资源获取即初始化)来管理资源。关于RAII的讨论可以参考前面的《40_面向对象编程--虚函数与多态(六)》。在使用boost库之前应该先下载后放在某个路径,并在VS
包含目录中添加。下面是boost库里面的智能指针:

二、unique_ptr<T>(原来scoped_ptr<T>)

1、示例

#include <iostream>
#include <memory>
using namespace std;

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

int main(void)
{
    cout << "Entering main ..." << endl;
    {
        unique_ptr<X> pp(new X);

        //boost::unique_ptr<X> p2(pp); //Error:所有权不能转移
    }
    cout << "Exiting main ..." << endl;

    return 0;
}

运行结果:

2、源码分析

来稍微看一下scoped_ptr的简单定义:

namespace boost
{

    template<typename T> class scoped_ptr : noncopyable
    {
    private:

        T *px;

        scoped_ptr(scoped_ptr const &);
        scoped_ptr &operator=(scoped_ptr const &);

        typedef scoped_ptr<T> this_type;

        void operator==( scoped_ptr const & ) const;
        void operator!=( scoped_ptr const & ) const;
    public:
        explicit scoped_ptr(T *p = 0);
        ~scoped_ptr();

        explicit scoped_ptr( std::auto_ptr<T> p ): px( p.release() );
        void reset(T *p = 0);

        T &operator*() const;
        T *operator->() const;
        T *get() const;

        void swap(scoped_ptr &b);
    };

    template<typename T>
    void swap(scoped_ptr<T> &a, scoped_ptr<T> &b);
}

与《40_面向对象编程--虚函数与多态(六)》的auto_ptr类似,内部也有一个T*
px; 成员 ,智能指针对象pp 生存期到了,调用析构函数,在析构函数内会delete  px; 当调用reset() 函数时也能够释放堆对象,如何实现的呢?

void reset(T *p = 0)  // never throws
{
    BOOST_ASSERT( p == 0 || p != px ); // catch self-reset errors
    this_type(p).swap(*this);
}
void swap(scoped_ptr &b)  // never throws
{
    T *tmp = b.px;
    b.px = px;
    px = tmp;
}

typedef scoped_ptr<T> this_type;  当调用pp.reset(),reset 函数构造一个临时对象,它的成员px=0, 在swap 函数中调换 pp.px  与 (this_type)(p).px,
即现在pp.px = 0; //解绑临时对象接管了裸指针(即所有权可以交换),reset 函数返回,栈上的临时对象析构,调用析构函数,进而delete px;

另外拷贝构造函数和operator= 都声明为私有,故所有权不能转移,且因为容器的push_back 函数需要调用拷贝构造函数,故也不能将scoped_ptr 放进vector,这点与auto_ptr
相同(不能共享所有权)。此外,还可以使用 auto_ptr 对象 构造一个scoped_ptr 对象:scoped_ptr( std::auto_ptr<T> p ): px( p.release() );

由于scoped_ptr是通过delete来删除所管理对象的,而数组对象必须通过deletep[]来删除,因此boost::scoped_ptr是不能管理数组对象的,如果要管理数组对象需要使用boost::scoped_array类。

boost::scoped_ptr和std::auto_ptr的功能和操作都非常类似,如何在他们之间选取取决于是否需要转移所管理的对象的所有权(如是否需要作为函数的返回值)。如果没有这个需要的话,大可以使用boost::scoped_ptr,让编译器来进行更严格的检查,来发现一些不正确的赋值操作。

三、shared_ptr<T>

1、示例

#include <iostream>
#include <memory>
using namespace std;

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

int main(void)
{
    cout << "Entering main ..." << endl;
    shared_ptr<X> p1(new X);
    cout << p1.use_count() << endl;
    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 的实现 比 scoped_ptr 要复杂许多,涉及到多个类。

2、shared_ptr<T>与auto_ptr<T>

(1)之前我们讲过auto_ptr<T>不能放置vector中

(2)但share_ptr<T>可以放置vector中

#include <iostream>
#include <memory>
#include <vector>
using namespace std;

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

int main(void)
{
    vector<auto_ptr<X> > v;
    auto_ptr<X> p(new X);
    //v.push_back(p);  //Error:auto_ptr调用了=运算符,而push_back传入是const参数,因此不能调用push_back

    vector<shared_ptr<X> > v2;
    shared_ptr<X> p2(new X);
    v2.push_back(p2);
    cout << p2.use_count() << endl;

    return 0;
}

运行结果:

3、总结一下:

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

shared_ptr并不是绝对安全,下面几条规则能使我们更加安全的使用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 函数内抛出了异常,那么由于裸指针还没有被智能指针接管,就会出现内存泄漏。

四、weak_ptr<T>

1、shared_ptr<T>的缺点:循环问题

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

#include <iostream>
#include <memory>
using namespace std;

class Parent;
class Child;
typedef shared_ptr<Parent> parent_ptr;
typedef 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);  //1
    child_ptr child(new Child);     //1
    parent->child_ = child;         //2
    child->parent_ = parent;        //2
    //parent->child_.reset();  //Error:解决析构问题
    return 0;
}

运行结果:

问题:没有调用析构函数

如上述程序的例子,运行程序可以发现Child 和 Parent 构造函数各被调用一次,但析构函数都没有被调用。由于Parent和Child对象互相引用,

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

其中一种解决循环引用问题的办法是 手动打破循环引用,如在return 0; 之前加上一句 parent->child_.reset(); 此时

当栈上智能指针对象child 析构,Child 对象引用计数为0,析构Chlid 对象,它的成员parent_ 被析构,则Parent 对象引用计数减为1,故当栈上智能指针对象parent
析构时,Parent 对象引用计数为0,被析构。

2、弱引用智能指针 weak_ptr<T>

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

强引用与弱引用:

(1)强引用,只要有一个引用存在,对象就不能释放

(2)弱引用,并不增加对象的引用计数(实际上是不增加use_count_, 会增加weak_count_);但它能知道对象是否存在

【1】如果存在,提升为shared_ptr(强引用)成功

【2】如果不存在,提升失败

(3)通过weak_ptr访问对象的成员的时候,要提升为shared_ptr

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

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

#include <iostream>
#include <memory>
using namespace std;

class Parent;
class Child;
typedef shared_ptr<Parent> parent_ptr;
typedef 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;
    }
    weak_ptr<Child> child_;
};

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

运行结果:

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

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

【1】lock():提升为share_ptr

【2】expired():的功能等价于use_count()==0,但更快,表示被观测的资源(也就是shared_ptr的管理的资源)已经不复存在。

#include <iostream>
#include <memory>
using namespace std;

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

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

        /*shared_ptr<X> */p3 = p.lock();
        cout << p3.use_count() << endl;
        if (!p3)
            cout << "object is destroyed" << endl;
        else
            p3->Fun();
    }
    /*
    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 就被析构了。

参考 :

C++ primer 第四版

Effective C++ 3rd

C++编程规范

时间: 2024-12-06 02:40:19

C++ Primer 学习笔记_56_STL剖析(十一)(原boost库):详解智能指针(unique_ptr(原scoped_ptr) 、shared_ptr 、weak_ptr源码分析)的相关文章

C++ Primer 学习笔记_55_STL剖析(十):容器适配器(stack、 queue 、priority_queue)源码浅析与使用示例

七种基本容器:vector.deque.list.set.multiset.map.multimap 一.容器适配器 stack queue priority_queue stack.queue.priority_queue 都不支持任一种迭代器,它们都是容器适配器类型,stack是用vector/deque/list对象创建了一个先进后出容器:queue是用deque或list对象创建了一个先进先出容器:priority_queue是用vector/deque创建了一个排序队列,内部用二叉堆实

C++ Primer 学习笔记_16_类与数据抽象(2)_隐含的this指针

C++ Primer 学习笔记_16_类与数据抽象(2)_隐含的this指针 1.引言 在前面提到过,成员函数具有一个附加的隐含形参,即指向该类对象的一个指针.这个隐含形参命名为this. 2.返回*this 成员函数有一个隐含的附加形参,即指向该对象的指针,这个隐含的形参叫做this指针(编译器自动传递)使用this指针保证了每个对象可以拥有不同数值的数据成员,但处理这些成员的代码可以被所有对象共享.成员函数是只读的代码,由所有对象共享,并不占对象的存储空间,因为this指针指向当前对象,所以

Ext.Net学习笔记22:Ext.Net Tree 用法详解

Ext.Net学习笔记22:Ext.Net Tree 用法详解 上面的图片是一个简单的树,使用Ext.Net来创建这样的树结构非常简单,代码如下: <ext:TreePanel runat="server"> <Root> <ext:Node Text="根节点" Expanded="true"> <Children> <ext:Node Text="节点1" Expand

Ext.Net学习笔记07:Ext.Net DirectMethods用法详解

前面两篇内容中,我们看到了DirectEvents方便调用服务器端方法.DirectEvents调用WebService方法的使用方法,今天我们来看看DirectMethods,这家伙可比DirectEvents更加灵活了,它可以像调用JS方法一样来异步调用服务器端的方法. 使用DirectMethods在JS中调用C#方法 我承认,这个标题有点噱头,其实应该是通过DirectMethods,在JS中通过异步调用的方式执行服务器端的方法. 来看一个例子吧: [DirectMethod] publ

C++ Primer 学习笔记_54_STL剖析(九):迭代器适配器{(插入迭代器back_insert_iterator)、IO流迭代器(istream_iterator、ostream_i

回顾 适配器 1.三种类型的适配器: (1)容器适配器:用来扩展7种基本容器,利用基本容器扩展形成了栈.队列和优先级队列 (2)迭代器适配器:(反向迭代器.插入迭代器.IO流迭代器) (3)函数适配器:函数适配器能够将仿函数和另一个仿函数(或某个值.或某个一般函数)结合起来. [1]针对成员函数的函数适配器 [2]针对一般函数的函数适配器 一.迭代器适配器 1.反向迭代器 2.插入迭代器 3.IO流迭代器 其中反向迭代器,利用正向迭代器实现可以参考以前<46_STL剖析(三)>. 二.插入迭代

C++ Primer 学习笔记_53_STL剖析(八):函数适配器:bind2nd 、mem_fun_ref 、函数适配器应用举例

回顾 五.STL中内置的函数对象 一.适配器 1.三种类型的适配器: (1)容器适配器:用来扩展7种基本容器,利用基本容器扩展形成了栈.队列和优先级队列 (2)迭代器适配器:(反向迭代器.插入迭代器.IO流迭代器) (3)函数适配器:函数适配器能够将仿函数和另一个仿函数(或某个值.或某个一般函数)结合起来. [1]针对成员函数的函数适配器 [2]针对一般函数的函数适配器 二.函数适配器 1.示例 #include <iostream> #include <algorithm> #i

Spring MVC 学习笔记(二):@RequestMapping用法详解

一.@RequestMapping 简介 在Spring MVC 中使用 @RequestMapping 来映射请求,也就是通过它来指定控制器可以处理哪些URL请求,相当于Servlet中在web.xml中配置 <servlet>     <servlet-name>servletName</servlet-name>     <servlet-class>ServletClass</servlet-class> </servlet>

Struts2学习笔记(九):Strut2通用标签详解

本节主要介绍Strus2中的通用标签,主要有<s:url>, <s:set>, <s:push>, <s:if>, <s:elseif>, <s:else>, <s:iterator>, <s:sort>, <s:date>, <s:a>等几个标签的具体用法,仍然采用代码加注释的形式进行说明,希望能对大家有帮助. 刚入门的朋友阅读本文前,请阅读: Struts2学习笔记(六):值栈(va

《Hibernate学习笔记十》:多对多关联关系详解

<Hibernate学习笔记十>:多对多关联关系 前面介绍了一对一.多对一和一对多的关联关系在Hibernate应如何实现,这篇博文就来介绍下最后一种关联关系:多对多.多对多关联关系在我们现实生活中的例子实在是太多太多,最典型的就是老师和学生的例子:一个老师可以教多个学生,而一个学生又可以被多个老师来教. 了解一点数据库的我们都知道,在数据库中表示多对多的关联关系,是借助于中间表来解决的. 如下: 还是和以往的思路一样,每一种关联关系都分为单向关联和双向关联,我们每种都会进行介绍,对于单向和双