您的位置:首页 > 其它

boost::shared_ptr 分析与实现

2013-07-13 17:46 330 查看


boost::shared_ptr 分析与实现(转载)

原文地址:http://blogold.chinaunix.net/u/14337/showart_299314.html

boost::shared_ptr 分析与实现


/************************************************************

* file: shared_ptr

*

* desc: 本文将对boost::shared_ptr作一详细的介绍。 本文介绍的不是用法,而是

* 智能指针的原理,结构以及boost对其的实现. 最后还会给出一个简化了的实现。

*

* author: whiteear

* date: 2007-05-10

* copyright: 任意发布

***********************************************************/

shared_ptr

1 boost 介绍

关于boost的介绍, 网上一大堆。 google一下就行。 作为初学者, 也没有能力去全面介绍它。

2 shared_ptr 介绍

在boost库中, 智能指针并不只shared_ptr一个。同族但有不同的功能目标的还有如下5个:

scoped_ptr

scoped_array

shared_ptr

shared_array

weak_ptr

前面两个, 与标准C++中的智能指针auto_ptr功能基本类似, 不过它不传递所有权, 不可复制。

从其名称就可以看出, 其主要目标就是在小范围, 小作用域中使用,以减少显式的delete, new配对操作,

提高代码的安全性。scoped_ptr是针对指针的版本, 而scoped_array同是专门针对数组的。(记住, 删除

一个指针使用delete p; 而删除一个数组使用delete[] p);

后面两个是本文的重点,它们在所有对象中, 共享所指向实体的所有权。 即只要是指向同一个实体

对象的shared_ptr对象, 都有权操作这个对象,并根据自己产生新的对象,并把所有权共享给新的对象。即它

是满足STL对对象的基本要求可复制,可赋值的。可以与所有的STL容器,算法结合使用。顾名思义, shared_ptr

是针对任意类型的指针的, 而shared_array则是专门针对任意类型的数组的。

最后一个weak_ptr。

3 shared_ptr 分析

3.1 一些思考

C++对内存数据的直接可操作性以及指针的使用,给编程带来了极大的灵活,

但同时也还来了很大的问题。 程序在运行时,如果出现segment fault,

大部分原因在于非法访问了内存。

或者访问了一个越界的地址,或者访问了一个已经失效的指针。我们可以随时、随地的NEW一个对象, 并把它赋值给某个指针。

在以后随时要吧通过此指针访问这个对象。

但问题时NEW出来的对象在内存的堆空间中,并不是函数调用栈中,也就是说一个New出来的对象,从生命期上来说,

是全局的, 只要不Delete, 它就一直存在。 相对的实际的内存是有限的,

我们不能只索取而不释放, 否则终有一刻会内存耗尽,New不出新对象。

问题1: 什么时候释放?

New很简单, Delete也很简单。 一个New配对一个Delete也很简单。但不简单的就是什么时候New, 什么时候Delete。New还好办些,

有需求的时候就可以New, 那什么时候一个New出来的对象不再有用呢?什么时候它该被Delete呢?

对于这个问题, Java采用的时GC的机制,由虚拟机通过一定的算法实现。

但Java的GC机制被人诟病也不是一天两天了。 它太慢,太吃内存了。

C++因为各种各样的原因而没有采用GC, 把这个问题直接丢给了程序员。

问题2: 资源申请即初始化

在C++之父Bjarne Stroustrup老大的巨著《The C++ Programming Language》第14章,

第4节资源管理介绍了“资源申请即初始化”。 利用局部对象管理资源的技术通常被说成是“资源申请即初始化”。

这种技术依赖于构造函数和析构函数的特性, 以及它们与异常机制的关系。

我们知道, 一个对象在超出它的作用域时, 会自动被析构。 那么我们就可以设想, 可不可以将New

出来的对象,托管给另一个对象,让它自动的在超出作用域时, 去释放New出来的对象。 这样我们就可以

高枕无忧的去New, 释放的时机就转嫁给托管对象(事实上转嫁给了编译器,因为它知道什么时候这个对

象超出作用域)。

问题3: 托管的资源

当一个New出来的资源被托管之后, 我们所有针对此资源的操作就应该通过这个代理商来进行。当然我

们也可以绕过这个代理商直接与资源对话。这时候我们是不是又回归到了原始的那种状态, 在得到一部分

灵活性, 直接性的同时, 也失去了代理商所具有的管理功能。

事实上, 考查一下C++的指针就会知道, 指针本身也是一个代理商。 只不过它是效率最高的代理商(也许

是资源厂商自己设置的一级代码吧,

)。 通过指针我们可以间接的访问它所指代的实体对象。 通过*(dereference)

操作可以得到实体, 通过->可以直接访问实体的内容。 抽象这些特征(类似于STL中Iterator的概念),

我们的新的代理也应该具有这样的功能。

问题4: 释放时机的确定

通过回答上面3个问题。我们似乎已经解决了所有的问题。

我们得到了一个代理, 将我们的资源托管给它。通过代理我们以熟悉的方式访问,

操作资源。在合适的时候代理自动帮助我们释放掉不再使用的资源。 一切都很美好!!!

类似下面的代码应该可以很好的工作:

work()

{

resource r = new resource();

proxy p(r);

// use p to do something

}

当work被执行完成时, 资源r自动被p释放掉。 我们得到的益处好象并不多,

只是少写了几个delete语句。

考虑一下,r是在堆栈中的,所以它的生命期是比较长的,

我们如果想在work之外使用r呢? 如果同时有多个p的复本时呢?

在那一个p被析构时真正的释放这个唯一的资源呢?

考虑一下现实生活中, 一个资源厂商通常会有多家代理,

任意一家代理都可以处理这个资源。 考虑一下,

如果没有任何一个代理愿意去代理这个资源厂商的资源,是不是表明这个资源并不被市场所接收,即它是无用的,应该被淘汰的。

这个时候厂商就应该去相关部门申请耍破产了。

我们的代理也应该这样,它自动的统计有多少个对当前资源的代理,

当没有代理去托管这个资源时, 就应该释放这个资源(delete)。

问题5: 同步

现在多线程编程已经非常普遍, 大家都喜欢一心二用, 三用, 甚至N用。

如果采用上面4个问题的回答来解决资源管理的问题。那么对第4个问题的回答就还缺少点。

当我们使用一个记数器去记录当前对此资源的代理有多少个时,

这个记数器本身是不是也成为一个关键性的资源!!!

对记数器的操作特别是有新代理加入或某个代理析构时, 都要对记数进行修改。

随着多线程的加入,就必须要保证记数器值在所有的代理中都是一样的, 是同步的。 否则后果真是难以想象。

因此在我们的代理设计中, 必须要对记数器部分作线程同步。

3.2 boost::shared_ptr的解决方案

上面我们分析了shared_ptr问题的产生,以及可能的实现方式和应该注意的问题。 下面我们来看看boost::shared_ptr是如何实现上面的思想的。

3.2.1 文件结构

定义boost::shared_ptr主要涉及到以下文件

shared_ptr.hpp

detail/shared_count.hpp

detail/sp_counted_base.hpp

detail/sp_counted_base_pt.hpp

detail/sp_counted_base_win32.hpp

detail/sp_counted_base_nt.hpp

detail/sp_counted_base_pt.hpp

detail/sp_counted_base_gcc_x86.hpp

...

detail/sp_counted_base_impl.hpp

涉及的类主要有以下几个:

shared_ptr

shared_count

sp_counted_base

sp_counted_base_impl

其中

shared_ptr定义在shared_ptr.hpp中

share_count定义在shared_count.hpp中

sp_counted_base定义在sp_counted_base_XXX.hpp中。

XXX指代针对特定平台的实现。

sp_counted_base_impl定义在sp_counted_base_impl.hpp中

3.2.2 类功能

sp_counted_base 是问题4中, 记数器的实现。

针对不同的平台使用了不同的同步机制。 如pt是针对linux, unix平台使用pthread接口进行同步的。

win32是针对windows平台, 使用InterlockedIncrement, InterlockedDecrement机制。

gcc_x86是针对AMD64硬件平台的, 内部使用汇编指令实现了atomic_increment,

atomic_decrement。

sp_counted_base_impl 是个模板, 它继承自sp_counted_base,

主要实现了父类中一个纯虚函数dispose。具体的由它来负责在记数值到0(即没有代理时)释放所托管的资源。

shared_count是个类模板。它存在的意义在于和代理类shared_ptr同生共死,

在构造函数中生成记数器,在代理的传递过程中驱动记数器增减。

shared_ptr是个类模板。 它是托管资源的代理类, 所有对资源的操作,

访问都通过它来完成。

下图展示了它们之间的相互关系


figure01 shared_ptr.jpeg

3.2.3 类详细介绍

shared_ptr 类模板。 它是我们直接使用的代理类。

两个属性

pn : shared_count 记数器类。 shared_ptr完全操作pn的生命期,

在构造时构造它,在析构时自动析构它。

这些都是利用构造与析构函数的特性自动完成的。

template<class Y>

explicit shared_ptr( Y * p ): px( p ), pn( p ) // Y must be

complete

{

detail::sp_enable_shared_from_this( pn, p, p );

}

px : T* 它是代理的资源的指针。 所有针对资源的操作*,

->都直接使用它来完成。

reference operator* () const // never throws

{

BOOST_ASSERT(px != 0);

return *px;

}

T * operator-> () const // never throws

{

BOOST_ASSERT(px != 0);

return px;

}

两个方法:

operator= 完成代理的赋值传递。

通过这样,就可以有多个代理同时托管同一个资源。在众多的代理中, 它们共享所托管资源的操作权。

shared_ptr & operator=(shared_ptr const & r) // never throws

{

px = r.px;

pn = r.pn; // shared_count::op= doesn't throw

return *this;

}

T* get() 资源原始位置的获取。通过这个方法,我们可以直接访问到资源,

并可以对它进行操作。

T * get() const // never throws

{

return px;

}

user_count 返回当前资源的代理个数,即有多少个对些资源的引用。

long use_count() const // never throws

{

return pn.use_count();

}

为了方便操作,并完全模拟原生指针的行为,boost::shared_ptr还定义了大量的其它操作函数。

shared_count类 记数器的包装。

一个属性:

pi : sp_counted_base * 它指向真正的记数器。

构造函数:

template<class Y> explicit shared_count( Y * p ): pi_( 0 )

#if defined(BOOST_SP_ENABLE_DEBUG_HOOKS)

, id_(shared_count_id)

#endif

{

#ifndef BOOST_NO_EXCEPTIONS

try

{

pi_ = new sp_counted_impl_p<Y>( p );

}

catch(...)

{

boost::checked_delete( p );

throw;

}

#else

pi_ = new sp_counted_impl_p<Y>( p );

if( pi_ == 0 )

{

boost::checked_delete( p );

boost::throw_exception( std::bad_alloc() );

}

#endif

}

当使用一个资源指针来构造一个shared_count时,它知道针对此资源要生成一个代理。

所以生成一个记数器pi。如果在构造记数器的过程中出现任何异常行为,即记数器资源的初始化未成功完成时, 就释放掉资源。

(这就是资源申请即初始化, 对于一个资源管理类来说,要不所有资源申请成功, 要不构造失败)

operator= 赋值函数。

在shared_ptr被赋值的时候,会调用它。经过复制后一个shared_ptr变成两个, 所以要对记数器进行增加。

同时如果被赋值的代理原有托管的资源将被释放。

shared_count & operator= (shared_count const & r) // nothrow

{

sp_counted_base * tmp = r.pi_;

if( tmp != pi_ )

{

if( tmp != 0 ) tmp->add_ref_copy();

if( pi_ != 0 ) pi_->release();

pi_ = tmp;

}

return *this;

}

析构函数 析构函数在shared_ptr超出作用域被析构时自动调用。

每析构一个shared_ptr,则代理数就少一个, 所以调用记数器的release函数,

减少记数值。

~shared_count() // nothrow

{

if( pi_ != 0 ) pi_->release();

#if defined(BOOST_SP_ENABLE_DEBUG_HOOKS)

id_ = 0;

#endif

}

sp_counted_base 记数器类。

这是一个普通类,没有被模板化。它定义了记数器公用的所有操作, 并实现了同步。

两个属性

use_count 记录当前的代理数目。

mutex 互斥锁(在不同平台的实现中,是不同的类型。以linux为例,

是pthread_mutex_t)

构造函数 初始化互斥锁和记数

sp_counted_base(): use_count_( 1 ), weak_count_( 1 )

{

#if defined(__hpux) && defined(_DECTHREADS_)

pthread_mutex_init( &m_, pthread_mutexattr_default );

#else

pthread_mutex_init( &m_, 0 );

#endif

}

析构函数 释放互斥锁

virtual ~sp_counted_base() // nothrow

{

pthread_mutex_destroy( &m_ );

}

同步增加记数值,即根据原有的复制构造出或通过赋值产生新的代理时记数加1

void add_ref_copy()

{

pthread_mutex_lock( &m_ );

++use_count_;

pthread_mutex_unlock( &m_ );

}

释放函数,在代理超界被析构时使用。 它首先减少引用记数,

然后查看记数值,如果为0, 则调用dispose释放资源。并销毁掉本身

void release() // nothrow

{

pthread_mutex_lock( &m_ );

long new_use_count = --use_count_;

pthread_mutex_unlock( &m_ );

if( new_use_count == 0 )

{

dispose();

weak_release();

}

}

释放托管资源的函数。 为纯虚函数, 要求在子类中实现。

只所以不同这个类中实现,是由于要托管的类型是未知的,如果要实现则要记录这个资源,则此类不得不模板化。

将此功能分离到子类中的好处就非常明显。子类可以直接利用父类的功能,只要模板化一下,实现一个函数就行。

这样会大大加快编译速度。

virtual void dispose() = 0; // nothrow

销毁函数。 在shared_count中的pi即记数器是New出来的。

所以在适当的时候要销毁掉。 而这个时机就是资源被释放的时候。

即记数器完成了对资源的管理, 同时完成了对自身的管理。

// destroy() is called when weak_count_ drops to zero.

virtual void destroy() // nothrow

{

delete this;

}

sp_counted_base_impl 类模板。

一个属性

px : T* 记录各种类型的资源。

实现了父类的dispose函数。 实现对资源的释放。

virtual void dispose() // nothrow

{

#if defined(BOOST_SP_ENABLE_DEBUG_HOOKS)

boost::sp_scalar_destructor_hook( px_, sizeof(X), this );

#endif

boost::checked_delete( px_ );

}

3.2.4 工作流程

1 用户申请一个资源p

p = new Something();

2 用户将资源p托管给shared_ptr;

shared_ptr(p);

2.1 shared_ptr 构造函数调用,构造出记数对象shared_count

shared_ptr() : px(p), pn(p)

2.1.1 shared_count 构造函数调用,构造出户数器对象

pi = new sp_counted_base_impl(p);

3 用户使用shared_ptr对象sp, 就象直接使用资源一样

*sp;

sp->func();

..

4 如果用户要传递sp

shared_ptr<T> spt = sp;

4.1 调用shared_ptr.operator=

4.1.1 调用shared_count.operator=

4.1.1.1 调用sp_counted_base复制构造

4.1.2 调用sp_counted_base.add_ref_copy, 增加引用记数

5 如果sp超出了它本身的作用域, 则调用析构函数

5.1 调用shared_count.~shared_count 析构记数

5.1.1 调用sp_counted_base.release, 减少引用记数

如果引用记数已经减少到0,则

调用sp_counted_base_impl.dispose, 销毁资源。

调用sp_counted_base.destroy, 销毁自身。

4 实践

上面是对boost::shared_ptr的分析。 整个过程就是对smart

pointer这种观点的思考过程。 基本上是想到什么记录什么。 光说不练不行,

根据对boost::shared_ptr的分析, 本人仿作了一个shared_ptr,

可以看成是对boost::shared_ptr的一个简略版。 有没有用先不说, 反正都是写程序。

全部原代码见附件shared_ptr.tar.gz

5 参考

[1] boost www.boost.org

[2] shared_ptr性能分析

http://blog.csdn.net/ralph623/archive/2005/08/18/458414.aspx

[3] smart pointer in boost

http://www.ddj.com/dept/cpp/184401507?pgno=1

[4] the new C++: smart pointer http://www.ddj.com/dept/cpp/184403837

[5] C++中的智能指针

http://www.stlchina.org/twiki/bin/view.pl/Main/BoostProgrammSmartPoint
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: