目錄
一?為什么需要智能指針?
二?智能指針的使用及原理?
1. RAII
2. auto_ptr
3. unique_ptr
5.?weak_ptr
三 內存泄漏
1.什么是內存泄漏,內存泄漏的危害
2.?如何避免內存泄漏?
一?為什么需要智能指針?
🚀為什么需要智能指針? 下面我們先分析一下下面這段程序有沒有什么內存方面的問題?
#include <iostream>
using namespace std;int div()
{int a, b;cin >> a >> b;if (b == 0)//拋異常throw invalid_argument("除0錯誤");return a / b;
}
void f1()
{int* p = new int;cout << div() << endl;delete p;
}
int main()
{//捕異常try{f1();}catch (exception& e){cout << e.what() << endl;}return 0;
}
運行結果
通過上面的程序中我們可以看到,new了以后,而且也delete了,但是因為拋異常有點早,程序執行不到delete的位置,所以就導致內存泄露了,此外如果我們在寫代碼的過程中忘了釋放資源的話也會導致內存泄漏。為了解決上述問題,接下來引入智能指針。
🍉:我們首先寫一個類
#pragma oncetemplate<class T>
class SmartPtr
{
public:SmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){if (_ptr){std::cout << "delete" << _ptr<<std::endl;delete _ptr;}}
private:T* _ptr;
};
#include <iostream>
#include "SmartPtr.h"
using namespace std;int div()
{int a, b;cin >> a >> b;if (b == 0)//拋異常throw invalid_argument("除0錯誤");return a / b;
}
void f1()
{/// 修改部分,將指針存儲在SmartPtr這個類中int* p = new int;SmartPtr<int> sp(p);cout << div() << endl;//delete p;
}
int main()
{//捕異常try{f1();}catch (exception& e){cout << e.what() << endl;}return 0;
}
測試結果:?
上面我們通過創建一個類SmartPtr ,讓?SmartPtr sp(p)對p進行管理資源的釋放。無論函數正常結束,還是拋異常,都會導致sp對象的生命周期到了以后,調用析構函數~SmartPtr()釋放內存。
?上述我們通過類對資源p進行管理,幫我們管理資源的釋放,這個類我們就叫智能指針。
二?智能指針的使用及原理?
1. RAII
RAII(Resource Acquisition Is Initialization)是一種利用對象生命周期來控制程序資源(如內存、互斥量等)的簡單技術。
在對象構造時獲取資源,接著控制對資源的訪問使之在對象的生命周期內始終保持有效,最后在對象析構的 時候釋放資源。借此,我們實際上把管理一份資源的責任托管給了一個對象。這種做法有兩大好處:
- 不需要顯式地釋放資源。
- 采用這種方式,對象所需的資源在其生命期內始終保持有效。
RAII和智能指針的關系:RAII是一種托管資源的思想,智能指針就是依靠這種RAII實現的。
觀察上述代碼
void f1()
{/// 修改部分,將指針存儲在SmartPtr這個類中int* p = new int;SmartPtr<int> sp(p);cout << div() << endl;//delete p;
}
我們還可以直接這樣
SmartPtr<int> sp(new int);/修改后的cout << div() << endl;
?但是這樣的話我們又會面臨一個問題那就是 如果我們想要訪問這個指針變量,我們需要再加以下成員函數。
T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
這樣的話我們就可以訪問和修改指針變量了
SmartPtr<int> sp1(new int);*sp1 = 10;SmartPtr<pair<int, int>> sp2(new pair<int, int>);sp2->first = 20;sp2->second = 30;
?并且會自動釋放內存
?以上是智能指針的簡單demo,但是上述代碼還存在很多問題,我們通過以下代碼進行測試
int main()
{SmartPtr<int> sp1(new int);SmartPtr<int> sp2 = sp1;//拷貝構造sp2(sp1);return 0;
}
測試結果:
🍎問題分析:我們并沒有給SmartPtr構造拷貝構造函數,編譯器會自動生成默認拷貝構造即值拷貝(淺拷貝),sp1---------->資源, sp2----------->資源 ,當sp1和sp2出了作用域會對資源進行析構,即sp2先對資源進行析構,sp1又對同一份資源進行了析構,同一份資源不能析構二次,因為第一次析構以及釋放了所以出現了報錯。
?上述原因就在于淺拷貝造成了報錯,但是我們不能說即然淺拷貝有問題,我們進行深拷貝不就行了嗎?答案是不可以的,智能指針是用來模擬原生指針的 ,原生指針 p1=p2;就是值拷貝代表著指向同一塊空間。
接下來我們引出解決上述問題的三種解決方法:
- 管理器轉移:c++98 auto_ptr?
- 防拷貝:? ? ? ?c++11 unique_ptr?
- 引言計數? ? ?c++11 shared_ptr? (循環引言的問題,又需要weak_ptr來解決)
2. auto_ptr
我們對auto_ptr的拷貝函數進行構造
//拷貝構造auto_ptr(auto_ptr<T>& ap):_ptr(ap._ptr){ap._ptr = nullptr;}
測試如下:
int main()
{lt::auto_ptr<int> sp1(new int);lt::auto_ptr<int> sp2 = sp1;//拷貝構造sp2(sp1);return 0;
}
為什么只析構一次,并且為什么auto_ptr叫做管理權轉移呢??我們通過下圖進行描述
?
?管理權轉移:早期c++98設計缺陷,因為它會把其他指針置為空,不建議使用。
3. unique_ptr
unique_ptr的實現原理:簡單粗暴的防拷貝
unique_ptr(unique_ptr<T>& up) = delete;unique_ptr<T>& operator==(unique_ptr<T>& up) = delete;
但是unique也有缺陷,如果有需要拷貝的場景,就無法使用。?所以c++11又搞出一個智能shared_ptr.
4. shared_ptr
shared_ptr :是通過引用計數的方式來實現多個shared_ptr對象之間共享資源
- shared_ptr在其內部,給每個資源都維護了著一份計數,用來記錄該份資源被幾個對象共享
- 在對象被銷毀時(也就是析構函數調用),就說明自己不使用該資源了,對象的引用計數減一。 3. 如果引用計數是0,就說明自己是最后一個使用該資源的對象,必須釋放該資源; 4. 如果不是0,就說明除了自己還有其他對象在使用該份資源,不能釋放該資源,否則其他對象就成野指 針了。?
#pragma oncenamespace lt
{template<class T>class shared_ptr{public:shared_ptr(T* ptr):_ptr(ptr),_pcount(new int(1)){}shared_ptr(shared_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount){++(*_pcount);}shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (this != &sp){if (--*(_pcount) == 0){delete _ptr;delete _pcount;}_ptr = sp._ptr;_pcount = sp._pcount;++(*_pcount); }}T& operator*()//const 這里加是為了const this 指針。不能說(const){return *_ptr;}T* operator->(){return _ptr;}~shared_ptr(){if (--(*_pcount)==0 && _ptr){std::cout << "delete" << _ptr << std::endl;delete _ptr;_ptr = nullptr;delete _pcount;_pcount = nullptr;}}private:T* _ptr;int* _pcount;};
}
接下來我們側重講一下拷貝構造?
這里有個注意的點就是當計數為1進行拷貝是需要注意 delete _ptr delete _pcount
5.?weak_ptr
shared_ptr?多數情況下管理資源?常合適,?持RAII,也?持拷?。但是在循環引?的場景下會導致資源沒得到釋放內存泄漏,所以我們要認識循環引?的場景和資源沒釋放的原因,并且學會使?weak_ptr解決這種問題。
struct listNode
{int _data;shared_ptr<listNode> _next;shared_ptr<listNode> _prev;};
?如圖所示:
?
- ? 右邊的節點什么時候釋放呢,左邊節點中的_next管著呢,_next析構后,右邊的節點就釋放了。
- next什么時候析構呢,_next是左邊節點的的成員,左邊節點釋放,_next就析構了。
- 左邊節點什么時候釋放呢,左邊節點由右邊節點中的_prev管著呢,_prev析構后,左邊的節點就釋放了。
- _prev什么時候析構呢,_prev是右邊節點的成員,右邊節點釋放,_prev就析構了。
此邏輯上成功形成回旋鏢似的循環引?,誰都不會釋放就形成了循環引?,導致內存泄漏。
?解決方法:把ListNode結構體中的_next和_prev改成weak_ptr,weak_ptr 不增加它的引?計數,就成功打破了循環引?,這就解決了這?的問題。
🍏weak_ptr 的簡單實現?
template<class T>class weak_ptr{public:weak_ptr() = default;weak_ptr(shared_ptr<T>& sp):_ptr(sp.get_ptr()){}weak_ptr<T>& operator=(const shared_ptr<T>& sp){_ptr = sp.get_ptr();return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;} private:T* _ptr;};
}
weak_ptr嚴格來說不是智能指針,因為它沒有RAII資源管理,weak_ptr是用來專門解決shared_ptr循環引用造成的問題。我們知道ListNode這個結點本身放在智能指針shared_ptr是沒有什么問題的,可以很好的對資源ListNode*進行管理,但是就是因為ListNode 結點中還包含 _next ,_prev這就造成了循環引用。我們希望的是ListNode內的指針不參與計數,
所以我們創建了weak_ptr(shared_ptr<T>& sp)?,目的就是希望把_next,_prev指針存儲再weak_ptr中不參與計數。
struct ListNode
{int val;lt::weak_ptr<ListNode> _spnext;lt::weak_ptr<ListNode> _spprev;~ListNode(){cout << "~ListNode()" << endl;}
};
int main()
{lt::shared_ptr<ListNode> spn1(new ListNode);lt::shared_ptr<ListNode> spn2(new ListNode);spn1->_spnext = spn2;//不想參與計數所以我們希望把spn2(shared_ptr<ListNode>)賦值給spn1->_spnext(weak_ptr<ListNode>)//這種操作不計數//所以我們構造了 _spnext為lt::weak_ptr<ListNode>類型,并且構造了weak_ptr<T>& operator=(const shared_ptr<T>& sp)//僅僅把指針拷貝不計數。spn2->_spprev = spn1;return 0;
}
三 內存泄漏
1.什么是內存泄漏,內存泄漏的危害
- ?什么是內存泄漏:內存泄漏指因為疏忽或錯誤造成程序未能釋放已經不再使?的內存,?般是忘記釋放或者發?異常釋放程序未能執?導致的。內存泄漏并不是指內存在物理上的消失,?是應?程序分配某段內存后,因為設計錯誤,失去了對該段內存的控制,因?造成了內存的浪費
- .內存泄漏的危害:普通程序運??會就結束了出現內存泄漏問題也不?,進程正常結束,?表的映射關系解除,物理內存也可以釋放。?期運?的程序出現內存泄漏,影響很?,如操作系統、后臺服務、?時間運?的客?端等等,不斷出現內存泄漏會導致可?內存不斷變少,各種功能響應越來越慢,最終卡死
2.?如何避免內存泄漏?
- 盡量使?智能指針來管理資源?
- 定期使?內存泄漏?具檢測