C++智能指針3——弱指針weak_ptr詳解

目錄

 

shared_ptr指針存在的問題

循環引用示例

代碼

運行結果

使用weak_ptr解決循環引用問題

代碼

運行結果


共享指針shared_ptr指針存在的問題

使用共享指針shared_ptr指針的主要原因是避免手動管理指針所關聯的資源。但是,在某些情況下共享指針shared_ptr不能實現預期的行爲:

  • 一種情況是循環引用。如果兩個對象使用shared_ptr指針相互引用,並且不存在對這些對象的其他引用,若要釋放這些對象及其關聯的資源,則共享指針shared_ptr不會釋放數據,因爲每個對象的引用計數仍爲1。在這種情況下,可能想使用普通的指針,但是這樣做需要手動管理相關資源的釋放。
  • 另一種情況是當明確想要共享但不擁有對象。這種情況下引用的生存期超過了它所引用的對象的生命週期。如果使用共享指針shared_ptr指針則其將永遠不會釋放對象。如果使用普通指針則可能出現指針所引用的對象不再有效,這會帶來訪問已釋放數據的風險。

對於這兩種情況都可以使用弱指針weak_ptr指針處理。弱指針weak_ptr是共享指針shared_ptr的輔助類。該類允許共享但不擁有對象。它的use_count()返回對象的共享指針shared_ptr擁有者數量,共享該對象的弱指針weak_ptr指針不計入該數量。

弱指針weak_ptr需要共享指針shared_ptr才能創建。每當擁有該對象的最後一個共享指針失去其所有權時,任何弱指針weak_ptr都會自動變爲空。因此,除了default和copy構造函數外,弱指針weak_ptr指針僅提供採用共享指針shared_ptr的構造函數。

使用初始指針所指對象的類型來對weak_ptr<>類進行模板化:

namespace std {
    template <typename T>
    class weak_ptr
    {
        public:
            typedef T element_type;
            ...
    };
}

不能使用運算符*和->直接訪問weak_ptr的引用對象。相反,必須從中創建一個共享指針。這有兩個原因:

  1. 根據弱指針創建共享指針,以檢查是否存在(仍然)關聯對象。如果不是,此操作將引發異常或創建一個空的共享指針(實際發生的情況取決於所使用的操作)。
  2. 在處理引用的對象時,共享指針無法釋放。

因此,弱指針weak_ptr僅提供少量操作:足以創建,複製和賦值一個弱指針,並將其轉換爲共享指針或檢查它是否指向對象。

循環引用示例

代碼

#include <iostream>
#include <string>
#include <vector>
#include <memory>

using namespace std;

class Person {
  public:
    string m_sName;
    shared_ptr<Person> m_pMother;
    shared_ptr<Person> m_pFather;
    vector<shared_ptr<Person>> m_oKids;

    Person (const string& sName,
            shared_ptr<Person> pMother = nullptr,
            shared_ptr<Person> pFather = nullptr)
     : m_sName(sName), m_pMother(pMother), m_pFather(pFather) {
    }

    ~Person() {
      cout << "刪除 " << m_sName << endl;
    }
};

shared_ptr<Person> initFamily (const string& sName)
{
    shared_ptr<Person> pMom(new Person(sName + "的母親"));
    shared_ptr<Person> pDad(new Person(sName + "的父親"));
    shared_ptr<Person> pKid(new Person(sName, pMom, pDad));
    pMom->m_oKids.push_back(pKid);
    pDad->m_oKids.push_back(pKid);
    return pKid;
}

int main()
{
    string sName = "張三";
    shared_ptr<Person> pPerson = initFamily(sName);

    cout << sName << "家存在" << endl;
    cout << "- " << sName << "被分享" << pPerson.use_count() << "次" << endl;
    cout << "- " << sName << "母親第一個孩子的名字是:"
         << pPerson->m_pMother->m_oKids[0]->m_sName << endl;

    sName = "李四";
    pPerson = initFamily(sName);
    cout << sName << "家已存在" << endl;
}

運行結果

張三家存在
- 張三被分享3次
- 張三母親第一個孩子的名字是:張三
李四家已存在

以上代碼中一個類Person具有名稱和對其他Person的可選引用,即父母(母親和父親)和孩子。

首先,initFamily()創建三個Person對象:pMom,pDad和pKid,並根據傳遞的參數使用相應的名稱進行初始化。另外,將孩子與父母一起初始化,並且對於兩個父母,將孩子插入父母對象的孩子列表中。最後,initFamily()返回孩子對象的共享指針shared_ptr。

下圖顯示了initFamily()末尾以及調用並將結果賦給pPerson之後的結果情況。

pPerson是家庭的最後一個句柄。但是,在內部,每個對象都有從孩子到每個父對象的引用。例如,在pPerson獲得新值之前,張三被共享了3次。現在,如果我們釋放該家族的最後一個句柄(通過爲pPerson分配一個新的Person對象或nullptr),則不會釋放任何Person對象,因爲每個Person對象仍然至少具有一個共享的指針指向它。結果,每個Person對象的析構函數(將顯示“刪除名稱”)都不會被調用。

使用weak_ptr解決循環引用問題

代碼

#include <iostream>
#include <string>
#include <vector>
#include <memory>

using namespace std;

class Person {
  public:
    string m_sName;
    shared_ptr<Person> m_pMother;
    shared_ptr<Person> m_pFather;
    vector<weak_ptr<Person>> m_oKids; //弱指針

    Person (const string& sName,
            shared_ptr<Person> pMother = nullptr,
            shared_ptr<Person> pFather = nullptr)
     : m_sName(sName), m_pMother(pMother), m_pFather(pFather) {
    }

    ~Person() {
      cout << "刪除 " << m_sName << endl;
    }
};

shared_ptr<Person> initFamily (const string& sName)
{
    shared_ptr<Person> pMom(new Person(sName + "的母親"));
    shared_ptr<Person> pDad(new Person(sName + "的父親"));
    shared_ptr<Person> pKid(new Person(sName, pMom, pDad));
    pMom->m_oKids.push_back(pKid);
    pDad->m_oKids.push_back(pKid);
    return pKid;
}

int main()
{
    string sName = "張三";
    shared_ptr<Person> pPerson = initFamily(sName);

    cout << sName << "家存在" << endl;
    cout << "- " << sName << "被分享" << pPerson.use_count() << "次" << endl;
    cout << "- " << sName << "母親第一個孩子的名字是:"
         << pPerson->m_pMother->m_oKids[0].lock()->m_sName << endl;

    sName = "李四";
    pPerson = initFamily(sName);
    cout << sName << "家已存在" << endl;
}

運行結果

張三家存在
- 張三被分享1次
- 張三母親第一個孩子的名字是:張三
刪除 張三
刪除 張三的父親
刪除 張三的母親
李四家已存在
刪除 李四
刪除 李四的父親
刪除 李四的母親

通過將vector中的共享指針shared_ptr指針換成弱指針weak_ptr指針,可以打破共享指針shared_ptr的循環,以便在一個方向上(從孩子到父母)使用共享指針,而從父母到孩子,則使用弱指針,如下圖所示。

一旦失去對創建的家庭的句柄(通過爲pPerson分配新值或通過離開main()函數),孩子的對象將失去其最後一個所有者,這又會使父母雙方都失去了最後的所有者。因此,最初由new創建的所有對象現在都將被刪除,以便調用它們的析構函數。

請注意,要使用弱指針,必須稍微修改通過弱指針訪問對象的方式。需要在表達式中插入lock()函數如下所示

pPerson->mother->kids[0].lock()->name

而不是調用

pPerson->mother->kids[0]->name

lock()函數從kids的向量所包含的弱指針weak_ptr中產生一個共享指針shared_ptr。如果無法進行此修改(例如,由於該對象的最後所有者同時釋放了該對象),則lock()函數會生成一個空的shared_ptr。在這種情況下,調用運算符*或->將導致未定義的行爲。

如果不確定弱指針指向的對象是否仍然存在,則可以使用以下幾種方法:

  1. 調用expired(),如果弱指針weak_ptr不再共享對象,則返回true。此選項等效於檢查use_count()是否等於0,但可能更快。
  2. 通過使用相應的共享指針shared_ptr構造函數將弱指針weak_ptr顯式轉換爲共享指針shared_ptr。如果沒有有效的引用對象,則此構造方法將引發bad_weak_ptr異常。這個異常是從std::exception派生的類的異常,其中what()會返回“ bad_weak_ptr”。
  3. 調用use_count()來詢問關聯對象擁有的所有者數量。如果返回值爲0,則不再有有效的對象。但是請注意,通常只應出於調試目的調用use_count(),因爲C++標準庫明確指出:“use_count()不一定有效。”

接口列表

下表爲弱指針提供的所有操作。

操作 結果
weak_ptr<T> wp 默認構造函數;創建一個空的弱指針
weak_ptr<T> wp(sp) 創建一個弱指針,共享由sp擁有的指針的所有權
weak_ptr<T> wp(wp2) 創建一個弱指針,共享由wp2擁有的指針的所有權
wp.~weak_ptr() 析構函數;銷燬弱指針,但對擁有的對象無效
wp = wp2 賦值(wp之後共享wp2的所有權,放棄先前擁有的對象的所有權)
wp = sp 用共享指針sp進行賦值(wp之後共享sp的所有權,放棄先前擁有的對象的所有權)
wp.swap(wp2) 交換wp和wp2的指針
swap(wp1,wp2) 交換wp1和wp2的指針
wp.reset() 放棄擁有對象的所有權(如果有的話),並重新初始化爲空的弱指針
wp.use_count() 返回共享所有者的數量(擁有對象的shared_ptr數目);如果弱指針爲空,則返回0
wp.expired() 返回wp是否爲空(等同於wp.use_count() == 0,但可能更快)
wp.lock() 返回共享指針,該共享指針共享弱指針擁有的指針的所有權(如果沒有共享指針,則爲空共享指針)
wp.owner_before(wp2) 提供嚴格的弱排序和另一個弱指針
wp.owner_before(sp) 通過共享指針提供嚴格的弱排序

構造函數創建一個空的弱指針,調用它的expired()會返回true。因爲lock()會返回一個共享指針,所以對象的使用計數在共享指針的生命週期內會增加。這是處理弱指針共享對象的唯一方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章