C++ 智能指針的魅力你都了解嗎
前情提要
我們知道除了靜態(tài)內(nèi)存和棧內(nèi)存外,每個程序還有一個內(nèi)存池,這部分內(nèi)存被稱為自由空間或者堆。程序用堆來存儲動態(tài)分配的對象即那些在程序運行時分配的對象,當(dāng)動態(tài)對象不再使用時,我們的代碼必須顯式的銷毀它們。
在C++中,動態(tài)內(nèi)存的管理是用一對運算符完成的:new和delete,ne:在動態(tài)內(nèi)存中為對象分配一塊空間并返回一個指向該對象的指針,delete指向一個動態(tài)獨享的指針,銷毀對象,并釋放與之關(guān)聯(lián)的內(nèi)存。
動態(tài)內(nèi)存管理經(jīng)常會出現(xiàn)兩種問題:一種是忘記釋放內(nèi)存,會造成內(nèi)存泄漏;一種是尚有指針引用內(nèi)存的情況下就釋放了它,就會產(chǎn)生引用非法內(nèi)存的指針。
為了更加容易(更加安全)的使用動態(tài)內(nèi)存,引入了智能指針的概念。智能指針的行為類似常規(guī)指針,重要的區(qū)別是它負(fù)責(zé)自動釋放所指向的對象
智能指針的原理
RAII:利用對象生命周期來控制程序資源。主要是通過對象的構(gòu)造函數(shù)來獲得資源的管理權(quán),然后再通過析構(gòu)函數(shù)來釋放所管理的資源。其原理就是把管理一份資源的責(zé)任托管給了一個對象
//RAII template<class T> class SmartPtr { public: //構(gòu)造函數(shù)獲取資源管理權(quán) SmartPtr(T* ptr) :_ptr(ptr) {} //通過析構(gòu)函數(shù)釋放資源 ~SmartPtr() { if (_ptr) delete _ptr; } private: T* _ptr; };
class A { private: int _a = 10; }; void test() { //錯誤寫法 int* ptr = new int[10]; SmartPtr<int> sp(ptr); //立即初始化--申請資源時就立即綁定資源 SmartPtr<int> sp2(new int); SmartPtr<A> sp3(new A); }
這并不是一個智能指針對象,智能指針應(yīng)該要滿足以下條件
- 實現(xiàn)RAII思想
- 使用方式和指針一樣,例如需要支持*解引用和->操作
應(yīng)在類中添加以下操作的重載
T* operator->() { return _ptr; } T& operator*() { return *_ptr; }
智能指針和普通的指針的區(qū)別之一是智能指針不需要手動釋放空間
void test() { //智能指針--編譯器調(diào)用析構(gòu)自動釋放資源--不存在內(nèi)存泄漏 SmartPtr<A> sp(new A); (*sp)._a = 10; sp->_a = 100; //普通指針--手動釋放內(nèi)存 int* p = new int; A* pa = new A; *p = 1; pa->_a = 10; //return //提前結(jié)束普通指針就會導(dǎo)致內(nèi)存泄漏 delete p; delete pa; }
C++標(biāo)準(zhǔn)庫中的智能指針的使用
庫中的智能指針分為 auto_ptr、unique_ptr、 share_ptr
他們都需要引入頭文件#include <memory>
才能使用
auto_ptr
auto_ptr是一種存在缺陷的智能指針(禁用)
#include <memory> using namespace std; void test() { auto_ptr<int> ap(new int); auto_ptr<int> ap2(new int(2)); *ap = 10; *ap2 = 20; }
auto_ptr指針進行賦值操作時會將資源進行轉(zhuǎn)移,目的是為了防止多個智能指針指向同一塊內(nèi)存資源。但是這種設(shè)計顯然不符合我們的需求
我們來簡單模擬實現(xiàn)auto_ptr,看他底層是如何進行資源權(quán)的轉(zhuǎn)移的
//實現(xiàn)auto_ptr template<class T> class Auto_ptr { public: Auto_ptr(T* ptr) :_ptr(ptr) {} ~Auto_ptr() { if (_ptr) delete _ptr; } T* operator->() { return _ptr; } T& operator*() { return *_ptr; } Auto_ptr(Auto_ptr<T>& ap) :_ptr(ap._ptr) { //資源管理權(quán)轉(zhuǎn)移 ap._ptr = nullptr; } Auto_ptr<T>& operator=(Auto_ptr<T>& ap) { if (this != &ap) { if (_ptr) delete _ptr; //資源管理權(quán)轉(zhuǎn)移 _ptr = ap._ptr; ap._ptr = nullptr; } return *this; } private: T* _ptr; };
unique_ptr
unique_ptr智能指針是通過防拷貝來解決資源管理權(quán)限轉(zhuǎn)移問題----將unique_ptr賦值運算符函數(shù)和拷貝構(gòu)造函數(shù)設(shè)置為刪除函數(shù)
void test() { unique_ptr<int> up(new int(10)); unique_ptr<int> up2(up);//error unique_ptr<int> up3(new int(20)); up = up3; //error }
報錯原因:拷貝構(gòu)造和賦值重載函數(shù)都是已經(jīng)刪除的函數(shù)
底層實現(xiàn):
template<class T> class Unique_ptr { public: Unique_ptr(T* ptr) :_ptr(ptr) {} Unique_ptr(const Unique_ptr<T>& up) = delete; Unique_ptr<T>& operator=(const Unique_ptr<T>& up) = delete; ~Unique_ptr() { if (_ptr) { delete _ptr; _ptr = nullptr; } } private: T* _ptr; };
shared_ptr
shared_ptr是C++11中新提供的一種智能指針,不僅解決了資源管理權(quán)限轉(zhuǎn)移問題,還提供靠譜的拷貝功能
class A { public: int _a = 10; ~A() { cout << "~A()" << endl; } }; void test() { shared_ptr<A> sp(new A); shared_ptr<A> sp2(new A); shared_ptr<A> sp3(sp2);//ok sp3 = sp;//ok sp->_a = 100; sp2->_a = 1000; sp3->_a = 10000; cout << sp->_a << endl; cout << sp2->_a << endl; cout << sp3->_a << endl; }
運行結(jié)果:
我們發(fā)現(xiàn)申請多少資源就會釋放多少資源,此時的sp和sp3共享一份資源,修改sp3也就相等于修改了sp。所以最終都會打印10000。那共享了一份資源,是如何實現(xiàn)資源只釋放一次呢?----引用計數(shù)
我們可以通過shared_ptr提供的接口use_count()
來查看,當(dāng)前有多少個智能指針來管理同一份資源
void test() { shared_ptr<A> sp(new A); cout << sp.use_count() << endl;//1 shared_ptr<A> sp2(sp); cout << sp.use_count() << endl;//2 cout << sp2.use_count() << endl;//2 shared_ptr<A> sp3(new A); cout << sp.use_count() << endl;//2 cout << sp2.use_count() << endl;//2 cout << sp3.use_count() << endl;//1 sp3 = sp; sp3 = sp2; cout << sp.use_count() << endl;//2 cout << sp2.use_count() << endl;//2 cout << sp3.use_count() << endl;//2 }
運行截圖:之所以中間會有調(diào)析構(gòu)函數(shù),是因為當(dāng)sp3指向sp時,sp3的引用計數(shù)為0,則會調(diào)用析構(gòu)函數(shù)來釋放資源。此時sp創(chuàng)建的資源就有3個指智能指針來管理
圖解:
在實現(xiàn)時,我們應(yīng)該確保一個資源只對應(yīng)一個計數(shù)器,而不是每個智能指針都有各自的計數(shù)器。所以我們可以將資源和計數(shù)器綁定在一起,此時指向同一份資源的智能指針,訪問的也都是同一個計數(shù)器
成員變量:成員變量應(yīng)該有兩個變量,分別是資源指針的變量_ptr和計數(shù)器變量_countPtr,他們都是一個指針類型的變量
拷貝構(gòu)造函數(shù):在拷貝構(gòu)造函數(shù)中,應(yīng)該將當(dāng)前對象的指針指向要拷貝的對象的資源,而且還要拷貝它的計數(shù)器,最后還需要將計數(shù)器進行++
賦值運算符重載:我們不能判斷兩個對象是否相等,而應(yīng)該為只要兩個對象的資源不同,那么才需要進行賦值。在賦值中,先讓當(dāng)前對象的計數(shù)器進行–,如果為0則表示當(dāng)前對象的資源是只被當(dāng)前對象管理,則需要釋放資源。然后再將當(dāng)前對象指針修改為要拷貝的對象的資源,并且拷貝它的計數(shù)器。最后還要對計數(shù)器進行++操作
析構(gòu)函數(shù):判斷當(dāng)前對象的資源的計數(shù)器,先進行–操作,在判斷計數(shù)器是否為0,如果為0才會將資源釋放,不為0則什么都不做
template<class T> class Shared_ptr { public: Shared_ptr(T* ptr) :_ptr(ptr) , _countPtr(new size_t(1))//初始化為1 {} Shared_ptr(const Shared_ptr<T>& sp) :_ptr(sp._ptr) , _countPtr(sp._countPtr) { //計數(shù)器累加 ++(*_countPtr); } Shared_ptr<T> operator=(const Shared_ptr<T>& sp) { if (_ptr != sp._ptr) { //本身計數(shù)器自減 //計數(shù)器為0,則當(dāng)前對象需要釋放資源 if (--(*_countPtr) == 0) { delete _ptr; delete _countPtr; } _ptr = sp._ptr; _countPtr = sp._countPtr; ++(*_countPtr); } return *this; } ~Shared_ptr() { //計數(shù)器自減 if (--(*_countPtr) == 0) { delete _ptr; delete _countPtr; _ptr = nullptr; _countPtr = nullptr; } } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } private: T* _ptr; size_t* _countPtr;//計數(shù)器指針 };
我們實現(xiàn)的shared_ptr智能指針在多線程的場景下其實是不存在線程安全問題的----引用計數(shù)器指針是一個共享變量,多個線程進行修改時會導(dǎo)致計數(shù)器混亂。導(dǎo)致資源提前被釋放或者會產(chǎn)生內(nèi)存泄漏問題
我們來看看一下代碼, 如果是安全的,那么最后析構(gòu)函數(shù)應(yīng)該只被調(diào)用一次
void fun(const Shared_ptr<A>& sp, int n) { for (int i = 0; i < n; ++i) Shared_ptr<A> copy(sp);//創(chuàng)建copy智能指針 } void test() { Shared_ptr<A> sp(new A); int n = 100000; thread t1(fun, ref(sp), n); thread t2(fun, ref(sp), n); t1.join(); t2.join(); }
運行結(jié)果1:我們發(fā)現(xiàn)并沒有調(diào)用對象的析構(gòu)函數(shù),說明此時產(chǎn)生了內(nèi)存泄漏的問題
運行結(jié)果2:調(diào)用兩次析構(gòu)函數(shù),也就說明一份資源被釋放兩次。
我們可以在類中提供獲取計數(shù)器的值的接口
size_t getCount() { return *_countPtr; }
然后再代碼中運行并獲取計數(shù)器的值,發(fā)現(xiàn)計數(shù)器的值并沒有為0,所以就不會調(diào)用調(diào)用析構(gòu)函數(shù)
所以我們可以在修改計數(shù)器的地方進行加鎖保護。而這個鎖不能為全局變量的鎖,資源之間不能被有影響,否則當(dāng)一個資源進行加鎖修改時,另一個資源會被收到影響,此時會影響代碼的執(zhí)行效率。應(yīng)當(dāng)給每一個計數(shù)器提供單獨的鎖
這里++操作和–操作都進行封裝
template<class T> class Shared_ptr { public: Shared_ptr(T* ptr) :_ptr(ptr) , _countPtr(new size_t(1))//初始化為1 , _mtx(new mutex) {} Shared_ptr(const Shared_ptr<T>& sp) :_ptr(sp._ptr) , _countPtr(sp._countPtr) ,_mtx(sp._mtx) { //計數(shù)器累加 //++(*_countPtr); addCount(); } Shared_ptr<T> operator=(const Shared_ptr<T>& sp) { if (_ptr != sp._ptr) { //本身計數(shù)器自減 //計數(shù)器為0,則當(dāng)前對象需要釋放資源 //if (--(*_countPtr) == 0) if (subCount() == 0) { delete _ptr; delete _countPtr; delete _mtx; } _ptr = sp._ptr; _countPtr = sp._countPtr; addCount(); } return *this; } ~Shared_ptr() { //計數(shù)器自減 if (subCount() == 0) { delete _ptr; delete _countPtr; delete _mtx; _ptr = nullptr; _countPtr = nullptr; _mtx = nullptr; } } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } size_t getCount() { return *_countPtr; } size_t addCount() { _mtx->lock(); ++(*_countPtr); _mtx->unlock(); return *_countPtr; } size_t subCount() { _mtx->lock(); --(*_countPtr); _mtx->unlock(); return *_countPtr; } private: T* _ptr; size_t* _countPtr;//計數(shù)器指針 mutex* _mtx; };
運行結(jié)果:我們發(fā)現(xiàn)多線程場景下,也是都可以正常釋放的
循環(huán)引用問題
shared_ptr其實也存在一些小問題,也就是循環(huán)引用問題
我們先來看看以下代碼
struct ListNode { shared_ptr<ListNode> _next; shared_ptr<ListNode> _prev; int _data; ~ListNode() { cout << "~ListNode()" << endl; } }; void test() { shared_ptr<ListNode> n1(new ListNode); shared_ptr<ListNode> n2(new ListNode); cout << n1.use_count() << endl; cout << n2.use_count() << endl; n1->_next = n2; n2->_prev = n1; cout << n1.use_count() << endl; cout << n2.use_count() << endl; }
運行結(jié)果:我們發(fā)現(xiàn)并沒有釋放資源,計數(shù)器也在自增
圖解:
在C++11中,專門為了解決這個問題,又引入了一種新的智能指針waek_ptr,這種指針稱為弱指針。在賦值或者拷貝的時候,計數(shù)器并不會進行++。析構(gòu)時也并不會進行真正資源的釋放。waek_ptr不能單獨使用,其最大的作用就是解決shared_ptr循環(huán)引用的問題。
struct ListNode { weak_ptr<ListNode> _next; weak_ptr<ListNode> _prev; int _data; ~ListNode() { cout << "~ListNode()" << endl; } }; void test() { shared_ptr<ListNode> n1(new ListNode); shared_ptr<ListNode> n2(new ListNode); cout << n1.use_count() << endl; cout << n2.use_count() << endl; n1->_next = n2; n2->_prev = n1; cout << n1.use_count() << endl; cout << n2.use_count() << endl; }
運行結(jié)果:
在我們自己實現(xiàn)的shared_ptr中,我們在釋放資源時都單單以delete來釋放,而在我們申請空間方式中并非就只用new來申請空間們也有可能是用malloc來申請,則此時就應(yīng)該要用free來釋放。所以我們還要給智能指針添加一個刪除器
void test() { Shared_ptr<A> sp(new A[100]);//調(diào)用析構(gòu)會報錯 }
刪除器主要可以通過仿函數(shù)來實現(xiàn)
template<class T> struct DeleteDel { void operator()(T* ptr) { delete ptr; } }; template<class T> struct FreeDel { void operator()(T* ptr) { free(ptr); } }; template<class T> struct DeleteArrDel { void operator()(T* ptr) { delete[] ptr; } };
template<class T, class Del = DeleteDel<T>> class Shared_ptr { public: Shared_ptr(T* ptr) :_ptr(ptr) , _countPtr(new size_t(1))//初始化為1 , _mtx(new mutex) {} Shared_ptr(const Shared_ptr<T>& sp) :_ptr(sp._ptr) , _countPtr(sp._countPtr) ,_mtx(sp._mtx) { //計數(shù)器累加 //++(*_countPtr); addCount(); } Shared_ptr<T> operator=(const Shared_ptr<T>& sp) { if (_ptr != sp._ptr) { //本身計數(shù)器自減 //計數(shù)器為0,則當(dāng)前對象需要釋放資源 //if (--(*_countPtr) == 0) if (subCount() == 0) { //delete _ptr; //通過刪除器來釋放空間 _del(_ptr); delete _countPtr; delete _mtx; } _ptr = sp._ptr; _countPtr = sp._countPtr; addCount(); } return *this; } ~Shared_ptr() { //計數(shù)器自減 if (subCount() == 0) { //delete _ptr; //通過刪除器來釋放空間 _del(_ptr); delete _countPtr; delete _mtx; _ptr = nullptr; _countPtr = nullptr; _mtx = nullptr; } } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } size_t getCount() { return *_countPtr; } size_t addCount() { _mtx->lock(); ++(*_countPtr); _mtx->unlock(); return *_countPtr; } size_t subCount() { _mtx->lock(); --(*_countPtr); _mtx->unlock(); return *_countPtr; } private: T* _ptr; size_t* _countPtr;//計數(shù)器指針 mutex* _mtx; Del _del; };
以上就是C++ 一篇文章讓你知道智能指針的魅力的詳細(xì)內(nèi)容,更多關(guān)于C++智能指針的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
在Ubuntu中安裝VSCode并配置C/C++開發(fā)環(huán)境的方法步驟
這篇文章主要介紹了在Ubuntu中安裝VSCode并配置C/C++開發(fā)環(huán)境的方法步驟,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05