C++筆記(11) 智慧指標

FishL發表於2021-08-28

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將不被執行,因此將導致記憶體洩露。可以用上一章介紹的方式修復這種問題:

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

然而這將增加疏忽和產生其他錯誤的機會。

我們需要的是,當remodel函式中止時(不管是正常中止還是異常中止),本地變數都將從棧記憶體中刪除,即指標ps佔據的記憶體將被釋放,同時ps指向的記憶體也被釋放

如果ps有一個解構函式,該解構函式在ps過期時釋放它指向的記憶體。但問題在於,ps只是一個常規指標,不是有解構函式的類物件。如果它是物件,則可以在物件過期時,讓它的解構函式刪除指向的記憶體。這正是智慧指標背後的思想。

(我的理解是,將指標封裝成為類,其解構函式可以釋放指標佔用的記憶體,在解構函式中增加delete釋放它指向的記憶體。)

 下面是使用智慧指標auto_ptr修改該函式的結果:

# include <memory>       //1.包含頭義件memory(智慧指標所在的標頭檔案)
void remodel (std::string & str)
{
    std::auto_ptr<std::string> ps (new std::string(str));//2.將指向string的指標替換為指向string的智慧指標物件
    ...//智慧指標模板在名稱空間std中
    if (weird_thing ())
        throw exception(); 
    str = *ps; 
    // delete ps; 3. 刪除delete語句
    return;
}

2. 使用智慧指標

  STL一共給我們提供了四種智慧指標:auto_ptr、unique_ptr、shared_ptr和weak_ptr(本文暫不討論)。模板auto_ptr是C++98提供的解決方案,C+11已將將其摒棄,並提供了另外兩種解決方案。然而,雖然auto_ptr被摒棄,但它已使用了好多年;同時,如果您的編譯器不支援其他兩種解決力案,auto_ptr將是唯一的選擇。
要建立智慧指標物件,

  • 必須包含標頭檔案memory,其中,auto_ptr的類别範本原型為:
template <class _Ty>
class auto_ptr { // wrap an object pointer to ensure destruction
public:
    using element_type = _Ty;
    explicit auto_ptr(_Ty* _Ptr = nullptr) noexcept : _Myptr(_Ptr) {}
  ...      
}
  • 然後使用使用通常的模板語法來例項化所需要型別的指標。
auto_ptr<string> pa1(new string("auto"));

注意事項:

1. 所有的智慧指標類都有一個explicit建構函式,以指標作為引數。因此不能自動將指標轉換為智慧指標物件,必須顯式呼叫:

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)

2. 智慧指標類的解構函式中的delete,只能用於堆記憶體中動態建立(new)的物件

string vacation("I wandered lonely as a cloud.");
shared_ptr<string> pvac(&vacation);   // No

pvac過期時,程式將把delete運算子用於非堆記憶體,這是錯誤的。

3. 為何摒棄auto_ptr?

先來看下面的賦值語句:

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

上述賦值語句將完成什麼工作呢?如果ps和vocation是常規指標,則兩個指標將指向同一個string物件。但對於智慧指標來說,這是不能接受的,因為程式將試圖刪除同一個物件兩次——一次是ps過期時,另一次是vocation過期時。要避免這種問題,方法有多種:

  • 定義賦值運算子,使之執行深複製。這樣兩個指標將指向不同的物件,其中的一個物件是另一個物件的副本,缺點是浪費空間,所以智慧指標都未採用此方案。
  • 建立所有權(ownership)概念。對於特定的物件,只能有一個智慧指標可擁有,這樣只有擁有物件的智慧指標的建構函式會刪除該物件。然後讓賦值操作轉讓所有權。這就是用於auto_ptr和unique_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. 
 
 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;
}

 執行下發現程式崩潰了,這裡的問題在於,

pwin = films[2];

所有權從films[2]轉讓給pwin,此時films[2]不再引用該字串從而變成空指標,下面輸出訪問空指標導致程式崩潰了。如果上述程式碼,

  • 使用unique_ptr:編譯出錯,與auto_ptr一樣,unique_ptr也採用所有權模型,但在使用unique_ptr時,程式不會等到執行階段崩潰,而在編譯器因下述程式碼行出現錯誤:
unique_ptr<string> pwin;
pwin = films[2]; // films[2] loses ownership.
  • 使用shared_ptr:執行正常,因為shared_ptr採用引用計數,pwin和films[2]都指向同一塊記憶體,在釋放空間時因為事先要判斷引用計數值的大小因此不會出現多次刪除一個物件的錯誤。

錯誤的使用auto_ptr可能導致問題(這種程式碼的行為是不確定的,其行為可能隨系統而異)。因此為了避免潛在的程式崩潰,要摒棄auto_ptr。

 4. unique_ptr為何優於auto_ptr

  • unique_ptr比auto_ptr更安全,編譯階段錯誤比潛在的程式崩潰更安全
  • 相比於auto_ptr,unique_ptr還有一個可用於陣列的變體
std::unique_ptr<double> pda(new double(5));//will use delete[]
  new/delete new[]/delete[]
auto_ptr  
unique_ptr
shared_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 後就會被銷燬。

當然,您可能確實想執行類似於#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類似。

 

源自:《C++ Primer Plus》16.2節 智慧指標模板類

相關文章