C++11 智能指针

原做者:Babu_Abdulsalam 本文翻译自CodeProject,转载请注明出处。程序员

引入

尽管有另一篇文章说C++11里的智能指针了。近来,我听到许多人谈论C++新标准,就是所谓的C++0x/C++11。 我研究了一下C++11的一些语言特性,发现确实它确实有一些巨大的改变。我将重点关注C++11的智能指针部分。数组

背景

普通指针(normal/raw/naked pointers)的问题?bash

让咱们一个接一个的讨论。函数

若是不恰当处理指针就会带来许多问题,因此人们老是避免使用它。这也是许多新手程序员不喜欢指针的缘由。指针老是会扯上不少问题,例如指针所指向对象的生命周期,挂起引用(dangling references)以及内存泄露。ui

若是一块内存被多个指针引用,但其中的一个指针释放且其他的指针并不知道,这样的状况下,就发生了挂起引用。而内存泄露,就如你知道的同样,当从堆中申请了内存后不释放回去,这时就会发生内存泄露。有人说,我写了清晰而且带有错误验证的代码,为何我还要使用智能指针呢?一个程序员也问我:“嗨,下面是个人代码,我从堆(heap)中申请了一块内存,使用完后,我又正确的把它归还给了堆,那么使用智能指针的必要在哪里?”spa

void Foo( )
{ 
    int* iPtr = new int[5];  
    //manipulate the memory block . . .  
    delete[ ] iPtr;
 }
复制代码

理想情况下,上面这段代码确实可以工做的很好,内存也可以恰当的释放回去。可是仔细思考一下实际的工做环境以及代码执行条件。在内存分配和释放的间隙,程序指令确实能作许多糟糕的事情,好比访问无效的内存地址,除以0,或者有另一个程序员在你的程序中修改了一个bug,他根据一个条件增长了一个过早的返回语句。翻译

在以上全部状况下,你的程序都走不到内存释放的那部分。前两种状况下,程序抛出了异常,而第三种状况,内存还没释放,程序就过早的return了。因此程序运行时,内存就已经泄露了。debug

解决以上全部问题的方法就是使用智能指针[若是它们足够智能的话]。3d

什么是智能指针?指针

智能指针是一个RAIIResource Acquisition is initialization)类模型,用来动态的分配内存。它提供全部普通指针提供的接口,却不多发生异常。在构造中,它分配内存,当离开做用域时,它会自动释放已分配的内存。这样的话,程序员就从手动管理动态内存的繁杂任务中解放出来了。

C++98提供了第一种智能指针:auto_ptr

auto_ptr

让咱们来见识一下auto_ptr如何解决上述问题的吧。

class Test
{
    public: 
    Test(int a = 0 ) : m_a(a) { }
    ~Test( )
    { 
       cout << "Calling destructor" << endl; 
    }
    public: int m_a;
};
void main( )
{ 
    std::auto_ptr<Test> p( new Test(5) ); 
    cout << p->m_a << endl;
}  
复制代码

上述代码会智能地释放与指针绑定的内存。做用的过程是这样的:咱们申请了一块内存来放Test对象,而且把他绑定到auto_ptr p上。因此当p离开做用域时,它所指向的内存块也会被自动释放。

//***************************************************************
class Test
{
public:
 Test(int a = 0 ) : m_a(a)
 {
 }
 ~Test( )
 {
  cout<<"Calling destructor"<<endl;
 }
public:
 int m_a;
};
//***************************************************************
void Fun( )
{
 int a = 0, b= 5, c;
 if( a ==0 )
 {
  throw "Invalid divisor";
 }
 c = b/a;
 return;
}
//***************************************************************
void main( )
{
 try
 {
  std::auto_ptr<Test> p( new Test(5) ); 
  Fun( );
  cout<<p->m_a<<endl;
 }
 catch(...)
 {
  cout<<"Something has gone wrong"<<endl;
 }
}
复制代码

上面的例子中,尽管异常被抛出,可是指针仍然正确地被释放了。这是由于当异常抛出时,栈松绑(stack unwinding),当try 块中的全部对象destroy后,p 离开了该做用域,因此它绑定的内存也就释放了。

Issue1:

目前为止,auto_ptr仍是足够智能的,可是它仍是有一些根本性的破绽的。当把一个auto_ptr赋给另一个auto_ptr时,它的全部权(ownship)也转移了。当我在函数间传递auto_ptr时,这就是一个问题。话说,我在Foo()中有一个auto_ptr,而后在Foo()中我把指针传递给了Fun()函数,当Fun()函数执行完毕时,指针的全部权不会再返还给Foo

//***************************************************************
class Test
{
public:
 Test(int a = 0 ) : m_a(a)
 {
 }
 ~Test( )
 {
  cout<<"Calling destructor"<<endl;
 }
public:
 int m_a;
};
 
 
//***************************************************************
void Fun(auto_ptr<Test> p1 )
{
 cout<<p1->m_a<<endl;
}
//***************************************************************
void main( )
{
 std::auto_ptr<Test> p( new Test(5) ); 
 Fun(p);
 cout<<p->m_a<<endl;
} 
复制代码

因为auto_ptr的野指针行为,上面的代码致使程序崩溃。在这期间发生了这些细节,p拥有一块内存,当Fun调用时, p把关联的内存块的全部权传给了auto_ptr p1, p1p的copy(注:这里从Fun函数的定义式看出,函数参数时值传递,因此把p的值拷进了函数中),这时p1就拥有了以前p拥有的内存块。目前为止,一切安好。如今Fun函数执行完了,p1离开了做用域,因此p1关联的内存块也就释放了。那么p呢?p什么都没了,这就是crash的缘由了,下一行代码还试图访问p,好像p还拥有什么资源似的。

Issue2:

还有另一个缺点。auto_ptr不能指向一组对象,就是说它不能和操做符new[]一块儿使用。

//***************************************************************
void main( ) {
 std::auto_ptr<Test> p(new Test[5]);
}
复制代码

上面的代码将产生一个运行时错误。由于当auto_ptr离开做用域时,delete被默认用来释放关联的内存空间。当auto_ptr只指向一个对象时,这固然是没问题的,可是在上面的代码里,咱们在堆里建立了一组对象,应该使用delete[]来释放,而不是delete.

Issue3:

auto_ptr不能和标准容器(vector,list,map....)一块儿使用。

由于auto_ptr容易产生错误,因此它也将被废弃了。C++11提供了一组新的智能指针,每个都各有用武之地。

  • shared_ptr
  • unique_ptr
  • weak_ptr

shared_ptr

好吧,准备享受真正的智能。第一种智能指针是shared_ptr,它有一个叫作共享全部权(sharedownership)的概念。shared_ptr的目标很是简单:多个指针能够同时指向一个对象,当最后一个shared_ptr离开做用域时,内存才会自动释放。

建立:

void main( )
{
 shared_ptr<int> sptr1( new int );
}
复制代码

使用make_shared宏来加速建立的过程。由于shared_ptr主动分配内存而且保存引用计数(reference count),make_shared 以一种更有效率的方法来实现建立工做。

void main( )
{
 shared_ptr<int> sptr1 = make_shared<int>(100);
}
复制代码

上面的代码建立了一个shared_ptr,指向一块内存,该内存包含一个整数100,以及引用计数1.若是经过sptr1再建立一个shared_ptr,引用计数就会变成2. 该计数被称为强引用(strong reference),除此以外,shared_ptr还有另一种引用计数叫作弱引用(weak reference),后面将介绍。

经过调用use_count()能够获得引用计数, 据此你能找到shared_ptr的数量。当debug的时候,能够经过观察shared_ptrstrong_ref的值获得引用计数。

reference count

析构

shared_ptr默认调用delete释放关联的资源。若是用户采用一个不同的析构策略时,他能够自由指定构造这个shared_ptr的策略。下面的例子是一个因为采用默认析构策略致使的问题:

class Test
{
public:
 Test(int a = 0 ) : m_a(a)
 {
 }
 ~Test( )
 {
  cout<<"Calling destructor"<<endl;
 }
public:
         int m_a;
};
void main( )
{
 shared_ptr<Test> sptr1( new Test[5] );
}
复制代码

在此场景下,shared_ptr指向一组对象,可是当离开做用域时,默认的析构函数调用delete释放资源。实际上,咱们应该调用delete[]来销毁这个数组。用户能够经过调用一个函数,例如一个lamda表达式,来指定一个通用的释放步骤。

void main( )
{
 shared_ptr<Test> sptr1( new Test[5], 
        [ ](Test* p) { delete[ ] p; } );
}
复制代码

经过指定delete[]来析构,上面的代码能够完美运行。

接口 就像一个普通指针同样,shared_ptr也提供解引用操做符*,->。除此以外,它还提供了一些更重要的接口:

  • get(): 获取shared_ptr绑定的资源.
  • reset(): 释放关联内存块的全部权,若是是最后一个指向该资源的shared_ptr,就释放这块内存。
  • unique: 判断是不是惟一指向当前内存的shared_ptr.
  • operator bool : 判断当前的shared_ptr是否指向一个内存块,能够用if 表达式判断。

OK,上面是全部关于shared_ptr的描述,可是shared_ptr也有一些问题: Issues:

void main( )
{
 shared_ptr<int> sptr1( new int );
 shared_ptr<int> sptr2 = sptr1;
 shared_ptr<int> sptr3;
 sptr3 =sptr1
复制代码

Issues: 下表是上面代码中引用计数变化状况:

引用计数变化

全部的shared_ptrs拥有相同的引用计数,属于相同的组。上述代码工做良好,让咱们看另一组例子。

void main( )
{
 int* p = new int;
 shared_ptr<int> sptr1( p);
 shared_ptr<int> sptr2( p );
}
复制代码

上述代码会产生一个错误,由于两个来自不一样组的shared_ptr指向同一个资源。下表给你关于错误缘由的图景:

引用计数

避免这个问题,尽可能不要从一个裸指针(naked pointer)建立shared_ptr.

class B;
class A
{
public:
 A(  ) : m_sptrB(nullptr) { };
 ~A( )
 {
  cout<<" A is destroyed"<<endl;
 }
 shared_ptr<B> m_sptrB;
};
class B
{
public:
 B(  ) : m_sptrA(nullptr) { };
 ~B( )
 {
  cout<<" B is destroyed"<<endl;
 }
 shared_ptr<A> m_sptrA;
};
//***********************************************************
void main( )
{
 shared_ptr<B> sptrB( new B );
 shared_ptr<A> sptrA( new A );
 sptrB->m_sptrA = sptrA;
 sptrA->m_sptrB = sptrB;
}
复制代码

上面的代码产生了一个循环引用.AB有一个shared_ptr, BA也有一个shared_ptr ,与sptrAsptrB关联的资源都没有被释放,参考下表:

sptrA&sptrB
sptrAsptrB离开做用域时,它们的引用计数都只减小到1,因此它们指向的资源并无释放!!!!!

  1. 若是几个shared_ptrs指向的内存块属于不一样组,将产生错误。
  2. 若是从一个普通指针建立一个shared_ptr还会引起另一个问题。在上面的代码中,考虑到只有一个shared_ptr是由p建立的,代码能够好好工做。万一程序员在智能指针做用域结束以前删除了普通指针p。天啦噜!!!又是一个crash。
  3. 循环引用:若是共享智能指针卷入了循环引用,资源都不会正常释放。

为了解决循环引用,C++提供了另一种智能指针:weak_ptr

Weak_Ptr

weak_ptr 拥有共享语义(sharing semantics)和不包含语义(not owning semantics)。这意味着,weak_ptr能够共享shared_ptr持有的资源。因此能够从一个包含资源的shared_ptr建立weak_ptr

weak_ptr不支持普通指针包含的*->操做。它并不包含资源因此也不容许程序员操做资源。既然如此,咱们如何使用weak_ptr呢?

答案是从weak_ptr中建立shared_ptr而后再使用它。经过增长强引用计数,当使用时能够确保资源不会被销毁。当引用计数增长时,能够确定的是从weak_ptr中建立的shared_ptr引用计数至少为1.不然,当你使用weak_ptr就可能发生以下问题:当shared_ptr离开做用域时,其拥有的资源会释放,从而致使了混乱。

建立

能够以shared_ptr做为参数构造weak_ptr.从shared_ptr建立一个weak_ptr增长了共享指针的弱引用计数(weak reference),意味着shared_ptr与其它的指针共享着它所拥有的资源。可是当shared_ptr离开做用域时,这个计数不做为是否释放资源的依据。换句话说,就是除非强引用计数变为0,才会释放掉指针指向的资源,在这里,弱引用计数(weak reference)不起做用。

void main( )
{
 shared_ptr<Test> sptr( new Test );
 weak_ptr<Test> wptr( sptr );
 weak_ptr<Test> wptr1 = wptr;
}
复制代码

能够从下图观察shared_ptrweak_ptr的引用计数:

shared_ptr 和weak_ptr变化

将一个weak_ptr赋给另外一个weak_ptr会增长弱引用计数(weak reference count)。

因此,当shared_ptr离开做用域时,其内的资源释放了,这时候指向该shared_ptrweak_ptr发生了什么?weak_ptr过时了(expired)。

如何判断weak_ptr是否指向有效资源,有两种方法:

  1. 调用use-count()去获取引用计数,该方法只返回强引用计数,并不返回弱引用计数。
  2. 调用expired()方法。比调用use_count()方法速度更快。

weak_ptr调用lock()能够获得shared_ptr或者直接将weak_ptr转型为shared_ptr

void main( )
{
 shared_ptr<Test> sptr( new Test );
 weak_ptr<Test> wptr( sptr );
 shared_ptr<Test> sptr2 = wptr.lock( );
}
复制代码

如以前所述,从weak_ptr中获取shared_ptr增长强引用计数。

如今让咱们见识一下weak_ptr如何解决循环引用问题:

class B;
class A
{
public:
 A(  ) : m_a(5)  { };
 ~A( )
 {
  cout<<" A is destroyed"<<endl;
 }
 void PrintSpB( );
 weak_ptr<B> m_sptrB;
 int m_a;
};
class B
{
public:
 B(  ) : m_b(10) { };
 ~B( )
 {
  cout<<" B is destroyed"<<endl;
 }
 weak_ptr<A> m_sptrA;
 int m_b;
};

void A::PrintSpB( )
{
 if( !m_sptrB.expired() )
 {  
  cout<< m_sptrB.lock( )->m_b<<endl;
 }
}

void main( )
{
 shared_ptr<B> sptrB( new B );
 shared_ptr<A> sptrA( new A );
 sptrB->m_sptrA = sptrA;
 sptrA->m_sptrB = sptrB;
 sptrA->PrintSpB( ); 
}

复制代码

引用计数

unique_ptr

unique_ptr也是对auto_ptr的替换。unique_ptr遵循着独占语义。在任什么时候间点,资源只能惟一地被一个unique_ptr占有。当unique_ptr离开做用域,所包含的资源被释放。若是资源被其它资源重写了,以前拥有的资源将被释放。因此它保证了他所关联的资源老是能被释放。

建立 unique_ptr的建立方法和shared_ptr同样,除非建立一个指向数组类型的unique_ptr

unique_ptr<int> uptr( new int );
复制代码

unique_ptr提供了建立数组对象的特殊方法,当指针离开做用域时,调用delete[]代替delete。当建立unique_ptr时,这一组对象被视做模板参数的部分。这样,程序员就不须要再提供一个指定的析构方法,以下:

unique_ptr<int[ ]> uptr( new int[5] );
复制代码

当把unique_ptr赋给另一个对象时,资源的全部权就会被转移。

记住unique_ptr不提供复制语义(拷贝赋值和拷贝构造都不能够),只支持移动语义(move semantics).

在上面的例子里,若是upt3upt5已经拥有了资源,只有当拥有新资源时,以前的资源才会释放。

接口

unique_ptr提供的接口和传统指针差很少,可是不支持指针运算。

unique_ptr提供一个release()的方法,释放全部权。releasereset的区别在于,release仅仅释放全部权但不释放资源,reset也释放资源。

使用哪个?

彻底取决于你想要如何拥有一个资源,若是须要共享资源使用shared_ptr,若是独占使用资源就使用unique_ptr.

除此以外,shared_ptrunique_ptr更加剧,由于他还须要分配空间作其它的事,好比存储强引用计数,弱引用计数。而unique_ptr不须要这些,它只须要独占着保存资源对象。

相关文章
相关标签/搜索