C++智慧指標unique_ptr詳解

2020-07-16 10:04:41
在一個大型程式中,指向動態分配記憶體的指標可能會在程式的各個部分使用。在這種情況下,確定哪些記憶體不再需要,或者程式的哪個部分應該負責刪除指標就變得比較困難。

程式可能會因此出現懸掛指標,也就是說,指標已經被刪除了,但其記憶體仍然在使用中;還可能出現記憶體漏失,也就是說,即使已經不再需要記憶體了,但指標仍然未被刪除。另外還有雙重刪除的問題,當程式的某 部分要刪除一個已經被刪除的指標時,即可出現這種情況。如果被刪除的記憶體已經進行了重新分配,則雙重刪除會對程式造成破壞。

C++ 11 引入了智慧指標的概念來解決該問題。智慧指標是一個可以像指標一樣工作的物件,但是當它不再被使用時,可以自動刪除動態分配的記憶體。

C++11 提供了 3 種智慧指標型別,它們分別由 unique_ptr 類、shared_ptr 類和 weak_ptr 類定義,所以又分別稱它們為獨占指標、共用指標和弱指標。

智慧指標背後的核心概念是動態分配記憶體的所有權。智慧指標被稱為可以擁有或管理它所指向的物件。當需要讓單個指標擁有動態分配的物件時,可以使用獨佔指標。物件的所有權可以從一個獨佔指標轉移到另一個指標,其轉移方式為:物件始終只能有一個指標作為其所有者。當獨佔指標離開其作用域或將要擁有不同的物件時,它會自動釋放自己所管理的物件。

共用指標將記錄有多少個指標共同享有某個物件的所有權。當有更多指標被設定為指向該物件時,參照計數隨之增加;當指標和物件分離時,則參照計數也相應減少。當參照計數降低至0時,該物件被刪除。

unique_ ptr、shared_ ptr 和 weak_ ptr 類是在 memory 標頭檔案中定義的,所以需要在使用它們的程式中包含以下語句:

#include <memory>

本節我們先來討論 unique_ ptr。

智慧指標實際上是一個物件,在物件的外面包圍了一個擁有該物件的普通指標。這個包圍的常規指標稱為裸指標

智慧指標類可以通過它所指向的物件型別設定形參。例如,unique_ptr<int> 就是一個指向 int 的指標;而 unique_ptr<double> 就是一個指向 double 的指標。以下程式碼顯示了如何建立獨佔指標:

unique_ptr<int> uptr1(new int);
unique_ptr<double> uptr2(new double);

當然,也可以先定義一個未初始化的指標,然後再給它賦值:

unique_ptr<int> uptr3;
uptr3 = unique_ptr<int> (new int);

為了避免記憶體漏失,通過智慧指標管理的物件應該沒有其他的參照指向它們。換句話說,指向動態分配儲存的指標應該立即傳遞給智慧指標建構函式,而不能先將它賦值給指標變數。

例如,應該避免按以下方式編寫程式碼:

int *p = new int;
unique_ptr<int> uptr(p);

智慧指標不支援指標的算術運算,所以下面的語句將導致編譯器錯誤:

uptr1 ++;
uptr1 = uptr1 + 2;

但是,智慧指標通過運算子過載支援常用指標運算子 *->。以下程式碼將解除參照一個獨佔指標,以給動態分配記憶體位置賦值,遞增該位置的值,然後列印結果:

unique_ptr<int> uptr(new int);
*uptr = 12;
*uptr = *uptr + 1;
cout << *uptr << endl;

不能使用其他 unique_ptr 物件的值來初始化一個 unique_ptr。同樣,也不能將一個 unique_ptr 物件賦值給另外一個。這是因為,這樣的操作將導致兩個獨佔指標共用相同物件的所有權,所以,以下語句都將出現編譯時錯誤:

unique_ptr<int> uptr1(new int);
unique_ptr<int> uptr2 = uptr1; // 非法初始化
unique_ptr<int> uptr3; // 正確
uptr3 = uptr1; // 非法賦值

C++ 提供了一個 move() 庫函數,可用於將物件的所有權從一個獨佔指標轉移到另外一個獨佔指標:

unique_ptr<int> uptr1(new int);
*uptr1 = 15;
unique_ptr<int> uptr3; // 正確
uptr3 = move (uptr1) ; // 將所有權從 uptr1 轉移到 uptr3
cout << *uptr3 << endl; // 列印 15

假設存在以下轉移語句:

U = move(V);

那麼,當執行該語句時,會發生兩件事情。首先,當前 U 所擁有的任何物件都將被刪除;其次,指標 V 放棄了原有的物件所有權,被置為空,而 U 則獲得轉移的所有權,繼續控制之前由 V 所擁有的物件。

不能直接通過值給函數傳遞一個智慧指標,因為通過值傳遞將導致複製真正的形參。如果要讓函數通過值接收一個獨佔指標,則在呼叫函數時,必須對真正的形參使用 move() 函數:
//函數使用通過值傳遞的形參
void fun(unique_ptr<int> uptrParam)
{
    cout << *uptrParam << endl;
}

int main()
{
    unique_ptr<int> uptr(new int);
    *uptr = 10;
    fun (move (uptr)); // 在呼叫中使用 move
}
以上程式碼將列印來自於函數 fun() 中的 10。

當然,如果通過參照傳遞的方式,那就不必對真正的形參使用 move() 函數了。範例程式碼如下:
//函數使用通過參照傳遞的值
void fun(unique_ptr<int>& uptrParam)
{
    cout << *uptrParam << endl;
}

int main()
{
    unique_ptr<int> uptr(new int);
    *uptr1 = 15;
    fun (uptr1) ; //在呼叫中無須使用move
}
以上程式碼在執行時將列印數位 15。

有趣的是,可以從函數中返回一個獨佔指標,這是因為在遇到返回 unique_ptr 物件的函數時,編譯器會自動應用 move() 操作以返回其值。來看以下程式碼:
//返回指向動態分配資源的獨佔指標
unique_ptr<int> makeResource()
{
    unique_ptr<int> uptrResult(new int);
    *uptrResult = 55;
    return uptrResult;
}

int main()
{
    unique_ptr<int> uptr;
    uptr = makeResource () ; // 自動移動
    cout << *uptr << endl;
}
該程式的輸出結果為 55。

永遠不要試圖去動態分配一個智慧指標,相反,應該像宣告函數的區域性變數那樣去宣告智慧指標。當 unique_ptr 將要離開作用域時,它管理的物件也將被刪除。如果要刪除智慧指標管理的物件,但同時又保留智慧指標在作用域中,則可以將其值設定為 nullptr,或者呼叫其 reset() 成員函數,範例如下:

uptr = nullptr;
uptr.reset();

從 C++14 開始,有一個庫函數 make_unique<T>() 可用於建立 unique_ptr 物件。該函數分配一個型別為 T 的物件,然後返回一個擁有該物件的獨佔指標。例如,來看下面的程式碼:

unique_ptr<int> uptr(new int);

現在可以棄用上面的程式碼,而改為使用以下程式碼:

unique_ptr<int> uptr = make_unique<int>();

指向陣列的獨佔指標

按上述方式建立的獨佔指標將對指向已刪除的被管理物件的包圍指標呼叫 delete,但是,如果該包圍指標指向的是一個物件陣列的話,那麼這種操作就是不正確的。要確保呼叫 delete[] 來處理被解除分配的物件陣列,則應該在物件型別後面包含一對空的方括號 []。

例如,要使用指向動態分配 5 個整數陣列的獨佔指標,需編寫以下語句:

unique_ptr<int[]> uptr(new int[5]);

前面介紹過,智慧指標 uptr 可以像一個指向int的普通指標那樣使用;前面還介紹過,可以對指標使用陣列符號,因此,可以如以下方式編寫一個程式,在像 up[k] 這樣的陣列中儲存整數的平方值:
int main()
{
    //指向陣列的獨佔指標
    uriique_ptr<int [ ] > up (new int [5]);
    //設定陣列元素為整數的平方值
    for (int k = 0; k < 5; k++)
    {
        up[k] = (k + l)*(k + 1);
    }
    //列印陣列元素
    for (int k = 0; k < 5; k++)
    {
        cout << up[k] <<" ";
    }
    cout << endl;
}
以上程式碼的輸出結果將是 "1 4 9 16 25"

當用於建立指向 T 型別物件陣列的獨佔指標時,make_unique<T []>() 將釆用整數形參作為陣列的大小:

unique_ptr<int[]> up = make_unique<int[]>(5);

unique_ptr 類的成員函數

unique_ptr 類有一些非常有用的範例成員函數,如表 1 所示。

表 1 unique_ptr成員函數
成員函數 描 述
reset() 銷毀由該智慧指標管理的任何可能存在的物件。該智慧指標被置為空
reset(T* ptr) 銷毀由該智慧指標當前管理的任何可能存在的物件。該智慧指標繼續控制由裸指標 ptr 指向的物件
get() 返回該智慧指標管理的由裸指標指向的物件。如果某個指標需要傳遞給函數,但是 該函數並不知道該如何操作智慧指標,則 get() 函數非常有用