該文章代碼均在gitee中開源
C++智能指針hpp
https://gitee.com/Ehundred/cpp-knowledge-points/tree/master/%E6%99%BA%E8%83%BD%E6%8C%87%E9%92%88???????
智能指針
傳統指針的問題
在C++自定義類型中,我們為了避免內存泄漏,會采用析構函數的方法釋放空間。但是對于一些情況,系統往往并沒有那么聰明,比如C語言里,我們malloc一塊空間;C++里,我們new一塊空間,系統不會對這些空間進行特別檢查, 最后便造成了內存泄漏
void func()
{int* a = new int(1);//...一通操作if (true){return;}//如果程序在中途就終止了,那這段delete便不會執行,內存泄漏了delete a;
}
有的時候并不是我們不想釋放或者忘了釋放,而是經常會發生函數異常終止或者中途結束,導致某一塊空間的釋放被跳過了
并且,在一些較大的程序中,某一個類似的函數會調用成千上萬次, 每一次去泄漏一點點內存,極少成多,漸漸內存便開始以肉眼無法看見的速度漸漸泄漏。
此時,C++便想出了一個C++獨有的解決方案:智能指針
為什么是C++獨有?因為只有C++才把這種史甩給程序員去自己解決
智能指針的原理
我們在文章剛開始便解釋到,對于自定義類型,C++會通過析構函數的方式將其釋放,但是new出來的空間并沒有析構函數。那為什么我們就不能強行給他一個析構函數呢??
而這個想法的實現方法其實也很簡單:只需要給一個類,讓這個類來裝這一個指針便可以了
template<class T>
class smart_ptr
{
public:smart_ptr(T* ptr=nullptr):_ptr(ptr){}~smart_ptr(){delete _ptr;}
private:T* _ptr;
};
我們在new一塊空間之后,把這個指針裝在smart_ptr這塊盒子里,當函數結束時,smart_ptr會自動調用析構函數銷毀,從而讓這個野指針實現自動銷毀的行為,這便是智能指針
void func()
{int* a = new int(1);smart_ptr<int> spa(a);//無論函數從哪里終止,只要函數被銷毀,spa就會被銷毀,從而釋放a
}
同時,為了方便,我們完全可以改造一下只能指針,將智能指針改造成智能指針來使用
//改造后的智能指針,與普通指針的使用方法便一致了
template<class T>
class smart_ptr
{
public:smart_ptr(T* ptr=nullptr):_ptr(ptr){}~smart_ptr(){delete _ptr;}T& operator*(){return *ptr;}T* operator->(){return &_ptr;}
private:T* _ptr;
};
改造之后,不僅可以實現指針的所有功能,而且被指針指向的空間也可以自動釋放,相當于指針plus
同時,我們在初始化時,不需要引入新變量了
void func()
{/*int* a = new int(1);smart_ptr<int> spa(a);*///直接簡化成smart_ptr<int> spa(new int(1));}
智能指針的問題
智能指針雖然看著好用,但是還是有著很多大問題。其中最大的便是賦值問題,如果我們想用一個智能指針去賦值另一個智能指針,那我們會發現一個嚴重的問題:?
那咋整?
而為了解決這一問題,C++標準庫給出了三種解決方案,這也便是C++智能指針的發展歷史。
std中的智能指針
其實智能指針的發展史很早。早在C++98中,std庫中便有了一個智能指針名為auto_ptr,但是一個字便可以概括:
不僅被開發者們詬病,而且很多公司還明確要求:不許使用庫中的智能指針。而這也導致了一個結果:不同的庫智能指針千奇百怪,程序和程序間的兼容依托稀爛。
而后C++11,對備受詬病的智能指針進行了改造,產生了兩種應用場景的智能指針:unique_ptr和shared_ptr,至此,智能指針的發展便已經完美畫上了句號,而我們如今最常用也最需要去學習的便是在C++11新加入的兩種智能指針
auto_ptr
C++98在剛開始接觸智能指針這一問題的時候,可能是項目經理開始催命了,便展現出了及其離譜的操作:權限轉移。這個操作雖然理論上確實解決了兩次delete的問題,但是就相當于餓到沒辦法才去赤石,沒有任何實際使用的價值
什么是權限轉移?就是在a賦值b的時候,將a裝著的指針清空,而原本的指針到了b身上,就相當于把a變成了b,然后a這一變量銷毀掉。
下面只展示賦值的情況代碼
template<class T>
class auto_ptr
{
public:auto_ptr(T* ptr=nullptr):_ptr(ptr){}auto_ptr(const auto_ptr& ptr){if (ptr != *this)swap(*this, ptr);}auto_ptr& operator=(const auto_ptr& ptr){if(ptr!=*this)swap(*this, ptr);return *this;}~auto_ptr(){delete _ptr;}
private:T* _ptr;
};
你說他賦值了嗎?好像賦值了,但是又好像沒有賦值
我們想要對智能指針進行賦值,為的就是產生兩份智能指針,但是你這一通轉移,最后還是只給了我一份智能指針,而且還到了最后連我自己都不知道轉移到哪去了。解決問題了嗎?好像解決了,但是實際上讓問題變得更麻煩了,這也是auto_ptr一直被詬病的原因——為了修一個小bug,引入了一個更大的bug
unique_ptr
?C++11里,為了解決掉auto_ptr亂賦值這一毛病,干脆采用了一個簡單粗暴的方法——既然賦值會有bug,那就都別賦值了
unique_ptr在最初的智能指針上加了一個新特性:私有化operator=和賦值構造函數,讓unique_ptr無法被賦值
template<class T>
class unique_ptr
{
public:unique_ptr(T* ptr=nullptr):_ptr(ptr){}~unique_ptr(){delete _ptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
private:unique_ptr& operator=(const unique_ptr& ptr) = default;unique_ptr(const unique_ptr& ptr) = default;T* _ptr;
};
這樣,和他的名字一樣,unique_ptr就是獨一無二的智能指針,只能產生一次,無法多次使用。
雖然這種方法聽起來也是拖史,但是我們不可否認,unique_ptr解決了賦值的問題,而且也沒有產生新的bug
shared_ptr
而從shared_ptr開始,才算是直視多次delete這一問題。既然不斷去賦值會導致delete很多次,那我就記錄一下指向某塊空間的智能指針的個數,當最后一個智能指針也被銷毀,我再去delete,這樣就不會產生delete多次的問題了。而這實際也是引用計數的思想。?
不過這種想法雖然看起來簡單,真正實現起來卻還是有著一些障礙:
- 引用計數怎么實現?
- 如果某一個智能指針已經指向了一塊空間,之后再對其進行賦值,那原來被指向的空間怎么辦?
- 自己賦值自己又是什么情況?
我們來一個一個看
引用計數怎么實現?
最直觀直接的方法便是,在類中加入一個新變量count來記錄指向這塊空間的數量,如果有一個新的智能指針指向了這塊空間,就將count++,然后將++后的count賦值給新的智能指針。雖然想法很好,但是也有著一個巨大的問題——count無法同步
比如count==3,表示有三個智能指針a,b,c指向了這塊空間,我們再將c賦值給d,然后count++變成4,?c和d中的count也變成了4,那a和b怎么辦?a和b里的count還是3
此刻便可以想出一個很簡單的解決方案——在類中存放一個count的地址,這樣一個count改變,所有的count也便隨之改變了。
如何賦值給已存放地址的智能指針
在之前,我們都只考慮了用智能指針進行初始化。但是其實賦值還有一種情況——改變智能指針的值。這種情況,如果我們直接修改,顯然會導致原先的內存泄漏,所以我們在賦值的時候,還需要將原先的count--,不然會導致多出一次count 的問題。
如何自己賦值給自己
這是在所有類型的賦值中,我們都要考慮的情況。一般,如果自己賦值給自己,我們直接跳過就可以了,否則最好的情況是效率的損耗,而最壞的情況則會導致野指針。
舉個例子,如果有一個智能指針sp,其中的count只有1,我們自己賦值給自己,上述情況是count--,最終count==0,sp指向的空間被銷毀。然后再去賦值,指針指向了一塊被銷毀的空間,count++,就導致了指向野指針的問題。
所以,自己賦值給自己必須要進行判斷并跳過,否則或大或小都會產生一些意料之外的問題。
而解決了上述的問題,shared_ptr也算是被暴力解決了
template<class T>
class share_ptr
{
public:share_ptr(T* ptr = nullptr):_ptr(ptr){_count = new int(1);}share_ptr(const share_ptr& ptr){_ptr = ptr._ptr;_count = ptr._count;++(*_count);}share_ptr& operator=(const share_ptr& ptr){if (_ptr != ptr._ptr){delete_ptr();_ptr = ptr._ptr;_count=ptr._count;++(*_count);}return *this;}~share_ptr(){delete_ptr();}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
private:void delete_ptr(){--(*_count);if (*_count == 0){delete _ptr;delete _count;}}T* _ptr;int* _count;
};
循環引用
shared_ptr雖然強大,但是shared_ptr也會有著內存泄漏的問題
我們來看雙向鏈表
struct ListNode
{ListNode():_pre(nullptr),_next(nullptr){}share_ptr<ListNode> _pre;share_ptr<ListNode> _next;
};void func()
{share_ptr<ListNode> head(new ListNode);share_ptr<ListNode> tail(new ListNode);head->_next = tail;tail->_pre = head;
}int main()
{func();
}
一個很經典的雙向鏈表問題,但是最終卻暗藏玄機。我們來看func函數內部
void func()
{share_ptr<ListNode> head(new ListNode);share_ptr<ListNode> tail(new ListNode);head->_next = tail;tail->_pre = head;//賦值之后,很正常的head和tail指向的空間count都為2//但是到了最后,調用析構函數,head的count--,tail的count--,兩個count都為1//最后head和tail都沒有被清理掉,內存泄漏了
}
而導致這個問題的本質原因是什么?是智能指針指向的對象,其內部還有一個無法被自動釋放的指針。?
而為了避免這個問題,C++采用了一個新的指針——weak_ptr。
weak_ptr顧名思義,是弱指針,其特性和shared_ptr基本相同,只不過在賦值的時候,count并不會增加
?也就是說,在類內部的智能指針,我們定義成weak_ptr,這樣就可以避免count異常的問題
unique_ptr和shared_ptr
光看解說量,我們都會發現,unique_ptr已經被shared_ptr完爆了。雖然如此,我們仍還是讓兩個不同的智能指針都進入了std標準庫,因為shared_ptr雖然在功能上遠遠戰勝了unique_ptr,但是產生的性能代價仍是非常大的。unique_ptr簡單粗暴,空間開銷少,性能極高,所以在不同的場合還是會在兩種智能指針之間取舍。
而auto_ptr
RAII
?看看得了,經常看我文章的都知道,我最不喜歡甩概念。
簡單說,RAII就是將空間的釋放自動化,我們不需要特意去delete,也不需要檢查內存是否泄漏,我們只需要把地址拋給一個對象,讓這個對象幫我們干這些事情就可以了
其實在很多語言中,都有一個垃圾回收機制,定期去回收掉被泄露的內存,而C++將這個責任甩給了程序員。但是,這并不是C++沒能力弄或者懶得弄,而是為了極致的性能,不得不去舍棄掉這個垃圾回收機制。往后無論C++如何發展,一些其他語言便捷的地方如果會導致性能的損耗,C++都不會去嘗試利用他們,而是讓我們程序員去想更好的解決方案,沒辦法,誰叫我們是站在語言歧視鏈頂端的程序員呢。