C++智慧指標

無鞋童鞋發表於2017-08-13

1 使用智慧指標的原因
  我們知道C/C++的指傳統針可以用來通過指向物件地址來間接訪問該物件,但是當某個指標指向人為申請的堆空間,我們時常忘記手動釋放該空間從而造成記憶體洩漏。我們可以看看下面的程式碼:

void outputStr(std::string & str)
{
    std::string *ps = new std::string(str);  
    std::cout<<*ps<<endl;
    ...

    return;
}

  上面的程式我們可以清晰的看出該函式輸出結果,但是並沒有手動釋放堆記憶體所以會造成記憶體洩漏。所以需要在return之前加入程式碼delete ps;
  但是有經驗的工程師都知道:凡涉及“別忘了”的解決方法,很少是最佳的。
  我們來看看下面的變體:

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

  當異常發生時,delete不會被執行,同樣會記憶體洩露。是時候智慧指標出場了,aoto_ptr、shared_ptr、unique_ptr。 aoto_ptr是C++98中提供的,c++11已經將其摒棄了。
2 智慧指標的介紹
  智慧指標(smart pointer)是儲存指向動態分配(堆)物件指標的類,用於生存期控制,能夠確保自動正確的銷燬動態分配的物件,防止記憶體洩露。
  智慧指標類通過顯示有參構造將new獲得的地址複製給智慧指標。當智慧指標過期時其解構函式將使用delete來釋放記憶體(例如智慧指標類在某個函式中區域性構造了一個其物件,那麼系統會在跳出函式時呼叫解構函式,而解構函式中系統有delete上面new的地址的語句)。無需記住稍後釋放這些記憶體,在智慧指標過期時,這些記憶體將自動被釋放。
  它的一種通用實現技術是使用引用計數(reference count)。智慧指標類將一個計數器與類指向的物件相關聯,引用計數跟蹤該類有多少個物件共享同一指標。每次建立類的新物件時,初始化指標並將引用計數置為1;當物件作為另一物件的副本而建立時,拷貝建構函式拷貝指標並增加與之相應的引用計數;對一個物件進行賦值時,賦值操作符減少左運算元所指物件的引用計數(如果引用計數為減至0,則刪除物件),並增加右運算元所指物件的引用計數;呼叫解構函式時,建構函式減少引用計數(如果引用計數減至0,則刪除基礎物件)。也就是說當多個智慧指標物件有共享同一個指標的時候,當系統依次析構這些物件的時候,引用計數依次減一,直到引用計數變為0,也就是最後一個智慧指標物件去釋放該空間,前面釋放的物件只起到減小引用計數的作用。
  智慧指標就是模擬指標動作的類。所有的智慧指標都會過載 -> 和 * 操作符。智慧指標還有許多其他功能,比較有用的是自動銷燬。這主要是利用棧物件的有限作用域以及臨時物件(有限作用域實現)解構函式釋放記憶體。當然,智慧指標還不止這些,還包括複製時可以修改源物件等。智慧指標根據需求不同,設計也不同(寫時複製,賦值即釋放物件擁有許可權、引用計數等,控制權轉移等)。
3 智慧指標的使用
  使用智慧指標的步驟如下:
  ①. 包含對應的標頭檔案:

#include<memory>

  ②. 顯示構造智慧指標類的物件,並複製一個堆空間給它:

aoto_ptr<double> pd(new double);
auto_ptr<string> ps(new string);

unique_ptr<double> pdu(new double);

shared_ptr<string> pss(new string);

  那如何修改上面的程式,讓普通指標變為智慧指標來管理呢?步驟如下:
  ①. 包含標頭檔案memory;
  ②. 將指向string的指標替換為指向string的智慧指標物件;
  ③. 刪除delete語句。
  具體實現如下:

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

4 智慧指標的顯示構造
  我們可以看一下auto_ptr如何定義的:

template<class X> 
class auto_ptr
{
public:
    explicit auto_ptr(X *p = 0) throw();
    ...
};

  智慧指標的建構函式使用了explicit關鍵字。 explicit關鍵字只能用於修飾只有一個引數的類的建構函式,它的作用是表面該建構函式是顯示的,而非隱式的。可以阻止不應該允許的經過轉換建構函式進行的隱式轉換的發生。
  我們看一下下面的使用示例:

shared_ptr<double> pd;

double *p_reg = new double;
pd = p_reg;   //錯誤,不允許隱式轉換

pd = shared_ptr<double>(p_reg);//正確

shared_ptr<double> pshared = p_reg;//錯誤,不允許隱式轉換

shared_ptr<double> pshared(p_reg); //正確

5 切忌把智慧指標用於非堆記憶體
  我們知道由於智慧指標類解構函式中有釋放賦予它地址的操作,一旦我們對一段非堆記憶體釋放將會發生系統報錯。如下:

string vacation("hello world");

shared_ptr<string>pvac(&vacation);

  當pvac過期時,程式將把delete運算子用於非堆記憶體,這將導致錯誤。
6 三種智慧指標類的區別

#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]不再引用該字串從而變成空指標

    //unique_ptr<string> pwin; //編譯錯誤
    //pwin = films[2];
    //shared_ptr<string> pwin; //沒有問題!

    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;

    return 0;
}

  當使用shared_ptr的時候,pwin和films[2]指向同一個物件,而引用計數從1增加到了2。在程式末尾return後,pwin首先呼叫其解構函式,該解構函式將引用計數降低到1,所以不會有問題。 即 :
  使用auto_ptr 所有權轉讓,執行將會有錯誤 ;
  使用shared_ptr 指向同一物件,引用計數增加,不會有問題 ;
  使用unique_ptr 採用所有權模型,編譯就會有錯誤。
  因此我們也可以看出unique_ptr優於auto_ptr的一個原因:
  unique_ptr比auto_ptr更安全
  還有一個原因就是:
  auto_ptr只能與new一起使用,不能與new[]一起使用(share_ptr同樣)。
  unique_ptr可以使用new,也可以使用new[] 。
  分析了這麼多,那麼歸根到底,我們如何選擇呢?
7 如何選擇哪種智慧指標
  (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())


  參考文獻:
  http://blog.csdn.net/wangshubo1989/article/details/48337955
  http://blog.csdn.net/hackbuteer1/article/details/7561235

相關文章