💓博主CSDN主頁:杭電碼農-NEO💓
?
?專欄分類:C++從入門到精通?
?
🚚代碼倉庫:NEO的學習日記🚚
?
🌹關注我🫵帶你學習C++
? 🔝🔝
C++11
- 1. 前言
- 2. 為什么要有智能指針?
- 3. RAII思想以及智能指針的設計
- 4. C++智能指針的發展歷史
- 5. shared_ptr模擬實現
- 6. shared_ptr的循環引用問題
- 7. 定制刪除器
- 8. 總結以及拓展
1. 前言
相信學C++的同學或多或少的聽說過
智能指針這個詞,博主剛聽見這個詞時
,覺得它應該很復雜,并且很高大上,但不
管是多牛的東西,都是人寫出來的,是可
學習的!不要懷著害怕的心理來學習它
本章重點:
本篇文章著重講解智能指針的發展歷史
中出現過的auto_ptr,unique_ptr以及主
角shared_ptr.并且會介紹什么是RAII思想
以及為什么要有智能指針這一話題,最后
會給大家分析shared_ptr的循環引用問題
以及定制刪除器的基本概念
2. 為什么要有智能指針?
在寫代碼時,我們經常在堆上申請空間
但是偶爾會忘記釋放空間,會造成內存
泄漏問題,當然,這不是最重要的,在某些
場景下即使你釋放了也會有問題:
int div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0錯誤");return a / b;
}
void Func()
{// 1、如果p1這里new 拋異常會如何?// 2、如果p2這里new 拋異常會如何?// 3、如果div調用這里又會拋異常會如何?int* p1 = new int;int* p2 = new int;cout << div() << endl;delete p1;delete p2;
}
int main()
{try{Func();}catch (exception& e){cout << e.what() << endl;}return 0;
}
在上面代碼的這種場景中,不管是使用
new還是調用div函數都有拋異常的風險
并且程序一旦拋異常就會直接跳到catch
處,所以上面的代碼一旦拋異常就代表著
delete p1和p2并不會執行,也就會出現
內存泄漏的問題!這個問題不使用智能
指針是很難解決的!!!
3. RAII思想以及智能指針的設計
RAII思想
RAII思想是一種 利用對象生命周期來控制程序資源
(如內存、文件句柄、網絡連接、互斥量等等)的簡單技術。在對象構造時獲取資源,接著控制對資源的訪問使之在對象的生命周期內始終保持有效,最后在對象析構的時候釋放資源。借此,我們實際上把管理一份資源的責任托管給了一個對象
這種做法有兩種好處:
- 不需要顯式地釋放資源
- 對象所需的資源在其生命期內始終有效
智能指針的基本設計
現在我們來寫一個類,構造函數的
時候創造資源,析構函數的時候釋放
資源,當對象出了作用域會自動調用析構!
// 使用RAII思想設計的SmartPtr類
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr): _ptr(ptr)
{}
~SmartPtr()
{if(_ptr!=nullptr)delete _ptr;
}
T& operator*() {return *_ptr;}
T* operator->() {return _ptr;}
private:T* _ptr;
};
現在我們來使用一下它:
SmartPtr<int> sp1(new int(10));
*sp = 20;
當然,重載了->是給自定義類型用的
4. C++智能指針的發展歷史
首先,我們要清楚智能指針的一個大坑
那就是當一個指針賦值給另外一個指針
時,我們需要的是淺拷貝,因為我們就是想
讓兩個指針指向同一塊空間,但是指向了
同一塊空間就會有析構函數調用兩次的風險
由于這一個大坑,智能指針進行了很多次迭代
- 在C++98的時候就已經在庫中實現
了智能指針了,它就是auto_ptr
既然智能指針是隨著歷史不斷發展的
就證明它前面的版本寫的不咋滴[doge]
事實也是如此,auto_ptr是這樣實現的,
既然有析構兩次的風險,那么當我把A
指針賦值給B指針后,A指針就銷毀不能用
了,對于不了解auto_ptr的人來說這無疑是
一個巨大的風險!
auto_ptr<int> ap1(new int(10));
auto_ptr<int> ap2(ap1);
//此時ap1已經失效了!
- 有了這一大坑后,C++11推出了全新
的智能指針:unique_ptr
unique_ptr的做法比auto_ptr還絕
智能指針不是拷貝有問題嗎?那么
unique_ptr就禁用了拷貝和賦值,
很顯然這也是一個坑,但是在實際
場景下,unique_ptr至少還能被用到
但auto_ptr是很多公司明令禁止使用的!
unique_ptr<int> up1(new int(10));
unique_ptr<int> up2(up1);//這里會直接報錯
- 經過兩次失敗的智能指針后,C++11
還推出了今天的主角:shared_ptr
shared_ptr可堪稱完美的智能指針
也是實際中使用的最多的智能指針
它采用的是引用計數的思想,當指向
這份空間的計數是1時才析構,大于1
時就將計數減一,非常的優雅!
由于智能指針在面試時讓手撕的概率很大
所以我們會模擬實現它
5. shared_ptr模擬實現
我們使用引用計數的方式來實現
shared_ptr,也就是在原先代碼的
基礎上增加一個int*成員變量來保存
還有幾個指針指向當前空間!
template<class T>
class Smart_Ptr //實現的C++11的shared_ptr版本
{
public:Smart_Ptr(T* ptr = nullptr):_ptr(ptr),_pcount(new int(1)){}~Smart_Ptr(){Release();}Smart_Ptr(const Smart_Ptr<T>& sp):_ptr(sp._ptr),_pcount(sp._pcount){Addcount();}Smart_Ptr<T>& operator=(const Smart_Ptr<T>& sp){if (_ptr != sp._ptr){Release();_ptr = sp._ptr;_pcount = sp._pcount;Addcount();}return *this;}void Release(){if (--(*_pcount) == 0)//銷毀最后一個變量時才釋放資源{delete _ptr;delete _pcount;delete _pmtx;}}void Addcount(){(*_pcount)++;}void Subcount(){Release();private:T* _ptr;int* _pcount;
};
我們將計數++賀計數- -特意的提出來
這是因為很多場景下都需要這兩個函數.
當計數不為1時就- -計數,當計數為一才
釋放資源,并且這樣寫的好處是相同類型
的指針對象即使指向不同的空間也不會
出錯,相反,使用static定義成員指針變量
就會出現上面的這種問題!
6. shared_ptr的循環引用問題
請看下面的代碼運行會崩潰:
struct ListNode
{int _data;shared_ptr<ListNode> prev;shared_ptr<ListNode> next;~ListNode(){ cout << "~ListNode()" << endl; }
};
int main()
{shared_ptr<ListNode> node1(new ListNode);shared_ptr<ListNode> node2(new ListNode);node1->next = node2;node2->prev = node1;return 0;
}
為啥會崩潰?下面我用畫圖加文字
的方式幫大家分析一下此問題:
現在來進一步分析:當main函數調用完,
node2會先析構,但是此時引用計數是2
所以不會釋放空間而是將計數變為1.
然后node1再析構,同上,它的引用計數
也減為一,但是這兩份空間并不會釋放,
因為要node2的prev釋放后,node1的空間
才會釋放,那node2的prev什么時候釋放?
答案是node2這份空間釋放了才會釋放
prev,那么node2這份空間什么時候釋放?
答案是node1的next釋放了它才釋放,這
就形成了一個死循環,我等你釋放了我才
能釋放,對方也在等我釋放了對方才能
釋放,這就是"循環引用問題"
最好的解決方案就是在使用智能指針
的時候跳過這個坑,不用將智能指針和
這種場景一起使用!!!
7. 定制刪除器
使用智能指針時可能會遇見下面的問題:
shared_ptr<int> sp1(new int[10]);
當變量出作用域銷毀時即報錯
因為new []對應的是delete [].
然而庫中寫法并不能識別有沒有[]
還有一些問題:
shared_ptr<FILE> sp3(fopen("Test.cpp", "r"));
此時智能指針管理的對象并不是堆上
開辟的空間,delete完全沒法用,此時需
要使用fclose,所以定制刪除器非常重要
在構造函數的地方可以傳入一個定制
刪除器,也就是一個函數對象,此函數
中有對應的刪除方法,請看下面的代碼:
shared_ptr<int> sp2(new int[10], [](int* ptr) {delete[] ptr; });
shared_ptr<FILE> sp3(fopen("Test.cpp", "r"), [](FILE* ptr) {fclose(ptr); });
注:定制刪除器屬于了解的部分
8. 總結以及拓展
智能指針在面試中是常客,經常會被
問到發展歷史和shared_ptr的手撕,
學到這里后,C++的所有重要的知識
差不多已經完結了,后面文章更新會慢一點
拓展:weak_ptr的拓展閱讀
既然weak_ptr可以解決shared_ptr的
循環引用問題,那么什么是weak_ptr?
有興趣的同學可以閱讀下面這篇文章:
weak_ptr詳解