天天看點

C++教育訓練 C++ 智能指針詳解

  c++教育訓練 c++ 智能指針詳解

  一、簡介

  由于 c++ 語言沒有自動記憶體回收機制,程式員每次 new 出來的記憶體都要手動 delete。程式員忘記 delete,流程太複雜,最終導緻沒有 delete,異常導緻程式過早退出,沒有執行 delete 的情況并不罕見。

  用智能指針便可以有效緩解這類問題,本文主要講解參見的智能指針的用法。包括:std::auto_ptr、boost::scoped_ptr、boost::shared_ptr、boost::scoped_array、boost::shared_array、boost::weak_ptr、boost:: intrusive_ptr。你可能會想,如此多的智能指針就為了解決new、delete比對問題,真的有必要嗎?看完這篇文章後,我想你心裡自然會有答案。

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

  };

  2、std::auto_ptr

  std::auto_ptr 屬于 stl,當然在 namespace std 中,包含頭檔案 #include 便可以使用。std::auto_ptr 能夠友善的管理單個堆記憶體對象。

  我們從代碼開始分析:

  void testautoptr() {

  std::auto_ptr my_memory(new simple(1)); // 建立對象,輸出:simple:1

  if (my_memory.get()) { // 判斷智能指針是否為空

  my_memory->printsomething(); // 使用 operator-> 調用智能指針對象中的函數

  my_memory.get()->info_extend = "addition"; // 使用 get() 傳回裸指針,然後給内部對象指派

  my_memory->printsomething(); // 再次列印,表明上述指派成功

  (*my_memory).info_extend += " other"; // 使用 operator* 傳回智能指針内部對象,然後用“.”調用智能指針對象中的函數

  } // my_memory 棧對象即将結束生命期,析構堆對象 simple(1)

  執行結果為:

  simple: 1

  printsomething:

  printsomething: addition

  printsomething: addition other

  ~simple: 1

  上述為正常使用 std::auto_ptr 的代碼,一切似乎都良好,無論如何不用我們顯示使用該死的 delete 了。

  其實好景不長,我們看看如下的另一個例子:

  void testautoptr2() {

  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() {

  my_memory.release();

  看到什麼異常了嗎?我們建立出來的對象沒有被析構,沒有輸出“~simple: 1”,導緻記憶體洩露。當我們不想讓 my_memory 繼續生存下去,我們調用 release() 函數釋放記憶體,結果卻導緻記憶體洩露(在記憶體受限系統中,如果my_memory占用太多記憶體,我們會考慮在使用完成後,立刻歸還,而不是等到 my_memory 結束生命期後才歸還)。

  正确的代碼應該為:

  simple* temp_memory = my_memory.release();

  delete temp_memory;

  或

  my_memory.reset(); // 釋放 my_memory 内部管理的記憶體

  原來 std::auto_ptr 的 release() 函數隻是讓出記憶體所有權,這顯然也不符合 c++ 程式設計思想。

  總結:std::auto_ptr 可用來管理單個對象的對記憶體,但是,請注意如下幾點:

  (1) 盡量不要使用“operator=”。如果使用了,請不要再使用先前對象。

  (2) 記住 release() 函數不會釋放對象,僅僅歸還所有權。

  (3) std::auto_ptr 最好不要當成參數傳遞(讀者可以自行寫代碼确定為什麼不能)。

  (4) 由于 std::auto_ptr 的“operator=”問題,有其管理的對象不能放入 std::vector 等容器中。

  (5) ……

  使用一個 std::auto_ptr 的限制還真多,還不能用來管理堆記憶體數組,這應該是你目前在想的事情吧,我也覺得限制挺多的,哪天一個不小心,就導緻問題了。

  由于 std::auto_ptr 引發了諸多問題,一些設計并不是非常符合 c++ 程式設計思想,是以引發了下面 boost 的智能指針,boost 智能指針可以解決如上問題。

  讓我們繼續向下看。

  3、boost::scoped_ptr

  boost::scoped_ptr 屬于 boost 庫,定義在 namespace boost 中,包含頭檔案 #include 便可以使用。boost::scoped_ptr 跟 std::auto_ptr 一樣,可以友善的管理單個堆記憶體對象,特别的是,boost::scoped_ptr 獨享所有權,避免了 std::auto_ptr 惱人的幾個問題。

  我們還是從代碼開始分析:

  void testscopedptr() {

  boost::scoped_ptr my_memory(new simple(1));

  my_memory->printsomething();

  my_memory.get()->info_extend = "addition";

  (*my_memory).info_extend += " other";

  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。

  4、boost::shared_ptr

  boost::shared_ptr 屬于 boost 庫,定義在 namespace boost 中,包含頭檔案 #include 便可以使用。在上面我們看到 boost::scoped_ptr 獨享所有權,不允許指派、拷貝,boost::shared_ptr 是專門用于共享所有權的,由于要共享所有權,其在内部使用了引用計數。boost::shared_ptr 也是用于管理單個堆記憶體對象的。

  void testsharedptr(boost::shared_ptr memory) { // 注意:無需使用 reference (或 const reference)

  memory->printsomething();

  std::cout << "testsharedptr usecount: " << memory.use_count() << std::endl;

  void testsharedptr2() {

  boost::shared_ptr my_memory(new simple(1));

  std::cout << "testsharedptr2 usecount: " << my_memory.use_count() << std::endl;

  testsharedptr(my_memory);

  //my_memory.release();// 編譯 error: 同樣,shared_ptr 也沒有 release 函數

  testsharedptr2 usecount: 1

  testsharedptr usecount: 2

  boost::shared_ptr 也可以很友善的使用。并且沒有 release() 函數。關鍵的一點,boost::shared_ptr 内部維護了一個引用計數,由此可以支援複制、參數傳遞等。boost::shared_ptr 提供了一個函數 use_count() ,此函數傳回 boost::shared_ptr 内部的引用計數。檢視執行結果,我們可以看到在 testsharedptr2 函數中,引用計數為 1,傳遞參數後(此處進行了一次複制),在函數testsharedptr 内部,引用計數為2,在 testsharedptr 傳回後,引用計數又降低為 1。當我們需要使用一個共享對象的時候,boost::shared_ptr 是再好不過的了。

  在此,我們已經看完單個對象的智能指針管理,關于智能指針管理數組,我們接下來講到。

  5、boost::scoped_array

  boost::scoped_array 屬于 boost 庫,定義在 namespace boost 中,包含頭檔案 #include 便可以使用。

  boost::scoped_array 便是用于管理動态數組的。跟 boost::scoped_ptr 一樣,也是獨享所有權的。

  void testscopedarray() {

  boost::scoped_array my_memory(new simple[2]); // 使用記憶體數組來初始化

  my_memory[0].printsomething();

  my_memory.get()[0].info_extend = "addition";

  (*my_memory)[0].info_extend += " other"; // 編譯 error,scoped_ptr 沒有重載 operator*

  my_memory[0].release(); // 同上,沒有 release 函數

  boost::scoped_array my_memory2;

  my_memory2 = my_memory; // 編譯 error,同上,沒有重載 operator=

  boost::scoped_array 的使用跟 boost::scoped_ptr 差不多,不支援複制,并且初始化的時候需要使用動态數組。另外,boost::scoped_array 沒有重載“operator*”,其實這并無大礙,一般情況下,我們使用 get() 函數更明确些。

  下面肯定應該講 boost::shared_array 了,一個用引用計數解決複制、參數傳遞的智能指針類。

  6、boost::shared_array

  boost::shared_array 屬于 boost 庫,定義在 namespace boost 中,包含頭檔案 #include 便可以使用。

  由于 boost::scoped_array 獨享所有權,顯然在很多情況下(參數傳遞、對象指派等)不滿足需求,由此我們引入 boost::shared_array。跟 boost::shared_ptr 一樣,内部使用了引用計數。

  void testsharedarray(boost::shared_array memory) { // 注意:無需使用 reference (或 const reference)

  std::cout << "testsharedarray usecount: " << memory.use_count() << std::endl;

  void testsharedarray2() {

  boost::shared_array my_memory(new simple[2]);

  my_memory.get()[0].info_extend = "addition 00";

  my_memory[1].printsomething();

  my_memory.get()[1].info_extend = "addition 11";

  //(*my_memory)[0].info_extend += " other"; // 編譯 error,scoped_ptr 沒有重載 operator*

  std::cout << "testsharedarray2 usecount: " << my_memory.use_count() << std::endl;

  testsharedarray(my_memory);

  simple: 0

  printsomething: addition 00

  printsomething: addition 11

  testsharedarray2 usecount: 1

  testsharedarray usecount: 2

  ~simple: 0

  跟 boost::shared_ptr 一樣,使用了引用計數,可以複制,通過參數來傳遞。

  至此,我們講過的智能指針有 std::auto_ptr、boost::scoped_ptr、boost::shared_ptr、boost::scoped_array、boost::shared_array。這幾個智能指針已經基本夠我們使用了,90% 的使用過标準智能指針的代碼就這 5 種。可如下還有兩種智能指針,它們肯定有用,但有什麼用處呢,一起看看吧。

  7、boost::weak_ptr

  boost::weak_ptr 屬于 boost 庫,定義在 namespace boost 中,包含頭檔案 #include 便可以使用。

  在講 boost::weak_ptr 之前,讓我們先回顧一下前面講解的内容。似乎 boost::scoped_ptr、boost::shared_ptr 這兩個智能指針就可以解決所有單個對象記憶體的管理了,這兒還多出一個 boost::weak_ptr,是否還有某些情況我們沒納入考慮呢?

  回答:有。首先 boost::weak_ptr 是專門為 boost::shared_ptr 而準備的。有時候,我們隻關心能否使用對象,并不關心内部的引用計數。boost::weak_ptr 是 boost::shared_ptr 的觀察者(observer)對象,觀察者意味着 boost::weak_ptr 隻對 boost::shared_ptr 進行引用,而不改變其引用計數,當被觀察的 boost::shared_ptr 失效後,相應的 boost::weak_ptr 也相應失效。

  void testweakptr() {

  boost::weak_ptr my_memory_weak;

  std::cout << "testweakptr boost::shared_ptr usecount: " << my_memory.use_count() << std::endl;

  my_memory_weak = my_memory;

  testweakptr boost::shared_ptr usecount: 1

  我們看到,盡管被指派了,内部的引用計數并沒有什麼變化,當然,讀者也可以試試傳遞參數等其他情況。

  現在要說的問題是,boost::weak_ptr 到底有什麼作用呢?從上面那個例子看來,似乎沒有任何作用,其實 boost::weak_ptr 主要用在軟體架構設計中,可以在基類(此處的基類并非抽象基類,而是指繼承于抽象基類的虛基類)中定義一個 boost::weak_ptr,用于指向子類的 boost::shared_ptr,這樣基類僅僅觀察自己的 boost::weak_ptr 是否為空就知道子類有沒對自己指派了,而不用影響子類 boost::shared_ptr 的引用計數,用以降低複雜度,更好的管理對象。

  8、boost::intrusive_ptr

  boost::intrusive_ptr屬于 boost 庫,定義在 namespace boost 中,包含頭檔案 #include 便可以使用。

  講完如上 6 種智能指針後,對于一般程式來說 c++ 堆記憶體管理就夠用了,現在有多了一種 boost::intrusive_ptr,這是一種插入式的智能指針,内部不含有引用計數,需要程式員自己加入引用計數,不然編譯不過(⊙﹏⊙b汗)。個人感覺這個智能指針沒太大用處,至少我沒用過。有興趣的朋友自己研究一下源代碼哦j。

  三、總結

  如上講了這麼多智能指針,有必要對這些智能指針做個總結:

  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 函數),因為可以用智能指針去管理。

繼續閱讀