C++智能指標簡單剖析,智能指標剖析

來源:互聯網
上載者:User

C++智能指標簡單剖析,智能指標剖析
導讀

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

下面是我在看智能指標時所做的筆記,希望能夠解決你對智能指標的一些困擾。

目錄
  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個步驟進行:

  • 包含頭義件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將是唯一的選擇。

使用注意點

  • 所有的智能指標類都有一個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]不再引用該字串從而變成null 指標 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]已經是null 指標了,下面輸出訪問null 指標當然會崩潰了。但這裡如果把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") ; //#1auto_ptr<string> p2;                       //#2p2 = p1;   

在語句#3中,p2接管string對象的所有權後,p1的所有權將被剝奪。前面說過,這是好事,可防止p1和p2的解構函式試圖刪同—個對象;
但如果程式隨後試圖使用p1,這將是件壞事,因為p1不再指向有效資料。

下面來看使用unique_ptr的情況:

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

編譯器認為語句#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 allowedunique_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後,原來的指標仍轉讓所有權變成null 指標,可以對其重新賦值。

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));   // okshared_ptr<int> spp(pup);                       // not allowed, pup as lvalueshared_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類似。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.