深入掌握智能指針
智能指針介紹
學習C++的人,一直在接觸裸指針,一邊感受著它的強大,一邊感受著它的坑爹。當然,坑不坑爹在于開發者,指針本身近乎完美,但奈何用的人比較猥瑣,給自己埋下無數的坑,還哭喊著指針不好用,那么今天要介紹的智能指針可以釋放大家在使用裸指針時的一些壓力,當然智能指針無法替代裸指針的全部功能。
裸指針到底有什么不好,寫過一些C++代碼的人應該都能知道,比如下面的原因:
- 忘記釋放資源,導致資源泄露(常發生內存泄漏問題)
- 同一資源釋放多次,導致釋放野指針,程序崩潰
- 明明代碼的后面寫了釋放資源的代碼,但是由于程序邏輯滿足條件,從中間return掉了,導致釋放資源的代碼未被執行到,懵
- 代碼運行過程中發生異常,隨著異常棧展開,導致釋放資源的代碼未被執行到,懵
總之,智能指針的智能二字,主要體現在用戶可以不關注資源的釋放,因為智能指針會幫你完全管理資源的釋放,它會保證無論程序邏輯怎么跑,正常執行或者產生異常,資源在到期的情況下,一定會進行釋放。
C++11庫里面,提供了帶引用計數的智能指針和不帶引用計數的智能指針,這篇文章主要介紹它們的原理和應用場景,包括auto_ptr,scoped_ptr,unique_ptr,shared_ptr,weak_ptr。
自己實現智能指針
為了更好理解C++庫中智能指針的原理,首先我們需要自己實現一個簡單的智能指針,窺探一下智能指針的基本原理,就是利用棧上的對象出作用域會自動析構這么一個特點(RAII),把資源釋放的代碼全部放在這個析構函數中執行,就達到了“智能”的效果。對比下面的兩塊代碼:
- 使用裸指針
int main() {
int *p = new int;
/*其它的代碼...*/
/*如果這里忘記寫delete,或者上面的代碼段中程序return掉了,沒有執行到這里,都會導致這里沒有釋放內存,內存泄漏*/
delete p;
return 0;
}
- 使用智能指針
template<typename T>
class CSmartPtr {
public:
CSmartPtr(T *ptr = nullptr) : m_ptr(ptr) {}
~CSmartPtr() { delete m_ptr; }
private:
T *m_ptr;
};
int main() {
CSmartPtr<int> ptr(new int);
/*其它的代碼...*/
/*由于ptr是棧上的智能指針對象,不管是函數正常執行完,還是運行過程中出現
異常,棧上的對象都會自動調用析構函數,在析構函數中進行了delete
操作,保證釋放資源*/
return 0;
}
上面這段代碼就是一個非常簡單的智能指針,主要用到了這兩點:
- 智能指針體現在把裸指針進行了一次面向對象的封裝,在構造函數中初始化資源地址,在析構函數中負責釋放資源
- 利用棧上的對象出作用域自動析構這個特點,在智能指針的析構函數中保證釋放資源
所以,智能指針一般都定義在棧上,曾經有一個面試問題,問“能不能在堆上定義智能指針?”,如這段代碼:
CSmartPtr *p = new CSmartPtr(new int);
大家應該能看出來,這里定義的p雖然是智能指針類型,但它實質上還是一個裸指針,因此p還是需要進行手動delete,又回到了最開始裸指針我們面臨的問題。
當然,在實現一個智能指針的時候,要讓智能指針做到和裸指針相似,還得提供裸指針常見的*和->兩種運算符的重載函數,這樣,智能指針使用起來才真正的和裸指針一樣,代碼擴充如下:
template<typename T>
class CSmartPtr {
public:
CSmartPtr(T *ptr = nullptr) : m_ptr(ptr) {}
~CSmartPtr() { delete m_ptr; }
T &operator*() { return *m_ptr; }
const T &operator*() const { return *m_ptr; }
T *operator->() { return m_ptr; }
const T *operator->() const { return m_ptr; }
private:
T *m_ptr;
};
int main() {
CSmartPtr<int> ptr(new int);
*ptr = 20;
cout << *ptr << endl;
return 0;
}
上面的這個智能指針,使用起來就和普通的裸指針非常相似了,但是它還存在很大的問題,看下面的代碼:
int main() {
CSmartPtr<int> ptr1(new int);
CSmartPtr<int> ptr2(ptr1);
return 0;
}
這個main函數運行,代碼直接崩潰,問題出在默認的拷貝構造函數做的是淺拷貝,兩個智能指針都持有一個new int資源,ptr2先析構釋放了資源,到ptr1析構的時候,就成了delete野指針了,造成程序崩潰。所以這里引出來智能指針需要解決的兩件事情:
- 怎么解決智能指針的淺拷貝問題
- 多個智能指針指向同一個資源的時候,怎么保證資源只釋放一次,而不是每個智能指針都釋放一次,造成代碼運行不可預期的嚴重后果
我們一起看看C++庫中提供的智能指針是怎么解決上面提到的問題的。
不帶引用計數的智能指針
C++庫中提供的不帶引用計數的智能指針主要包括:auto_ptr,scoped_ptr,unique_ptr,下面一一進行介紹。
auto_ptr
先瀏覽一下auto_ptr的主要源碼,如下:
template<class _Ty>
class auto_ptr
{ // wrap an object pointer to ensure destruction
public:
typedef _Ty element_type;
explicit auto_ptr(_Ty * _Ptr = nullptr) noexcept
: _Myptr(_Ptr)
{ // construct from object pointer
}
/*這里是auto_ptr的拷貝構造函數,_Right.release()函數中,把_Right的_Myptr賦為nullptr,也就是換成當前auto_ptr持有資源地址*/
auto_ptr(auto_ptr& _Right) noexcept
: _Myptr(_Right.release())
{ // construct by assuming pointer from _Right auto_ptr
}
_Ty * release() noexcept
{ // return wrapped pointer and give up ownership
_Ty * _Tmp = _Myptr;
_Myptr = nullptr;
return (_Tmp);
}
private:
_Ty * _Myptr; // the wrapped object pointer
};
從auto_ptr的源碼可以看到,只有最后一個auto_ptr智能指針持有資源,原來的auto_ptr都被賦nullptr了,考慮如下代碼:
int main() {
auto_ptr<int> p1(new int);
/*經過拷貝構造,p2指向了new int資源,p1現在為nullptr了,如果使用p1,相當于訪問空指針了,很危險*/
auto_ptr<int> p2 = p1;
*p1 = 10;
return 0;
}
上面的程序,如果用戶不了解auto_ptr的實現,代碼就會出現嚴重的問題。記得曾經一個面試題問過“auto_ptr能不能使用在容器當中?”,看下面的代碼描述:
int main() {
vector<auto_ptr<int>> vec;
vec.push_back(auto_ptr<int>(new int(10)));
vec.push_back(auto_ptr<int>(new int(20)));
vec.push_back(auto_ptr<int>(new int(30)));
// 這里可以打印出10
cout << *vec[0] << endl;
vector<auto_ptr<int>> vec2 = vec;
/*這里由于上面做了vector容器的拷貝,相當于容器中的每一個元素都進行了拷貝構造,原來vec中的智能指針全部為nullptr了,再次訪問就成訪問空指針了,程序崩潰*/
cout << *vec[0] << endl;
return 0;
}
所以不要在容器中使用auto_ptr,C++建議最好不要使用auto_ptr,除非應用場景非常簡單。
總結:auto_ptr智能指針不帶引用計數,那么它處理淺拷貝的問題,是直接把前面的auto_ptr都置為nullptr,只讓最后一個auto_ptr持有資源
scoped_ptr
先瀏覽一下scoped_ptr的源碼,如下:
template<class T> class scoped_ptr // noncopyable
{
private:
T * px;
/*私有化拷貝構造函數和賦值函數,這樣scoped_ptr的智能指針對象就不支持這兩種操作,從根本上杜絕淺拷貝的發生*/
scoped_ptr(scoped_ptr const &);
scoped_ptr & operator=(scoped_ptr const &);
typedef scoped_ptr<T> this_type;
/*私有化邏輯比較運算符重載函數,不支持scoped_ptr的智能指針對象的比較操作*/
void operator==( scoped_ptr const& ) const;
void operator!=( scoped_ptr const& ) const;
public:
typedef T element_type;
explicit scoped_ptr( T * p = 0 ) : px( p ) // never throws
{
#if defined(BOOST_SP_ENABLE_DEBUG_HOOKS)
boost::sp_scalar_constructor_hook( px );
#endif
}
#ifndef BOOST_NO_AUTO_PTR
/*支持從auto_ptr構造一個scoped_ptr智能指針對象,但是auto_ptr因為調用release()函數,導致其內部指針為nullptr*/
explicit scoped_ptr( std::auto_ptr<T> p ) BOOST_NOEXCEPT : px( p.release() )
{
#if defined(BOOST_SP_ENABLE_DEBUG_HOOKS)
boost::sp_scalar_constructor_hook( px );
#endif
}
#endif
/*析構函數,釋放智能指針持有的資源*/
~scoped_ptr() // never throws
{
#if defined(BOOST_SP_ENABLE_DEBUG_HOOKS)
boost::sp_scalar_destructor_hook( px );
#endif
boost::checked_delete( px );
}
};
從scoped_ptr的源碼可以看到,該智能指針由于私有化了拷貝構造函數和operator=賦值函數,因此從根本上杜絕了智能指針淺拷貝的發生,所以scoped_ptr也是不能用在容器當中的,如果容器互相進行拷貝或者賦值,就會引起scoped_ptr對象的拷貝構造和賦值,這是不允許的,代碼會提示編譯錯誤。
auto_ptr和scoped_ptr這一點上的區別,有些資料上用所有權的概念來描述,道理是相同的,auto_ptr可以任意轉移資源的所有權,而scoped_ptr不會轉移所有權(因為拷貝構造和賦值被禁止了)。
unique_ptr
如果要深入了解unique_ptr,需要先了解C++的右值引用原理。
先看看unique_ptr的部分源碼如下:
template<class _Ty,
class _Dx> // = default_delete<_Ty>
class unique_ptr
: public _Unique_ptr_base<_Ty, _Dx>
{ // non-copyable pointer to an object
public:
typedef _Unique_ptr_base<_Ty, _Dx> _Mybase;
typedef typename _Mybase::pointer pointer;
typedef _Ty element_type;
typedef _Dx deleter_type;
/*提供了右值引用的拷貝構造函數*/
unique_ptr(unique_ptr&& _Right) noexcept
: _Mybase(_Right.release(),
_STD forward<_Dx>(_Right.get_deleter()))
{ // construct by moving _Right
}
/*提供了右值引用的operator=賦值重載函數*/
unique_ptr& operator=(unique_ptr&& _Right) noexcept
{ // assign by moving _Right
if (this != _STD addressof(_Right))
{ // different, do the move
reset(_Right.release());
this->get_deleter() = _STD forward<_Dx>(_Right.get_deleter());
}
return (*this);
}
/*
交換兩個unique_ptr智能指針對象的底層指針
和刪除器
*/
void swap(unique_ptr& _Right) noexcept
{ // swap elements
_Swap_adl(this->_Myptr(), _Right._Myptr());
_Swap_adl(this->get_deleter(), _Right.get_deleter());
}
/*通過自定義刪除器釋放資源*/
~unique_ptr() noexcept
{ // destroy the object
if (get() != pointer())
{
this->get_deleter()(get());
}
}
/*unique_ptr提供->運算符的重載函數*/
_NODISCARD pointer operator->() const noexcept
{ // return pointer to class object
return (this->_Myptr());
}
/*返回智能指針對象底層管理的指針*/
_NODISCARD pointer get() const noexcept
{ // return pointer to object
return (this->_Myptr());
}
/*提供bool類型的重載,使unique_ptr對象可以
直接使用在邏輯語句當中,比如if,for,while等*/
explicit operator bool() const noexcept
{ // test for non-null pointer
return (get() != pointer());
}
/*功能和auto_ptr的release函數功能相同,最終也是只有一個unique_ptr指針指向資源*/
pointer release() noexcept
{ // yield ownership of pointer
pointer _Ans = get();
this->_Myptr() = pointer();
return (_Ans);
}
/*把unique_ptr原來的舊資源釋放,重置新的資源_Ptr*/
void reset(pointer _Ptr = pointer()) noexcept
{ // establish new pointer
pointer _Old = get();
this->_Myptr() = _Ptr;
if (_Old != pointer())
{
this->get_deleter()(_Old);
}
}
/*
刪除了unique_ptr的拷貝構造和operator=賦值函數,
因此不能做unique_ptr智能指針對象的拷貝構造和
賦值,防止淺拷貝的發生
*/
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
};
從上面看到,unique_ptr有一點和scoped_ptr做的一樣,就是去掉了拷貝構造函數和operator=賦值重載函數,禁止用戶對unique_ptr進行顯示的拷貝構造和賦值,防止智能指針淺拷貝問題的發生。
但是unique_ptr提供了帶右值引用參數的拷貝構造和賦值,也就是說,unique_ptr智能指針可以通過右值引用進行拷貝構造和賦值操作,或者在產生unique_ptr臨時對象的地方,如把unique_ptr作為函數的返回值時,示例代碼如下:
// 示例1
unique_ptr<int> ptr(new int);
unique_ptr<int> ptr2 = std::move(ptr); // 使用了右值引用的拷貝構造
ptr2 = std::move(ptr); // 使用了右值引用的operator=賦值重載函數
// 示例2
unique_ptr<int> test_uniqueptr() {
unique_ptr<int> ptr1(new int);
return ptr1;
}
int main() {
/*此處調用test_uniqueptr函數,在return ptr1代碼處,調用右值引用的拷貝構造函數,由ptr1拷貝構造ptr*/
unique_ptr<int> ptr = test_uniqueptr();
return 0;
}
unique_ptr還提供了reset重置資源,swap交換資源等函數,也經常會使用到。可以看到,unique_ptr從名字就可以看出來,最終也是只能有一個該智能指針引用資源,因此建議在使用不帶引用計數的智能指針時,可以優先選擇unique_ptr智能指針。
帶引用計數的智能指針
這里主要介紹shared_ptr和weak_ptr兩個智能指針,什么是帶引用計數的智能指針?當允許多個智能指針指向同一個資源的時候,每一個智能指針都會給資源的引用計數加1,當一個智能指針析構時,同樣會使資源的引用計數減1,這樣最后一個智能指針把資源的引用計數從1減到0時,就說明該資源可以釋放了,由最后一個智能指針的析構函數來處理資源的釋放問題,這就是引用計數的概念。
要對資源的引用個數進行計數,那么大家知道,對于整數的++或者--操作,它并不是線程安全的操作,因此shared_ptr和weak_ptr底層的引用計數已經通過CAS操作,保證了引用計數加減的原子特性,因此shared_ptr和weak_ptr本身就是線程安全的帶引用計數的智能指針。
曾經有一道面試的問題這樣問“shared_ptr智能指針的引用計數在哪里存放?”,當然,這個問題需要看shared_ptr的源碼了,如下:
private:
/*
下面這兩個是shared_ptr的成員變量:
_Ptr是指向內存資源的指針
_Rep是指向new出來的計數器對象的指針,該計數器對象包含了資源的一個引用計數器count
*/
element_type * _Ptr{nullptr};
_Ref_count_base * _Rep{nullptr};
因此,shared_ptr智能指針的資源引用計數器在內存的heap堆上。shared_ptr一般被稱作強智能指針,weak_ptr被稱作弱智能指針,它們有下邊兩個非常重要的應用場景需要注意:
- 智能指針的交叉引用(循環引用)問題
- 多線程訪問共享對象問題
智能指針的交叉引用(循環引用)問題
請看下面的這個代碼示例:
#include <iostream>
#include <memory>
using namespace std;
class B; // 前置聲明類B
class A {
public:
A() { cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }
shared_ptr<B> _ptrb; // 指向B對象的智能指針
};
class B {
public:
B() { cout << "B()" << endl; }
~B() { cout << "~B()" << endl; }
shared_ptr<A> _ptra; // 指向A對象的智能指針
};
int main() {
shared_ptr<A> ptra(new A());// ptra指向A對象,A的引用計數為1
shared_ptr<B> ptrb(new B());// ptrb指向B對象,B的引用計數為1
ptra->_ptrb = ptrb;// A對象的成員變量_ptrb也指向B對象,B的引用計數為2
ptrb->_ptra = ptra;// B對象的成員變量_ptra也指向A對象,A的引用計數為2
cout << ptra.use_count() << endl; // 打印A的引用計數結果:2
cout << ptrb.use_count() << endl; // 打印B的引用計數結果:2
/**
* 出main函數作用域,ptra和ptrb兩個局部對象析構,分別給A對象和B對象的引用計數從2減到1,
* 達不到釋放A和B的條件(釋放的條件是A和B的引用計數為0),因此造成兩個new出來的A和B對象
* 無法釋放,導致內存泄露,這個問題就是“強智能指針的交叉引用(循環引用)問題”
**/
return 0;
}
代碼打印結果:
A()
B()
2
2
可以看到,A和B對象并沒有進行析構,通過上面的代碼示例,能夠看出來“交叉引用”的問題所在,就是對象無法析構,資源無法釋放,那怎么解決這個問題呢?請注意強弱智能指針的一個重要應用規則:定義對象時,用強智能指針shared_ptr,在其它地方引用對象時,使用弱智能指針weak_ptr。
弱智能指針weak_ptr區別于shared_ptr之處在于:
- weak_ptr不會改變資源的引用計數,只是一個觀察者的角色,通過觀察shared_ptr來判定資源是否存在
- weak_ptr持有的引用計數,不是資源的引用計數,而是同一個資源的觀察者的計數
- weak_ptr沒有提供常用的指針操作(如
*和->運算符重載),無法直接訪問資源,需要先通過lock方法提升為shared_ptr強智能指針,才能訪問資源
那么上面的代碼怎么修改,也就是如何解決帶引用計數的智能指針的交叉引用問題,代碼如下:
#include <iostream>
#include <memory>
using namespace std;
class B; // 前置聲明類B
class A {
public:
A() { cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }
weak_ptr<B> _ptrb; // 指向B對象的弱智能指針。引用對象時,用弱智能指針
};
class B {
public:
B() { cout << "B()" << endl; }
~B() { cout << "~B()" << endl; }
weak_ptr<A> _ptra; // 指向A對象的弱智能指針。引用對象時,用弱智能指針
};
int main() {
// 定義對象時,用強智能指針
shared_ptr<A> ptra(new A());// ptra指向A對象,A的引用計數為1
shared_ptr<B> ptrb(new B());// ptrb指向B對象,B的引用計數為1
// A對象的成員變量_ptrb也指向B對象,B的引用計數為1,因為是弱智能指針,引用計數沒有改變
ptra->_ptrb = ptrb;
// B對象的成員變量_ptra也指向A對象,A的引用計數為1,因為是弱智能指針,引用計數沒有改變
ptrb->_ptra = ptra;
cout << ptra.use_count() << endl; // 打印結果:1
cout << ptrb.use_count() << endl; // 打印結果:1
/**
* 出main函數作用域,ptra和ptrb兩個局部對象析構,分別給A對象和B對象的引用計數從1減到0,
* 達到釋放A和B的條件,因此new出來的A和B對象被析構掉,解決了“強智能指針的交叉引用(循環引用)問題”
**/
return 0;
}
代碼打印如下:
A()
B()
1
1
~B()
~A()
可以看到,A和B對象正常析構,問題解決!
多線程訪問共享對象問題
C++實現的開源網絡庫muduo,該源碼中對于智能指針的應用非常優秀,其中借助shared_ptr和weak_ptr解決了這樣一個問題,多線程訪問共享對象的線程安全問題,解釋如下:線程A和線程B訪問一個共享的對象,如果線程A正在析構這個對象的時候,線程B又要調用該共享對象的成員方法,此時可能線程A已經把對象析構完了,線程B再去訪問該對象,就會發生不可預期的錯誤。muduo將weak_ptr指向該對象,并通過提權的方式,使得將要被釋放的對象的生命周期得到延續。關于muduo網絡庫的實現細節,具體可參考這篇文章:??長文梳理muduo網絡庫核心代碼、剖析優秀編程細節
先看如下代碼:
#include <iostream>
#include <thread>
using namespace std;
class Test {
public:
// 構造Test對象,_ptr指向一塊int堆內存,初始值是20
Test() : _ptr(new int(20)) { cout << "Test()" << endl; }
// 析構Test對象,釋放_ptr指向的堆內存
~Test() {
delete _ptr;
_ptr = nullptr;
cout << "~Test()" << endl;
}
// 該show會在另外一個線程中被執行
void show() { cout << *_ptr << endl; }
private:
int *volatile _ptr;
};
void threadProc(Test *p) {
// 睡眠兩秒,此時main主線程已經把Test對象給delete析構掉了
std::this_thread::sleep_for(std::chrono::seconds(2));
/**
* 此時當前線程訪問了main線程已經析構的共享對象,結果未知,隱含bug。
* 此時通過p指針想訪問Test對象,需要判斷Test對象是否存活,如果Test對象存活,調用show方法沒有問題;
* 如果Test對象已經析構,調用show有問題!
**/
p->show();
}
int main() {
// 在堆上定義共享對象
Test *p = new Test();
// 使用C++11的線程類,開啟一個新線程,并傳入共享對象的地址p
std::thread t1(threadProc, p);
// 在main線程中析構Test共享對象
delete p;
// 等待子線程運行結束
t1.join();
return 0;
}
運行上面的代碼,發現在main主線程已經delete析構Test對象以后,子線程threadProc再去訪問Test對象的show方法,無法打印出*_ptr的值20。可以用shared_ptr和weak_ptr來解決多線程訪問共享對象的線程安全問題,上面代碼修改如下:
#include <iostream>
#include <thread>
#include <memory>
using namespace std;
class Test {
public:
// 構造Test對象,_ptr指向一塊int堆內存,初始值是20
Test() : _ptr(new int(20)) { cout << "Test()" << endl; }
// 析構Test對象,釋放_ptr指向的堆內存
~Test() {
delete _ptr;
_ptr = nullptr;
cout << "~Test()" << endl;
}
// 該show會在另外一個線程中被執行
void show() { cout << *_ptr << endl; }
private:
int *volatile _ptr;
};
void threadProc(weak_ptr<Test> pw) { // 通過弱智能指針觀察強智能指針
// 睡眠兩秒
std::this_thread::sleep_for(std::chrono::seconds(2));
/*
如果想訪問對象的方法,先通過pw的lock方法進行提升操作,把weak_ptr提升
為shared_ptr強智能指針,提升過程中,是通過檢測它所觀察的強智能指針保存
的Test對象的引用計數,來判定Test對象是否存活,ps如果為nullptr,說明Test對象
已經析構,不能再訪問;如果ps!=nullptr,則可以正常訪問Test對象的方法。
*/
shared_ptr<Test> ps = pw.lock();
if(ps != nullptr) {
ps->show();
}
}
int main() {
// 在堆上定義共享對象
shared_ptr<Test> p(new Test);
// 使用C++11的線程,開啟一個新線程,并傳入共享對象的弱智能指針
std::thread t1(threadProc, weak_ptr<Test>(p));
// 在main線程中析構Test共享對象
// 等待子線程運行結束
t1.join();
return 0;
}
運行上面的代碼,show方法可以打印出20,因為main線程調用了t1.join()方法等待子線程結束,此時pw通過lock提升為ps成功,見上面代碼示例。
如果設置t1為分離線程,讓main主線程結束,p智能指針析構,進而把Test對象析構,此時show方法已經不會被調用,因為在threadProc方法中,pw提升到ps時,lock方法判定Test對象已經析構,提升失敗!main函數代碼可以如下修改測試:
int main() {
// 在堆上定義共享對象
shared_ptr<Test> p(new Test);
// 使用C++11的線程,開啟一個新線程,并傳入共享對象的弱智能指針
std::thread t1(threadProc, weak_ptr<Test>(p));
// 在main線程中析構Test共享對象
// 設置子線程分離
t1.detach();
return 0;
}
該main函數運行后,最終的threadProc中,show方法不會被執行到。以上是在多線程中訪問共享對象時,對shared_ptr和weak_ptr的一個典型應用。
自定義刪除器
我們經常用智能指針管理的資源是堆內存,當智能指針出作用域的時候,在其析構函數中會delete釋放堆內存資源,但是除了堆內存資源,智能指針還可以管理其它資源,比如打開的文件,此時對于文件指針的關閉,就不能用delete了,這時我們需要自定義智能指針釋放資源的方式,先看看unique_ptr智能指針的析構函數代碼,如下:
~unique_ptr() noexcept
{ // destroy the object
if (get() != pointer())
{
this->get_deleter()(get()); // 這里獲取底層的刪除器,進行函數對象的調用
}
}
從unique_ptr的析構函數可以看到,如果要實現一個自定義的刪除器,實際上就是定義一個函數對象而已,示例代碼如下:
class FileDeleter {
public:
// 刪除器負責刪除資源的函數
void operator()(FILE *pf) {
fclose(pf);
}
};
int main() {
// 由于用智能指針管理文件資源,因此傳入自定義的刪除器類型FileDeleter
unique_ptr<FILE, FileDeleter> filePtr(fopen("data.txt", "w"));
return 0;
}
當然這種方式需要定義額外的函數對象類型,增加代碼復雜度和維護成本,所以不推薦額外實現函數對象,可以用C++11提供的函數對象function和lambda表達式更好的處理自定義刪除器,代碼如下:
int main() {
// 自定義智能指針刪除器,關閉文件資源
unique_ptr<FILE, function<void(FILE *)>>
filePtr(fopen("data.txt", "w"), [](FILE *pf)->void{fclose(pf);});
// 自定義智能指針刪除器,釋放數組資源
unique_ptr<int, function<void(int *)>>
arrayPtr(new int[100], [](int *ptr)->void{delete[]ptr;});
return 0;
}
總結
本篇文章介紹了智能指針的基本原理以及如何去實現一個智能指針,同時介紹了C++中不帶引用計數和帶引用計數的智能指針,特別要注意帶引用計數智能指針weak_ptr所能解決的兩個場景,其一是解決智能指針循環引用問題;其二是延長智能指針指向對象生命周期,保證多線程能安全訪問共享對象的問題。

浙公網安備 33010602011771號