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<Simple> 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->PrintSomething(); // 再次列印,表明上述賦值成功 } } // my_memory 棧物件即將結束生命期,析構堆物件 Simple(1)
執行結果為:
Simple: 1 PrintSomething: PrintSomething: Addition PrintSomething: Addition other ~Simple: 1
上述為正常使用 std::auto_ptr 的程式碼,一切似乎都良好,無論如何不用我們顯示使用該死的delete 了。
其實好景不長,我們看看如下的另一個例子:
void TestAutoPtr2() { std::auto_ptr<Simple> my_memory(new Simple(1)); if (my_memory.get()) { std::auto_ptr<Simple> 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() { std::auto_ptr<Simple> my_memory(new Simple(1)); if (my_memory.get()) { my_memory.release(); } }
執行結果為:
Simple: 1
看到什麼異常了嗎?我們建立出來的物件沒有被析構,沒有輸出“~Simple: 1”,導致記憶體洩露。當我們不想讓 my_memory 繼續生存下去,我們呼叫 release() 函式釋放記憶體,結果卻導致記憶體洩露(在記憶體受限系統中,如果my_memory佔用太多記憶體,我們會考慮在使用完成後,立刻歸還,而不是等到 my_memory 結束生命期後才歸還)。
正確的程式碼應該為:
void TestAutoPtr3() { std::auto_ptr<Simple> my_memory(new Simple(1)); if (my_memory.get()) { Simple* temp_memory = my_memory.release(); delete temp_memory; } }
或
void TestAutoPtr3() { std::auto_ptr<Simple> my_memory(new Simple(1)); if (my_memory.get()) { 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/smart_ptr.hpp> 便可以使用。boost::scoped_ptr 跟 std::auto_ptr 一樣,可以方便的管理單個堆記憶體物件,特別的是,boost::scoped_ptr 獨享所有權,避免了 std::auto_ptr 惱人的幾個問題。
我們還是從程式碼開始分析:
void TestScopedPtr() { boost::scoped_ptr<Simple> my_memory(new Simple(1)); if (my_memory.get()) { my_memory->PrintSomething(); my_memory.get()->info_extend = "Addition"; my_memory->PrintSomething(); (*my_memory).info_extend += " other"; my_memory->PrintSomething(); my_memory.release(); // 編譯 error: scoped_ptr 沒有 release 函式 std::auto_ptr<Simple> 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/smart_ptr.hpp> 便可以使用。在上面我們看到 boost::scoped_ptr 獨享所有權,不允許賦值、拷貝,boost::shared_ptr 是專門用於共享所有權的,由於要共享所有權,其在內部使用了引用計數。boost::shared_ptr 也是用於管理單個堆記憶體物件的。
我們還是從程式碼開始分析:
void TestSharedPtr(boost::shared_ptr<Simple> memory) { // 注意:無需使用 reference (或 const reference) memory->PrintSomething(); std::cout << "TestSharedPtr UseCount: " << memory.use_count() << std::endl; } void TestSharedPtr2() { boost::shared_ptr<Simple> my_memory(new Simple(1)); if (my_memory.get()) { my_memory->PrintSomething(); my_memory.get()->info_extend = "Addition"; my_memory->PrintSomething(); (*my_memory).info_extend += " other"; my_memory->PrintSomething(); } std::cout << "TestSharedPtr2 UseCount: " << my_memory.use_count() << std::endl; TestSharedPtr(my_memory); std::cout << "TestSharedPtr2 UseCount: " << my_memory.use_count() << std::endl; //my_memory.release();// 編譯 error: 同樣,shared_ptr 也沒有 release 函式 }
執行結果為:
Simple: 1 PrintSomething: PrintSomething: Addition PrintSomething: Addition other TestSharedPtr2 UseCount: 1 PrintSomething: Addition other TestSharedPtr UseCount: 2 TestSharedPtr2 UseCount: 1 ~Simple: 1
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/smart_ptr.hpp> 便可以使用。
boost::scoped_array 便是用於管理動態陣列的。跟 boost::scoped_ptr 一樣,也是獨享所有權的。
我們還是從程式碼開始分析:
void TestScopedArray() { boost::scoped_array<Simple> my_memory(new Simple[2]); // 使用記憶體陣列來初始化 if (my_memory.get()) { my_memory[0].PrintSomething(); my_memory.get()[0].info_extend = "Addition"; my_memory[0].PrintSomething(); (*my_memory)[0].info_extend += " other"; // 編譯 error,scoped_ptr 沒有過載 operator* my_memory[0].release(); // 同上,沒有 release 函式 boost::scoped_array<Simple> 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/smart_ptr.hpp> 便可以使用。
由於 boost::scoped_array 獨享所有權,顯然在很多情況下(引數傳遞、物件賦值等)不滿足需求,由此我們引入 boost::shared_array。跟 boost::shared_ptr 一樣,內部使用了引用計數。
我們還是從程式碼開始分析:
void TestSharedArray(boost::shared_array<Simple> memory) { // 注意:無需使用 reference (或 const reference) std::cout << "TestSharedArray UseCount: " << memory.use_count() << std::endl; } void TestSharedArray2() { boost::shared_array<Simple> my_memory(new Simple[2]); if (my_memory.get()) { my_memory[0].PrintSomething(); my_memory.get()[0].info_extend = "Addition 00"; my_memory[0].PrintSomething(); my_memory[1].PrintSomething(); my_memory.get()[1].info_extend = "Addition 11"; my_memory[1].PrintSomething(); //(*my_memory)[0].info_extend += " other"; // 編譯 error,scoped_ptr 沒有過載 operator* } std::cout << "TestSharedArray2 UseCount: " << my_memory.use_count() << std::endl; TestSharedArray(my_memory); std::cout << "TestSharedArray2 UseCount: " << my_memory.use_count() << std::endl; }
執行結果為:
Simple: 0 Simple: 0 PrintSomething: PrintSomething: Addition 00 PrintSomething: PrintSomething: Addition 11 TestSharedArray2 UseCount: 1 TestSharedArray UseCount: 2 TestSharedArray2 UseCount: 1 ~Simple: 0 ~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/smart_ptr.hpp> 便可以使用。
在講 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<Simple> my_memory_weak; boost::shared_ptr<Simple> my_memory(new Simple(1)); std::cout << "TestWeakPtr boost::shared_ptr UseCount: " << my_memory.use_count() << std::endl; my_memory_weak = my_memory; std::cout << "TestWeakPtr boost::shared_ptr UseCount: " << my_memory.use_count() << std::endl; }
執行結果為:
Simple: 1 TestWeakPtr boost::shared_ptr UseCount: 1 TestWeakPtr boost::shared_ptr UseCount: 1 ~Simple: 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<boost/smart_ptr.hpp> 便可以使用。
講完如上 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 函式),因為可以用智慧指標去管理。
相關文章
- c++ 智慧指標用法詳解C++指標
- 詳解c++指標的指標和指標的引用C++指標
- C++智慧指標C++指標
- C++ 智慧指標詳解: std::unique_ptr 和 std::shared_ptrC++指標
- C++指標的概念解讀 超詳細C++指標
- 指標詳解指標
- C++進階(智慧指標)C++指標
- C++筆記(11) 智慧指標C++筆記指標
- C++ 用智慧指標這樣包裝 this 指標是否可行C++指標
- C++標準庫有四種智慧指標C++指標
- C++智慧指標之shared_ptr與右值引用(詳細)C++指標
- C++基於模板實現智慧指標C++指標
- 【C++】智慧指標的正確使用方式C++指標
- C++ this 指標C++指標
- C++ 指標C++指標
- 指標的詳細講解指標
- 【C++】 61_智慧指標類别範本C++指標
- 聊聊 C++ 中的幾種智慧指標 (上)C++指標
- C++中指標與引用詳解C++指標
- C++指標理解C++指標
- C語言指標詳解(一)C語言指標
- C語言指標詳解(二)C語言指標
- 智慧指標指標
- C++智慧指標學習——小談引用計數C++指標
- c++智慧指標中的reset成員函式C++指標函式
- c++動態記憶體管理與智慧指標C++記憶體指標
- 陣列,函式與指標 詳解陣列函式指標
- c++ 函式指標C++函式指標
- C++(函式指標)C++函式指標
- C++中的this指標C++指標
- C++基礎回顧4——智慧指標shared_ptrC++指標
- [CPP] 智慧指標指標
- 什麼是智慧指標?為什麼要用智慧指標?指標
- [C++] 成員函式指標和函式指標C++函式指標
- 【C++系列】指標物件和物件指標的區別C++指標物件
- C/C++指標總結C++指標
- 1-7 C++指標C++指標
- C++ 類成員指標C++指標
- openfoam 智慧指標探索指標