目錄
- 1. RAII與智能指針
- 2. C++庫中的智能指針
- 2.1 智能指針auto_ptr
- 2.2 智能指針unique_ptr
- 2.3 智能指針shared_ptr
- 3. shared_ptr的循環引用
- 4. 智能指針的定值刪除器
1. RAII與智能指針
??上一篇文章學習了異常相關的知識,其中遺留了一個異常安全相關的問題。那就是異常的拋出會打亂執行流,可能使得動態開辟的資源無法被正常釋放導致內存泄漏。而之前我們是通過異常再拋出的方式去解決這一問題的,可是,此種方式會使得代碼的可讀性極差。下面就來學習一種更好的也是現今一般會使用的解決異常安全的方式,智能指針。
??在正式學習智能指針之前,先來了解一個概念RAII(Resource Acquisition Is Initializatio)
,RAII是C++中的一種編程設計思路,直譯而來是資源獲得后立即初始化。而實際上是指將資源交給一個對象去幫忙管理,利用對象的生命周期來管理資源,智能指針就是RAII思想設計而得一個產物。另外的應用場景,還有,打開文件與關閉文件,打開文件的返回值一般都是指針。
智能指針的特性與功能:
- 1. 智能指針會將資源管理起來,利用本身對象的生命周期在析構時釋放資源,防止了資源的內存泄漏
- 2. 智能指針支持像指針一樣的操作,諸如,
解引用*
,箭頭->
- 3. 智能指針的拷貝不會進行深拷貝與迭代器類似,雖然其能對資源進行管理與操作,但與數據結構的存儲不一樣,數據結構中所存儲的資源是自己的,而智能指針的資源是代為持有,多個智能指針是共享一份資源的(一般為引用計數)
//智能指針管理資源的邏輯與支持指針操作
template<class T>
class SmartPtr
{
public://管理資源SmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){delete _ptr;}//像指針一樣的操作:*, ->T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
private:T* _ptr;
};
2. C++庫中的智能指針
2.1 智能指針auto_ptr
- 歷史上的一個智能指針auto_ptr:
??在C++98標準時,就已經有了C++中歷史上的第一個智能指針auto_ptr
。其除了具備智能指針管理資源與指針操作的功能外,對智能指針的拷貝也做了相關的實現,其設計思路為拷貝后,將指針轉移,將原有指針懸空。此種方法多有漏洞,大部分公司都禁止其的使用,可以說是C++語言歷史上的一個語法污點。可能是受此影響,C++98標準后,一些C++標準委員會庫工作組成員合理制作了一個名為boost的準標準庫,其會將一個些新的語法點進行先探索與嘗試實現,C++標準庫后續的很多語法都是從boost庫中吸收而來。
- auto_ptr的拷貝后指針懸空與簡單實現模擬:
//模擬實現
template<class T>
class auto_ptr
{
public:auto_ptr(T* ptr):_ptr(ptr){}//指針懸空auto_ptr(const auto_ptr<T>& p){_ptr = p._ptr;p._ptr = nullptr;}~auto_ptr(){delete _ptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;
};
2.2 智能指針unique_ptr
??因為C++98中auto_ptr的缺陷,boost庫中又嘗試創建實現了新的智能指針,如shared_ptr(配合new)/shared_array(配合new[])
共享指針、scoped_ptr/scoped_arrary
守衛指針、weak_ptr
弱指針、instrusive_ptr。其中,shared_ptr
,scoped_ptr
與weak_ptr
都后續被納入標準庫。后續出現的這些指針都是旨在采用不同的方式去處理智能指針拷貝的問題。boost庫中的scoped_ptr
就是現在C++標準庫中的unique_ptr
。unique_ptr
解決拷貝問題的方式是,直接禁止本身進行拷貝操作,其原理為禁止拷貝構造與賦值重載的生成,C++11前的實現方法與C++11后的實現方法不同。
??所有智能指針都包含在<memory>頭文件中,boost庫中將boost作為命名空間。
//unique_ptr簡單模擬實現
template<class T>
class unique_ptr
{
public:unique_ptr(T* ptr):_ptr(ptr){}~unique_ptr(){delete _ptr;}unique_ptr(const unique_ptr<T>& p) = delete;//拷貝構造unique_ptr<T>& operator=(const unique_ptr<T>& p) = delete;//賦值T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;
};
2.3 智能指針shared_ptr
??與unique_ptr
不同shared_ptr
支持拷貝操作,其底層是以引用計數的方式來支持拷貝構造與賦值操作的實現的。但選取怎樣一個變量當作為引用計數的載體是一個值得思考的問題,智能指針的引用計數是指當前有多少個智能指針指向同一份資源。
- 選取普通的成員變量顯然是不可行的,其無法保證拷貝時引用計數的共享性。
- 那么,屬于整個類的靜態成員變量呢?初步考量這好像是一個可行的方案,但這個方法其實還是有漏洞,當同一類型的
shared_ptr
指向不同的資源時,靜態成員變量就無法解決了。
- C++標準庫中給出的方法是,動態開辟new出一個變量,讓其存儲引用計數,這樣指向不同資源的
shared_ptr
就不會互相印象,當引用計數歸零時,對資源進行釋放。
shared_ptr的簡單模擬實現:
template<class T>
class shared_ptr
{
public:shared_ptr(T* ptr = nullptr)//構造參數賦予缺省值,充當默認構造:_ptr(ptr){_pcount = new int(1);}void release(){if (--(*_pcount) == 0)//引用計數為0時,釋放資源{delete _ptr;delete _pcount;}}~shared_ptr(){release();}shared_ptr(const shared_ptr<T>& p){_ptr = p._ptr;_pcount = p._pcount;(*_pcount)++;}shared_ptr<T>& operator=(const shared_ptr<T>& p){//不能自己給自己賦值//if(*this != p)//指向同一份資源的智能指針賦值,特殊處理if (_ptr != p._ptr){release();_ptr = p._ptr;_pcount = p._pcount;(*_pcount)++;}return *this;}int use_count(){return *_pcount;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* get() const//指針指向的對象不能被改變{return _ptr;}private:T* _ptr;int* _pcount;
};
3. shared_ptr的循環引用
??shared_ptr
在多種智能指針中,綜合而論已經是最優秀的智能指針了,可時它真的就完全不會造成內存泄漏的問題了嗎,我們來看下面這個場景。
??自定義一個雙向鏈表的節點,鏈表的每個節點都是動態開辟而出的,這里我們采用智能指針的方式去定義與管理。但當程序執行結束時,兩個鏈表節點的資源并沒有被釋放。
??當程序運行結束,node1、node2兩個shared_ptr智能指針銷毀之后。還有兩個指向節點資源的智能指針_next
與_prev
。這就使得指向節點資源的智能指針其引用計數沒有歸0,所指向的資源也就無法釋放。被智能指針管理的資源想要被釋放,其引用計數就需要歸0,想要引用計數歸0,那么,所有指向該資源的智能指針都必須要銷毀。但在這一過程中,會出現下圖的邏輯閉環,導致節點1,節點2互相指向無法釋放的邏輯閉環,造成循環引用,內存泄漏。
循環引用的解決方法weak_ptr:
??為了解決上述shared_ptr的循環引用導致內存泄漏的問題,C++庫中設計了weak_ptr
這樣一個智能指針,其的種種普通特性都與shared_ptr智能指針相同,但特殊的是,使用它指向shared_ptr管理的資源,shared_ptr的引用計數不增加。weak_ptr只做鏈接功能,因為weak_ptr與shared_ptr不是一個類型的智能指針,weak_ptr想要從shared_ptr獲取資源只有兩個方式,一是被聲明為友元,二是為shared_ptr添加get接口,get接口必須使用const修飾this指針。
template<class T>
class weak_ptr
{
public:weak_ptr():_ptr(nullptr){}~weak_ptr(){}weak_ptr(const shared_ptr<T>& p){_ptr = p.get();}weak_ptr<T>& operator=(const shared_ptr<T>& p){_ptr = p.get();return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;
};
??在上面weak_ptr智能指針的模擬實現中,沒有為其添加引用計數。但在C++標準庫中weak_ptr其實也是有引用計數的。只不過它的引用計數不參與空間的釋放,weak_ptr的引用計數更像是一種監視,其的存在是為了防止weak_ptr去釋放引用計數為0已經被釋放過的空間。
4. 智能指針的定值刪除器
??上面所有關于智能指針的學習,對于shared_ptr智能指針地模擬實現,都是基于使用智能指針對單個動態開辟new出的對象做管理的情況。但當需要使用智能指針管理多個對象,或是管理非動態開辟的資源(文件指針)時,就無法去正確地釋放資源了。
??boost庫中對于管理多個對象的資源創造了專門與之相對應的shared_array與scoped_array。但在C++標準庫中,卻沒有采用這種方式,而是設計了一種定值刪除器的方法來控制對資源的刪除方式,使用方式如下:
//方法1:C++中特化模板,專門用于釋放new[]的資源
shared_ptr<ListNode[]> p1(new ListNode[10]);//方法2:定值刪除器,構造時傳入以仿函數對象形式傳入對應的資源釋放方法
template<class T>
struct DeleteArray
{void operator()(T* ptr){delete[] ptr;}
};//仿函數、lambda表達式、函數指針皆可
shared_ptr<ListNode> p2(new ListNode[10], DeleteArray<ListNode>());
C++標準庫中的調用接口:
- shared_ptr中定值刪除器的模擬實現
namespace zyc
{template<class T>class shared_ptr{public://定值刪除器function<void(T*)> _del = [](T* ptr) { delete ptr; };//delete普通new對象的缺省處理方法template<class D>shared_ptr(T* ptr, D del):_del(del){}shared_ptr(T* ptr = nullptr):_ptr(ptr){_pcount = new int(1);}void release(){if (--(*_pcount) == 0){_del(_ptr);//控制釋放方式delete _pcount;}}~shared_ptr(){release();}shared_ptr(const shared_ptr<T>& p){_ptr = p._ptr;_pcount = p._pcount;(*_pcount)++;}shared_ptr<T>& operator=(const shared_ptr<T>& p){if (_ptr != p._ptr){release();_ptr = p._ptr;_pcount = p._pcount;(*_pcount)++;}return *this;}private:T* _ptr;int* _pcount;};
}
??含有定值刪除器的模板構造中,其中定值包裝器的類型是獨屬于此構造函數的模板參數類型,而不是整個類。因此,想要通過成員變量的方式讓析構函數拿到這一仿函數對象,就需要采用定義包裝器對象的方式來實現(釋放資源的仿函數其參數與返回值類型是確定的)。
- 內存泄漏與資源泄漏:
??內存泄漏是指動態開辟(malloc/realloc/new)出的空間已經不再使用,可是因為疏忽(忘記free/delete)/錯誤(循環引用)的原因沒有去釋放。而資源泄漏是指申請的資源(文件描述符,管道等)在使用完成忘記釋放,資源描述符是有限的。在程序長期運行的環境下,內存泄漏可能會導致程序直接崩潰,而資源泄漏可能就會導致出現無法再打開文件等問題。
??一般出現上述問題,在不同環境下都有內存泄漏的檢測工具可以幫助我們發現問題,但再好的檢測工具都不如我們在編寫代碼時多加注意,提高代碼的規范性。每到必要時就去使用智能指針管理相關資源,如此就能預防避免幾乎所有的資源泄漏問題。再好的事后檢測手段,都不如事前做好預防。
??使用cout打印char類型指針變量時,需要進行(void)強制類型轉換,因為cout會默認char*類型為打印字符串。