C++ share_ptr智慧指標使用詳解

2020-07-16 10:04:41
share_ptr 智慧指標,也被稱為共用指標,用於管理可以由多個智慧指標共同擁有的動態分配物件。特別是,型別 shared_ptr<T> 用於管理 T 型別物件的所有權。

類建構函式 shared_ptr<T>(T * ptr) 可用於建立共用指標,管理由裸指標 ptr 給定地址的物件。shared_ptr 類可以過載指標運算子 *->。以下範例建立了一個由共用指標管理的動態分配整數,然後即可通過該指標存取它:
int main()
{
    shared_ptr<int> p(new int);
    *p = 45;
    cout << *p + 1;
    return 0;
}
該程式碼將列印值 46。

以下是另外一個範例。假設要管理以下類物件的共用所有權:
class Person
{
    string name;
    int age;
   
    public:
        string to_string()
        {
            return name + "" + to_string(age) + "n";
        }
        //建構函式
        Person () { name = " ";age = 0;}
        Person(const strings name, int age)
        {
            this->name = name;
            this->age = age;
        }
}
現在可以建立 2 個共用指標,每一個都可以管理不同的物件,就像下面這樣;

shared_ptr<Person> p1(new Person());
shared_ptr<Person> p2 (new Person ("Maria Wu", 23));

或者,也可以寫成以下形式:

shared_ptr<Person> p1 = shared_ptr<Person>(new Person ());
shared_ptr<Person> p2 = shared_ptr<Person>(new Person("Maria Wu", 23));

share_ptr組

假設已經設定了一個共用指標來管理由裸指標 rPtr 指向的物件:

T * rPtr = new T();
shared_ptr<T> sPtr1(rPtr);

此時,sPtr1 變成了一個擁有 rPtr 的共用指標組中的唯一成員。該組維護了一個參照計數,即屬於該組的共用指標數量。這個"組"被稱為控制塊,它本身實際上就是動態分配物件,可以保持跟蹤參照計數和指向被管理物件的裸指標。當參照計數降至 0 時,控制塊負責刪除被管理的物件。可以將共用指標視為指向控制塊的指標,而將控制塊視為指向被管理的物件。

現在,如果 sPtr1 被用於初始化其他的共用指標,如以下語句所示:

shared_ptr<T> sPtr2 = sPtr1;

則 sPtr2 將變成和 sPtr1 同組的成員,共用 rPtr 物件的所有權,並且該組的參照計數也會增加 1。如果在此之後,sPtr2 被賦給了其他共用指標的值:

sPtr2 = sPtr3;

則 sPtr2 將放棄 rPtr 的所有權,離開 sPtr1 的組,轉而加入 sPtr3 的組。第一個組(sPtr1 所在的組)的參照計數將減去 1,而第二個組(sPtr3 所在的組)的參照計數則增加 1。

雙重管理的危險

在使用 share_ptr 時,應該避免出現可能導致兩個指標組管理相同物件的情況。例如,來看以下程式碼:
T * rPtr = new T();
shared_ptr<T> sPtr1(rPtr);
shared_ptr<T> sPtr2(rPtr);
這兩個共用指標指向兩個不同的控制塊,但控制塊管理的卻是相同的物件。如果第一個組的參照計數降至 0 並刪除該物件,那麼這將導致其他組的指標變成懸掛指標。為了避免出現這種問題,給定的裸指標應該最多只能初始化一個共用指標。

make_shared<T>()函數

再來看一下 share_ptr 建立語句:

shared_ptr<Person> p1(new Person());

該語句的執行涉及兩個獨立的記憶體分配:一個分配控制塊,第二個則分配記憶體給被管理的 Person 物件。每個記憶體分配都會產生相當一部分開銷,所以,更高效的做法是,分配一個足夠大的記憶體塊,以同時儲存控制塊和被管理的物件。

下面這個庫函數就可以做到這一點:

make_shared<T>()

使用該函數即可將上述共用指標建立語句改寫為以下形式:

shared_ptr<Person> p1 = make_shared<Person>();

該版本的 make_Shared 語句可以使用預設建構函式初始化被管理的物件。

還有一個版本的 make_shared 語句可以採用傳遞給非建構函式的形參。因此,以下面的語句為例:

shared_ptr<Person> p2 (new Person ("Maria Wu", 23));

該語句可以被改寫為以下形式:

shared_ptr<Person> p2 = make_shared<Person>("Maria Wu", 23);

推薦使用 make_shared 函數方式建立共用指標。除了更高效之外,它還無須直接處理裸指標,因此也消除了雙重管理的可能性。

shared_ptr 成員函數

表 1 列出了處理 share_ptr 時最用的成員函數。

表 1 shared_ptr成員函數
成員函數 描 述
T* get() 返回指向被管理物件的裸指標,如果沒有被管理的物件,則返回空指標
void reset() 釋放任何可能存在的被管理物件的所有權。呼叫的共用指標被置為空
void reset(T* ptr) 釋放當前被管理的物件的所有權。獲取由 ptr 指向的物件的所有權
long use_count() 返回參照相同的被管理物件的共用指標數量

除此之外,還可以通過測試共用指標的值,檢查某個共用指標是否有被管理的物件, 範例如下:
shared_ptr<T> p =....;
if ( P )
{
    //被管理的物件
}
else
{
    //共用指標為空
}

指向陣列的share_ptr

預設情況下,shared_ptr 使用 delete 銷毀被管理的物件。與 unique_ptr 不同的是,不能編寫以下形式的語句:

shared_ptr<T[ ] > sPtr; // 錯誤

不能使用以上語句指定被管理的物件型別為陣列。有一種簡單方法可以繞過這種限制,那就是使用指向 T 型別向量的共用指標:

shared_ptr<vector<T>> sVecPtr;

當向量被銷毀時,其解構函式將執行並銷毀所有的向量元素。