VC驿站

 找回密码
 加入驿站

QQ登录

只需一步,快速开始

有编程疑问吗?还请到提问专区发帖提问!
搜索
查看: 186|回复: 0

[转载] C++11 智能指针

[复制链接]
51_avatar_middle
online_admins Syc 发表于 2018-11-9 11:08:25 | 显示全部楼层 |阅读模式
原作者:Babu_Abdulsalam 本文翻译自CodeProject,转载请注明出处。

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

背景###

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

让我们一个接一个的讨论。

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

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

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


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

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

解决以上所有问题的方法就是使用智能指针[如果它们足够智能的话]。

什么是智能指针?

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

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

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

  1. class Test
  2. {
  3.     public:
  4.     Test(int a = 0 ) : m_a(a) { }
  5.     ~Test( )
  6.     {
  7.        cout << "Calling destructor" << endl;
  8.     }
  9.     public: int m_a;
  10. };
  11. void main( )
  12. {
  13.     std::auto_ptr<Test> p( new Test(5) );
  14.     cout << p->m_a << endl;
  15. }
复制代码


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

  1. //***************************************************************
  2. class Test
  3. {
  4. public:
  5. Test(int a = 0 ) : m_a(a)
  6. {
  7. }
  8. ~Test( )
  9. {
  10.   cout<<"Calling destructor"<<endl;
  11. }
  12. public:
  13. int m_a;
  14. };
  15. //***************************************************************
  16. void Fun( )
  17. {
  18. int a = 0, b= 5, c;
  19. if( a ==0 )
  20. {
  21.   throw "Invalid divisor";
  22. }
  23. c = b/a;
  24. return;
  25. }
  26. //***************************************************************
  27. void main( )
  28. {
  29. try
  30. {
  31.   std::auto_ptr<Test> p( new Test(5) );
  32.   Fun( );
  33.   cout<<p->m_a<<endl;
  34. }
  35. catch(...)
  36. {
  37.   cout<<"Something has gone wrong"<<endl;
  38. }
  39. }
复制代码


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

Issue1:

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

  1. //***************************************************************
  2. class Test
  3. {
  4. public:
  5. Test(int a = 0 ) : m_a(a)
  6. {
  7. }
  8. ~Test( )
  9. {
  10.   cout<<"Calling destructor"<<endl;
  11. }
  12. public:
  13. int m_a;
  14. };


  15. //***************************************************************
  16. void Fun(auto_ptr<Test> p1 )
  17. {
  18. cout<<p1->m_a<<endl;
  19. }
  20. //***************************************************************
  21. void main( )
  22. {
  23. std::auto_ptr<Test> p( new Test(5) );
  24. Fun(p);
  25. cout<<p->m_a<<endl;
  26. }
复制代码


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

Issue2:

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

  1. //***************************************************************
  2. void main( )
  3. {
  4. std::auto_ptr<Test> p(new Test[5]);
  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离开作用域时,内存才会自动释放。

创建:

  1. void main( )
  2. {
  3. shared_ptr<int> sptr1( new int );
  4. }
复制代码

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

  1. void main( )
  2. {
  3. shared_ptr<int> sptr1 = make_shared<int>(100);
  4. }
复制代码

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

通过调用use_count()可以得到引用计数, 据此你能找到shared_ptr的数量。当debug的时候,可以通过观察shared_ptr中strong_ref的值得到引用计数。

C++11 智能指针

析构

shared_ptr默认调用delete释放关联的资源。如果用户采用一个不一样的析构策略时,他可以自由指定构造这个shared_ptr的策略。下面的例子是一个由于采用默认析构策略导致的问题:
  1. class Test
  2. {
  3. public:
  4. Test(int a = 0 ) : m_a(a)
  5. {
  6. }
  7. ~Test( )
  8. {
  9.   cout<<"Calling destructor"<<endl;
  10. }
  11. public:
  12.          int m_a;
  13. };
  14. void main( )
  15. {
  16. shared_ptr<Test> sptr1( new Test[5] );
  17. }
复制代码


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

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

通过指定delete[]来析构,上面的代码可以完美运行。

接口
就像一个普通指针一样,shared_ptr也提供解引用操作符*,->。除此之外,它还提供了一些更重要的接口:
  • get(): 获取shared_ptr绑定的资源.
  • reset(): 释放关联内存块的所有权,如果是最后一个指向该资源的shared_ptr,就释放这块内存。
  • unique: 判断是否是唯一指向当前内存的shared_ptr.
  • operator bool : 判断当前的shared_ptr是否指向一个内存块,可以用if 表达式判断。


OK,上面是所有关于shared_ptr的描述,但是shared_ptr也有一些问题:
Issues:
  1. void main( )
  2. {
  3. shared_ptr<int> sptr1( new int );
  4. shared_ptr<int> sptr2 = sptr1;
  5. shared_ptr<int> sptr3;
  6. sptr3 =sptr1
复制代码


Issues:
下表是上面代码中引用计数变化情况:
C++11 智能指针

所有的shared_ptrs拥有相同的引用计数,属于相同的组。上述代码工作良好,让我们看另外一组例子。

  1. void main( )
  2. {
  3. int* p = new int;
  4. shared_ptr<int> sptr1( p);
  5. shared_ptr<int> sptr2( p );
  6. }
复制代码

上述代码会产生一个错误,因为两个来自不同组的shared_ptr指向同一个资源。下表给你关于错误原因的图景:
C++11 智能指针

避免这个问题,尽量不要从一个裸指针(naked pointer)创建shared_ptr.
  1. class B;
  2. class A
  3. {
  4. public:
  5. A(  ) : m_sptrB(nullptr) { };
  6. ~A( )
  7. {
  8.   cout<<" A is destroyed"<<endl;
  9. }
  10. shared_ptr<B> m_sptrB;
  11. };
  12. class B
  13. {
  14. public:
  15. B(  ) : m_sptrA(nullptr) { };
  16. ~B( )
  17. {
  18.   cout<<" B is destroyed"<<endl;
  19. }
  20. shared_ptr<A> m_sptrA;
  21. };
  22. //***********************************************************
  23. void main( )
  24. {
  25. shared_ptr<B> sptrB( new B );
  26. shared_ptr<A> sptrA( new A );
  27. sptrB->m_sptrA = sptrA;
  28. sptrA->m_sptrB = sptrB;
  29. }
复制代码


上面的代码产生了一个循环引用.A对B有一个shared_ptr, B对A也有一个shared_ptr ,与sptrA和sptrB关联的资源都没有被释放,参考下表:
C++11 智能指针

当sptrA和sptrB离开作用域时,它们的引用计数都只减少到1,所以它们指向的资源并没有释放!!!!!
如果几个shared_ptrs指向的内存块属于不同组,将产生错误。
如果从一个普通指针创建一个shared_ptr还会引发另外一个问题。在上面的代码中,考虑到只有一个shared_ptr是由p创建的,代码可以好好工作。万一程序员在智能指针作用域结束之前删除了普通指针p。天啦噜!!!又是一个crash。
循环引用:如果共享智能指针卷入了循环引用,资源都不会正常释放。
为了解决循环引用,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)不起作用。

  1. void main( )
  2. {
  3. shared_ptr<Test> sptr( new Test );
  4. weak_ptr<Test> wptr( sptr );
  5. weak_ptr<Test> wptr1 = wptr;
  6. }
复制代码


可以从下图观察shared_ptr和weak_ptr的引用计数:
C++11 智能指针

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

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

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

调用use-count()去获取引用计数,该方法只返回强引用计数,并不返回弱引用计数。
调用expired()方法。比调用use_count()方法速度更快。
从weak_ptr调用lock()可以得到shared_ptr或者直接将weak_ptr转型为shared_ptr

  1. void main( )
  2. {
  3. shared_ptr<Test> sptr( new Test );
  4. weak_ptr<Test> wptr( sptr );
  5. shared_ptr<Test> sptr2 = wptr.lock( );
  6. }
复制代码


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

现在让我们见识一下weak_ptr如何解决循环引用问题:
  1. class B;
  2. class A
  3. {
  4. public:
  5. A(  ) : m_a(5)  { };
  6. ~A( )
  7. {
  8.   cout<<" A is destroyed"<<endl;
  9. }
  10. void PrintSpB( );
  11. weak_ptr<B> m_sptrB;
  12. int m_a;
  13. };
  14. class B
  15. {
  16. public:
  17. B(  ) : m_b(10) { };
  18. ~B( )
  19. {
  20.   cout<<" B is destroyed"<<endl;
  21. }
  22. weak_ptr<A> m_sptrA;
  23. int m_b;
  24. };

  25. void A::PrintSpB( )
  26. {
  27. if( !m_sptrB.expired() )
  28. {  
  29.   cout<< m_sptrB.lock( )->m_b<<endl;
  30. }
  31. }

  32. void main( )
  33. {
  34. shared_ptr<B> sptrB( new B );
  35. shared_ptr<A> sptrA( new A );
  36. sptrB->m_sptrA = sptrA;
  37. sptrA->m_sptrB = sptrB;
  38. sptrA->PrintSpB( );
  39. }
复制代码

C++11 智能指针

unique_ptr###
unique_ptr也是对auto_ptr的替换。unique_ptr遵循着独占语义。在任何时间点,资源只能唯一地被一个unique_ptr占有。当unique_ptr离开作用域,所包含的资源被释放。如果资源被其它资源重写了,之前拥有的资源将被释放。所以它保证了他所关联的资源总是能被释放。

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

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


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

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

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

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

在上面的例子里,如果upt3和upt5已经拥有了资源,只有当拥有新资源时,之前的资源才会释放。

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

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

使用哪一个?##
完全取决于你想要如何拥有一个资源,如果需要共享资源使用shared_ptr,如果独占使用资源就使用unique_ptr.

除此之外,shared_ptr比unique_ptr更加重,因为他还需要分配空间做其它的事,比如存储强引用计数,弱引用计数。而unique_ptr不需要这些,它只需要独占着保存资源对象。




上一篇:每个C++开发者都应该使用的十个C++11特性
下一篇:测试题

发帖求助前要善用论坛搜索功能,那里可能会有你要找的答案;

如果你已经在论坛发帖求助,并且从坛友或者管理的回复中解决了问题,请编辑帖子并把分类改成【已解决】

如何回报帮助你解决问题的坛友?可以给对方加【热心】【驿站币】,加分不会扣除自己的积分,做一个热心并受欢迎的人!

您需要登录后才可以回帖 登录 | 加入驿站 qq_login

本版积分规则

QQ|小黑屋|手机版|VC驿站 ( 辽ICP备09019393号tongdun|网站地图wx_jqr

GMT+8, 2018-12-14 00:57

Powered by Discuz! X3.4

© 2009-2018 cctry.com

快速回复 返回顶部 返回列表