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

