c++中的智能指針

原文鏈接:https://blog.csdn.net/Miss_Monster/article/details/89174315

c++中的智能指針主要是基於RAII思想的。

不懂RAII思想的同學可以看看這篇博文-->RAII思想---利用對象生命週期來控制程序資源

智能指針的兩大要素

  1. RAII思想:把資源交給這個對象管理
  2. 像指針一樣的行爲(重載operator* 和operator->
  1. template<class T>
  2. class SmartPtr
  3. {
  4. public://交給對象去管理
  5. SmartPtr(T* ptr=nullptr)
  6. :_ptr(ptr)
  7. {}
  8. ~SmartPtr()
  9. {
  10. if (_ptr)
  11. delete[] _ptr;
  12. }
  13. //像指針一樣的行爲
  14. T& operator*()//對象出了作用域還在,所以返回引用
  15. {
  16. return *_ptr;
  17. }
  18. T* operator->()
  19. {
  20. return _ptr;//返回原生指針
  21. }
  22. private:
  23. T* _ptr;
  24. };

std::auto_ptr(C++98)

C++98版本的庫中提供了auto_ptr的智能指針。

想要實現一個智能指針就要實現這幾個功能:RAII思想,像指針一樣的行爲。但是對象會有拷貝構造和賦值,auto_ptr的原理是進行了管理權的轉移(管理權的轉移是當把a的值給b之後,就把a置成空),這是一種帶有缺陷的智能指針,會導致對象懸空。

實現的原理如下:

  1. template<class T>
  2. class auto_ptr
  3. {
  4. public:
  5. auto_ptr(T* ptr)
  6. :_ptr(ptr)
  7. {}
  8. ~auto_ptr()
  9. {
  10. if (_ptr != nullptr)
  11. {
  12. cout << "delete:" << _ptr << endl;
  13. delete _ptr;
  14. }
  15. }
  16. //拷貝構造ap1(ap2)---ap2拷貝構造ap1,此時把ap2置空,管理權交給ap1
  17. //this指針是ap1,ap是ap2
  18. auto_ptr(auto_ptr<T>& ap)
  19. :_ptr(ap._ptr)
  20. {
  21. ap._ptr = nullptr;//不執行此句會崩潰,同一資源釋放了兩次
  22. }
  23. //賦值 ap1=ap2
  24. auto_ptr<T>& operator=(auto_ptr<T>& ap)
  25. {
  26. if (this != &ap)//檢測是否給自己賦值
  27. {
  28. //釋放當前對象ap的資源
  29. if (_ptr)
  30. delete _ptr;
  31. //轉移ap中的資源到當前對象去
  32. _ptr = ap._ptr;
  33. ap._ptr = NULL;
  34. }
  35. return *this;//支持連續賦值
  36. }
  37. T& operator*()
  38. {
  39. return *_ptr;
  40. }
  41. T* operator->()
  42. {
  43. return _ptr;
  44. }
  45. private:
  46. T* _ptr;
  47. };

auto_ptr是一種管理權轉移的思想,由於轉移之後原來的對象會被置空,會導致對象的懸空,如下:

  1. void test_auto_ptr()
  2. {
  3. cpp::auto_ptr<int> ap1(new int);
  4. cpp::auto_ptr<int> ap2(ap1);
  5. // *ap1 = 10;//拷貝構造後管理權轉移,ap1懸空
  6. *ap2 = 20;
  7. cpp::auto_ptr<int> ap3(new int);
  8. //ap3 = ap2;//賦值把p2置空了
  9. *ap3 = 40;
  10. }

這裏導致原來的對象懸空了,auto_ptr是帶有缺陷的早期設計,因此很多公司都會禁用這個auto_ptr。

std::unique_ptr(C++11)

C++11中開始提供更靠譜的unique_ptr。

unique_ptr是簡單粗暴的防止拷貝,這種比較簡單,效率高,但是功能不全面,不支持拷貝和賦值操作。具體實現思想如下:

  1. template<class T>
  2. class unique_ptr
  3. {
  4. public:
  5. unique_ptr(T* ptr)
  6. :_ptr(ptr)
  7. {}
  8. ~unique_ptr()
  9. {
  10. delete _ptr;
  11. }
  12. //拷貝
  13. unique_ptr(unique_ptr<T>& ap) = delete;
  14. //賦值
  15. unique_ptr<T>& operator=(unique_ptr<T>& ap) = delete;
  16. T& operator*(){return *_ptr;}
  17. T* operator->(){return _ptr;}
  18. private:
  19. T* _ptr;
  20. };

std::shared_ptr(C++11)

C++11中開始提供更靠譜的並且支持拷貝的shared_ptr。

shared_ptr它的原理是引用計數,它的功能更加全面,支持拷貝構造。但是設計複雜,會有循環引用的問題。

這裏提到一個概念,引用計數,引用計數表示這塊空間被幾個對象引用,有一個對象析構了,引用計數就會減一,但是並不是每次析構都會釋放空間,而是最後一個引用它的對象纔去釋放它。

我們這裏的引用計數只需要一個就行了,所以很容易想到把它定義成靜態的,但是其實這也不可以,因爲一旦一個類中的對象管理不同的空間,就如下圖,Sp1和Sp2管理一塊空間,Sp3和Sp4和Sp5管理另一塊空間,用同一個引用計數的話,我們期望3,4,5析構後,這塊空間也被釋放掉,但是由於引用計數只減爲2,,沒有變爲1,他認爲你不是最後一個引用它的,所以不會釋放掉空間,就會有內存泄漏問題。

所以這裏的引用計數就不能跟着對象走,而是應該跟着空間走的,有一個空間就動態開闢一個引用計數跟着它,每個智能指針都能拿到該引用計數如下圖:

  1. template<class T>
  2. class shared_ptr
  3. {
  4. public:
  5. shared_ptr(T* ptr)
  6. :_ptr(ptr)
  7. ,_pcount(new int(1))//初始化,動態開闢
  8. {}
  9. ~shared_ptr()
  10. {
  11. if (--(*_pcount) == 0)//引用計數爲0時,最後一個指向它的釋放它
  12. {
  13. cout << "delete:" << _ptr << endl;
  14. delete _ptr;
  15. delete _pcount;
  16. }
  17. }
  18. //拷貝
  19. shared_ptr(const shared_ptr<T>& sp)
  20. :_ptr(sp._ptr)
  21. , _pcount(sp._pcount)
  22. {
  23. ++(*_pcount);//同一個引用計數
  24. }
  25. //賦值
  26. //sp1 = sp3;
  27. shared_ptr<T>& operator=(shared_ptr<T>& sp)
  28. {
  29. //if (this != &sp)//防止自己給自己賦值
  30. //兩個對象的ptr如果相同就不進入,否則--再++是無用功
  31. //要是相同的對象,ptr相同就不作操作
  32. //要是不同的對象,但是指向同一塊空間,ptr相同也不作操作
  33. if (_ptr != sp._ptr)
  34. {
  35. //判斷要賦值對象的引用計數是不是1
  36. //如果是1,釋放指針和引用計數再指向新的空間進行賦值
  37. if (--(*_pcount) == 0)
  38. {
  39. delete _ptr;
  40. delete _pcount;
  41. }
  42. //兩個指針指向同一塊空間,再++引用計數
  43. _ptr = sp._ptr;
  44. _pcount = sp._pcount;
  45. ++(*_pcount);
  46. }
  47. return *this;
  48. }
  49. T& operator*(){return *_ptr;}
  50. T* operator->(){return _ptr;}
  51. int use_count()
  52. {
  53. return *_pcount;
  54. }//查看引用計數是多少
  55. private:
  56. T* _ptr;
  57. int* _pcount;//引用計數
  58. };

看到這裏,很多童鞋肯定像我一樣以爲shared_ptr就到此結束了,其實並沒有,它在用起來沒有大問題,但是存在着一個問題,就是多線程的情況下的線程安全問題,來看下面這段代碼:

  1. void test_multi_thread_copy(cpp::shared_ptr<int>& sp,size_t n)
  2. {
  3. for (size_t i = 0; i < n; ++i)//拷貝n次ptr
  4. {
  5. cpp::shared_ptr<int> copy(sp);
  6. }
  7. }
  8. void test_shared_ptr_safe()
  9. {
  10. cpp::shared_ptr<int> sp(new int);
  11. std::thread t1(test_multi_thread_copy, sp, 1000);
  12. std::thread t2(test_multi_thread_copy, sp, 1000);
  13. cout << sp.use_count() << endl;
  14. t1.join();
  15. t2.join();
  16. }

t1和t2是我們創建的兩個線程,這兩個線程都去拷貝ptr1000次,此時多個線程同時對該空間進行++,--操作,那麼導致結果錯誤,並且打印出來的引用計數也是不確定的,說明此時的引用計數已經出現了問題,導致內存沒有釋放,會出現內存泄漏的問題。因爲這裏的 ++操作和 - - 操作不是原子的操作 ,存在線程安全問題。因此多線程編程時,要給這裏的引用計數進行加鎖操作,基於以上我們進一步完善shared_ptr。

  1. template<class T>
  2. class shared_ptr
  3. {
  4. public:
  5. shared_ptr(T* ptr)
  6. :_ptr(ptr)
  7. ,_pcount(new int(1))//初始化,動態開闢
  8. , _pmtx(new std::mutex)
  9. {}
  10. ~shared_ptr()
  11. {
  12. Release();
  13. //if (--(*_pcount) == 0)//引用計數爲0時,最後一個指向它的釋放它
  14. //{
  15. // cout << "delete:" << _ptr << endl;
  16. // delete _ptr;
  17. // delete _pcount;
  18. //}
  19. }
  20. void AddRef()//增加引用計數
  21. {
  22. _pmtx->lock();
  23. ++(*_pcount);//同一個引用計數
  24. _pmtx->unlock();
  25. }
  26. void Release()//釋放引用計數
  27. {
  28. _pmtx->lock();
  29. int flag = 0;
  30. if (--(*_pcount) == 0)
  31. {
  32. cout << "delete: " << _ptr << endl;
  33. delete _ptr;
  34. delete _pcount;
  35. flag = 1;
  36. }
  37. _pmtx->unlock();
  38. if (flag == 1){delete _pmtx;}
  39. }
  40. //拷貝
  41. shared_ptr(const shared_ptr<T>& sp)
  42. :_ptr(sp._ptr)
  43. , _pcount(sp._pcount)
  44. , _pmtx(sp._pmtx)
  45. {
  46. //++(*_pcount);//同一個引用計數
  47. AddRef();//加鎖,封裝成函數
  48. }
  49. //賦值
  50. //sp1 = sp3;
  51. shared_ptr<T>& operator=(shared_ptr<T>& sp)
  52. {
  53. //if (this != &sp)//防止自己給自己賦值
  54. if (_ptr != sp._ptr)
  55. {
  56. //if (--(*_pcount) == 0)
  57. //{
  58. // delete _ptr;
  59. // delete _pcount;
  60. //}
  61. Release();
  62. //兩個指針指向同一塊空間,再++引用計數
  63. _ptr = sp._ptr;
  64. _pcount = sp._pcount;
  65. _pmtx = sp._pmtx;
  66. AddRef();
  67. /*++(*_pcount);*/
  68. }
  69. return *this;
  70. }
  71. T& operator*() {return *_ptr;}
  72. T* operator->() {return _ptr;}
  73. int use_count() { return *_pcount;}//查看引用計數是多少
  74. private:
  75. T* _ptr;
  76. int* _pcount;//引用計數
  77. std::mutex* _pmtx;//定義一個鎖,大家都鎖在該鎖上,所以定義爲指針
  78. };

這樣加鎖之後,智能指針的引用計數的++和- - 是安全的,也支持拷貝構造,所以shared_ptr本身是安全的,智能指針的線程安全體現在引用計數的++和- -是線程安全的,但是它指向的對象不一定是線程安全的,因爲智能指針指向的對象本身並不受它的管控。

shared_ptr的循環引用以及weak_ptr()

  1. struct ListNode
  2. {
  3. std::shared_ptr<ListNode> _prev;
  4. std::shared_ptr<ListNode> _next;
  5. ~ListNode()
  6. {
  7. cout << "~ListNode()" << endl;
  8. }
  9. };
  10. void test_shared_ptr_cycle_ref()
  11. {
  12. std::shared_ptr<ListNode> cur(new ListNode);
  13. std::shared_ptr<ListNode> next(new ListNode);
  14. cur->_next = next;
  15. next->_prev = cur;
  16. }

這一段代碼中用shared_ptr管理了cur和next,同時也管理了cur和next裏面的_prev和_next,讓智能指針share_ptr指向cur和next時,兩個智能指針的引用計數都是1,再讓cur的_next指向了next,讓next的_prev指向了cur,此時兩個智能指針的引用計數都變成了2,出了test_shared_ptr_cycle_ref函數之後,cur和next應該去調析構函數,但是此時智能指針的引用計數不爲1,所以兩個智能指針的引用計數- -,那麼此時就沒有完成節點的釋放,因爲發生了循環引用。可以理解爲相互牽制,誰也無法進行釋放,畫個圖理解一下:

  • cur和next兩個智能指針對象指向兩個節點,引用計數變成1,我們不需要手動delete。
  • cur的_next指向next,next的_prev指向cur,引用計數變成2。
  • cur和next析構,引用計數減到1,但是_next還指向下一個節點。但是_prev還指向上一個節點。
  • 也就是說_next析構了,next就釋放了。
  • 也就是說_prev析構了,cur就釋放了。
  • 但是_next屬於cur的成員,cur釋放了,_next纔會析構,而cur由_prev管理,_prev屬於next成員,所以這就叫循環引用,誰也不會釋放。

那麼這種問題要怎麼解決呢?解決方案就是使用weak_ptr

  1. struct ListNode
  2. {
  3. std::weak_ptr<ListNode> _prev;
  4. std::weak_ptr<ListNode> _next;
  5. ~ListNode()
  6. {
  7. cout << "~ListNode()" << endl;
  8. }
  9. };

因爲在執行cur->_next = next;next->_prev = cur;時,weak_ptr的_next和_prev不會增加cur和next的引用計數,這樣就保證了正常的釋放。

但是weak_ptr只能用於循環引用場景下,其他時候不能用

shared_ptr定製刪除器

在上面寫的代碼中,都是一次只申請了一塊空間,然後讓智能指針管理,智能指針釋放的時候直接調用delete即可,但是不排除有這麼一種情況,那就是如果一次申請多塊空間呢?

  1. struct A
  2. {
  3. ~A()
  4. {
  5. cout << "~A()" << endl;
  6. }
  7. };
  8. //定製刪除器
  9. void test_shared_ptr_deletor()
  10. {
  11. std::shared_ptr<A> sp(new A[10]);
  12. }
  13. int main()
  14. {
  15. test_shared_ptr_deletor();
  16. system("pause");
  17. return 0;
  18. }

那此時就需要用delete[]來進行釋放,因此可以利用仿函數來定製刪除器。

  1. struct A
  2. {
  3. ~A()
  4. {
  5. cout << "~A()" << endl;
  6. }
  7. };
  8. template<class T>
  9. struct DeleteArray
  10. {
  11. void operator()(T* ptr)
  12. {
  13. delete[] ptr;
  14. }
  15. };
  16. //定製刪除器
  17. void test_shared_ptr_deletor()
  18. {
  19. DeleteArray<A> del;
  20. std::shared_ptr<A> sp(new A[10],del);
  21. }
  22. int main()
  23. {
  24. test_shared_ptr_deletor();
  25. system("pause");
  26. return 0;
  27. }

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章