C++ 智能指针详解
一、简介 |
由于 C++ 语言没有自动内存回收机制,程序员每次 new 出来的内存都要手动 delete。程序员忘记 delete,流程太复杂,最终导致没有 delete,异常导致程序过早退出,没有执行 delete 的情况并不罕见。
用智能指针便可以有效缓解这类问题,本文主要讲解参见的智能指针的用法。包括:
std::auto_ptr、boost::scoped_ptr、boost::shared_ptr、boost::scoped_array、
boost::shared_array、boost::weak_ptr、boost::intrusive_ptr。
你可能会想,如此多的智能指针就为了解决new、delete匹配问题,真的有必要吗?看完这篇文章后,我想你心里自然会有答案。
下面就按照顺序讲解如上 7 种智能指针(smart_ptr)。
二、具体使用 |
1、总括 |
对于编译器来说,智能指针实际上是一个栈对象,并非指针类型,在栈对象生命期即将结束时,智能指针通过析构函数释放有它管理的堆内存。所有智能指针都重载了“operator->”操作符,直接返回对象的引用,用以操作对象。访问智能指针原来的方法则使用“.”操作符。
访问智能指针包含的裸指针则可以用 get() 函数。由于智能指针是一个对象,所以if (my_smart_object)永远为真,要判断智能指针的裸指针是否为空,需要这样判断:if (my_smart_object.get())。
智能指针包含了 reset() 方法,如果不传递参数(或者传递 NULL),则智能指针会释放当前管理的内存。如果传递一个对象,则智能指针会释放当前对象,来管理新传入的对象。
我们编写一个测试类来辅助分析:
class Simple {
public:
Simple(int param=0)
{
number= param;
std::cout<<"Simple: "<< number << std::endl;
}
~Simple()
{
std::cout<<"~Simple: "<< number <<std::endl;
}
void PrintSomething()
{
std::cout<<"PrintSomething: "<< info_extend.c_str() << std::endl;
}
std::string info_extend;
int number;
};
2、std::auto_ptr |
std::auto_ptr 属于 STL,当然在 namespace std 中,包含头文件 #include <memory>便可以使用。std::auto_ptr 能够方便的管理单个堆内存对象。
我们从代码开始分析:
void TestAutoPtr()
{
std::auto_ptr my_memory(new Simple(1));//创建对象,输出:Simple:1
if (my_memory.get())// 判断智能指针是否为空
{
//使用 operator->调用智能指针对象中的函数
my_memory->PrintSomething();
//使用 get() 返回裸指针,然后给内部对象赋值
my_memory.get()->info_extend= "Addition";
//再次打印,表明上述赋值成功
my_memory->PrintSomething();
//使用 operator*返回智能指针内部对象,然后用“.”调用智能指针对象中的函数
(*my_memory).info_extend+=" other";
//再次打印,表明上述赋值成功
my_memory->PrintSomething(); }
}// my_memory栈对象即将结束生命期,析构堆对象 Simple(1)
执行结果为:
Simple:1
PrintSomething:
PrintSomething:Addition
PrintSomething:Addition other
~Simple:1
上述为正常使用 std::auto_ptr 的代码,一切似乎都良好,无论如何不用我们显式使用该死的 delete 了。
其实好景不长,我们看看如下的另一个例子:
voidTestAutoPtr2()'
{
std::auto_ptr my_memory(new Simple(1));
if (my_memory.get())
{
std::auto_ptr my_memory2;//创建一个新的 my_memory2对象
my_memory2= my_memory;//复制旧的my_memory给 my_memory2
my_memory2->PrintSomething();// 输出信息,复制成功
my_memory->PrintSomething();//崩溃
}
}
最终如上代码导致崩溃,如上代码时绝对符合 C++ 编程思想的,居然崩溃了,跟进 std::auto_ptr 的源码后,我们看到,罪魁祸首是“my_memory2 = my_memory”,这行代码,my_memory2 完全夺取了 my_memory 的内存管理所有权,导致 my_memory 悬空,最后使用时导致崩溃。
所以,使用 std::auto_ptr 时,绝对不能使用“operator=”操作符。作为一个库,不允许用户使用,确没有明确拒绝[1],多少会觉得有点出乎预料。
看完 std::auto_ptr 好景不长的第一个例子后,让我们再来看一个:
void TestAutoPtr3()
{
std::auto_ptr my_memory(new Simple(1));
if (my_memory.get())
{
my_memory.release();
}
}
执行结果为:
Simple:1
看到什么异常了吗?我们创建出来的对象没有被析构,没有输出“~Simple: 1”,导致内存泄露。当我们不想让 my_memory 继续生存下去,我们调用 release() 函数释放内存,结果却导致内存泄露(在内存受限系统中,如果my_memory占用太多内存,我们会考虑在使用完成后,立刻归还,而不是等到 my_memory 结束生命期后才归还)。
正确的代码应该为:
void TestAutoPtr3()
{
std::auto_ptr my_memory(new Simple(1));
if (my_memory.get())
{
Simple* temp_memory=my_memory.release();
delete temp_memory;
}
}
或
void TestAutoPtr3()
{
std::auto_ptr my_memory(new Simple(1));
if (my_memory.get())
{
my_memory.reset();//释放my_memory内部管理的内存
}
}
原来 std::auto_ptr 的 release() 函数只是让出内存所有权,这显然也不符合 C++ 编程思想。
总结:std::auto_ptr 可用来管理单个对象的对内存,但是,请注意如下几点:
(1)尽量不要使用“operator=”。如果使用了,请不要再使用先前对象。
(2)记住 release() 函数不会释放对象,仅仅归还所有权。
(3)std::auto_ptr 最好不要当成参数传递(读者可以自行写代码确定为什么不能)。
(4)由于 std::auto_ptr 的“operator=”问题,有其管理的对象不能放入 std::vector 等容器中,因为容器中经常存在这元素拷贝的情况。
(5)不能指向数组,因为其中使用的是delete而不是delete[]。
使用一个 std::auto_ptr 的限制还真多,还不能用来管理堆内存数组,这应该是你目前在想的事情吧,我也觉得限制挺多的,哪天一个不小心,就导致问题了。
由于 std::auto_ptr 引发了诸多问题,一些设计并不是非常符合 C++ 编程思想,所以引发了下面 boost 的智能指针,boost 智能指针可以解决如上问题。
下面分析下auto_ptr的源码。
/* #ifndef __SGI_STL_MEMORY #define __SGI_STL_MEMORY
#include<stl_algobase.h> #include<stl_alloc.h> #include<stl_construct.h> #include<stl_tempbuf.h> #include<stl_uninitialized.h> #include<stl_raw_storage_iter.h>
__STL_BEGIN_NAMESPACE //如果定义了 auto_ptr转换以及支持成员函数模板 #ifdefined(__SGI_STL_USE_AUTO_PTR_CONVERSIONS)&& \ defined(__STL_MEMBER_TEMPLATES) //定义 auto_ptr_ref template结构体① template< class _Tp1> struct auto_ptr_ref { _Tp1 * _M_ptr ; auto_ptr_ref ( _Tp1* __p ) : _M_ptr ( __p ) {} } ;
#endif
template< class _Tp> class auto_ptr { private: _Tp * _M_ptr ;
public: typedef _Tp element_type ; // explicit修饰构造函数,防止从原始指针隐式转换 explicit auto_ptr ( _Tp* __p = 0 ) __STL_NOTHROW : _M_ptr ( __p ) {} // Copy构造函数,注意这里是直接引用传参(非 const),同时转移指针所有权 auto_ptr ( auto_ptr& __a ) __STL_NOTHROW : _M_ptr ( __a. release ()) {}
//如果允许定义成员函数模板( Member Function Templates)② #ifdef __STL_MEMBER_TEMPLATES //如果可以从 _Tp1*转换为 _Tp*,则可以从 auto_ptr<_Tp1>构造 auto_ptr<_Tp> //同时转移指针所有权 template< class _Tp1> auto_ptr ( auto_ptr< _Tp1 > & __a ) __STL_NOTHROW : _M_ptr ( __a. release ()) {} #endif/* __STL_MEMBER_TEMPLATES */
//赋值操作符,同样是非 const引用传参,有证同测试③ auto_ptr& operator= ( auto_ptr & __a ) __STL_NOTHROW { //如果是自我赋值,就直接返回 if (& __a ! = this ) { delete _M_ptr ; _M_ptr= __a. release () ; } return* this ; }
#ifdef __STL_MEMBER_TEMPLATES //赋值操作符的 Member Function Templates template< class _Tp1> auto_ptr& operator= ( auto_ptr < _Tp1> & __a ) __STL_NOTHROW { if ( __a. get ()! = this - > get ()) { delete _M_ptr ; _M_ptr= __a. release () ; } return* this ; } #endif/* __STL_MEMBER_TEMPLATES */
// Note: The C++ standard says there is supposed to be an empty throw // specification here, but omitting it is standard conforming. Its // presence can be detected only if _Tp::~_Tp() throws, but (17.4.3.6/2) // this is prohibited. // auto_ptr的析构函数 ~auto_ptr () {delete _M_ptr ; } // operator*定义,返回值 _Tp & operator * () const __STL_NOTHROW { return* _M_ptr ; } // operator->定义,返回指针 _Tp * operator - > () const __STL_NOTHROW { return _M_ptr ; } // const成员函数 get定义,返回指针 _Tp * get () const __STL_NOTHROW { return _M_ptr ; } // release函数定义,释放指针,而不是释放资源!!! _Tp * release () __STL_NOTHROW { _Tp* __tmp = _M_ptr ; _M_ptr= 0 ; return __tmp ; } // reset函数定义,重置指针 void reset ( _Tp* __p = 0 ) __STL_NOTHROW { if ( __p! = _M_ptr ) { delete _M_ptr ; _M_ptr= __p ; } }
// According to the C++ standard, these conversions are required. Most // present-day compilers, however, do not enforce that requirement---and, // in fact, most present-day compilers do not support the language // features that these conversions rely on.
#ifdefined(__SGI_STL_USE_AUTO_PTR_CONVERSIONS)&& \ defined(__STL_MEMBER_TEMPLATES)
public: //从 auto_ptr_ref<_Tp>构造 auto_ptr<_Tp> auto_ptr ( auto_ptr_ref< _Tp > __ref ) __STL_NOTHROW : _M_ptr ( __ref._M_ptr ) {} //从 auto_ptr_ref<_Tp>对 auto_ptr<_Tp>进行赋值操作。 //注意这里是普通传参,没有引用④ auto_ptr& operator= ( auto_ptr_ref < _Tp> __ref ) __STL_NOTHROW { if ( __ref._M_ptr! = this - > get ()) { delete _M_ptr ; _M_ptr= __ref._M_ptr ; } return* this ; } //成员函数模板( Member Function Templates)② //如果可以从 _Tp*转换为 _Tp1*,则可以从 auto_ptr<_Tp>转换为 auto_ptr_ref<_Tp1> template< class _Tp1> operator auto_ptr_ref< _Tp1 > () __STL_NOTHROW { return auto_ptr_ref < _Tp1> ( this- > release ()) ; } //成员函数模板( Member Function Templates)② //如果可以从 _Tp*转换为 _Tp1*,则可以从 auto_ptr<_Tp>转换为 auto_ptr<_Tp1> template< class _Tp1> operator auto_ptr< _Tp1 > () __STL_NOTHROW { return auto_ptr < _Tp1> ( this- > release ()) ; }
#endif/* auto ptr conversions && member templates */ } ;
__STL_END_NAMESPACE
#endif/* __SGI_STL_MEMORY */
// Local Variables: // mode:C++ // End: |
注解:
①auto_ptr_ref结构体
我们看到,auto_ptr源代码中的Copy构造函数的参数是普通的引用传参(不是const引用,也不是普通的传值),这是为了方便指针拥有权的转移(如果是const引用,那么拥有权无法转移;如果是普通的传值,oh my god,整个世界都彻底混乱了)。那如果以一个临时对象(也就是所谓的右值)进行拷贝构造,那样就无法通过编译了(普通指针或引用不能指向const对象,即不能指向右值)。幸好有auto_ptr_ref的存在,可以从auto_ptr_ref临时对象构造或者赋值为auto_ptr对象:
public:
// 从auto_ptr_ref<_Tp>构造auto_ptr<_Tp>
auto_ptr(auto_ptr_ref<_Tp> __ref)__STL_NOTHROW
:_M_ptr(__ref._M_ptr) {}
// 从auto_ptr_ref<_Tp>对auto_ptr<_Tp>进行赋值操作。
// 注意这里是普通传参,没有引用④
auto_ptr& operator=(auto_ptr_ref<_Tp> __ref)__STL_NOTHROW{
if(__ref._M_ptr!= this->get()){
delete _M_ptr;
_M_ptr= __ref._M_ptr;
}
return*this;
}
而auto_ptr对象也可以隐式的转化为auto_ptr_ref类型的对象:
1 |
template<class _Tp1> operator auto_ptr_ref<_Tp1>() __STL_NOTHROW |
于是乎,就完美的完成了auto_ptr从右值到左值的转换工作。也可以看这里:为什么需要auto_ptr_ref
②成员函数模板(Member Function Templates)
③证同测试,见《Effective C++》条款11:在operator=中处理“自我赋值”(Item 11. handleassignment to self in operator=)
④见①
然后笔者从而推荐的是boost的shared_ptr,然后看完shared_ptr关于智能指针的介绍与例子。
5种针对auto_ptr不足的指针如下:需要详细了解可以去查看相当文档,与测试新代码。
scoped_ptr |
<boost/scoped_ptr.hpp> |
简单的单一对象的唯一所有权。不可拷贝。 |
scoped_array |
<boost/scoped_array.hpp> |
简单的数组的唯一所有权。不可拷贝。 |
shared_ptr |
<boost/shared_ptr.hpp> |
在多个指针间共享的对象所有权。 |
shared_array |
<boost/shared_array.hpp> |
在多个指针间共享的数组所有权。 |
weak_ptr |
<boost/weak_ptr.hpp> |
一个属于 shared_ptr 的对象的无所有权的观察者。 |
intrusive_ptr |
<boost/intrusive_ptr.hpp> |
带有一个侵入式引用计数的对象的共享所有权。 |
让我们继续向下看。
3、boost::scoped_ptr |
boost::scoped_ptr 属于 boost 库,定义在 namespace boost 中。 boost::scoped_ptr跟std::auto_ptr一样,可以方便的管理单个堆内存对象,特别的是,boost::scoped_ptr 独享所有权,避免了std::auto_ptr恼人的几个问题。
我们还是从代码开始分析:
void TestScopedPtr()
{
boost::scoped_ptr my_memory(new Simple(1));
if(my_memory.get())
{
my_memory->PrintSomething();
my_memory.get()->info_extend="Addition";
my_memory->PrintSomething();
(*my_memory).info_extend+=" other";
my_memory->PrintSomething();
my_memory.release();// 编译 error: scoped_ptr 没有 release 函数
std::auto_ptr my_memory2;
my_memory2= my_memory;// 编译 error:scoped_ptr 没有重载 operator=,不会导致所有权转移
}
}
首先,我们可以看到,boost::scoped_ptr 也可以像 auto_ptr 一样正常使用。但其没有 release() 函数,不会导致先前的内存泄露问题。其次,由于 boost::scoped_ptr 是独享所有权的,所以明确拒绝用户写“my_memory2 = my_memory之类的语句,可以缓解 std::auto_ptr 几个恼人的问题。
由于 boost::scoped_ptr 独享所有权,当我们真真需要复制智能指针时,需求便满足不了了,如此我们再引入一个智能指针,专门用于处理复制,参数传递的情况,这便是如下的 boost::shared_ptr。
三、总结 |
如上讲了这么多智能指针,有必要对这些智能指针做个总结:
1、在可以使用 boost 库的场合下,拒绝使用 std::auto_ptr,因为其不仅不符合 C++ 编程思想,而且极容易出错[2]。
2、在确定对象无需共享的情况下,使用 boost::scoped_ptr(当然动态数组使用 boost::scoped_array)。
3、在对象需要共享的情况下,使用 boost::shared_ptr(当然动态数组使用 boost::shared_array)。
4、在需要访问 boost::shared_ptr 对象,而又不想改变其引用计数的情况下,使用 boost::weak_ptr,一般常用于软件框架设计中。
5、最后一点,也是要求最苛刻一点:在你的代码中,不要出现 delete 关键字(或 C 语言的 free 函数),因为可以用智能指针去管理。
---------------------------------------
[1]参见《effective C++(3rd)》,条款06 。
[2]关于 boost 库的使用,可本博客另外一篇文章:《在 Windows 中编译 boost1.42.0》。
[3]读者应该看到了,在我所有的名字前,都加了命名空间标识符std::(或boost::),这不是我不想写 using namespace XXX 之类的语句,在大型项目中,有可能会用到 N 个第三方库,如果把命名空间全放出来,命名污染(Naming conflicts)问题很难避免,到时要改回来是极端麻烦的事情。当然,如果你只是写 Demo,可以例外。