分享

八 智能指针类

 Fredanf 2012-11-13
 

八 智能指针类

智能指针类
auto_ptr
    98 C++标准只规定了一个智能指针,就是
template <class Type>
class auto_ptr
    下面是示例代码:
#include <memory>
using namespace std;


int _tmain(int argc, _TCHAR* argv[])
{
    auto_ptr<string> spString(new string("hello,world"));
    size_t length=spString->length();
    return 0;
}
    模板参数Type是auto_ptr管理的类型,auto_ptr<string>的构造函数接受类型为string*的指针,析构函数将用delete释放该指针。
    auto_ptr模板类提供了get函数用来获得裸指针T*,也可以通过reset来重新接受一个新的指针,同时释放内部的指针。
    auto_ptr模板类提供了operator ->用来模仿指针的调用函数行为,提供了operator*用来模仿指针的间接引用功能。
    如果想手动回收指针,可以调用release函数,该函数将返回内部管理的裸指针并且
内部成员置为0。
    auto_ptr经过专门的设计,使它拥有一个奇特的功能,所有权转移,也是很多人的烦恼之源。下面是所有权转移导致的几种烦恼:
    1)auto_ptr不能用于STL容器,这是因为当我们使用像sort这样的函数对容器排序时,该函数内部会创建auto_ptr的副本,所有权转移到副本上,而容器中的auto_ptr元素已经不再管理原来的指针,所以这是错误的用法。大部分的auto_ptr的实现中将拷贝构造函数和拷贝赋值函数的参数故意定为非常量引用,而大多数容器插入函数都接受常量引用,所以,像下面的代码,通常是不能编译通过的。
    vector<auto_ptr<char> > v;
    auto_ptr<char> spBuffer(new char[100]);
    v.push_back(spBuffer); 
    2)如果你使用auto_ptr来管理你的成员指针,小心的处理对象拷贝机制

class A
{
public:
    A(string const& str):_spStr(new string(str))
    {
    }

private:
    auto_ptr<string> _spStr;
};


int _tmain(int argc, _TCHAR* argv[])
{
    string str("hello,world");
    A a1(str);
    A a2(a1);
    return 0;
}
    当A a2(a1)执行后,a2._spStr将拥有指针所有权,而a1._spStr将释放指针所有权,这可能是你不想要的情况。你可以将拷贝构造函数和拷贝赋值函数禁止,或者提供自己的深拷贝版本,或者使用const auto_ptr作为成员变量,因为const auto_ptr不可被拷贝,也就不可能失去所有权。

class A
{
public:
    A(){}
    A(string const& str):_spStr(new string(str))
    {
    }
    A(A const& rhs):_spStr(new string(*rhs._spStr))
    {
        
    }
    A& operator = (A const& rhs)
    {
        if(this!=&rhs)
        {
            _spStr.reset(new string(*rhs._spStr));
        }
        return *this;
    }
private:
    auto_ptr<string> _spStr;
};


int _tmain(int argc, _TCHAR* argv[])
{
    string str("hello,world");
    A a1(str);
    A a2;
    a2=a1;
    return 0;
}

使用auto_ptr const 成员
class A
{
public:
    A(){}
    A(string const& str):_spStr(new string(str))
    {
    }

private:
    auto_ptr<string> const _spStr;
};


int _tmain(int argc, _TCHAR* argv[])
{
    string str("hello,world");
    A a1(str);
    A a2(a1);
    return 0;
}
error C2558: class 'std::auto_ptr<_Ty>' : no copy constructor available or copy constructor is declared 'explicit'

    另外,由于auto_ptr在析构函数中使用delete语句而不是delete[],所以auto_ptr不能用于包装指向数组的指针,除非包装的是预定义类型。因为
char* p=new char[100];
...
delete p;
是可以的,所以你可以使用:
auto_ptr<char> sp(new char[100]());
...
    所有权转移也会给我们带来一些好东西。比如:
string* F1()
{
    return new string("hello,world");
}

auto_ptr<string> F2()
{
    return auto_ptr<string>(new string("hello,world"));
}

int _tmain(int argc, _TCHAR* argv[])
{
    string* p=F1();
    auto_ptr<string> sp=F2();
    return 0;
}

    如果忽略了这两个函数的返回值,F1返回的指针将再也找不到,内存泄漏,F2会返回临时变量,并且会被安全的释放掉,没有内存泄漏。并且当F2函数较为复杂时,返回auto_ptr提升了异常安全性。异常安全性将在后面介绍。
shared_ptr
    shared_ptr源自于著名的boost库,智能指针自从Scott Meyers在其作品<<More Effective C++>>首次提出并给出一个示范版本后得到了很大的发展。Loki和Boost是影响广泛的两个,Boost的智能指针较易使用,而Loki以功能强大思想深邃著称于世。C++标准委员会的人虽然对Loki赞赏有加,但是易用性似乎占了上风,于是Boost中的shared_ptr和weak_ptr出现在了C++标准的修改版中。现在可以肯定的是,C++09标准中一定会出现的是shared_ptr和weak_ptr两个指针。
    下面的关于shared_ptr的介绍,摘自马维达几年前的一篇文章《智能指针的标准之争:Boost vs. Loki》:
    shared_ptr:意在用于对被指向对象的所有权进行共享。被指向对象也保证会被删
除,但是这将发生在最后一个指向它的shared_ptr 被销毁时,或是调用reset 方法时。    shared_ptr符合C++标准库的“可复制构造”(CopyConstructible)和“可赋值”(Assignable)要求,所以可被用于标准的库容器中。另外它还提供了比较操作符,所以可与标准库的关联容器一起工作。shared_ptr 不能用于存储指向动态分配的数组的指针,这样的情况应该使用shared_array。该模板的实现采用了引用计数技术,所以无法正确处理循环引用的情况。可以使用weak_ptr 来“打破循环”。shared_ptr 还可在多线程环境中使用。
    下面的例子演示了如何在vector中使用shared_ptr:
class CTest
{
    ...
};
typedef boost::shared_ptr<CTest> TestPtr;
void PT(const TestPtr &t)
{
    std::cout << "id: " << t->GetId() << "/t/t";
    std::cout<< "use count: " << t.use_count() << '/n';
}
void main()
{
    std::vector<TestPtr> TestVector;
    TestPtr pTest0(new CTest(0));
    TestVector.push_back(pTest0);

    TestPtr pTest1(new CTest(1));
    TestVector.push_back(pTest1);

    TestPtr pTest2(new CTest(2));
    TestVector.push_back(pTest2);

    std::for_each(TestVector.begin(), TestVector.end(), PT);
    std::cout << '/n';
    pTest0.reset();
    pTest1.reset();
    pTest2.reset();
    
    std::for_each(TestVector.begin(), TestVector.end(), PT);
    std::cout << '/n';
    
    TestVector.clear();
    std::cout << '/n';
    std::cout << "exiting.../n";
}
其运行结果为:
id: 0 use count: 2
id: 1 use count: 2
id: 2 use count: 2
id: 0 use count: 1
id: 1 use count: 1
id: 2 use count: 1
id: 0 - Destructor is being called
id: 1 - Destructor is being called
id: 2 - Destructor is being called
exiting...
    运行结果中的“use count”是通过shared_ptr 的use_count()方法获得的“使用计数”,也就是,对所存储指针进行共享的shared_ptr 对象的数目。我们可以看到,在通过new 分配了3 个CTest 对象,并将相应的shared_ptr 对象放入TestVector 后,三个使用计数都为2;而在我们使用reset()方法复位pTest0、pTest1 和pTest2 后,TestVector 中的各个shared_ptr 对象的使用计数变成了1。这时,我们调用TestVector的clear()方法清除它所包含的shared_ptr 对象;因为已经没有shared_ptr 对象再指向我们先前分配的3
个CTest 对象,这3 个对象也随之被删除,并导致相应的析构器被调用
    有意思的是,虽然使用者对于支持Loki还是Boost分歧很大,他们的作者倒是和平相处,在Boost的网站上,我们可以看到Andrei推荐使用Boost,而在Boost的智能指针文档中,我们看到Boost对喜爱基于策略的智能指针的程序员推荐看看Andrei的大作<<Modern C++ Design>>。
    shared_ptr是基于引用计数的智能指针,引用计数技术会带来两个问题:
1)多个对象如何共享引用计数
在前面的浅拷贝和深拷贝一节中,我曾经给出一个很简单的例子,使用了静态成员变量来统计引用计数。如下图:

所有A类的对象共享一个变量_ref,这是一个简易装置,以至于在现实中几乎不可用。假如A的两个对象共享同一个字符串指针char* _p,并且使用_ref管理该指针的引用计数,那么现在又有两个A对象共享另外的字符串指针,他们用什么变量来管理引用计数呢?
现实世界中有两个解决方案,有一种string内部使用这样的引用计数器:

如果string对象要存储的是一个”hello,world”,一共占据11个字节空间,内部分配逻辑类似如下代码:
char* _pData=new char[12];
++_pData;
这样,_pData前面的一个字节将表示引用计数(有可能是4个字节),_pData仍然表示字符串的起始位置。这样两组string对象互不干扰,每组都有一个引用计数器。

shared_ptr的做法复杂点,请看下图:

shared_ptr<T>内部有两个成员变量,一个是裸指针,一个是标志该指针有几个shared_ptr<T>对象使用的类型为shared_count的变量 pn。
shared_count类有一个成员变量pi_,是一个sp_counted_base类型的指针,实际上指向sp_counted_impl_p类对象。

当shared_count对象构造时,将创建sp_counted_impl_p对象。
template<class Y> 
explicit shared_count( Y * p ): pi_( 0 )
    {
        try
        {
            pi_ = new sp_counted_impl_p<Y>( p );
        }
        catch(...)
        {
            boost::checked_delete( p );
            throw;
        }
  }
sp_counted_base类的拥有成员变量user_count,并且在默认构造函数中初始化为1。
当第二个shared_ptr<T>对象根据第一个对象创建时,使用编译器默认提供的拷贝构造函数,作为成员变量shared_count pn的拷贝构造函数因此被调用。
    shared_count(shared_count const & r): pi_(r.pi_) // nothrow
    {
        if( pi_ != 0 ) pi_->add_ref_copy();
 }
我们可以看到引用计数器被增加了1。
析构管理不再详述。
你也可以使用shared_count类作为自己的引用计数管理器,名字空间为boost::detail。注意shared_count类是模板类,构造时需要传递你的T* p给它。
2)引用计数在多线程环境下的性能问题
    引用技术变量是一个整数,在单线程环境下是简单的加1和减1,在多线程环境中,如果两个线程都各自有一个shared_ptr对象,这两个shared_ptr对象共享相同的裸指针,当其中一个线程通过拷贝构造又创建出一个新的shared_ptr对象的时候,就必然会修改引用计数,这时候,我们需要在修改引用计数的时候使用保护技术,以确保正确的修改引用计数变量的值。
    但是,如果有一个shared_ptr对象始终都在一个线程里面使用,尽管衍生出来它的很多副本,但都没有离开这个线程,我们是不应该使用同步技术保护引用计数的修改操作的。
    boost的作者选择了较简单但并不完美的方案,如果我们的程序中有多线程,就通过宏告诉shared_ptr,它的内部就会不管实际情况如何,都用锁。
    BOOST_SP_DISABLE_THREADS告诉shared_ptr这只是单线程,不用考虑同步;其他相关宏定义在sp_counted_base.hpp文件中。
    即便是锁也是有讲究的。请看下面这段描述:
It should be pointed out that both IRIX and Linux support optimized atomic operations that are much faster than the following code sequence:
pthread_mutex_lock ( &count_mutex ); 
   count++; 
pthread_mutex_unlock ( &count_mutex );
On IRIX __fetch_and_add while under Linux __sync_fetch_and_add (gcc) or _InterlockedIncrement (Intel compiler) would be much faster. 
    所以刚才介绍的宏还有帮助我们选择使用哪种同步机制的功能.Boost的配置环境做得比较智能(默认是多线程)。经过测试,在windows 、VC、AMD 2800+cpu环境下,更新引用计数器的方法是调用_InterlockedIncrement方法,在linux、InterX86 cpu、GNU C++环境下采用内联汇编方法,因此性能比使用mutex的方法好很多。
    下面引自csdn博客上一位程序员的测试结论:boost 智能指针 --- 关于性能的少数派报告
    比起 boost 1.33 ,这个时候的 boost 1.32 shared_ptr 平均运行时间多出了约 112% ,weak_ptr 的平均运行时间多出了约 126.2% !我们终于看到了新的实现在性能上的好处
    注意:boost1.32使用的是Mutex锁,1.33以后才改进为我前面描述的原子操作。我已经将shared_ptr从boost库中分离出来,毕竟boost太大了。我已经在Linux和windows上测试过了,大家感兴趣的可以使用。在以下路径下//192.168.22.26/ShareD/Training/C++TrainingHistory/2007-03/CC1/new
有一个tr1子目录(代表C++标准技术报告1)。
使用示例:
#include "tr1/shared_ptr.hpp"
#include <iostream>
#include <string>
using namespace boost;
using namespace std;

void f()
{
    shared_ptr<string> spStr1(new string("hello,world"));
    cout<<spStr1.use_count()<<endl;
    shared_ptr<string> spStr(spStr1);
    cout<<spStr1.use_count()<<endl;
}
int main(void)
{
    f();
    return 0;
}
    我已经在下列平台测试过shared_ptr:
操作系统
编译器
CPU
测试结果
Windows XP SP2
VC8
AMD 2800+
通过
Linux SuSe Enterprise Server 9
gcc3.3.3
x86
通过
Solaris 
gcc 3.4.2
sparc v9
g++ -mcpu=v9 通过
Solaris
CC
sparc v9
通过

    shared_ptr是如何实现整数的原子操作呢?在sp_counted_base.hpp文件中,提供了一大堆宏判断:(这些宏通常不需要设置)
#include "../config.hpp"
#if defined( BOOST_SP_DISABLE_THREADS )//单线程
# include "sp_counted_base_nt.hpp"
#elif defined( BOOST_SP_USE_PTHREADS )//使用PThread线程模型,mutex
# include "sp_counted_base_pt.hpp"
#elif defined( __GNUC__ ) && ( defined( __i386__ ) || defined( __x86_64__ ) )
# include "sp_counted_base_gcc_x86.hpp"//gcc + x86
#elif defined( __GNUC__ ) && defined( __ia64__ ) && !defined( __INTEL_COMPILER )
# include "sp_counted_base_gcc_ia64.hpp"//gcc + ia64
#elif defined( __MWERKS__ ) && defined( __POWERPC__ )
# include "sp_counted_base_cw_ppc.hpp"
#elif defined( __GNUC__ ) && ( defined( __powerpc__ ) || defined( __ppc__ ) )
# include "sp_counted_base_gcc_ppc.hpp"
#elif defined(__GNUC__) && ( defined( __sparcv8 ) || defined( __sparcv9 ) )
# include "sp_counted_base_gcc_sparc.hpp"//gcc + sparc
#elif defined( WIN32 ) || defined( _WIN32 ) || defined( __WIN32__ )
# include "sp_counted_base_w32.hpp"
#elif !defined( BOOST_HAS_THREADS )
# include "sp_counted_base_nt.hpp"
#elif defined( BOOST_HAS_PTHREADS )
# include "sp_counted_base_pt.hpp"
#else
// Use #define BOOST_DISABLE_THREADS to avoid the error
# error Unrecognized threading platform
#endif

    根据不同的情况,包含不同的文件,每个文件内部都实现了sp_counted_base类,但是类的实现细节和平台相关,多数使用了特定cpu的内联汇编代码:
如sp_counted_base_gcc_x86.hpp文件中
namespace detail
{

inline int atomic_exchange_and_add( int * pw, int dv )
{
    // int r = *pw;
    // *pw += dv;
    // return r;

    int r;

    __asm__ __volatile__
    (
        "lock/n/t"
        "xadd %1, %0":
        "=m"( *pw ), "=r"( r ): // outputs (%0, %1)
        "m"( *pw ), "1"( dv ): // inputs (%2, %3 == %1)
        "memory", "cc" // clobbers
    );

    return r;
}

inline void atomic_increment( int * pw )
{
    //atomic_exchange_and_add( pw, 1 );

    __asm__
    (
        "lock/n/t"
        "incl %0":
        "=m"( *pw ): // output (%0)
        "m"( *pw ): // input (%1)
        "cc" // clobbers
    );
}

inline int atomic_conditional_increment( int * pw )
{
    // int rv = *pw;
    // if( rv != 0 ) ++*pw;
    // return rv;

    int rv, tmp;

    __asm__
    (
        "movl %0, %%eax/n/t"
        "0:/n/t"
        "test %%eax, %%eax/n/t"
        "je 1f/n/t"
        "movl %%eax, %2/n/t"
        "incl %2/n/t"
        "lock/n/t"
        "cmpxchgl %2, %0/n/t"
        "jne 0b/n/t"
        "1:":
        "=m"( *pw ), "=&a"( rv ), "=&r"( tmp ): // outputs (%0, %1, %2)
        "m"( *pw ): // input (%3)
        "cc" // clobbers
    );

    return rv;
}
    boost一共提供了10个这样的文件,除了一个不使用外,也就是有九种实现,如果超过了这九种,编译器将报错表示不支持。
    注意:Fedora7已经安装了boost库。头文件路径在/usr/include/boost。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多