C++智能指針簡單剖析

導讀

最近在補看《C++ Primer Plus》第六版,這的確是本好書,其中關於智能指針的章節解析的很是清晰,一解我之前的多處困惑。C++面試過程當中,不少面試官都喜歡問智能指針相關的問題,好比你知道哪些智能指針?shared_ptr的設計原理是什麼?若是讓你本身設計一個智能指針,你如何完成?等等……。並且在看開源的C++項目時,也能隨處看到智能指針的影子。這說明智能指針不只是面試官愛問的題材,更是很是有實用價值。ios

下面是我在看智能指針時所作的筆記,但願可以解決你對智能指針的一些困擾。面試

目錄

  1. 智能指針背後的設計思想
  2. C++智能指針簡單介紹
  3. 爲何摒棄auto_ptr?
  4. unique_ptr爲什麼優於auto_ptr?
  5. 如何選擇智能指針?

正文

1. 智能指針背後的設計思想

咱們先來看一個簡單的例子:算法

void remodel(std::string & str) { std::string * ps = new std::string(str); ... if (weird_thing()) throw exception(); str = *ps; delete ps; return; }

當出現異常時(weird_thing()返回true),delete將不被執行,所以將致使內存泄露。數組

如何避免這種問題?有人會說,這還不簡單,直接在throw exception();以前加上delete ps;不就好了。是的,你本應如此,問題是不少人都會忘記在適當的地方加上delete語句(連上述代碼中最後的那句delete語句也會有不少人忘記吧),若是你要對一個龐大的工程進行review,看是否有這種潛在的內存泄露問題,那就是一場災難!
這時咱們會想:當remodel這樣的函數終止(不論是正常終止,仍是因爲出現了異常而終止),本地變量都將自動從棧內存中刪除—所以指針ps佔據的內存將被釋放,若是ps指向的內存也被自動釋放,那該有多好啊。
咱們知道析構函數有這個功能。若是ps有一個析構函數,該析構函數將在ps過時時自動釋放它指向的內存。但ps的問題在於,它只是一個常規指針,不是有析構凼數的類對象指針。若是它指向的是對象,則能夠在對象過時時,讓它的析構函數刪除指向的內存。安全

這正是 auto_ptr、unique_ptr和shared_ptr這幾個智能指針背後的設計思想。我簡單的總結下就是:將基本類型指針封裝爲類對象指針(這個類確定是個模板,以適應不一樣基本類型的需求),並在析構函數裏編寫delete語句刪除指針指向的內存空間。函數

所以,要轉換remodel()函數,應按下面3個步驟進行:spa

  • 包含頭義件memory(智能指針所在的頭文件);
  • 將指向string的指針替換爲指向string的智能指針對象;
  • 刪除delete語句。

下面是使用auto_ptr修改該函數的結果:設計

# include <memory>
void remodel (std::string & str) { std::auto_ptr<std::string> ps (new std::string(str)); ... if (weird_thing ()) throw exception(); str = *ps; // delete ps; NO LONGER NEEDED
    return; }

 

2. C++智能指針簡單介紹

STL一共給咱們提供了四種智能指針:auto_ptr、unique_ptr、shared_ptr和weak_ptr(本文章暫不討論)。
模板auto_ptr是C++98提供的解決方案,C+11已將將其摒棄,並提供了另外兩種解決方案。然而,雖然auto_ptr被摒棄,但它已使用了好多年:同時,若是您的編譯器不支持其餘兩種解決力案,auto_ptr將是惟一的選擇。指針

使用注意點code

  • 全部的智能指針類都有一個explicit構造函數,以指針做爲參數。好比auto_ptr的類模板原型爲:
    templet<class T>
    class auto_ptr { explicit auto_ptr(X* p = 0) ; ... };

    所以不能自動將指針轉換爲智能指針對象,必須顯式調用:

    shared_ptr<double> pd; double *p_reg = new double; pd = p_reg;                               // not allowed (implicit conversion)
    pd = shared_ptr<double>(p_reg);           // allowed (explicit conversion)
    shared_ptr<double> pshared = p_reg;       // not allowed (implicit conversion)
    shared_ptr<double> pshared(p_reg);        // allowed (explicit conversion)

     

  • 對所有三種智能指針都應避免的一點:
    string vacation("I wandered lonely as a cloud."); shared_ptr<string> pvac(&vacation);   // No

    pvac過時時,程序將把delete運算符用於非堆內存,這是錯誤的。

使用舉例

#include <iostream> #include <string> #include <memory>

class report { private: std::string str; public: report(const std::string s) : str(s) { std::cout << "Object created.\n"; } ~report() { std::cout << "Object deleted.\n"; } void comment() const { std::cout << str << "\n"; } }; int main() { { std::auto_ptr<report> ps(new report("using auto ptr")); ps->comment(); } { std::shared_ptr<report> ps(new report("using shared ptr")); ps->comment(); } { std::unique_ptr<report> ps(new report("using unique ptr")); ps->comment(); } return 0; }

 

3. 爲何摒棄auto_ptr?

先來看下面的賦值語句:

auto_ptr< string> ps (new string ("I reigned lonely as a cloud.」);
auto_ptr<string> vocation; vocaticn = ps;

上述賦值語句將完成什麼工做呢?若是ps和vocation是常規指針,則兩個指針將指向同一個string對象。這是不能接受的,由於程序將試圖刪除同一個對象兩次——一次是ps過時時,另外一次是vocation過時時。要避免這種問題,方法有多種:

  • 定義陚值運算符,使之執行深複製。這樣兩個指針將指向不一樣的對象,其中的一個對象是另外一個對象的副本,缺點是浪費空間,因此智能指針都未採用此方案。
  • 創建全部權(ownership)概念。對於特定的對象,只能有一個智能指針可擁有,這樣只有擁有對象的智能指針的構造函數會刪除該對象。而後讓賦值操做轉讓全部權。這就是用於auto_ptr和uniqiie_ptr 的策略,但unique_ptr的策略更嚴格。
  • 建立智能更高的指針,跟蹤引用特定對象的智能指針數。這稱爲引用計數。例如,賦值時,計數將加1,而指針過時時,計數將減1,。當減爲0時才調用delete。這是shared_ptr採用的策略。

固然,一樣的策略也適用於複製構造函數。
每種方法都有其用途,但爲什麼說要摒棄auto_ptr呢?
下面舉個例子來講明。

#include <iostream> #include <string> #include <memory>
using namespace std; int main() { auto_ptr<string> films[5] = { auto_ptr<string> (new string("Fowl Balls")), auto_ptr<string> (new string("Duck Walks")), auto_ptr<string> (new string("Chicken Runs")), auto_ptr<string> (new string("Turkey Errors")), auto_ptr<string> (new string("Goose Eggs")) }; auto_ptr<string> pwin; pwin = films[2]; // films[2] loses ownership. 將全部權從films[2]轉讓給pwin,此時films[2]再也不引用該字符串從而變成空指針
 cout << "The nominees for best avian baseballl film are\n"; for(int i = 0; i < 5; ++i) cout << *films[i] << endl; cout << "The winner is " << *pwin << endl; cin.get(); return 0; }

運行下發現程序崩潰了,緣由在上面註釋已經說的很清楚,films[2]已是空指針了,下面輸出訪問空指針固然會崩潰了。但這裏若是把auto_ptr換成shared_ptr或unique_ptr後,程序就不會崩潰,緣由以下:

  • 使用shared_ptr時運行正常,由於shared_ptr採用引用計數,pwin和films[2]都指向同一塊內存,在釋放空間時由於事先要判斷引用計數值的大小所以不會出現屢次刪除一個對象的錯誤。
  • 使用unique_ptr時編譯出錯,與auto_ptr同樣,unique_ptr也採用全部權模型,但在使用unique_ptr時,程序不會等到運行階段崩潰,而在編譯器因下述代碼行出現錯誤:
    unique_ptr<string> pwin; pwin = films[2]; // films[2] loses ownership.
    指導你發現潛在的內存錯誤。

這就是爲什麼要摒棄auto_ptr的緣由,一句話總結就是:避免潛在的內存崩潰問題。

 

4. unique_ptr爲什麼優於auto_ptr?

可能你們認爲前面的例子已經說明了unique_ptr爲什麼優於auto_ptr,也就是安全問題,下面再敘述的清晰一點。
請看下面的語句:

auto_ptr<string> p1(new string ("auto") ; //#1
auto_ptr<string> p2;                       //#2
p2 = p1;                                   //#3

在語句#3中,p2接管string對象的全部權後,p1的全部權將被剝奪。前面說過,這是好事,可防止p1和p2的析構函數試圖刪同—個對象;

但若是程序隨後試圖使用p1,這將是件壞事,由於p1再也不指向有效的數據。

下面來看使用unique_ptr的狀況:

unique_ptr<string> p3 (new string ("auto");   //#4
unique_ptr<string> p4;                       //#5
p4 = p3;                                      //#6

編譯器認爲語句#6非法,避免了p3再也不指向有效數據的問題。所以,unique_ptr比auto_ptr更安全。

但unique_ptr還有更聰明的地方。
有時候,會將一個智能指針賦給另外一個並不會留下危險的懸掛指針。假設有以下函數定義:

unique_ptr<string> demo(const char * s) { unique_ptr<string> temp (new string (s)); return temp; }

並假設編寫了以下代碼:

unique_ptr<string> ps; ps = demo('Uniquely special");

demo()返回一個臨時unique_ptr,而後ps接管了本來歸返回的unique_ptr全部的對象,而返回時臨時的 unique_ptr 被銷燬,也就是說沒有機會使用 unique_ptr 來訪問無效的數據,換句話來講,這種賦值是不會出現任何問題的,即沒有理由禁止這種賦值。實際上,編譯器確實容許這種賦值,這正是unique_ptr更聰明的地方。

總之,黨程序試圖將一個 unique_ptr 賦值給另外一個時,若是源 unique_ptr 是個臨時右值,編譯器容許這麼作;若是源 unique_ptr 將存在一段時間,編譯器將禁止這麼作,好比:

unique_ptr<string> pu1(new string ("hello world")); unique_ptr<string> pu2; pu2 = pu1;                                      // #1 not allowed
unique_ptr<string> pu3; pu3 = unique_ptr<string>(new string ("You"));   // #2 allowed

其中#1留下懸掛的unique_ptr(pu1),這可能致使危害。而#2不會留下懸掛的unique_ptr,由於它調用 unique_ptr 的構造函數,該構造函數建立的臨時對象在其全部權讓給 pu3 後就會被銷燬。這種隨狀況而已的行爲代表,unique_ptr 優於容許兩種賦值的auto_ptr 。

固然,您可能確實想執行相似於#1的操做,僅當以非智能的方式使用摒棄的智能指針時(如解除引用時),這種賦值纔不安全。要安全的重用這種指針,可給它賦新值。C++有一個標準庫函數std::move(),讓你可以將一個unique_ptr賦給另外一個。下面是一個使用前述demo()函數的例子,該函數返回一個unique_ptr<string>對象:
使用move後,原來的指針仍轉讓全部權變成空指針,能夠對其從新賦值。

unique_ptr<string> ps1, ps2; ps1 = demo("hello"); ps2 = move(ps1); ps1 = demo("alexia"); cout << *ps2 << *ps1 << endl;

 

5. 如何選擇智能指針?

在掌握了這幾種智能指針後,你們可能會想另外一個問題:在實際應用中,應使用哪一種智能指針呢?
下面給出幾個使用指南。

(1)若是程序要使用多個指向同一個對象的指針,應選擇shared_ptr。這樣的狀況包括:

  • 有一個指針數組,並使用一些輔助指針來標示特定的元素,如最大的元素和最小的元素;
  • 兩個對象包含都指向第三個對象的指針;
  • STL容器包含指針。不少STL算法都支持複製和賦值操做,這些操做可用於shared_ptr,但不能用於unique_ptr(編譯器發出warning)和auto_ptr(行爲不肯定)。若是你的編譯器沒有提供shared_ptr,可以使用Boost庫提供的shared_ptr。

(2)若是程序不須要多個指向同一個對象的指針,則可以使用unique_ptr。若是函數使用new分配內存,並返還指向該內存的指針,將其返回類型聲明爲unique_ptr是不錯的選擇。這樣,全部權轉讓給接受返回值的unique_ptr,而該智能指針將負責調用delete。可將unique_ptr存儲到STL容器在那個,只要不調用將一個unique_ptr複製或賦給另外一個算法(如sort())。例如,可在程序中使用相似於下面的代碼段。

unique_ptr<int> make_int(int n) { return unique_ptr<int>(new int(n)); } void show(unique_ptr<int> &p1) { cout << *a << ' '; } int main() { ... vector<unique_ptr<int> > vp(size); for(int i = 0; i < vp.size(); i++) vp[i] = make_int(rand() % 1000);              // copy temporary unique_ptr
    vp.push_back(make_int(rand() % 1000));     // ok because arg is temporary
    for_each(vp.begin(), vp.end(), show);           // use for_each()
 ... }

其中push_back調用沒有問題,由於它返回一個臨時unique_ptr,該unique_ptr被賦給vp中的一個unique_ptr。另外,若是按值而不是按引用給show()傳遞對象,for_each()將非法,由於這將致使使用一個來自vp的非臨時unique_ptr初始化pi,而這是不容許的。前面說過,編譯器將發現錯誤使用unique_ptr的企圖。

在unique_ptr爲右值時,可將其賦給shared_ptr,這與將一個unique_ptr賦給一個須要知足的條件相同。與前面同樣,在下面的代碼中,make_int()的返回類型爲unique_ptr<int>:

unique_ptr<int> pup(make_int(rand() % 1000));   // ok
shared_ptr<int> spp(pup);                       // not allowed, pup as lvalue
shared_ptr<int> spr(make_int(rand() % 1000));   // ok

模板shared_ptr包含一個顯式構造函數,可用於將右值unique_ptr轉換爲shared_ptr。shared_ptr將接管原來歸unique_ptr全部的對象。

在知足unique_ptr要求的條件時,也可以使用auto_ptr,但unique_ptr是更好的選擇。若是你的編譯器沒有unique_ptr,可考慮使用Boost庫提供的scoped_ptr,它與unique_ptr相似。

相關文章
相關標籤/搜索