C++ 智能指针 :内存泄漏、 RAII、智能指针、auto_ptr、unique_ptr、shared_ptr、weak_ptr、定制删除器deleter
文章目錄
- 內(nèi)存泄漏
- 什么是內(nèi)存泄漏
- 內(nèi)存泄漏的危害:
- 如何避免內(nèi)存泄漏
- RAII
- 智能指針
- auto_ptr
- unique_ptr
- shared_ptr
- 循環(huán)引用問題
- weak_ptr
- 定制刪除器
內(nèi)存泄漏
什么是內(nèi)存泄漏
內(nèi)存泄漏指因為疏忽或錯誤造成程序未能釋放已經(jīng)不再使用的內(nèi)存的情況。內(nèi)存泄漏并不是指內(nèi)存在物理上的消失,而是應(yīng)用程序分配某段內(nèi)存后,因為設(shè)計錯誤,失去了對該段內(nèi)存的控制,因而造成了內(nèi)存的浪費。
內(nèi)存泄漏的危害:
長期運(yùn)行的程序出現(xiàn)內(nèi)存泄漏,影響很大,如操作系統(tǒng)、后臺服務(wù)等等,出現(xiàn)內(nèi)存泄漏會導(dǎo)致響應(yīng)越來越慢,最終卡死。
一般來說,內(nèi)存泄漏大多數(shù)存在于c/c++程序中,因為現(xiàn)在的主流語言如java,python,c#等都具有完善的垃圾回收機(jī)制,所以一般不會存在內(nèi)存泄漏的情況,但也因為這種上述語言存在這種垃圾回收機(jī)制,所以在回收內(nèi)存的時候也會花費寶貴的CPU資源,導(dǎo)致速度有所下降,所以對于c和c++,這是一把雙刃劍,全靠程序員如何掌控。
C/C++程序中一般我們關(guān)心兩種方面的內(nèi)存泄漏:
- 堆內(nèi)存泄漏(Heap leak) 堆內(nèi)存指的是程序執(zhí)行中依據(jù)須要分配通過malloc / calloc / realloc / new等從堆中分配的一塊內(nèi)存,用完后必須通過調(diào)用相應(yīng)的 free或者delete
刪掉。假設(shè)程序的設(shè)計錯誤導(dǎo)致這部分內(nèi)存沒有被釋放,那么以后這部分空間將無法再被使用,就會產(chǎn)生Heap Leak。 - 系統(tǒng)資源泄漏指程序使用系統(tǒng)分配的資源,比方套接字、文件描述符、管道等沒有使用對應(yīng)的函數(shù)釋放掉,導(dǎo)致系統(tǒng)資源的浪費,嚴(yán)重可導(dǎo)致系統(tǒng)效能減少,系統(tǒng)執(zhí)行不穩(wěn)定。
如何避免內(nèi)存泄漏
RAII
RAII ,也稱為“資源獲取就是初始化”,是C++語言的一種管理資源、避免泄漏的慣用法,是利用對象生命周期來控制程序資源(如內(nèi)存、文件句柄、網(wǎng)絡(luò)連接、互斥量等等) 的簡單技術(shù)。C++標(biāo)準(zhǔn)保證任何情況下,已構(gòu)造的對象最終會銷毀,即它的析構(gòu)函數(shù)最終會被調(diào)用。簡單的說,RAII 的做法是使用一個對象,在其構(gòu)造時獲取資源,在對象生命期控制對資源的訪問使之始終保持有效,最后在對象析構(gòu)的時候釋放資源。
這種利用對象的聲明周期來進(jìn)行資源管理的方法有以下好處
- 不需要顯式地釋放資源。
- 采用這種方式,對象所需的資源在其生命期內(nèi)始終保持有效。
智能指針
智能指針就是借助RAII思想來完成的,利用智能指針的生命周期來管理指針指向的資源。
實現(xiàn)的思路很簡單
這樣一個簡單的智能指針雛形就完成了,他已經(jīng)可以完成基本的智能指針的功能,但是還有一個問題沒有解決,也就是當(dāng)多個智能指針管理同一個資源的問題,如果不進(jìn)行處理,就會對同一個資源進(jìn)行多次釋放,導(dǎo)致錯誤。
在C++中,對于這個問題,通過管理權(quán)轉(zhuǎn)移、防拷貝、引用計數(shù)的方法分別實現(xiàn)了auto_ptr、unique_ptr、shared_ptr
auto_ptr
auto_ptr文檔
在C++98中給出了第一個智能指針auto_ptr,他的實現(xiàn)思路就是管理權(quán)的轉(zhuǎn)移,當(dāng)通過賦值或者拷貝使兩個智能指針指向同一對象時,就會將管理權(quán)從老的智能指針轉(zhuǎn)到新的智能指針,此時老的智能指針就會懸空,不再指向資源。
這也是其最大的缺陷,因為被轉(zhuǎn)移的指針懸空,此時訪問再訪問它就會導(dǎo)致訪問空指針報錯,對于不熟悉它特性的人很容易就會導(dǎo)致錯誤,所以在C++11中已經(jīng)不再推薦使用auto_ptr
下面是auto_ptr的模擬實現(xiàn),具體思路都在注釋中
namespace lee {/*c++98 auto_ptr實現(xiàn)思路:管理權(quán)轉(zhuǎn)移缺陷:當(dāng)管理權(quán)轉(zhuǎn)移后會導(dǎo)致被轉(zhuǎn)移的指針懸空,訪問就會報錯,如果不熟悉它的特性就會出問題。*/template<class T>class auto_ptr{public:auto_ptr(T* ptr): _ptr(ptr){}~auto_ptr(){if (_ptr){delete _ptr;_ptr = nullptr;}}//管理權(quán)轉(zhuǎn)移,被轉(zhuǎn)移的指針懸空auto_ptr(auto_ptr<T>& ap): _ptr(ap._ptr){ap._ptr = nullptr;}auto_ptr<T>& operator=(auto_ptr<T>& ap){if (this != &ap){//釋放當(dāng)前資源if (_ptr){delete _ptr;}//管理權(quán)轉(zhuǎn)移_ptr = ap._ptr;ap._ptr = nullptr;}return *this;}T& operator *(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;}; };unique_ptr
unique_ptr文檔
C++11中又引入了unique_ptr,他的實現(xiàn)思路非常簡單粗暴,就是防拷貝,既然多個智能指針指向同一資源會導(dǎo)致問題,那就干脆不讓你這樣做,這樣問題自然也就解決了。C++11中也是非常推薦使用這種智能指針,因為其比起shared_ptr和auto_ptr來說較為穩(wěn)定,不會導(dǎo)致嚴(yán)重的錯誤
但是其也存在缺陷,就是在需要拷貝的場景下他沒有辦法使用。
下面是unique_ptr的模擬實現(xiàn),具體思路都在注釋中
namespace lee {/*c++11 unique_ptr實現(xiàn)思路:防拷貝缺陷:對于需要拷貝的場景,他無法使用*/template<class T>class unique_ptr{public:unique_ptr(T* ptr): _ptr(ptr){}//防拷貝,簡單粗暴unique_ptr(unique_ptr<T>&) = delete;unique_ptr<T>& operator=(unique_ptr<T>&) = delete;~unique_ptr(){if (_ptr){delete _ptr;_ptr = nullptr;}}T& operator *(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;}; };shared_ptr
shared_ptr文檔
為了彌補(bǔ)unique_ptr不能拷貝的缺陷,C++11中還引入了shared_ptr,他的實現(xiàn)思路是引用計數(shù),通過計數(shù)的方式來實現(xiàn)多個智能指針共同管理一個資源。
針了。
相較于unique_ptr,它彌補(bǔ)了不能拷貝的缺陷,但是因為需要保證多線程并發(fā)時的線程安全問題,所以對于計數(shù)操作要進(jìn)行加鎖,所以導(dǎo)致其效率相對來說會低一些,并且還存在循環(huán)引用的問題,所以大部分情況下如果不需要進(jìn)行拷貝,都會使用unique_ptr,需要拷貝時才使用shared_ptr.
下面是shared_ptr的模擬實現(xiàn),具體思路都在注釋中
namespace lee {/*c++11 shared_ptr實現(xiàn)思路:引用計數(shù)缺陷:會出現(xiàn)循環(huán)引用的問題*/template<class T>class shared_ptr{public:shared_ptr(T* ptr): _ptr(ptr), _pcount(new int(1)), _pmtx(new std::mutex){}shared_ptr(shared_ptr<T>& sp): _ptr(sp._ptr), _pcount(sp._pcount), _pmtx(sp._pmtx){//新增指向同一資源的指針,計數(shù)器+1add_ref_count();}shared_ptr<T>& operator=(shared_ptr<T>& sp){//防止自己拷貝自己if (this != &sp){//釋放之前指向的資源release();//此時指向同一資源,計數(shù)器加一_ptr = sp._ptr;_pcount = sp._pcount;_pmtx = sp._pmtx;add_ref_count();}return *this;}~shared_ptr(){release();}T& operator *(){return *_ptr;}T* operator->(){return _ptr;}T* get() const{return _ptr;}size_t use_count() const {return *_pcount;}private://加鎖保證線程安全void add_ref_count(){_pmtx->lock();++(*_pcount);_pmtx->unlock();}void release(){//需要用到一個標(biāo)志位,當(dāng)需要釋放資源時,就在解鎖后把鎖給釋放了bool flag = false;_pmtx->lock();//如果為0,則釋放資源if(--(*_pcount) == 0){if (_ptr){delete _ptr;_ptr = nullptr;}delete _pcount;_pcount = nullptr;flag = true;}_pmtx->unlock();if (flag == true){delete _pmtx;_pmtx = nullptr;}}int* _pcount;std::mutex* _pmtx;T* _ptr;}; };循環(huán)引用問題
例如我們用shared_ptr來管理一個雙向鏈表節(jié)點
template<class T> struct ListNode {T data;struct ListNode<T>* next;struct ListNode<T>* prev; };```cpp int main() {shared_ptr<ListNode> Node1(new ListNode);shared_ptr<ListNode> Node2(new ListNode);Node1->_next = Node2;Node2->_prev = Node1;return 0; }在c++11中,引入了weak_ptr來解決這個問題。
weak_ptr
weak_ptr文檔
weak_ptr并不是智能指針,其沒有RAII資源管理機(jī)制,他是專門用來解決shared_ptr的循環(huán)引用問題
它的實現(xiàn)思路就是對于會導(dǎo)致循環(huán)引用的地方,如上面的**Node1->_next = Node2, Node2->_prev = Node1;**這兩條語句,直接進(jìn)行賦值,不再進(jìn)行計數(shù)。
所以只需要將ListNode結(jié)構(gòu)體中的指針用weak_ptr管理即可。
template<class T> struct ListNode {T data;struct weak_ptr<ListNode<T>> next;struct weak_ptr<ListNode<T>> prev; };下面是weak_ptr的模擬實現(xiàn),具體思路都在注釋中
namespace lee {/*用于解決shared_ptr的循環(huán)引用問題在循環(huán)引用時直接賦值,不再計數(shù)*/template<class T>class weak_ptr{public:weak_ptr() = default;weak_ptr(const shared_ptr<T>& sp): _ptr(sp){}weak_ptr<T>& operator=(const shared_ptr<T>& sp){//直接賦值,不進(jìn)行計數(shù)_ptr = sp.get();return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;}; };定制刪除器
從上面的實現(xiàn)可以看到,智能指針的刪除方式都是默認(rèn)的調(diào)用一個delete(庫中也是),但是如果我們管理的是一個文件描述符、一個數(shù)組、又或者是一個malloc出來的資源,此時就會因為無法釋放資源或者資源釋放不完全導(dǎo)致程序崩潰。
因為根據(jù)類型的不同,銷毀資源的方式有很多種,所以要向正確的釋放資源,就需要通過仿函數(shù)的方式,來為智能指針傳遞一個對應(yīng)資源的釋放方法,這種方法也被稱為刪除器,在shared_ptr和unique_ptr中也為我們提供了對應(yīng)的接口。
例如以下幾種常見的刪除器
//默認(rèn)delete template<class T> struct Del {void operator()(T* p){delete p;} };template<class T> struct DelArray {void operator()(T* p){delete[] p;} };struct Free {void operator()(void* p){free(p);} };struct Fclose {void operator()(FILE* p){fclose(p);} };使用時根據(jù)需求傳遞對應(yīng)刪除器即可
int main() {std::shared_ptr<A> sp1(new A);std::shared_ptr<A> sp2(new A[10], DelArray<A>());std::shared_ptr<A> sp3((A*)malloc(sizeof(A)), Free());std::shared_ptr<FILE> sp4(fopen("test.txt", "w"), Fclose());return 0; }總結(jié)
以上是生活随笔為你收集整理的C++ 智能指针 :内存泄漏、 RAII、智能指针、auto_ptr、unique_ptr、shared_ptr、weak_ptr、定制删除器deleter的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C++ 特殊类设计:只能在堆、栈上创建的
- 下一篇: C++ 类型转换 :C语言的类型转换、C