浅析C++中的智能指针

由于最近比较多的接触到这块的代码,因此有必要做个总结。众所周知,C/C++中的堆内存分配和释放的方式主要是: malloc/free 以及 new/delete 等,但这些方式对程序员要求较高,一不小心很可能就会导致内存泄漏而不自知。请看下面的代码:

void func()
{
    T *pt = new T();
    
    ... /* 此处代码省略若干行 */
    
    delete pt;
    pt = NULL;
    return;
}

如果 func 函数能顺利执行到 delete 处当然是最理想的。如果由于某种原因导致函数在中途返回了,或者在还没执行到 delete 语句时发生了异常,那么就悲剧了,为指针pt分配的内存得不到释放,产生了一个经典的内存泄漏。如果 func 函数是一个执行频率较高的函数,那么就尴尬了…

为了消除传统的内存分配方式上存在的隐患,C++提供了一些强大的智能指针模版类,其核心思想就是:栈上对象在离开作用范围时会自动析构。

一. auto_ptr类

字面意义上看,auto_ptr 就是自动指针的意思,当分配的内存不需要使用了,它可以自动回收。

1.1 用法

1. 构造函数

explicit auto_ptr(_Ty *_Ptr = 0) _THROW0()
        : _Myptr(_Ptr)     // 将指针交由auto_ptr托管
{   // construct from object pointer
}

2. 析构函数

// 释放了托管的对象所占用的内存空间
~auto_ptr()
{   // destroy the object
    delete _Myptr;
}

3. get方法

// 返回保存的指针
_Ty *get() const _THROW0()
{   // return wrapped pointer  
    return (_Myptr);
}

4. release方法

_Ty *release() _THROW0()
{    // return wrapped pointer and give up ownership  返回保存的指针,对象中不保留原来的指针,原来的指针直接赋值为0
    _Ty *_Tmp = _Myptr;
    _Myptr = 0;
    return (_Tmp);
}

5. reset方法

重置auto_ptr使之拥有另一个对象。如果这个auto_ptr已经拥有了一个对象,那么它会先删除已经拥有的对象,因此调用reset()就如同销毁这个auto_ptr,然后新建一个并拥有一个新对象

void reset(_Ty* _Ptr = 0)
{   // destroy designated object and store new pointer
    if (_Ptr != _Myptr)
        delete _Myptr;
    _Myptr = _Ptr;
}

6. 拷贝构造函数

// 明显可看出会发生托管权的转移
auto_ptr(auto_ptr<_Ty>& _Right) _THROW0()
        : _Myptr(_Right.release())
{    // construct by assuming pointer from _Right auto_ptr
}

7. 赋值运算符

// 很明显也发生了托管权的转移
template<class _Other> auto_ptr<_Ty>& operator=(auto_ptr<_Other>& _Right) _THROW0()
{   // assign compatible _Right (assume pointer)
    reset(_Right.release());
    return (*this);
}

1.2 例子

下面用个例子说明auto_ptr的用法

// 纯演示函数,没有实际作用, 需要包含相应的头文件, #include <memory>
void fun2()
{
   T *pt = new T();

   // 将分配的堆内存指针交由auto_ptr托管
   std::auto_ptr apt(pt); 
   
   // 像正常使用指针一样使用,相当于*pt= 10
   *apt = 10;
   
   // 相当于 pt->memFunc()
   apt->memFunc(); 
   
   // 使用get函数可获取它托管的指针
   T *pt2 = apt.get(); 

   // 可调用reset函数更改托管对象
   // 这里删除了之前托管的 pt
   apt.reset(new T()); 

   // 可调用release函数放弃托管
   T *pt3 = apt.release(); 

   
   // 放弃托管意味着又需要自己手动释放内存了
   delete pt3;
   pt3 = NULL;
  
   
   return;
}

1.3 注意事项

1. auto_ptr没有使用引用计数,如果多个auto_ptr指向同一个对象,就会造成对象被删除一次以上的错误。因此一个对象只能由一个auto_ptr所拥有,在给其他auto_ptr赋值的时候,会转移这种拥有关系。所以,在赋值、参数传递的时候会转移所有权,因此不要轻易进行此类操作,详见下面代码:

/* 1. 演示转移所有权 */
std::auto_ptr<int> aptr1(new int(3));  

// 执行后aptr1不再有效
std::auto_ptr<int> aptr2 = aptr1;  // or aptr2(aptr1)     

// 强行访问会发生不可预料的问题
*aptr1 = 4;        

// --------------------------------------------------------

/* 2. 演示参数传递的所有权转移 */
void lose(std::auto_ptr<int> a)  
{
    // 空函数,仅仅为了演示参数传递
}

std::auto_ptr<int> aptr3(new int(4));    

// 所有权转移,aptr3不再有效
lose(aptr3);

// 强行访问会发生不可预料的问题
*aptr3 = 10;

2. auto_ptr的析构函数内部释放资源时调用的是delete而不是delete[],因此不要让auto_ptr托管数组

// 类似这样的代码是个很糟糕的用法
std::auto_ptr<int> aptr(new int[10]);

3. auto_ptr不能作为容器对象,因为容器中的元素经常要进行拷贝,赋值等操作,在这过程中auto_ptr会失去所有权

二. unique_ptr类

unique_ptr是指”唯一”地拥有其所指对象的智能指针,同一时刻只能有一个unique_ptr指向给定对象(使用移动语义来实现),与auto_ptr相比,有以下几个不同

  • 可以通过间接的方式用于容器中
unique_ptr<int> sp(new int(10));

vector<unique_ptr<int> > vec;

vec.push_back(std::move(sp));   // 通过这种移动语义来实现在容器中使用

vec.push_back(sp);  // 这样直接使用不行,会报错
cout << *sp << end; // 这样也不行,因为sp添加到容器中后,它自己就报废了
  • 无法直接进行复制构造与赋值操作,要使用move函数进行所有权的转移
unique_ptr<int> uq(new int(10));

unique_ptr<int> uq2 = uq;   // 会报错
unique_ptr<int> uq3(uq);     // 同样会报错


unique_ptr<int> uq4 = std::move(up);  // 这样直接显式的所有权转移是可以的
  • 可以用于函数的返回值
// 函数定义
unique_ptr<int> myFunc()
{
    unique_ptr<int> up(new int(10));
    return up;
}

// 函数使用
unique_ptr<int> upRet = myFunc();

2.1 例子

unique_ptr<int> up(new int(3));   // 托管一个对象

// 更改所有权
unique_ptr<int> up2 = std::move(up); // 所有权转移,转移后,up变为空指针
int *p = up.release();   // 释放所有权

up.reset();  // 显式销毁所有权

三. shared_ptr类

同样从字面意义上看,shared_ptr表明它是一种共享型的指针,相对于auto_ptr,有很多的优点,比如可以自由的拷贝和赋值,并且可以用在容器对象中,其采用了引用计数的方式对内存进行管理

相对于auto_ptr,它有以下几个不同的地方:

  • 使用一个引用计数shared_count,用来表示当前有多少个智能指针对象共享指针指向的内存块
  • 析构函数中对引用计数进行判断,如果 shared_count > 1,则不释放内存只是将引用计数减1,当shared_count == 1的时候释放内存
  • 复制构造与赋值操作符除了提供复制功能之外,还将引用计数加1

3.1 用法

下面是boost中shared_ptr类声明(http://www.boost.org/doc/libs/1_52_0/libs/smart_ptr/shared_ptr.htm#BestPractices),如果要详细了解shared_ptr的用法,请参考里面的注释,如果想快速上手,可以直接看3.2节的例子程序

namespace boost {

  class bad_weak_ptr: public std::exception;

  template<class T> class weak_ptr;

  template<class T> class shared_ptr {

    public:

      typedef T element_type;      
      
      // 构造函数
      shared_ptr(); // never throws   创建一个空的shared_ptr, 即:user_count == 0 && get() == 0
      template<class Y> explicit shared_ptr(Y * p);  // 将指针p交由shared_ptr托管
      template<class Y, class D> shared_ptr(Y * p, D d);
      template<class Y, class D, class A> shared_ptr(Y * p, D d, A a);
      
      // 析构函数
      ~shared_ptr(); // never throws
      
      // 复制构造函数
      shared_ptr(shared_ptr const & r); // never throws
      template<class Y> shared_ptr(shared_ptr<Y> const & r); // never throws
      template<class Y> shared_ptr(shared_ptr<Y> const & r, T * p); // never throws
      template<class Y> explicit shared_ptr(weak_ptr<Y> const & r);
      template<class Y> explicit shared_ptr(std::auto_ptr<Y> & r);

      // 赋值运算符,相当于 shared_ptr(r).swap(*this)
      shared_ptr & operator=(shared_ptr const & r); // never throws 
      template<class Y> shared_ptr & operator=(shared_ptr<Y> const & r); // never throws
      template<class Y> shared_ptr & operator=(std::auto_ptr<Y> & r);

    // reset之类的函数,即重置托管对象,相当于调用 shared_ptr(xxx).swap(*this);
      void reset(); // never throws
      template<class Y> void reset(Y * p);
      template<class Y, class D> void reset(Y * p, D d);
      template<class Y, class D, class A> void reset(Y * p, D d, A a);
      template<class Y> void reset(shared_ptr<Y> const & r, T * p); // never throws

    // 重载的指针操作符,使得shared_ptr可以像使用正常一样指针进行使用
      T & operator*() const; // never throws
      T * operator->() const; // never throws
      
    
      T * get() const; // never throws 获取shared_ptr中托管的指针

      bool unique() const; // never throws 用于确定shared_ptr管理的内存对象的引用计数是否为1

      long use_count() const; // never throws  返回shared_ptr管理的内存对象的引用计数

      operator unspecified-bool-type() const; // never throws

      void swap(shared_ptr & b); // never throws  交换两个智能指针的内容
  };

  template<class T, class U>
    bool operator==(shared_ptr<T> const & a, shared_ptr<U> const & b); // never throws   return a.get() == b.get()

  template<class T, class U>
    bool operator!=(shared_ptr<T> const & a, shared_ptr<U> const & b); // never throws   return a.get() != b.get()

  template<class T, class U>
    bool operator<(shared_ptr<T> const & a, shared_ptr<U> const & b); // never throws

  template<class T> void swap(shared_ptr<T> & a, shared_ptr<T> & b); // never throws     相当于 a.swap(b)

  template<class T> T * get_pointer(shared_ptr<T> const & p); // never throws   相当于 p.get()

  template<class T, class U>
    shared_ptr<T> static_pointer_cast(shared_ptr<U> const & r); // never throws  

  template<class T, class U>
    shared_ptr<T> const_pointer_cast(shared_ptr<U> const & r); // never throws

  template<class T, class U>
    shared_ptr<T> dynamic_pointer_cast(shared_ptr<U> const & r); // never throws

  template<class E, class T, class Y>
    std::basic_ostream<E, T> & operator<< (std::basic_ostream<E, T> & os, shared_ptr<Y> const & p);

  template<class D, class T>
    D * get_deleter(shared_ptr<T> const & p);
}

3.2 例子

1. 例子一:普通使用方式

// 1. 构造方法
// 将指针交由shared_ptr托管  还有一种方式也可以创建shared_ptr对象,且比较常用,
//     是通过make_shared函数: shared_ptr<int> shPtr = make_shared<int>(10); 
shared_ptr<int> shPtr(new int(10)); 
int num = *shPtr;   // 像使用正常指针一样使用它,此时num == 10

// 2. 复制构造函数
shared_ptr<int> shPtr2(shPtr);    // 复制构造,此时引用计数会增加
// 两个shared_ptr相等,指向同一个对象,引用计数为2
assert(shPtr == shPtr2 && shPtr.use_count() == 2); 
// 原先的shPtr还可以继续使用,如果是auto_ptr,是不能使用的,因为有所有权的转移
num = *shPtr;      
*shPtr = 20;    
assert(*shPtr2 == 20);  // 在改一个shared_ptr的同时,另一个也会更改

// 3. 赋值运算符
shared_ptr<int> shPtr3 = shPtr2;  // 赋值操作符


// 4. 停止使用
shPtr.reset();
assert(!shPtr);   // shPtr停止使用后会变成空指针

2. 例子二:类中使用方式

class myClass
{
    public:   
        // 构造函数
        myClass(shared_ptr<int> shp_): m_shpMem(shp_) {}
        
        // 回显函数
        void print()
        {
              printf("count: %d, v= %d \n", m_shpMem.use_count(), *m_shpMem);
         }

    private:
       shared_ptr<int> m_shpMem;           
};

3. 例子三:容器中的使用方式

  • 第一种方式:shared_ptr<vector<T> > , 将容器作为shared_ptr管理的对象,可以使得容器被安全的共享
  • 第二种方式:vector<shrared_ptr<T> >, 将shared_ptr作为容器的中的元素
vector<shared_ptr<int> > v(10);   // 声明一个拥有10个元素的容器,元素被初始化为空指针
int i = 0;
for ( vector<shared_ptr<int> >::iterator it = v.begin(); it != v.end(); ++it)
{
    *pos = make_shared<int>(++i);  // 给容器中的元素赋值
    cout << *(*pos) << ",";    // 输出刚赋给它的值
}

3.3 注意事项

1. 不能再对shared_ptr所管理的对象再进行一些直接的内存管理操作,会造成对象的重复释放,导致崩溃

int *p = new int(10)
{
    // 将p交由shared_ptr托管,则在此作用域后,就把它所托管的对象的内存释放掉了
    shared_ptr<int> shp(p);    
}
delete p;  // 又去释放该对象的内存,会崩溃

2. shared_ptr不能对循环引用的对象的内存进行自动管理,见下面的例子

class B;
class A
{
public:
    A()
    {
        cout << "Class A Constructor is called." << endl;
    }

    ~A()
    {
        cout << "Class A Deconstructor is called." << endl;
    }

    // 可以通过使用weak_ptr<B>的方式解决,详细的打破循环引用的方法和原理见本文3.2节
    tr1::shared_ptr<B> m_shB;    
};

class B
{
public:
    B()
    {
        cout << "Class B Constructor is called." << endl;
    }

    ~B()
    {
        cout << "Class B Deconstructor is called." << endl;
    }

    tr1::shared_ptr<A> m_shA;
};


int main()
{
   
   {
        tr1::shared_ptr<A> shA(new A());   // A的引用计数 1
        tr1::shared_ptr<B> shB(new B());   // B的引用计数 2
        
        if (shA && shB)
        {
           shA->m_shB = shB;   // B的引用计数变为2
           shB->m_shA = shA;   // A的引用计数变为2
        }
        
        cout << "要离开shA和shB的作用域了,正常情况下在这之后会执行shA和shB的析构函数的" 
             << endl;
    
    // 这里是要执行析构函数的
    // 但是,对shB这个对象而言,它要析构B的话,得先去判断下托管B的shared_ptr的引用计数,
    //       这里是2,所以它不能去析构B,B的成员对象A自然也不能析构
    // 而对shA这个对象而言,它要析构A的话,也得先去判断下托管的A的shared_ptr的引用计数,
    //       这里也是2,它也不能析构A
   }
   
   cout << "已经离开shA和shB的作用域了,请观察shA和shB的析构函数有没有被执行" << endl;

   return 0;
}
  

执行结果见下图,可发现析构函数没有执行

3. 不要构造一个临时的shared_ptr作为函数的参数,存在内存泄漏的风险为

void  f(shared_ptr<int>, int);
int g();

// 正确的使用方式
void OK()
{
    shared_ptr<int> p(new int(2));
    f(p, g());
}

// 错误的使用方式
void Bad()
{
    // 如果执行顺序是先 new int(2), 然后g(), 
    // 最后将 new int(2) 的指针给shared_ptr的构造函数的话,
    // 当g()中抛出异常的时候, 第一个new int(2)就造成了内存泄漏了
    f(shared_ptr<int>(new int(2)), g());
}

四. weak_ptr类

相对于shared_ptr这种强引用类型的智能指针, weak_ptr是一种弱引用型的指针,是为了配合shared_ptr而引入的一种智能指针,可以看成是shared_ptr的助手而不是真正的智能指针,因为它不会托管资源,它的构造也不会引起引用计数的增加。

  • 它没有重载 operator * 和 operator ->,不具有普通指针的行为
  • 它只有资源的观察权,没有资源的托管权。获取资源观察权的方法是使用另一个 shared_ptr 或者 weak_ptr 去构造
  • 其成员函数 use_count  可以获取被观察资源的引用计数,expire函数表示被观察的资源已经不复存在,lock成员用于获取被观察的shared_ptr对象,如果expire()==true,则lock函数将返回一个存储空指针的shared_ptr

4.1 用法

下面是boost中weak_ptr类声明(http://www.boost.org/doc/libs/1_52_0/libs/smart_ptr/weak_ptr.htm),如果要详细了解weak_ptr的用法,请参考里面的注释,如果想快速上手,可以直接看3.2节的例子程序

namespace boost {

  template<class T> class weak_ptr {

    public:
      typedef T element_type;

      weak_ptr();  // 构造一个空的weak_ptr

      // 如果r是空,则会构造一个空的weak_ptr;否则,获取r的资源观察权
      template<class Y> weak_ptr(shared_ptr<Y> const & r);  
      weak_ptr(weak_ptr const & r);
      template<class Y> weak_ptr(weak_ptr<Y> const & r);

      // 析构函数不会对其观察的资源产生任何影响
      ~weak_ptr();

      // 等价于 weak_ptr(r).swap(*this)
      weak_ptr & operator=(weak_ptr const & r);
      template<class Y> weak_ptr & operator=(weak_ptr<Y> const & r);
      template<class Y> weak_ptr & operator=(shared_ptr<Y> const & r);

      // 获取被其观察的资源的引用计数
      long use_count() const;
      
      // 当其被观察的资源的引用计数为0的时候,返回true
      bool expired() const;
     
      // 用于获取被观察的shared_ptr
      shared_ptr<T> lock() const;
      
      // 等价于 weak_ptr().swap(*this)
      void reset();
     
      // 交换两个智能指针的内容
      void swap(weak_ptr<T> & b);
  };

  template<class T, class U>
    bool operator<(weak_ptr<T> const & a, weak_ptr<U> const & b);

  template<class T>
    void swap(weak_ptr<T> & a, weak_ptr<T> & b);
}

4.2 例子

1. 例子一:普通使用方式

class B;
class A
{
public:
    A()
    {
        cout << "Class A Constructor is called." << endl;
    }

    ~A()
    {
        cout << "Class A Deconstructor is called." << endl;
    }

    // tr1::shared_ptr<B> m_shB;
    tr1::weak_ptr<B> m_shB;
};

class B
{
public:
    B()
    {
        cout << "Class B Constructor is called." << endl;
    }

    ~B()
    {
        cout << "Class B Deconstructor is called." << endl;
    }

    tr1::shared_ptr<A> m_shA;
};

int _tmain(int argc, _TCHAR* argv[])
{
    {
        // 测试重复引用
        tr1::shared_ptr<A> shA(new A());
        tr1::shared_ptr<B> shB(new B());
        
        if (shA && shB)
        {
            shA->m_shB = shB;
            shB->m_shA = shA;
        }

        cout << "A的引用计数:" << shA.use_count() << " B的引用计数:" << shB.use_count() << endl;
      
        cout << "要离开shA和shB的作用域了,正常情况下在这之后会执行shA和shB的析构函数的" << endl;
            
       // 这里是要执行析构函数的
       // 首先,会执行shB这个B对象的析构函数,要析构B的话,得先去判断下托管B的shared_ptr的引用计数,
       //       这里是1,所以去析构B,B析构后紧接着去析构其成员对象A,此时A的引用计数为2,所以会使A的引用计数减为1
       // 然后,会执行shA这个A对象的析构函数,要析构A的话,也得先去判断下托管的A的shared_ptr的引用计数,这里是1,它可以析构
    }

    cout << "已经离开shA和shB的作用域了,请观察shA和shB的析构函数有没有被执行" << endl;
}

执行结果如下图,可发现,析构函数被正常调用了