一、引言
為什么需要智能指針?
在上一篇異常中,關于內存釋放,我們提到過一個問題---當我們申請資源之后,由于異常的執行,代碼可能直接跳過資源的釋放語句到達catch,從而造成內存的泄露,對于這種情況,我們當時的解決方案是在拋出異常后,我們先對異常進行捕獲,將資源釋放,再將異常拋出,但這樣做會使得代碼變得很冗長,那有沒有什么辦法能讓它自動釋放內存資源呢?用智能指針
什么是智能指針?
說到自動釋放資源,是不是有點熟悉,我們在學習創建類對象時,就知道當類對象的生命周期結束后,系統會自動調用它的析構函數,完成資源的釋放,那么我將指針放入這樣一個類對象中,將釋放資源的工作交給析構函數,只要該對象生命周期結束,那么就釋放該資源,如此就不用在關心資源的釋放問題,只要函數棧幀銷毀,即該對象被銷毀,資源就會自動釋放,這就叫智能指針。
智能指針的使用和原理
1.RAII(Resource Acquisition Is Initialization)是一種利用對象生命周期來控制程序資源(如內存、文件句柄、網絡連接、互斥量等等)的簡單技術。在對象構造時獲取資源,接著控制對資源的訪問使之在對象的生命周期內始終保持有效,最后在對象析構的時候釋放資源。借此,我們實際上把管理一份資源的責任托管給了一個對象
- 不需要顯式地釋放資源。
- 采用這種方式,對象所需的資源在其生命期內始終保持有效
2.具有指針的行為,可以解引用,也可以通過->去訪問所指空間中的內容
下面寫一個簡單的智能指針
namespace zxws
{template<class T>class smart_ptr{public:smart_ptr(T* ptr = nullptr):_ptr(ptr){}~smart_ptr(){cout << "delete _ptr" << endl;delete _ptr;_ptr = nullptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
}
但是上面這個智能指針有個嚴重的問題,一旦有兩個對象同時指向同一個資源,那么析構函數就會被調用兩次,即資源要被釋放兩次,會報錯,如下
二、庫中的智能指針
C++官方給出了3個智能指針
1.auto_ptr
auto_ptr:管理權轉移的思想,即一個資源只能有一個指針能對它進行管理,其他的指向這一資源的指針均為空,實現如下
namespace zxws
{template<class T>class auto_ptr{public:auto_ptr(T* ptr = nullptr):_ptr(ptr){}//管理權限的轉移auto_ptr(const auto_ptr& tmp):_ptr(tmp._ptr){tmp._ptr = nullptr;}auto_ptr& operator=(const auto_ptr& tmp){if (this != &tmp)//注意自己給自己賦值的情況不需要處理,否則會出問題{if (_ptr)//釋放當前對象中資源delete _ptr;//管理權限轉移_ptr = tmp._ptr;tmp._ptr = nullptr;}return *this;}~auto_ptr(){if (_ptr){delete _ptr;_ptr = nullptr;}}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
}
?2.unique_ptr
unique_ptr:簡單粗暴的防拷貝,即一個指針只能被初始化一次,且只能用不同的資源初始化
實現如下
namespace zxws
{template<class T>class unique_ptr{public:unique_ptr(T* ptr = nullptr):_ptr(ptr){}//將拷貝構造和賦值重載直接ban掉unique_ptr(const unique_ptr& tmp) = delete;unique_ptr& operator=(const unique_ptr& tmp) = delete;~unique_ptr(){if (_ptr){delete _ptr;_ptr = nullptr;}}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
}
3.shared_ptr
shared_ptr是通過引用計數的方式來實現多個shared_ptr對象之間共享資源
具體原理如下
1. shared_ptr在其內部,給每個資源都維護了著一份計數,用來記錄該份資源被幾個對象共享。2. 在對象被銷毀時(也就是析構函數調用),就說明自己不使用該資源了,對象的引用計數減一。3. 如果引用計數是0,就說明自己是最后一個使用該資源的對象,必須釋放該資源4. 如果不是0,就說明除了自己還有其他對象在使用該份資源,不能釋放該資源,否則其他對象就成野指針了。
namespace zxws
{ template<class T>class shared_ptr{public:shared_ptr(T* ptr = nullptr):_ptr(ptr),_pcount(new int(1)){}shared_ptr(const shared_ptr& tmp):_ptr(tmp._ptr),_pcount(tmp._pcount){(*_pcount)++;}shared_ptr& operator=(const shared_ptr& tmp){//這里注意自己給自己賦值的情況!!!//當引用計數為1時,就會出現將資源釋放后,在賦值的尷尬情況//用this!=&tmp也沒用,可能出現兩個不同對象指向同一塊資源的情況//所以用資源的地址來判斷最準確if (_ptr != tmp._ptr){release();_ptr = tmp._ptr;_pcount = tmp._pcount;(*_pcount)++;}return *this;}void release(){if (--(*_pcount)==0){delete _ptr;delete _pcount;_pcount = nullptr;_ptr = nullptr;}}~shared_ptr(){release();}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* get() const{return _ptr;}int use_count() const{return *_pcount;}private:T* _ptr;int* _pcount;};
}
那么引用計數,為什么要用指針開辟的空間,而不是成員變量或者靜態成員變量?
1、如果是成員變量,那么每一個shared_ptr對象都會有一個_pcount
2、如果是靜態成員變量,那么_pcount將屬于一個類
兩者都不能滿足我們的需求
關于shared_ptr還存在一個循環引用的問題,場景如下
當我們將循環鏈表的兩個結點連接起來的時候,就不會釋放結點空間,但是只要有一條邊沒鏈接就都能釋放,為什么???
而只連接一條邊,這個閉環就不復存在,所以兩個結點都能釋放,那如何解決這種情況?
針對這種情況,C++官方設計出了weak_ptr來和shared_ptr搭配使用,也就是說weak_ptr不增加shared_ptr的引用計數,且不參與資源的釋放
實現如下
namespace zxws
{ template<class T>class weak_ptr{public:weak_ptr():_ptr(nullptr){}weak_ptr(const shared_ptr<T>& tmp):_ptr(tmp.get()){}weak_ptr& operator=(const shared_ptr<T>& tmp){_ptr = tmp.get();return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
}
(上面三個智能指針的模擬實現是被簡化過的,功能不全,但是核心就是這些)
其中auto_ptr這個智能指針基本不用
上面寫的三個智能指針還有一個缺陷,就是釋放資源的delete寫死了,如果我們開的是一個數組,就需要用delete[],否則資源的釋放就會出現問題,所以就需要我們定制化它們的釋放資源的方式,根據前面的知識,我們可以給它傳一個釋放資源的仿函數,如下
template<class T>
struct Destroy {void operator()(T*_ptr){delete[] _ptr;}
};
template<class T, class D>
class shared_ptr
{//....
};
shared_ptr<int, Destroy<int>>p;
但是庫中只寫了一個模板參數
我們如果想實現和庫中一樣的效果,該怎么寫?
既然傳模板參數不行,我們只能傳函數對象了,用function包裝器和lambda表達式實現如下
namespace zxws
{template<class T>class shared_ptr{public:shared_ptr(T* ptr = nullptr):_ptr(ptr),_pcount(new int(1)){}shared_ptr(T* ptr,function<void(T*)> del):_ptr(ptr),_pcount(new int(1)),_del(del){}shared_ptr(const shared_ptr& tmp):_ptr(tmp._ptr),_pcount(tmp._pcount),_del(tmp._del){(*_pcount)++;}shared_ptr& operator=(const shared_ptr& tmp){//這里注意自己給自己賦值的情況!!!//當引用計數為1時,就會出現將資源釋放后,在賦值的尷尬情況//用this!=&tmp也沒用,可能出現兩個不同對象指向同一塊資源的情況//所以用資源的地址來判斷最準確if (_ptr != tmp._ptr){release();_ptr = tmp._ptr;_pcount = tmp._pcount;_del = tmp._del;(*_pcount)++;}return *this;}void release(){if (--(*_pcount)==0){_del(_ptr);delete _pcount;_ptr = nullptr;_pcount = nullptr;}}~shared_ptr(){release();}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* get() const{return _ptr;}int use_count() const{return *_pcount;}private:T* _ptr;int* _pcount;function<void(T*)>_del = [](T* ptr) {delete ptr; };};
}
其他幾個智能指針寫法類似,就不寫了。