目錄
共享指針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的引用對象。相反,必須從中創建一個共享指針。這有兩個原因:
- 根據弱指針創建共享指針,以檢查是否存在(仍然)關聯對象。如果不是,此操作將引發異常或創建一個空的共享指針(實際發生的情況取決於所使用的操作)。
- 在處理引用的對象時,共享指針無法釋放。
因此,弱指針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。在這種情況下,調用運算符*或->將導致未定義的行爲。
如果不確定弱指針指向的對象是否仍然存在,則可以使用以下幾種方法:
- 調用expired(),如果弱指針weak_ptr不再共享對象,則返回true。此選項等效於檢查use_count()是否等於0,但可能更快。
- 通過使用相應的共享指針shared_ptr構造函數將弱指針weak_ptr顯式轉換爲共享指針shared_ptr。如果沒有有效的引用對象,則此構造方法將引發bad_weak_ptr異常。這個異常是從std::exception派生的類的異常,其中what()會返回“ bad_weak_ptr”。
- 調用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()會返回一個共享指針,所以對象的使用計數在共享指針的生命週期內會增加。這是處理弱指針共享對象的唯一方法。