C++ 智能指针详解

C++ 智能指针详解

一、简介

由于 C++ 语言没有自动内存回收机制,程序员每次 new 出来的内存都要手动 delete。程序员忘记 delete,流程太复杂,最终导致没有 delete,异常导致程序过早退出,没有执行 delete 的情况并不罕见。

 

用智能指针便可以有效缓解这类问题,本文主要讲解参见的智能指针的用法。包括:

std::auto_ptrboost::scoped_ptrboost::shared_ptrboost::scoped_array

boost::shared_arrayboost::weak_ptrboost::intrusive_ptr

你可能会想,如此多的智能指针就为了解决newdelete匹配问题,真的有必要吗?看完这篇文章后,我想你心里自然会有答案。

 

下面就按照顺序讲解如上 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;

};

 

 

2std::auto_ptr

 

std::auto_ptr 属于 STL,当然在 namespace std 中,包含头文件 #include <memory>便可以使用。std::auto_ptr 能够方便的管理单个堆内存对象。

 

我们从代码开始分析:

void TestAutoPtr()

{

    std::auto_ptr my_memory(new Simple(1));//创建对象,输出:Simple1

 

    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() 函数不会释放对象,仅仅归还所有权

3std::auto_ptr 最好不要当成参数传递(读者可以自行写代码确定为什么不能)。

4)由于 std::auto_ptr 的“operator=”问题,有其管理的对象不能放入 std::vector 等容器中,因为容器中经常存在这元素拷贝的情况

5不能指向数组,因为其中使用的是delete而不是delete[]

 

使用一个 std::auto_ptr 的限制还真多,还不能用来管理堆内存数组,这应该是你目前在想的事情吧,我也觉得限制挺多的,哪天一个不小心,就导致问题了。

 

由于 std::auto_ptr 引发了诸多问题,一些设计并不是非常符合 C++ 编程思想,所以引发了下面 boost 的智能指针,boost 智能指针可以解决如上问题。

 

下面分析下auto_ptr的源码。

/*
* Copyright (c) 1997-1999
* Silicon Graphics Computer Systems, Inc.
*
* Permission to use, copy, modify, distribute and sell this software
* and its documentation for any purpose is hereby granted without fee,
* provided that the above copyright notice appear in all copies and
* that both that copyright notice and this permission notice appear
* in supporting documentation.  Silicon Graphics makes no
* representations about the suitability of this software for any
* purpose.  It is provided "as is" without express or implied warranty.
*
*/

 

#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
2

        template<class _Tp1> operator auto_ptr_ref<_Tp1>() __STL_NOTHROW
        
{ return auto_ptr_ref<_Tp1>(this->release());}

于是乎,就完美的完成了auto_ptr从右值到左值的转换工作。也可以看这里:为什么需要auto_ptr_ref

成员函数模板(Member Function Templates

③证同测试,见《Effective C++》条款11:在operator=中处理“自我赋值”Item 11. handleassignment to self in operator=

④见①

 

然后笔者从而推荐的是boostshared_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>

带有一个侵入式引用计数的对象的共享所有权。

 

 

让我们继续向下看。

 

3boost::scoped_ptr

 

boost::scoped_ptr 属于 boost 库,定义在 namespace boost 中。 boost::scoped_ptrstd::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,可以例外。

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