C/C++智能指针
目錄
1.1RAII(資源獲取幾初始化)
?1.2auto_ptr
1.3unique_ptr
1.5weak_ptr
?我們在在動態開辟空間的時候,malloc出來的空間如果沒有進行釋放,那么回傳在內存泄漏問題。或者在malloc與free之間如果存在拋異常,那么還是有內存泄漏安全。因此我們在這里引入了智能指針來對資源進行管理。(內存泄漏)
1.1RAII(資源獲取及初始化)
RAII(Resource Acquisition Is Initialization)是一種利用對象生命周期來控制程序資源(如內存、文件句柄、網絡連接、互斥量等等)的簡單技術。 在對象構造時獲取資源,接著控制對資源的訪問使之在對象的生命周期內始終保持有效,最后在對象析構的時候釋放資源。借此,我們實際上把管理一份資源的責任托管給了一個對象。這種做法的好處:
- 不需要顯示的釋放資源。
- 采用這種方式,對象所需要的資源在其生命期內始終保持有效。
總結:RAII就是一種管理資源自動釋放的一種機制,初步看來,他通過類將資源包裝起來。在進行資源初始化時,巧妙地利用編譯器會自動調用構造函數預計析構函數的特性,來完成對資源的自動釋放。在構造方法中,將資源放入,讓對象進行釋放,在析構方法中,將資源釋放掉。
#include<iostream> using namespace std; //智能指針的原理:RAII+具有指針類似的行為 //我們在這里自己進行封裝 template<class T> class Smartptr{ public:Smartptr(T* p = nullptr) :ptr(p){}~Smartptr(){if (ptr){//此時指針如果不為空且具有釋放的權利的時候,則將其釋放,且將owner重新職位falsedelete ptr;ptr = nullptr;}}//在使用指針是我們有*與->的使用,因此在這里要對齊進行運行算符重載//重載*T& operator*(){return *ptr;}//他只能在指針指向的是對象或者是結構體的時候來使用T& operator->(){return ptr;}//某些情況下使用原生態指針T* get(){return ptr;} private:T* ptr;//采用類進行指針管理 }; int main(){Smartptr<int> st1(new int);Smartptr<int> at2(st1);//此時調用拷貝構造函數,但是這個類里面沒有,因此只能使用默認的拷貝構造//因此是淺拷貝return 0; }根據上面代碼,我們先簡單的模擬了一下智能指針發現了存在這一個致命的問題,如果當一個對象對另一個對象進行拷貝構造時,由于沒有定義拷貝構造函數,那么就會使用到默認的拷貝構造函數,產生淺拷貝問題。又因為所有的智能指針都是一樣的,那如何解決淺拷貝問題呢?我們在前面學習string類時,對淺拷貝的解決方式時使用深拷貝,但是在這里我們不能使用深拷貝,在string類中,因為其內部要存字符串,需要申請空間,而string類中的空間是自己申請與維護的,而智能指針的資源是用戶提供的,如下圖:
?智能指針不能申請資源只能提用戶來管理資源,因此此處不能使用深拷貝的方式來解決問題。
?1.2auto_ptr
?資源完全轉移
我們參考C++98版本的庫中就提供了auto_ptr的智能指針是如何解決淺拷貝問題的。
namespace bite{template<class T>class auto_ptr{public:// RAII : 保證資源可以自動釋放auto_ptr(T* ptr = nullptr): _ptr(ptr){}~auto_ptr(){if (_ptr){delete _ptr;_ptr = nullptr;}}// 解決淺拷貝方式:資源轉移// auto_ptr<int> ap2(ap1)auto_ptr(auto_ptr<T>& ap): _ptr(ap._ptr){ap._ptr = nullptr;}// ap1 = ap2;auto_ptr<T>& operator=(auto_ptr<T>& ap){if (this != &ap){// 此處需要將ap中的資源轉移給this// 但是不能直接轉移,因為this可能已經管理資源了,否則就會造成資源泄漏if (_ptr){delete _ptr;}// ap就可以將其資源轉移給this_ptr = ap._ptr;ap._ptr = nullptr; // 讓ap與之前管理的資源斷開聯系,因為ap中的資源已經轉移給this了}return *this;}// 對象具有指針類似的行為T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* Get(){return _ptr;}private:T* _ptr;}; } int main(){auto_ptr<int> st1(new int);auto_ptr<int> at2(st1);return 0; }我們觀察上述代碼,雖然他解決了淺拷貝問題,但是他又引入了新的問題,。當對象拷貝或者賦值后,前面的對象就懸空了。它的缺陷就是當我們想訪問或者修改st1對象的時候,代碼會崩潰。
資源管理權限轉移
?為了解決上面的問題有使用了轉移資源管理權限的思想。
#include<iostream> using namespace std; //智能指針的原理:RAII+具有指針類似的行為 //我們在這里自己進行封裝 template<class T> class autoptr{ public:autoptr(T* p = nullptr) :ptr(p), owner(true){}~autoptr(){if (ptr && owner){//此時指針如果不為空且具有釋放的權利的時候,則將其釋放,且將owner重新職位falsedelete ptr;owner = false;}}//在使用指針是我們有*與->的使用,因此在這里要對齊進行運行算符重載//重載*T& operator*(){return *ptr;}//他只能在指針指向的是對象或者是結構體的時候來使用T& operator->(){return ptr;}//某些情況下使用原生態指針T* get(){return ptr;}//因此在這里解決淺拷貝問題//資源管理權限的轉移autoptr(autoptr<T>& p) :ptr(p.ptr), owner(p.owner){p.owner = false;}T& operator=(autoptr<T>& p){//賦值運算符的重載if (this == p){//首先判斷是否是自己給自己復制return p;}if (ptr && owner){//如果此時ptr不為空且具有權限,那么此時就將現在的資源釋放掉,順便拿到p的權限delete ptr;ptr = p.ptr;owner = p.owner;p.owner = false;}}//某些情況下使用原生態指針 private:T* ptr;//采用類進行指針管理 }; int main(){autoptr<int> st1(new int);autoptr<int> at2(st1);//此時調用拷貝構造函數,但是這個類里面沒有,因此只能使用默認的拷貝構造函數//因此是淺拷貝return 0; }?如上面代碼,當發生拷貝構造或者賦值時,將被拷貝對象中資源轉移給新對象,然后讓被拷貝對象與資源斷開聯系,這樣就解決了一塊空間被多個對象使用而造成程序崩潰問題。但是在這里存在著致命缺陷。再對st1進行拷貝后將其的指針賦值為空,導致了st1對象懸空,通過st1對象訪問資源就會出現問題,會造成野指針,使代碼崩潰。因此要在這里說明什么情況下對不要使用auto_ptr。
1.3unique_ptr
?上面的問題都是因為發生了拷貝構造然后造成的,因此unique_ptr在這里采用的方式是禁止拷貝。也就是說,一份資源只能被一個對象來進行管理,對象之見不能共享資源(資源獨占)。解決淺拷貝方式--資源獨占,防止拷貝,在這里有兩種方案,第一種:C++98中的方案,將拷貝構造函數以及賦值運算符重載方法只進行聲明不進行定義,并且將其權限給成私有的,這樣就防止其被拷貝。第二種:C++11種的方案:可以讓編譯器不生成默認的拷貝構造以及賦值運算符delete,delete關鍵字它的擴展功能就是從堆上進行釋放資源,用其修飾默認的構造函數,表明編譯器不會生成了。
#include<iostream> using namespace std; //智能指針的原理:RAII+具有指針類似的行為 //我們在這里自己進行封裝 template<calss T> class DF_new{ public:void operatr()(T*& ptr){if(ptr){delete ptr;ptr = nullptr;}} }; template<calss T> class DF_free{ public:void operatr()(T*& ptr){if(ptr){free(ptr);ptr = nullptr;}} }; //關閉文件指針 template<calss T> class DF_close{ public:void operatr()(FILE*& ptr){if(ptr){fclose(ptr);ptr = nullptr;}} }; //T:資源中所放的數據的類型 //DF:資源的釋放方式 template<class T,class DF = DF_new<T>>//DF釋放的方式 class uniqueptr{ public:uniqueptr(T* p = nullptr) :ptr(p){}~uniqueptr(){if (ptr){//對于ptr管理的資源,有可能是從堆上申請的內存空間,文件指針,malloc空間...//因此他在釋放的是否是要進行考慮的,是不同的,解決的方式就是對這個類再加上一個模板參數列表即可ptr = nullptr;}}//在使用指針是我們有*與->的使用,因此在這里要對齊進行運行算符重載//重載*T& operator*(){return *ptr;}//他只能在指針指向的是對象或者是結構體的時候來使用T& operator->(){return ptr;}//某些情況下使用原生態指針T* get(){return ptr;}//解決淺拷貝方式--資源獨占,防止拷貝,在這里有兩種方案//第一種:C++98中的方案: private:uniqueptr(const uniqueptr<T,DF>&);uniqueptr<T&>operator=(const uniqueptr<T,DF>&);//第二種:C++11中的方案:可以讓編譯器不生成默認的拷貝構造以及賦值運算符--deleteuniqueptr(const uniqueptr<T,DF>&) = delete;//表明編譯器不會生成默認的賦值運算符重載uniqueptr<T,DF>& operator=(const uniqueptr<T,DF>&) = delete; private:T* ptr;//采用類進行指針管理 };在這里說明一下為什么在C++98中對其拷貝構造函數與賦值運算符重載只進行定義,不聲明不定義,且將其權限給成私有的。如果沒有將其設置為私有的,那么用戶就會在外部對其方法進行定義。
unique_ptr指針適用于資源被一個對象管理并且不會被共享。他的缺陷就是多個對象中資源無法進行共享,因此使用到了shared_ptr指針。
1.4shared_ptr
共享指針,對個對象之間可以共享資源。在這里采用引用計數的方式來進行淺拷貝的。引用計數實際上就是一個整形空間,記錄使用資源的對象的個數,在釋放之前,讓最后一個使用資源的的對象來進行釋放。
#include<iostream> using namespace std; //智能指針的原理:RAII+具有指針類似的行為 //我們在這里自己進行封裝 template<class T,class DF = DF_new<T>> class sharedptr{ public:sharedptr(T* p = nullptr) :ptr(p),p_count(nullptr){if(ptr){//此時只有當前建好的一個對象在使用該份資源p_count = new int(1);}}~sharedptr(){if (ptr && 0 == --(*count)){DF df;df(ptr);delete p_count;p_count = nullptr;}}//在使用指針是我們有*與->的使用,因此在這里要對齊進行運行算符重載//重載*T& operator*(){return *ptr;}//他只能在指針指向的是對象或者是結構體的時候來使用T& operator->(){return ptr;}//某些情況下使用原生態指針T* get(){return ptr;}//用戶可能需要獲取引用計數int use_count()const{return *p_count;}//解決淺拷貝方式,引用計數sharedptr(const sharedptr<T,DF>& sp):ptr(sp.ptr),p_count(sp.p_count){if(ptr){++(*p_count);}}sharedptr<T,DF>& operator=(const sharedptr<T,DF>& sp){if(this != &sp){//在sp共享之前,需要將之前的資源進行釋放if(ptr && 0 == --*(p_count)){//如果此時之前的內容只有他一個進行管理,那么直接進行釋放DF df;df(ptr);delete p_count;}//this就可以與sp進行共享了ptr = sp->ptr;p_count = sp->p_count;if(p_count){p_count++;}}return *this;}private:T* ptr;//采用類進行指針管理int* p_count;//指向的是使用資源的對象的個數 };釋放的操作:先檢測是否有資源,有資源即是pcount>=1,先給計數器進行-1操作,然后檢測計數器是否為0,如果是0,則說明當前對象是最后使用資源的對象,,需要將資源以及計數空間進行釋放,當為非0的時候,說明還有其他對象在使用資源,當前資源不需要釋放。
我們觀察上面的代碼,可以判斷吹他在單線程下是沒有出現問題的,但是在多線程下可能是有問題的。多線程下有多個執行流,CPU也是多核的,多個線程同時往下執行,假設現在連個線程中的智能指針共享的是同一份資源,兩個線程結束時,需要將其管理的資源釋放掉。也有情況下,線程同事進行判斷,使得最后導致資源沒有進行釋放,而引起資源泄漏。因此,在遇到共享的資源,變量等等之類的,需要考慮多線程環境下的安全性。因此最常見的方式是對其進行加鎖。在這里進行加鎖,是為了保證自身的安全性。
#include<iostream> using namespace std; //智能指針的原理:RAII+具有指針類似的行為 //我們在這里自己進行封裝 template<class T,class DF = DF_new<T>> class sharedptr{ public:sharedptr(T* p = nullptr) :ptr(p),p_count(nullptr),mutex(new mutex){if(_ptr){p_count = new int(1);}}~sharedptr(){reldef();}//在使用指針是我們有*與->的使用,因此在這里要對齊進行運行算符重載//重載*T& operator*(){return *ptr;}//他只能在指針指向的是對象或者是結構體的時候來使用T& operator->(){return ptr;}//某些情況下使用原生態指針T* get(){return ptr;}//用戶可能需要獲取引用計數int use_count()const{return *p_count;}//解決淺拷貝方式,引用計數sharedptr(const sharedptr<T,DF>& sp):ptr(sp.ptr),p_count(sp.p_count),_pmutex(sp._pmutex){Addref();}sharedptr<T,DF>& operator=(const sharedptr<T,DF>& sp){if(this != &sp){//在sp共享之前,需要將之前的資源進行釋放reldef();//this就可以與sp進行共享了ptr = sp->ptr;p_count = sp->p_count;_pmutex = sp._pmutex;Addref();}return *this;} private:void Addref(){//對加法進行處理if(!ptr) return;_pmutex->lock();++(*p_count);_pmutex->unlock();}//此時我們還需要判斷鎖是否需要釋放void reldef(){//對減法進行處理if(ptr) return; bool isdelete = false;_pmutex->lock();if (ptr && 0 == --(*count)){DF df;df(ptr);delete p_count;p_count = nullptr;//當資源釋放完畢后,對其進行標記isdelete = true;}_pmutex->unlock();if(isdelete){delete(_pmutex);}} private:T* ptr;//采用類進行指針管理int* p_count;//指向的是使用資源的對象的個數mutex* _pmutex;//加上鎖的原因是要保證在這里引用計數的操作是原子性的 };雖然shared_ptr在這里是可以避免拷貝構造帶來的錯誤,但是他自身也有缺陷。在使用shared_ptr時可能會引起循環引用。什么是循環引用呢?我們先舉個例子。
#incldue<memory> struct ListNode{shared_ptr<ListNode*> next;shared_ptr<ListNode*> prve;int data;shared(int x):next(nullptr),prev(nullptr),data(x){cout<<"ListNode(int)"<<this<<endl;}~ListNode(){cout<<"~ListNode():"<<this<<endl;} }; void Looptest(){//將兩個節點分別交給智能指針來管理shared_ptr<ListNode> sp1(new ListNode(10));shared_ptr<ListNode> sp2(new ListNode(20));cout<<sp1.use_count()<<endl;cout<<sp2.use_count()<<endl;sp1->next = sp2;sp2->prev = sp1;cout<<sp1.use_count()<<endl;cout<<sp2.use_count()<<endl; } int main(){Looptest(); }當shared_ptr管理的資源在相互指向的時候,我們看上面代碼的運行情況:在結果中,我們發現運行時并未出現調用析構函數的結果,在這里沒有釋放掉資源,因此會引起資源泄露問題。也就是說,循環引用是指兩個對象之間形成了環路,在智能指針shared_ptr中存在這個問題,他的引用計數不為0。也就是兩份資源分別等待對方先進行釋放,最后導致了內存泄漏。處理這種現象十分簡單,只需要只使用一個weak_ptr即可。
1.5weak_ptr
weak_ptr的實現原理是使用了引用計數進行實現的,他不可以進行資源的管理,唯一的作用就是配合shared_ptr解決循環引用的問題。
#incldue<memory> struct ListNode{weak_ptr<ListNode*> next;weak_ptr<ListNode*> prve;int data;shared(int x):next(nullptr),prev(nullptr),data(x){cout<<"ListNode(int)"<<this<<endl;}~ListNode(){cout<<"~ListNode():"<<this<<endl;} }; void Looptest(){//將兩個節點分別交給智能指針來管理shared_ptr<ListNode> sp1(new ListNode(10));shared_ptr<ListNode> sp2(new ListNode(20));cout<<sp1.use_count()<<endl;cout<<sp2.use_count()<<endl;sp1->next = sp2;sp2->prev = sp1;cout<<sp1.use_count()<<endl;cout<<sp2.use_count()<<endl; } int main(){Looptest(); }
我們看上面的代碼,此時析構函數執行了,并沒有發生引用循環。
question:為什么weak_ptr可以解決循環引用?
原因是在他的引用計數上。如上圖代碼,我們進行分析:
?在標準庫中,weak_ptr的引用計數維護了兩份,由圖可知,當開始執行時,use=weak=1;此時在執行sp1->next=sp2,因為sp1->next的類型是一個weak_ptr,因此此時的sp2的引用計數的weak++,再執行sp2->prve=sp1,因為sp2->prve的類型也是一個weak_ptr,因此此時的sp1的引用計數weak++;此時sp1指向空間中的計數use=1,weak=2,sp2指向的資源空間的計數也是一樣。
現在要對資源進行釋放。首先釋放sp2,因為sp2的類型是一個shared_ptr,use--等于0,說明此時資源是可以進行釋放的,因此就要對對象內部的每一個資源進行釋放掉,sp2->prev是weak_ptr類型,將其銷毀,那么左面資源的中的引用計數weak--,然后sp2->prve與sp1斷開,next指針也銷毀掉了,因此此時的節點也銷毀掉了,所以sp2的pcount與資源的引用計數斷開,右面的資源的引用計數weak--。
現在進行釋放sp1,因為sp1的類型是一個shared_ptr,use--等于0,說明此時資源是可以進行釋放的,因此就要對對象內部的每一個資源進行釋放掉,sp1->next是weak_ptr類型,將其銷毀,那么右面資源的中的引用計數weak--,此時右面的引用計數的weak=0,因此就可以將右面資源的引用計數進行釋放;左面資源的prve指針此時也銷毀了,此時節點進行銷毀,所以sp1的pcount與資源的引用計數斷開,左面的weak--等于0,此時將左面的資源的引用計數進行銷毀。
總結:當一個資源被shared_ptr共享時,use++;當一個資源被weak_ptr共享時,weak++。且只有shared_ptr可以獨立的管理資源。
question:unique_ptr與shared_ptr能否可以管理一塊連續空間?
可以。如果要管理里一段連續的空間,我們必須自己實現刪除器,operator()(T*&ptr){delete[] ptr;ptr=nullptr;}。但是沒有什么意義,對于連續空間,一般是不會直接交給智能指針進行管理的,因為在STL中已經有了vector。
總結
- 上一篇: 软件工程期末总复习
- 下一篇: linux下的I2c 和展锐8310下的