C++的智能指針的原理解析
由于C++沒有GC(垃圾回收機制),所有動態(tài)開辟的空間都需要我們手動釋放,這就會導致
- 忘記釋放
- 異常安全問題
不管哪種情況,都會導致內(nèi)存泄漏
下面先來簡單復習一下異常安全問題
int Div() throw(invalid_argument)
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0錯誤");
return a / b;
}
void fun()
{
int* p = new int;
cout << Div() << endl;
delete p;
}
int main()
{
try
{
fun();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}此時,如果在Div中拋出異常,就會跳過fun()函數(shù)最后的 delete p; 語句,從而導致內(nèi)存泄漏,這就是異常安全問題
那么異常是怎么解決這個問題的呢?
int Div() throw(invalid_argument)
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0錯誤");
return a / b;
}
void fun()
{
int* p = new int;
try
{
cout << Div() << endl;
}
catch (...)//若拋出異常,這里會先捕獲異常,釋放空間后再重新拋出
{
delete p;
throw;
}
delete p;
}
int main()
{
try
{
fun();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}使用了異常的重新拋出,先在內(nèi)層的catch語句中完成空間的釋放,再將該異常重新拋出交給外面的catch處理
但這樣的解決方法會涌現(xiàn)很多問題
例如,當我們開辟了多個空間,如果DIv拋異常,就需要先將這3個空間都釋放掉
void fun()
{
int* p1 = new int;
int* p2 = new int;
int* p3 = new int;
try
{
cout << Div() << endl;
}
catch (...)
{
delete p1;
delete p2;
delete p3;
throw;
}
delete p1;
delete p2;
delete p3;
}但是開辟空間如果失敗了也是會拋異常的,如果這里的new拋異常怎么辦?并且還需要清楚是哪個變量的new拋出的異常
有讀者可能會想,給每個new都放在一個單獨的try塊中,再給每個try寫一個單獨的catch,不就可以解決了嗎?
首先,如果new語句在try塊內(nèi),那么在try塊外的delete語句就會找不到p1,p2,p3
再者,就算可以找到,那怎么判斷哪個new拋的異常,從而不去delete對應對象,又是個問題
這時候,就要請出本文的主角——智能指針
智能指針的原理
智能指針其實是一個類
template <class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
if(_ptr)
{
delete _ptr;
_ptr = nullptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};在該類中,定義了構(gòu)造、析構(gòu)、*重載、->重載,而類中存的是一個指針
現(xiàn)在我如果把一個原生指針存進去,就不需要我手動釋放了
int* p = new int; SmartPtr<int> sp(p);
因為類在生命周期結(jié)束后會自動調(diào)用析構(gòu)函數(shù),而現(xiàn)在原生指針交給智能指針管理后,當原生指針的生命周期結(jié)束時,智能指針的生命周期也會結(jié)束,此時調(diào)用該類的析構(gòu)函數(shù),就可以釋放該原生指針的空間
*重載 和->重載是為了讓智能指針可以更好的模擬原生指針的行為
既然智能指針可以完全模擬原生指針的行為了,那其實我們可以這么定義
SmartPtr<int> sp(new int);//或SmartPtr<int> sp = new int;//這里調(diào)用的也是構(gòu)造
有了智能指針,再發(fā)生異常安全問題時,就無需重新拋出異常了
int Div() throw(invalid_argument)
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0錯誤");
return a / b;
}
void fun()
{
SmartPtr<int> sp = new int;
cout << Div() << endl;
}
int main()
{
try
{
fun();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}此時如果Div函數(shù)內(nèi)部拋出異常,那么在異常拋出后會先執(zhí)行SmartPtr的析構(gòu)函數(shù),再執(zhí)行catch塊中的代碼
即使沒有拋異常,在fun函數(shù)執(zhí)行完后,也會調(diào)用SmartPtr的析構(gòu)函數(shù),相當于將指針托管給了智能指針
RAII
智能指針的這種做法,就被叫做RAII(Resource Acquisition Is Initialization),它是一種利用對象生命周期來控制程序資源(如內(nèi)存、文件句柄、網(wǎng)絡連接、互斥量等等)的簡單技術(shù)。
詳細點解釋,就是在對象構(gòu)造時獲取資源,接著控制對資源的訪問使之在對象的生命周期內(nèi)始終保持有效,最后在對象析構(gòu)的時候釋放資源。借此,我們實際上把管理一份資源的責任托管給了一個對象。這種做法有兩大好處:
- 不需要顯式地釋放資源。
- 采用這種方式,對象所需的資源在其生命期內(nèi)始終保持有效。
需要注意的是,RAII是一種托管資源的思想,智能指針是依靠RAII實現(xiàn)的,除了智能指針之外,還有例如unique_lock/lock_gurad等等也是依托RAII實現(xiàn)的
智能指針的問題
當我們想將一個智能指針的值賦給另一個智能指針時,問題就出現(xiàn)了
SmartPtr<int> sp1 = new int; SmartPtr<int> sp2 = sp1;//調(diào)用默認拷貝構(gòu)造
此時會報錯

這是因為sp1和sp2中指針的地址是同一個,那么就會導致兩次釋放,和淺拷貝問題很像

針對這個問題,C++一共給出了三種解決方案
- C++98:auto_ptr 管理權(quán)轉(zhuǎn)移 (了解)
- C++11:unique_ptr 防拷貝
- C++11:shared_ptr 引用計數(shù)的共享拷貝 循環(huán)拷貝問題需要用weak_ptr解決
auto_ptr
auto_ptr是C++98中自帶的類,因此如果要實現(xiàn)自己的auto_ptr,需要將它放在一個命名空間中
namespace valkyrie
{
template <class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
auto_ptr(auto_ptr<T>& ap)//管理權(quán)轉(zhuǎn)移,將ap存的地址轉(zhuǎn)移給*this
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
}
auto_ptr& operator=(auto_ptr<T>& ap)//依舊管理權(quán)轉(zhuǎn)移,不過需要先將原先this中的空間釋放
{
if (this != &ap)
{
if (_ptr)
delete _ptr;
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
~auto_ptr()
{
if (_ptr)
{
delete _ptr;
_ptr = nullptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}大部分代碼都和之前我們實現(xiàn)的SmartPtr一樣,唯一區(qū)別在于拷貝構(gòu)造和賦值重載顯式定義了
該拷貝構(gòu)造的核心在于原先ap1管理的空間,交給ap2管理,而ap1再置空

賦值重載也一樣,但需要先將原先存的空間釋放掉

那此時如果再想 *ap2 = 1; 就會引發(fā)空指針解引用問題,而崩潰
這也是C++早期的設計缺陷,一般都是公司明令禁止使用的
unique_ptr
unique_ptr 是C++11新引入的智能指針,它對于拷貝構(gòu)造和賦值重載的做法是直接禁止,已達到防拷貝的目的
namespace valkyrie
{
template <class T>
class unique_ptr
{
public:
unique_ptr(unique_ptr<T>&) = delete;//刪除該類的拷貝構(gòu)造
unique_ptr<T>& operator=(unique_ptr<T>&) = delete;//刪除該類的賦值重載
unique_ptr(T* ptr)
:_ptr(ptr)
{
}
~unique_ptr()
{
if (_ptr)
{
delete _ptr;
_ptr = nullptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}這樣在我們嘗試調(diào)用拷貝構(gòu)造或賦值重載時,就會報錯

unique_ptr的思路簡單粗暴,是比較推薦實用的,但缺陷也很明顯:如果有需要拷貝的場景,它就沒法使用
shared_ptr
shared_ptr 也是C++11引入的智能指針,它和 unique_ptr 的區(qū)別在于它可以拷貝構(gòu)造和賦值
shared_ptr 的思路是通過引用計數(shù)的方式來實現(xiàn)多個 shared_ptr 對象之間共享資源。
每個資源都對應一個計數(shù)指針,用來記錄該份資源被幾個對象共享。
拷貝構(gòu)造:

為什么計數(shù)的變量是指針?因為只有指針才可以實現(xiàn)在多個對象中同步變化
賦值重載時,需要先判斷原先存儲的空間需不需要釋放,再執(zhí)行如同拷貝構(gòu)造一樣的操作

此時就是直接釋放的情況,下面再演示一下不需要釋放的情況

namespace valkyrie
{
template <class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr)
,_pcount(new int(1))
{}
shared_ptr(const shared_ptr<T>& sp)//拷貝構(gòu)造,將指針和計數(shù)指針都拷貝給它,并將引用計數(shù)+1,表示又多一個托管這塊內(nèi)存的智能指針
:_ptr(sp._ptr)
,_pcount(sp._pcount)
{
(*_pcount)++;
}
shared_ptr<T>& operator=(shared_ptr<T>& sp)//賦值重載,先判斷原先該智能指針中的空間需不需要釋放(等于先調(diào)用了一次析構(gòu)函數(shù)),再執(zhí)行和拷貝構(gòu)造一樣的步驟
{
if (this != &sp)
{
if (--(*_pcount) == 0 && _ptr)
{
delete _ptr;
_ptr = nullptr;
delete _pcount;
_pcount = nullptr;
}
_ptr = sp._ptr;
_pcount = sp._pcount;
(*_pcount)++;
}
return *this;
}
~shared_ptr()
{
if (--(*_pcount) == 0 && _ptr)//先將引用計數(shù)-1,看看是否為0,如果為0,就代表沒有智能指針在托管這塊空間了,直接釋放;但如果不為0,就代表還有*pcount個智能指針在托管這塊空間,不能釋放
{
delete _ptr;
_ptr = nullptr;
delete _pcount;
_pcount = nullptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;//引用計數(shù),用來統(tǒng)計目前有幾個智能指針在同時托管這塊空間
};
}shared_ptr的線程安全問題
當有多個線程用 shared_ptr 管理著同一份共享資源時,由于引用計數(shù)需要++(析構(gòu)時也需要--),因此就會有多個線程同時對一個引用計數(shù)++(--)而導致的數(shù)據(jù)不一致問題。
C++11的shared_ptr是線程安全的,下面簡單實現(xiàn)一下通過將引用計數(shù)包裝成 atomic 原子類型而實現(xiàn)的線程安全版本:
namespace val
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr)
,_count(new atomic<int>(1))
{}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
,_count(sp._count)
{
if(_count)
(*_count)++;
}
shared_ptr<T>& operator=(shared_ptr sp)
{
//現(xiàn)代寫法,并且保證了線程安全
if(this != &sp)
{
swap(_ptr,sp._ptr);
swap(_count,sp._count);
}
return *this;
}
~shared_ptr()
{
if(_count && --(*_count) == 0)
{
delete _ptr;
delete _count;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
atomic<int>* _count;//將引用計數(shù)包裝成原子類型
};
}shared_ptr的循環(huán)引用問題
當shared_ptr托管的對象需要雙向依賴關系,例如雙向鏈表(next節(jié)點和prev節(jié)點),或樹的父子節(jié)點,就會因為引用計數(shù)無法歸零而導致內(nèi)存泄漏
例如下面這種情況:
template <class T>
struct ListNode
{
T val;
val::shared_ptr<ListNode> prev;
val::shared_ptr<ListNode> next;
};
int main()
{
val::shared_ptr<ListNode<int>> ln1(new ListNode<int>);
val::shared_ptr<ListNode<int>> ln2(new ListNode<int>);
cout << ln1.getcnt()/*獲取當前智能指針的引用計數(shù)*/ << endl << ln2.getcnt() << endl;
ln1->next = ln2;
ln2->prev = ln1;
cout << ln1.getcnt() << endl << ln2.getcnt() << endl;
return 0;
}在這段代碼里,ln1內(nèi)托管的對象由ln1和ln2->prev同時托管,ln2內(nèi)托管的對象由ln2和ln1->next同時托管,那么ln1和ln2的引用計數(shù)都為2

當程序結(jié)束,ln1和ln2調(diào)用析構(gòu)函數(shù)后,各自都把引用計數(shù)減到1,各自都以為還有別的shared_ptr在托管該資源(next和prev),所以都沒釋放,這就造成了內(nèi)存泄漏
要想解決,就需要把雙向依賴關系里的指針不再引用計數(shù)++,因此C++11還有一個 weak_ptr ,它不是智能指針,而是專門用于解決shared_ptr循環(huán)引用問題的指針,將shread_ptr賦給weak_ptr時,引用計數(shù)不會++,而只是像個普通指針一樣
template <class T>
class weak_ptr
{
public:
weak_ptr() = default; // 默認構(gòu)造函數(shù)
weak_ptr(const shared_ptr<T> &sp)
: _ptr(sp.operator->())
{
}
weak_ptr<T> &operator=(shared_ptr<T> sp)//參數(shù)是shared_ptr
{
_ptr = sp.operator->(); // 獲取sp._ptr
return *this;
}
~weak_ptr()
{
_ptr = nullptr;//不釋放,只置空
}
T &operator*()
{
return *_ptr;
}
T *operator->()
{
return _ptr;
}
private:
T *_ptr;
};只要將雙向依賴關系中的資源交給weak_ptr管理,就解決了循環(huán)引用問題
template <class T>
struct ListNode
{
T val;
val::weak_ptr<ListNode> prev;
val::weak_ptr<ListNode> next;
};
int main()
{
val::shared_ptr<ListNode<int>> ln1(new ListNode<int>);
val::shared_ptr<ListNode<int>> ln2(new ListNode<int>);
cout << ln1.getcnt()/*獲取當前智能指針的引用計數(shù)*/ << endl << ln2.getcnt() << endl;
ln1->next = ln2;
ln2->prev = ln1;
cout << ln1.getcnt() << endl << ln2.getcnt() << endl;
return 0;
}定制刪除器
在C++11之前,也就是C++98后,智能指針只有auto_ptr,這之間的十幾年由于官方一直沒有作為,一個名為 boost 的第三方庫就出現(xiàn)了,它給出了 scoped_ptr 、 shared_ptr 、 weak_ptr ,并且除了智能指針也有非常多的實現(xiàn),后續(xù)C++11的 unique_ptr 、 shared_ptr 、 weak_ptr 就是參考 boost 庫實現(xiàn)的
boost庫中除了有scoped_ptr和shared_ptr,還有 scoped_array 和 shared_array 。由于智能指針默認的釋放資源的方式都是delete,如果是以new []創(chuàng)建的數(shù)組,就不能用delete了,因此scoped_array/shared_array是boost中專門給new []準備的智能指針,它們的釋放資源的方式是delete[]
但C++11沒有引入xxx_array的智能指針,它靠定制刪除器來實現(xiàn)(也是借鑒的boost庫)
如果直接將new []的數(shù)據(jù)交給智能指針管理,在析構(gòu)時會報錯
class A
{
public:
~A()
{
cout << "~A()" << endl;
}
private:
int a;
int b;
};
int main()
{
shared_ptr<A>(new A[10]);
return 0;
}
此時可以定義一個仿函數(shù)。讓智能指針在釋放資源時自動調(diào)用仿函數(shù)內(nèi)的 operator()
template<class T>
struct deleteArr
{
void operator()(T* v)
{
delete[] v;
}
};
int main()
{
shared_ptr<A>(new A[10],deleteArr<A>());
return 0;
}這就叫做定制刪除器
除了new,new[]之外,malloc/fopen等等需要回收的類型都可以使用定制刪除器交給智能指針管理
class A
{
public:
~A()
{
cout << "~A()" << endl;
}
private:
int a;
int b;
};
template<class T>
struct deleteArr
{
void operator()(T* v)
{
delete[] v;
}
};
template<class T>
struct deleteFree
{
void operator()(T* v)
{
free(v);
}
};
struct deleteFclose
{
void operator()(FILE* v)
{
fclose(v);
}
};
int main()
{
shared_ptr<A>(new A[10],deleteArr<A>());
shared_ptr<A>((A*)malloc(sizeof(A)),deleteFree<A>());
shared_ptr<FILE>(fopen("tmp.txt","w"),deleteFclose());
return 0;
}需要注意的是,上面只是為了演示定制刪除器的用法,若真需要用智能指針管理數(shù)組,應該用unique_ptr,它有operator[]重載
lock_guard/unique_lock(RAII機制托管鎖)
平時有加鎖解鎖的操作時,如果中間的某個函數(shù)拋出異常,就會出現(xiàn)死鎖問題,需要通過捕獲重新拋出的方式對其解鎖
mtx.lock();
try
{
f();
}
catch(...)
{
mtx.unlock();//捕獲重新拋出
throw;
}
mtx.unlock();C++11中,專門為鎖的RAII寫了兩個對象, lock_guard 和 unique_lock ,它們可以在出生命周期時自動釋放鎖
lcok_guard就是純粹的RAII的管理鎖,構(gòu)造時加鎖,析構(gòu)時解鎖,該對象只有構(gòu)造和析構(gòu)兩個接口

template<class lock>
class Lock_Guard
{
public:
Lock_Guard(lock& lk)//引用傳參,這樣傳過去的鎖也是同一個
:_lk(lk)
{
//_lk = lk;
_lk.lock();
}
~Lock_Guard()
{
_lk.unlock();
}
Lock_Guard(const Lock_Guard<lock>&) = delete;//lock_guard不允許拷貝
Lock_Guard<lock>& operator=(Lock_Guard<lock>) = delete;
private:
lock& _lk;//引用類型
};
mutex mtx;
void fun()
{
Lock_Guard<mutex> lg(mtx);
throw(string("拋出異常"));
}需要注意的是lock_guard是不支持拷貝的
unique_lock 除了可以像lock_guard一樣RAII,還可以自己加鎖或解鎖

void fun()
{
unique_lock<mutex> lg(mtx);
throw(string("拋出異常"));
lg.unlock();
//...其他工作
lg.lock();
//...
}到此這篇關于【語法】C++的智能指針的文章就介紹到這了,更多相關C++的智能指針內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
如何統(tǒng)計在一篇文章中某個單詞出現(xiàn)了幾次,以及第一次出現(xiàn)的位置
本文的主要內(nèi)容就是統(tǒng)計某個單詞在一篇文章中出現(xiàn)了幾次,以及第一次出現(xiàn)的位置,需要的朋友可以參考下2015-08-08
c++實現(xiàn)十進制轉(zhuǎn)換成16進制示例
這篇文章主要介紹了c++實現(xiàn)十進制轉(zhuǎn)換成16進制示例,需要的朋友可以參考下2014-05-05
C語言中的內(nèi)存管理之掌握動態(tài)分配的技巧(最新推薦)
在C語言編程中,內(nèi)存管理是一項至關重要的技能,它直接關系到程序的性能和穩(wěn)定性,特別是在處理大型數(shù)據(jù)集或需要靈活內(nèi)存布局的場景下,本文將深入探討C語言中的動態(tài)內(nèi)存分配技巧,幫助開發(fā)者更好地掌握這一核心技能2025-03-03
C++應用Eigen庫對應實現(xiàn)matlab中部分函數(shù)問題
這篇文章主要介紹了C++應用Eigen庫對應實現(xiàn)matlab中部分函數(shù)問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-12-12

