引入:智能指針的意義是什么?
RAll是一種利用對象生命周期來控制程序資源(如內存、文件句柄、網絡連接、互斥量等等)的簡單技術。
在對象構造時獲取資源,接著控制對資源的訪問使之在對象的生命周期內始終保持有效,最后在
對象析構的時候釋放資源。借此,我們實際上把管理一份資源的責任托管給了一個對象。這種做
法有兩大好處:
?· 不需要顯式地釋放資源。
?· 采用這種方式,對象所需的資源在其生命期內始終保持有效。
????????說白了就是為了解決異常引起的內存泄漏!我們知道,如果一個內存在申請和釋放這二者之間被拋出異常了,那么有可能就會出現內存泄漏,而智能指針的本質就是將指向這塊內存的指針封裝成一個類,該指針作為類的對象以后,就會在出作用域是自動的進行析構,所以我們再也不用擔心異常引出的內存泄漏問題了!
一:智能指針的使用場景
1:異常導致的內存泄漏
#include<exception>
int div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0錯誤");return a / b;
}
void func()
{int* ptr = new int(1); //賦值1 方便監視窗口觀察//...cout << div() << endl;//...delete ptr;
}
int main()
{try{func();}catch (exception& e){cout << e.what() << endl;}return 0;
}
運行結果:?
?
通過監視窗口體現內存泄漏:
?
2:異常的重新拋出
根據上篇博客,可知,其實這種簡單的用異常的重新拋出也可以解決,代碼:
int div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0錯誤");return a / b;
}
void func()
{int* ptr = new int;try{cout << div() << endl;}catch (...){delete ptr;throw;}delete ptr;
}
int main()
{try{func();}catch (exception& e){cout << e.what() << endl;}return 0;
}
運行結果及監視窗口:
?
解釋:即使是拋出異常,但仍然沒有內存泄漏!
3:智能指針的實現
對于上面這個內存泄漏的問題,我們還可以采取智能指針來解決:
// SmartPtr類(譯為智能指針類)
template<class T>
class SmartPtr
{
public:SmartPtr(T* ptr = nullptr): _ptr(ptr){}~SmartPtr(){if (_ptr)delete _ptr;cout << "析構被調用" << endl;}private:T* _ptr;
};int div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0錯誤");return a / b;
}
void Func()
{SmartPtr<int> sp1(new int);SmartPtr<int> sp2(new int);cout << div() << endl;
}int main()
{try {Func();}catch (const exception& e){cout << e.what() << endl;}return 0;
}
運行結果:
?
解釋:和引入中說的一樣,將指向申請的內存的指針封裝成一個類,該類的析構函數會被自動的調用,再也不用擔心異常引發的內存泄漏了!至于析構函數怎么寫,就根據內存怎么申請的來寫,這里若是申請的數組,則delete的時候加上[ ]即可!
但是智能指針,是能像指針一樣去使用的,也就是可以* -> 等操作,所以我們還要加上*和->的重載,完整版如下:
// SmartPtr類(譯為智能指針類)
template<class T>
class SmartPtr
{
public:SmartPtr(T* ptr = nullptr): _ptr(ptr){}~SmartPtr(){if (_ptr)delete _ptr;cout << "析構被調用" << endl;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;
};
此時我們func函數變成這樣,方便體現*的作用:
void Func()
{SmartPtr<int> sp1(new int(1));SmartPtr<int> sp2(new int(2));cout << "sp1值為->" << *sp1 << endl;cout << "sp2值為->" << *sp2 << endl;cout << div() << endl;}
運行結果:
?
看過上篇博客的人都知道,有這么一種場景,連異常的重新拋出也解決不了:
void riskyOperation() {int* ptr1 = new int(100); // 內存1int* ptr2 = new int(200); // 內存2int* ptr3 = new int(300); // 內存3// 模擬后續操作拋出異常throw runtime_error("操作失敗");// 正常釋放(永遠不會執行)delete ptr1;delete ptr2;delete ptr3;
}
我們現在有了智能指針,簡直小case啦:
// SmartPtr類(譯為智能指針類)
template<class T>
class SmartPtr
{
public:SmartPtr(T* ptr = nullptr): _ptr(ptr){}~SmartPtr(){if (_ptr)delete _ptr;cout << "析構被調用" << endl;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;
};void riskyOperation() {SmartPtr<int> ptr1(new int(100)); // 內存1SmartPtr<int> ptr2(new int(200)); // 內存2SmartPtr<int> ptr3(new int(300)); // 內存3// 模擬后續操作拋出異常throw runtime_error("操作失敗");// 正常釋放(永遠不會執行)}int main() {try {riskyOperation();}catch (const exception& e) {cerr << "捕獲異常: " << e.what() << endl;// 問題:ptr1/ptr2/ptr3 內存泄漏!}
}
運行結果:
?完美?~!
?
智能指針乍一看,很簡單啊,真的如此嗎,實則不然~
?
二:智能指針的問題
1:問題場景
先把智能指針類放這:
// SmartPtr類(譯為智能指針類)
template<class T>
class SmartPtr
{
public:SmartPtr(T* ptr = nullptr): _ptr(ptr){}~SmartPtr(){if (_ptr)delete _ptr;cout << "析構被調用" << endl;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;
};
智能指針的難點在于兩個智能指針間的拷貝或賦值會出問題:
當在main中如下的時候:
int main()
{SmartPtr<int> sp1(new int);SmartPtr<int> sp2(sp1); //拷貝構造 會報錯SmartPtr<int> sp3(new int);SmartPtr<int> sp4(new int);sp3 = sp4; //拷貝賦值 也會報錯return 0;
}
報錯:
?
?
2:拷貝賦值的問題本質
①:編譯器默認生成的拷貝構造函數對內置類型完成值拷貝(淺拷貝),因此用sp1拷貝構造sp2后,相當于這sp1和sp2管理了同一塊內存空間,當sp1和sp2析構時就會導致這塊空間被釋放兩次。
②:編譯器默認生成的拷貝賦值函數對內置類型也是完成值拷貝(淺拷貝),因此將sp4賦值給sp3后,相當于sp3和sp4現在管理的都是sp4管理的空間,當sp3和sp4析構時就會導致這塊空間被釋放兩次,并且還會導致sp3原來管理的空間沒人管理了,所以沒有得到釋放。
Q:那去手動寫拷貝和賦值的深拷貝類型就能解決問題了,對嗎?
A:如果這么想,那么就更錯了~因為智能指針就是要模擬原生指針的行為,當我們將一個指針賦值給另一個指針時,目的就是讓這兩個指針指向同一塊內存空間,所以這里本就應該進行淺拷貝,但單純的淺拷貝又會導致空間被多次釋放,因此根據解決智能指針拷貝問題方式的不同,從而衍生出了不同版本的智能指針。
體現指針間的賦值和拷貝本身就是讓兩個指針指向同一塊內存空間的例子:
int main()
{int a = 1;int b = 2;int* p1 = &a;int* p2 = &b;cout << *p1 << *p2 << endl;//拷貝int* p3(p1);cout << *p1 << *p3 << endl;//賦值p1 = p2;cout << *p1 << *p2 << endl;return 0;
}
運行結果:
Q:那豈不是,沒辦法了,咱們雖然白嫖了類的析構自動調用去成功解決異常引發的內存泄漏,但是呢在拷貝和賦值的時候,卻又想避開類帶來的影響->類的兩次析構,那怎么辦呢o(╥﹏╥)o?
A:C++官方對于這個問題的解決過程中,過程是曲折的,也走過彎路~,C++的庫中的常用的智能指針類我會按照產生時間(也正好是從不優秀 到 優秀)來介紹:
①:auto_ptr? ?-> 極其大坑,大多數公司也都明確規定了禁止使用auto_ptr
②:unique_ptr -> 略有不妥
③:shared_ptr ->完美
④:weak_ptr ->某些場景,會和shared_ptr一起使用
三:智能指針的種類
1:auto_ptr
是C++98中引入的智能指針,auto_ptr通過管理權轉移的方式解決智能指針的拷貝問題,保證一個資源在任何時刻都只有一個對象在對其進行管理,這時同一個資源就不會被多次釋放了。比如:
int main()
{std::auto_ptr<int> ap1(new int(1));std::auto_ptr<int> ap2(ap1);*ap2 = 10;//*ap1 = 10; //errorstd::auto_ptr<int> ap3(new int(1));std::auto_ptr<int> ap4(new int(2));ap3 = ap4;//*ap4 = 10; //errorreturn 0;
}
但你解開任意一個注釋的時候,就會報錯:
?
解釋:被拷貝/賦值對象把資源管理權轉移給拷貝/賦值對象,導致被拷貝/賦值對象懸空!
這是一個極其不好的設計,進了公司,用這個就GG,而且面試的時候,問到你了解哪種智能指針,你說你了解這個,你也GG~
2:unique_ptr
unique_ptr是C++11中引入的智能指針,unique_ptr通過防拷貝/賦值的方式解決智能指針的拷貝問題,也就是簡單粗暴的防止對智能指針對象進行拷貝/賦值,這樣也能保證資源不會被多次釋放。比如:
int main()
{std::unique_ptr<int> up1(new int(0));//std::unique_ptr<int> up2(up1); //errorstd::unique_ptr<int> up3(new int(0));//up3 = up1; //errorreturn 0;
}
解釋:當你解開注釋的時候,會報錯嘗試引用已經刪除的函數!但防拷貝/賦值,其實也不是一個很好的辦法,因為總有一些場景需要進行拷貝或者賦值。
3:shared_ptr
最好用最優秀的智能指針就是shared_ptr!
C++11中引入的智能指針shared_ptr,通過引用計數的方式解決智能指針的拷貝問題。
· ?每一個被管理的資源都有一個對應的引用計數,通過這個引用計數記錄著當前有多少個對象在管理著這塊dain當新增一個對象管理這塊資源時則將該資源對應的引用計數進行++,當一個對象不再
· ?管理這塊資源或該對象被析構時則將該資源對應的引用計數進行--。
· ?當一個資源的引用計數減為0時說明已經沒有對象在管理這塊資源了,這時就可以將該資源進行釋放了。
引用計數例子:老師晚上在下班之前都會通知,讓最后走的學生記得把門鎖下。所以只要不是最后一個學生離開教室,都不會鎖門,直到最后一個學生離開,才會鎖門
同理,只有該資源的引用計數到了0,才會釋放資源,反之不會
注意:博主會說資源 也會說空間 知道是一個意思就行啦
通過這種引用計數的方式就能支持多個對象一起管理某一個資源,也就是支持了智能指針的拷貝和賦值,并且只有當一個資源對應的引用計數減為0時才會釋放資源,因此保證了同一個資源不會被釋放多次。比如:
須知:?use_count成員函數,用于獲取當前對象管理的資源對應的引用計數。
直接用庫中的share_ptr ,可以進行隨意的賦值拷貝!:?
int main()
{shared_ptr<int> sp1(new int(1));shared_ptr<int> sp2(sp1);//體現二者的地址一致cout << sp1 << endl;cout << sp2 << endl;//內容一致cout << *sp1 << endl;cout << *sp2 << endl;//打印內存的引用計數cout << sp1.use_count() << endl; //2shared_ptr<int> sp3(new int(1));shared_ptr<int> sp4(new int(2));sp3 = sp4;//體現二者的地址一致cout << sp3 << endl;cout << sp4 << endl;//內容一致cout << *sp3 << endl;cout << *sp4 << endl;//打印內存的引用計數cout << sp3.use_count() << endl; //2return 0;
}
運行結果:
??
用誰都會用,重點是自己模擬實現shared_ptr,智能指針面試問到,跑不了的模擬實現
①:典型錯誤實現shared_ptr
實現shared_ptr有一種非常經典的錯誤實現,理解錯誤在哪,會提升自己,代碼:
class wtt
{
public:template<class T>class shared_ptr{public:// RAIIshared_ptr(T* ptr):_ptr(ptr){_count = 1;}// sp2(sp1)shared_ptr(const shared_ptr<T>& sp){_ptr = sp._ptr;++_count;}~shared_ptr(){if (--_count == 0){cout << "delete:" << _ptr << endl;delete _ptr;}}int use_count(){return _count;}// 像指針一樣T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;static int _count;};};template<class T>
int wtt::shared_ptr<T>::_count = 0; // 正確:加上 wtt::
解釋:
首先,先不看為什么錯誤,咱們先看好處
細節1:
類的靜態成員必須在類外初始化,而模板類的靜態成員,不僅要在類外初始化,還要帶上模版
template<class T>
int shared_ptr<T>::_count = 0; // 正確:加上 wtt::
細節2:
當你想不和庫中的shared_ptr沖突的時候,你選擇在自己實現的shared_ptr外面套一層域的時候,要記住嵌套類是外層類的?private 成員!所以此時你必須在兩個類之間加上public,用來將嵌套類聲明為?public:?
class wtt
{
public://一定要加template<class T>class shared_ptr{//.....};};
細節3:
當你采取細節2的方法的時候,內層類的靜態變量的初始化是在類外,此時的類外指的是,嵌套類的類外(也就是兩個類的類外),而不是兩個類之間進行初始化,而且你還要在原先的基礎上,再加上外層類wtt域名:
template<class T>
int wtt::shared_ptr<T>::_count = 0; // 正確:加上 wtt::
?
現在再來看為什么錯?
首先如果shared_ptr類只有一個對象的時候,也就是只開辟了一塊空間A的時候,那么此時的對象無論是拷貝還是賦值去生成新的對象的時候,這些新生成的對象,都是指向的空間?A,所以引用計數都可以正確++,例子如下:
int main() {wtt::shared_ptr<int> sp1(new int(42));cout << "sp1.use_count(): " << sp1.use_count() << endl; // 輸出 1(正確)wtt::shared_ptr<int> sp2(sp1);cout << "sp1.use_count(): " << sp1.use_count() << endl; // 輸出 2(正確)cout << "sp2.use_count(): " << sp2.use_count() << endl; // 輸出 2(正確)return 0;
}
運行結果:?
?正確!
但是問題是,如果你現在通過構造創建一個新的對象的時候,那么引用計數將會出錯,例子:
int main() {wtt::shared_ptr<int> sp1(new int(42));cout << "sp1.use_count(): " << sp1.use_count() << endl; // 輸出 1(但實際是 1)wtt::shared_ptr<int> sp2(sp1);cout << "sp1.use_count(): " << sp1.use_count() << endl; // 輸出 2(但實際是 2)cout << "sp2.use_count(): " << sp2.use_count() << endl; // 輸出 2(但實際是 2)wtt::shared_ptr<int> sp3(new int(100)); // 錯誤:sp3 的計數會干擾 sp1/sp2cout << "sp1.use_count(): " << sp1.use_count() << endl; //應該輸出 3(但是是1)return 0;
}
運行結果:?
?錯誤!
解釋:這不符合我們的預期,我們的預期是sp3對象對應的空間的引用計數是1,而不是將sp1和sp2共同的空間對應的引用計數2影響到了1!!
Q:為什么會發生這種情況?
A:因為這寫法就是錯的,?所有對象共享 static _count,當我們sp1、sp2指向同一塊空間的時候,此時還看不出錯,但是當一個sp3指向新的空間的時候,此時所有對象的引用計數都會被置為1!因為我們的構造函數里面,將引用計數初始化為了1,本意是讓每一塊空間在第一次開辟的時候讓其自己的引用計數為1,但是卻變成了每次有新空間都會影響所有空間的引用計數為1
②:正確實現shared_ptr
namespace wtt
{template<class T>class shared_ptr{public://構造shared_ptr(T* ptr = nullptr):_ptr(ptr), _pcount(new int(1)){}//拷貝shared_ptr(shared_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount){(*_pcount)++;}//賦值shared_ptr& operator=(shared_ptr<T>& sp){if (_ptr != sp._ptr) //管理同一塊空間的對象之間無需進行賦值操作{if (--(*_pcount) == 0) //將管理的資源對應的引用計數--{cout << "delete: " << _ptr << endl;delete _ptr;delete _pcount;}_ptr = sp._ptr; //與sp對象一同管理它的資源_pcount = sp._pcount; //獲取sp對象管理的資源對應的引用計數(*_pcount)++; //新增一個對象來管理該資源,引用計數++}return *this;}//析構~shared_ptr(){if (--(*_pcount) == 0){if (_ptr != nullptr){cout << "delete: " << _ptr << endl;delete _ptr;_ptr = nullptr;}delete _pcount;_pcount = nullptr;}}//獲取引用計數int use_count(){ return *_pcount;}//*重載T& operator*(){ return *_ptr;}//->重載T* operator->(){ return _ptr;}private:T* _ptr; //管理的資源int* _pcount; //管理的資源對應的引用計數};
}int main() {wtt::shared_ptr<int> sp1(new int(42));cout << "sp1.use_count(): " << sp1.use_count() << endl; // 輸出 1(但實際是 1)wtt::shared_ptr<int> sp2(sp1);cout << "sp1.use_count(): " << sp1.use_count() << endl; // 輸出 2(但實際是 2)cout << "sp2.use_count(): " << sp2.use_count() << endl; // 輸出 2(但實際是 2)wtt::shared_ptr<int> sp3(new int(100)); // 錯誤:sp3 的計數會干擾 sp1/sp2cout << "sp1.use_count(): " << sp1.use_count() << endl; // 輸出 3(錯誤!)return 0;
}
運行結果:
正確!每個空間的引用計數互不干擾!
解釋:先知道是對的就行了,下面慢慢解釋
①:成員變量的改變
shared_ptr類增加一個int*變量,int*指向一個整形,該整形表示引用計數的值,?因為你只有構造的時候,就會新增一份新的引用計數,新的對象意味著新的空間,所以需要新的引用計數,而不是像錯誤方法:只有第一次實例化對象的時候,才會有引用計數 而每次構造的時候,不會出現新的引用計數,而是在原有的上面++
說白了,現在就變成了每個空間對應的引用計數在獨立的空間之中,因為引用計數是new出來的一個整形的空間
?
②:構造
//構造shared_ptr(T* ptr = nullptr):_ptr(ptr), _pcount(new int(1)){}
正如①所言,構造意味著新的對象,則意味著新的空間要產生了,所以需要一個新的獨立的引用計數來跟隨這塊空間,所以每次構造進來就是給成員變量引用計數new上一個空間,初始化為1
③:拷貝函數
shared_ptr(shared_ptr<T>& sp): _ptr(sp._ptr), _pcount(sp._pcount)
{(*_pcount)++; // 引用計數 +1
}
-
功能:用另一個?
shared_ptr
(sp
)構造新對象,共享同一塊內存和引用計數。 -
引用計數:遞增計數器(表示多了一個?
shared_ptr
?管理該資源)。
Q:(*_pcount)++; 對嗎?不是應該加對象sp的成員變量pcount嗎?
A:兩種寫法完全等價
在拷貝構造函數中:
-
_pcount
?已經被初始化為?sp._pcount
(通過成員初始化列表?:_pcount(sp._pcount)
)。 -
因此,
(*_pcount)++
?和?(*sp._pcount)++
?訪問的是同一個內存地址,效果完全相同。
④:賦值函數
shared_ptr& operator=(shared_ptr<T>& sp) {if (_ptr != sp._ptr) { // 避免自賦值// 1. 減少原資源的引用計數if (--(*_pcount) == 0) {cout << "delete: " << _ptr << endl;delete _ptr;delete _pcount;}// 2. 共享新資源_ptr = sp._ptr;_pcount = sp._pcount;(*_pcount)++; // 引用計數 +1}return *this;
}
賦值是難點,假設B要賦值A,所以A對象會指向和B相同的空間,所以A原先的空間的引用計數就需要事先被--,然后再++B指向的空間的對應的引用計數
-
功能:將當前?
shared_ptr
?改為管理?sp
?的資源。 -
關鍵步驟:
-
釋放原資源:
-
減少原引用計數,若歸零則釋放內存和計數器。
-
-
共享新資源:
-
指向?
sp
?的資源,并遞增其引用計數。
-
-
-
自賦值檢查:
if (_ptr != sp._ptr)
?避免無意義操作。
⑤:析構函數
~shared_ptr() {if (--(*_pcount) == 0) // 1. 引用計數減1{ if (_ptr != nullptr) // 2. 檢查資源是否有效{ delete _ptr; // 3. 釋放管理的資源_ptr = nullptr; // 4. 置空指針(避免懸空指針)}delete _pcount; // 5. 釋放引用計數器_pcount = nullptr; // 6. 置空計數器指針}
}
-
功能:遞減引用計數,若歸零則釋放資源。
-
細節:
-
只有最后一個?
shared_ptr
?析構時(計數為?0
),才會釋放內存。 -
安全處理?
nullptr
?情況。
-
if (_ptr != nullptr)
-
確保?
_ptr
?不是空指針(避免對?nullptr
?調用?delete
,這是安全的編程習慣)。
?至此 才是正確的實現shared_ptr!
4:weak_ptr
但是智能指針在某些場景(循環引用)下,還需要weak_ptr的使用,才能完美的應對所有的場景,所以shared_ptr也不例外
①:循環引用的定義
循環引用(Circular Reference)指?兩個或多個對象通過智能指針互相持有對方的引用,導致它們的引用計數始終無法歸零,從而無法釋放內存。
場景:循環引用
shared_ptr的循環引用問題在一些特定的場景下才會產生。比如如下的結點類,
struct ListNode
{ListNode* _next;ListNode* _prev;int _val;~ListNode(){cout << "~ListNode()" << endl;}
};
現在以new
的方式構建兩個結點,并將這兩個結點連接起來,在程序的最后以delete
的方式釋放這兩個結點。比如:
int main()
{ListNode* node1 = new ListNode;ListNode* node2 = new ListNode;node1->_next = node2;node2->_prev = node1;//...delete node1;delete node2;return 0;
}
上述程序是沒有問題的,兩個結點都能夠正確釋放!
但現在我們既然學了智能指針,那肯定要給它安排上了,期望有效的防止拋異常導致的內存泄漏
我們將這兩個結點分別交給兩個shared_ptr對象進行管理,這時為了讓連接節點時的賦值操作能夠執行,就需要把ListNode類中的next和prev成員變量的類型也改為shared_ptr類型。如下:
struct ListNode
{std::shared_ptr<ListNode> _next;std::shared_ptr<ListNode> _prev;int _val;~ListNode(){cout << "~ListNode()" << endl;}
};
此時我們在main中進行:
int main()
{std::shared_ptr<ListNode> node1(new ListNode);std::shared_ptr<ListNode> node2(new ListNode);node1->_next = node2;return 0;
}
運行結果:
正確!
?解釋:沒有引發循環引用的本質是n1
?和?n2
?的引用關系是單向的
Q:為什么沒有觸發循環引用?
A:分析如下
a:創建?n1
?和?n2
-
n1
?的引用計數 = 1(main
?中的?n1
) -
n2
?的引用計數 = 1(main
?中的?n2
)
b:
n1->_next = n2
-
n2
?的引用計數?+1 → 2(n1->_next
?也指向?n2
) -
n1
?的引用計數?不變(n2
?沒有指向?n1
)
c:?main
?函數結束
-
n2
?析構:-
n2
?的引用計數?-1 → 1(n1->_next
?仍然持有?n2
)
-
-
n1
?析構:-
n1
?的引用計數?-1 → 0(n1
?被釋放) -
n1->_next
?析構 →?n2
?的引用計數?-1 → 0(n2
?被釋放)
-
總結:
“
n1->_next = n2
?本質是復制?n2
?的?shared_ptr
,使?n2
?的引用計數變為 2。
當?n2
?離開作用域時,其引用計數減為 1(因?n1->_next
?仍持有它)。
接著?n1
?離開作用域,引用計數減為 0,觸發?n1
?的析構。
在?n1
?的析構過程中,其成員?_next
(類型為?shared_ptr
)也會析構,導致?n2
?的引用計數歸零,從而釋放?n2。
由于?n2
?從未持有?n1
?的?shared_ptr
,因此沒有形成循環引用。”
所以單獨的node2->_prev = node1;也不會引發循環引用!
②:循環引用的場景
而下面這個場景則會形成引用循環:
int main()
{std::shared_ptr<ListNode> node1(new ListNode);std::shared_ptr<ListNode> node2(new ListNode);node1->_next = node2;node2->_prev = node1;return 0;
}
解釋:
Q:為什么會引發循環引用?
A:分析如下
(1) 初始化階段
-
node1
?的引用計數 = 1(由?std::shared_ptr<ListNode> node1
?管理)。 -
node2
?的引用計數 = 1(由?std::shared_ptr<ListNode> node2
?管理)。
(2) 建立雙向鏈接
-
node1->_next = node2
:
node2
?的引用計數?+1 → 2(node1->_next
?持有?node2
)。 -
node2->_prev = node1
:
node1
?的引用計數?+1 → 2(node2->_prev
?持有?node1
)。
(3)?main
?函數結束時
-
node1
?和?node2
?離開作用域,觸發析構:-
node1
?的引用計數?-1 → 1(因?node2->_prev
?仍持有?node1
)。 -
node2
?的引用計數?-1 → 1(因?node1->_next
?仍持有?node2
)。
-
內存泄漏的根源
-
循環依賴:
node1
?和?node2
?的成員變量?_next
?和?_prev
?互相持有對方的?shared_ptr
。 -
引用計數無法歸零:
即使外部的?node1
?和?node2
?被銷毀,它們的成員變量仍然保持對方的引用計數為?1
。 -
結果:
兩個?ListNode
?對象永遠不會被釋放(內存泄漏),它們的析構函數也不會被調用。
驗證現象
-
輸出結果:
運行代碼后,不會輸出?~ListNode()
,說明析構函數未被調用。
錯誤!永遠不會析構
所以此時就需要weak_ptr了!
weak_ptr是C++11中引入的智能指針,weak_ptr不是用來管理資源的釋放的,它主要是用來解決shared_ptr的循環引用問題的。
weak_ptr支持用shared_ptr對象來構造weak_ptr對象,構造出來的weak_ptr對象與shared_ptr對象管理同一個資源,但不會增加這塊資源對應的引用計數。
?
所以將ListNode中的next和prev成員的類型換成weak_ptr就不會導致循環引用問題了,此時當node1和node2生命周期結束時兩個資源對應的引用計數就都會被減為0,進而釋放這兩個結點的資源。比如:?
struct ListNode
{std::weak_ptr<ListNode> _next;std::weak_ptr<ListNode> _prev;int _val;~ListNode(){cout << "~ListNode()" << endl;}
};
int main()
{std::shared_ptr<ListNode> node1(new ListNode);std::shared_ptr<ListNode> node2(new ListNode);cout << node1.use_count() << endl;cout << node2.use_count() << endl;node1->_next = node2;node2->_prev = node1;//...cout << node1.use_count() << endl;cout << node2.use_count() << endl;return 0;
}
?運行結果:
通過use_count獲取這兩個資源對應的引用計數就會發現,在結點連接前后這兩個資源對應的引用計數就是1,根本原因就是weak_ptr不會增加管理的資源對應的引用計數。
weak_ptr的模擬實現:
namespace cl
{template<class T>class weak_ptr{public:weak_ptr():_ptr(nullptr){}weak_ptr(const shared_ptr<T>& sp):_ptr(sp.get()){}weak_ptr& operator=(const shared_ptr<T>& sp){_ptr = sp.get();return *this;}//可以像指針一樣使用T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr; //管理的資源};
}
解釋:很簡單
構造和賦值均不會增加增加?shared_ptr
?的引用計數(與?shared_ptr
?的拷貝賦值不同)。
?
?
四:C++11和boost庫中智能指針的關系
C++98中產生了第一個智能指針auto_ptr。
C++boost給出了更實用的scoped_ptr、shared_ptr和weak_ptr。
C++TR1,引入了boost中的shared_ptr等。不過注意的是TR1并不是標準版。
C++11,引入了boost中的unique_ptr、shared_ptr和weak_ptr。需要注意的是,unique_ptr對應的就是boost中的scoped_ptr,并且這些智能指針的實現原理是參考boost中實現的。
說明一下:boost庫是為C++語言標準庫提供擴展的一些C++程序庫的總稱,boost庫社區建立的初衷之一就是為C++的標準化工作提供可供參考的實現,比如在送審C++標準庫TR1中,就有十個boost庫成為標準庫的候選方案。
?
?
本文還有智能指針和線程安全的問題沒講,后面會增加在此篇博客中~?