C++ 智能指针最佳实践源码分析
作者:lucasfan,騰訊 IEG Global Pub.Tech. 客戶端工程師
智能指針在 C++11 標(biāo)準(zhǔn)中被引入真正標(biāo)準(zhǔn)庫(C++98 中引入的 auto_ptr 存在較多問題),但目前很多 C++開發(fā)者仍習(xí)慣用原生指針,視智能指針為洪水猛獸。但很多實(shí)際場景下,智能指針卻是解決問題的神器,尤其是一些涉及多線程的場景下。本文將介紹智能指針可以解決的問題,用法及最佳實(shí)踐。并且根據(jù)源碼分析智能指針的實(shí)現(xiàn)原理。
一、為什么需要使用智能指針
1.1 內(nèi)存泄漏
C++在堆上申請內(nèi)存后,需要手動對內(nèi)存進(jìn)行釋放。代碼的初創(chuàng)者可能會注意內(nèi)存的釋放,但隨著代碼協(xié)作者加入,或者隨著代碼日趨復(fù)雜,很難保證內(nèi)存都被正確釋放。
尤其是一些代碼分支在開發(fā)中沒有被完全測試覆蓋的時候,就算是內(nèi)存泄漏檢查工具也不一定能檢查到內(nèi)存泄漏。
void?test_memory_leak(bool?open) {A?*a?=?new?A();if(open){//?代碼變復(fù)雜過程中,很可能漏了?delete(a);return;}delete(a);return; }1.2 多線程下對象析構(gòu)問題
多線程遇上對象析構(gòu),是一個很難的問題,稍有不慎就會導(dǎo)致程序崩潰。因此在對于 C++開發(fā)者而言,經(jīng)常會使用靜態(tài)單例來使得對象常駐內(nèi)存,避免析構(gòu)帶來的問題。這勢必會造成內(nèi)存泄露,當(dāng)單例對象比較大,或者程序?qū)?nèi)存非常敏感的時候,就必須面對這個問題了。
先以一個常見的 C++多線程問題為例,介紹多線程下的對象析構(gòu)問題。
比如我們在開發(fā)過程中,經(jīng)常會在一個 Class 中創(chuàng)建一個線程,這個線程讀取外部對象的成員變量。
//?日志上報Class class?ReportClass { private:ReportClass()?{}ReportClass(const?ReportClass&)?=?delete;ReportClass&?operator=(const?ReportClass&)?=?delete;ReportClass(const?ReportClass&&)?=?delete;ReportClass&?operator=(const?ReportClass&&)?=?delete;private:std::mutex?mutex_;int?count_?=?0;void?addWorkThread();public:void?pushEvent(std::string?event);private:static?void?workThread(ReportClass?*report);private:static?ReportClass*?instance_;static?std::mutex?static_mutex_;public:static?ReportClass*?GetInstance();static?void?ReleaseInstance(); };std::mutex?ReportClass::static_mutex_; ReportClass*?ReportClass::instance_;ReportClass*?ReportClass::GetInstance() {//?單例簡單實(shí)現(xiàn),非本文重點(diǎn)std::lock_guard<std::mutex>?lock(static_mutex_);if?(instance_?==?nullptr)?{instance_?=?new?ReportClass();instance_->addWorkThread();}return?instance_; }void?ReportClass::ReleaseInstance() {std::lock_guard<std::mutex>?lock(static_mutex_);if(instance_?!=?nullptr){delete?instance_;instance_?=?nullptr;} }//?輪詢上報線程 void?ReportClass::workThread(ReportClass?*report) {while(true){//?線程運(yùn)行過程中,report可能已經(jīng)被銷毀了std::unique_lock<std::mutex>?lock(report->mutex_);if(report->count_?>?0){report->count_--;}usleep(1000*1000);} }//?創(chuàng)建任務(wù)線程 void?ReportClass::addWorkThread() {std::thread?new_thread(workThread,?this);new_thread.detach(); }//?外部調(diào)用 void?ReportClass::pushEvent(std::string?event) {std::unique_lock<std::mutex>?lock(mutex_);this->count_++; }使用 ReportClass 的代碼如下:
ReportClass::GetInstance()->pushEvent("test");但當(dāng)這個外部對象(即ReportClass)析構(gòu)時,對象創(chuàng)建的線程還在執(zhí)行。此時線程引用的對象指針為野指針,程序必然會發(fā)生異常。
解決這個問題的思路是在對象析構(gòu)的時候,對線程進(jìn)行join。
//?日志上報Class class?ReportClass { private://...~ReportClass();private://...bool?stop_?=?false;std::thread?*work_thread_;//... };//?輪詢上報線程 void?ReportClass::workThread(ReportClass?*report) {while(true){std::unique_lock<std::mutex>?lock(report->mutex_);//?如果上報停止,不再輪詢上報if(report->stop_){break;}if(report->count_?>?0){report->count_--;}usleep(1000*1000);} }//?創(chuàng)建任務(wù)線程 void?ReportClass::addWorkThread() {//?保存線程指針,不再使用分離線程work_thread_?=?new?std::thread(workThread,?this); }ReportClass::~ReportClass() {//?通過join來停止內(nèi)部線程stop_?=?true;work_thread_->join();delete?work_thread_;work_thread_?=?nullptr; }這種方式看起來沒問題了,但是由于這個對象一般是被多個線程使用。假如某個線程想要釋放這個對象,但另外一個線程還在使用這個對象,可能會出現(xiàn)野指針問題。就算釋放對象的線程將對象釋放后將指針置為nullptr,但仍然可能在多線程下在指針置空前被另外一個線程取得地址并使用。
| ReportClass::GetInstance()->ReleaseInstance(); | ReportClass *report = ReportClass::GetInstance(); if(report) { // 此時切換到線程 A report->pushEvent("test"); } |
此種場景下,鎖機(jī)制已經(jīng)很難解決這個問題。對于多線程下的對象析構(gòu)問題,智能指針可謂是神器。接下來我們先對智能指針的基本用法進(jìn)行說明。
二、智能指針的基本用法
智能指針設(shè)計的初衷就是可以幫助我們管理堆上申請的內(nèi)存,可以理解為開發(fā)者只需要申請,而釋放交給智能指針。
目前 C++11 主要支持的智能指針為以下幾種
unique_ptr
shared_ptr
weak_ptr
2.1 unique_ptr
先上代碼
class?A { public:void?do_something()?{} };void?test_unique_ptr(bool?open) {std::unique_ptr<A>?a(new?A());a->do_something();if(open){//?不再需要手動釋放內(nèi)存return;}//?不再需要手動釋放內(nèi)存return; }unique_ptr的核心特點(diǎn)就如它的名字一樣,它擁有對持有對象的唯一所有權(quán)。即兩個unique_ptr不能同時指向同一個對象。
那具體這個唯一所有權(quán)如何體現(xiàn)呢?
1、unique_ptr不能被復(fù)制到另外一個unique_ptr
2、unique_ptr所持有的對象只能通過轉(zhuǎn)移語義將所有權(quán)轉(zhuǎn)移到另外一個unique_ptr
std::unique_ptr<A>?a1(new?A()); std::unique_ptr<A>?a2?=?a1;//編譯報錯,不允許復(fù)制 std::unique_ptr<A>?a3?=?std::move(a1);//可以轉(zhuǎn)移所有權(quán),所有權(quán)轉(zhuǎn)義后a1不再擁有任何指針智能指針有一個通用的規(guī)則,就是->表示用于調(diào)用指針原有的方法,而.則表示調(diào)用智能指針本身的方法。
unique_ptr本身擁有的方法主要包括:
1、get() 獲取其保存的原生指針,盡量不要使用
2、bool() 判斷是否擁有指針
3、release() 釋放所管理指針的所有權(quán),返回原生指針。但并不銷毀原生指針。
4、reset() 釋放并銷毀原生指針。如果參數(shù)為一個新指針,將管理這個新指針
std::unique_ptr<A>?a1(new?A()); A?*origin_a?=?a1.get();//盡量不要暴露原生指針 if(a1) {//?a1?擁有指針 }std::unique_ptr<A>?a2(a1.release());//常見用法,轉(zhuǎn)義擁有權(quán) a2.reset(new?A());//釋放并銷毀原有對象,持有一個新對象 a2.reset();//釋放并銷毀原有對象,等同于下面的寫法 a2?=?nullptr;//釋放并銷毀原有對象2.2 shared_ptr
與unique_ptr的唯一所有權(quán)所不同的是,shared_ptr強(qiáng)調(diào)的是共享所有權(quán)。也就是說多個shared_ptr可以擁有同一個原生指針的所有權(quán)。
std::shared_ptr<A>?a1(new?A()); std::shared_ptr<A>?a2?=?a1;//編譯正常,允許所有權(quán)的共享shared_ptr 是通過引用計數(shù)的方式管理指針,當(dāng)引用計數(shù)為 0 時會銷毀擁有的原生對象。
shared_ptr本身擁有的方法主要包括:
1、get() 獲取其保存的原生指針,盡量不要使用
2、bool() 判斷是否擁有指針
3、reset() 釋放并銷毀原生指針。如果參數(shù)為一個新指針,將管理這個新指針
4、unique() 如果引用計數(shù)為 1,則返回 true,否則返回 false
5、use_count() 返回引用計數(shù)的大小
std::shared_ptr<A>?a1(new?A()); std::shared_ptr<A>?a2?=?a1;//編譯正常,允許所有權(quán)的共享A?*origin_a?=?a1.get();//盡量不要暴露原生指針if(a1) {//?a1?擁有指針 }if(a1.unique()) {//?如果返回true,引用計數(shù)為1 }long?a1_use_count?=?a1.use_count();//引用計數(shù)數(shù)量2.3 weak_ptr
weak_ptr 比較特殊,它主要是為了配合shared_ptr而存在的。就像它的名字一樣,它本身是一個弱指針,因?yàn)樗旧硎遣荒苤苯诱{(diào)用原生指針的方法的。如果想要使用原生指針的方法,需要將其先轉(zhuǎn)換為一個shared_ptr。那weak_ptr存在的意義到底是什么呢?
由于shared_ptr是通過引用計數(shù)來管理原生指針的,那么最大的問題就是循環(huán)引用(比如 a 對象持有 b 對象,b 對象持有 a 對象),這樣必然會導(dǎo)致內(nèi)存泄露。而weak_ptr不會增加引用計數(shù),因此將循環(huán)引用的一方修改為弱引用,可以避免內(nèi)存泄露。
weak_ptr可以通過一個shared_ptr創(chuàng)建。
std::shared_ptr<A>?a1(new?A()); std::weak_ptr<A>?weak_a1?=?a1;//不增加引用計數(shù)weak_ptr本身擁有的方法主要包括:
1、expired() 判斷所指向的原生指針是否被釋放,如果被釋放了返回 true,否則返回 false
2、use_count() 返回原生指針的引用計數(shù)
3、lock() 返回 shared_ptr,如果原生指針沒有被釋放,則返回一個非空的 shared_ptr,否則返回一個空的 shared_ptr
4、reset() 將本身置空
std::shared_ptr<A>?a1(new?A()); std::weak_ptr<A>?weak_a1?=?a1;//不增加引用計數(shù)if(weak_a1.expired()) {//如果為true,weak_a1對應(yīng)的原生指針已經(jīng)被釋放了 }long?a1_use_count?=?weak_a1.use_count();//引用計數(shù)數(shù)量if(std::shared_ptr<A>?shared_a?=?weak_a1.lock()) {//此時可以通過shared_a進(jìn)行原生指針的方法調(diào)用 }weak_a1.reset();//將weak_a1置空三、智能指針的最佳實(shí)踐
以上只是智能指針的基本用法,但是真正上手實(shí)踐的時候,卻發(fā)現(xiàn)程序在不經(jīng)意間崩潰了。踩過了幾次坑后,很多同學(xué)就罵罵咧咧的放棄了(什么辣雞東西)。因此想要用好智能指針還需要進(jìn)一步了解智能指針,甚至需要了解智能指針源碼實(shí)現(xiàn)。
這一節(jié)我們會基于基本用法,進(jìn)一步說明智能指針的實(shí)踐用法,一起馴服智能指針這頭野獸。
3.1 智能指針如何選擇
在介紹指針如何選擇之前,我們先回顧一下這幾個指針的特點(diǎn)
1、unique_ptr獨(dú)占對象的所有權(quán),由于沒有引用計數(shù),因此性能較好
2、shared_ptr共享對象的所有權(quán),但性能略差
3、weak_ptr配合shared_ptr,解決循環(huán)引用的問題
由于性能問題,那么可以粗暴的理解:優(yōu)先使用unique_ptr。但由于unique_ptr不能進(jìn)行復(fù)制,因此部分場景下不能使用的。
3.1.1 unique_ptr 的使用場景
unique_ptr一般在不需要多個指向同一個對象的指針時使用。但這個條件本身就很難判斷,在我看來可以簡單的理解:這個對象在對象或方法內(nèi)部使用時優(yōu)先使用unique_ptr。
1、對象內(nèi)部使用
class?TestUnique { private:std::unique_ptr<A>?a_?=?std::unique_ptr<A>(new?A()); public:void?process1(){a_->do_something();}void?process2(){a_->do_something();}~TestUnique(){//此處不再需要手動刪除a_} };2、方法內(nèi)部使用
void?test_unique_ptr() {std::unique_ptr<A>?a(new?A());a->do_something(); }3.1.2 shared_ptr 的使用場景及最佳實(shí)踐
shared_ptr一般在需要多個執(zhí)行同一個對象的指針使用。在我看來可以簡單的理解:這個對象需要被多個 Class 同時使用的時候。
class?B { private:std::shared_ptr<A>?a_;public:B(std::shared_ptr<A>&?a):?a_(a)?{} };class?C { private:std::shared_ptr<A>?a_;public:C(std::shared_ptr<A>&?a):?a_(a)?{} };std::shared_ptr<B>?b_; std::shared_ptr<C>?c_;void?test_A_B_C() {std::shared_ptr<A>?a?=?std::make_shared<A>();b_?=?std::make_shared<B>(a);c_?=?std::make_shared<C>(a); }在上面的代碼中需要注意,我們使用std::make_shared代替new的方式創(chuàng)建shared_ptr。
因?yàn)槭褂胣ew的方式創(chuàng)建shared_ptr會導(dǎo)致出現(xiàn)兩次內(nèi)存申請,而std::make_shared在內(nèi)部實(shí)現(xiàn)時只會申請一個內(nèi)存。因此建議后續(xù)均使用std::make_shared。
如果A想要調(diào)用B和C的方法怎么辦呢?可否在A中定義B和C的shared_ptr呢?答案是不可以,這樣會產(chǎn)生循環(huán)引用,導(dǎo)致內(nèi)存泄露。
此時就需要weak_ptr出場了。
class?A { private:std::weak_ptr<B>?b_;std::weak_ptr<C>?c_; public:void?do_something()?{}void?set_B_C(const?std::shared_ptr<B>&?b,?const?std::shared_ptr<C>&?c){b_?=?b;c_?=?c;} };a->set_B_C(b_,?c_);如果想要在A內(nèi)部將當(dāng)前對象的指針共享給其他對象,需要怎么處理呢?
class?D { private:std::shared_ptr<A>?a_;public:std::shared_ptr<A>&?a):?a_(a)?{} };class?A { //上述代碼省略public:void?new_D(){//錯誤方式,用this指針重新構(gòu)造shared_ptr,將導(dǎo)致二次釋放當(dāng)前對象std::shared_ptr<A>?this_shared_ptr1(this);std::unique_ptr<D>?d1(new?D(this_shared_ptr1));} };如果采用this指針重新構(gòu)造shared_ptr是肯定不行的,因?yàn)橹匦聞?chuàng)建的shared_ptr與當(dāng)前對象的shared_ptr沒有關(guān)系,沒有增加當(dāng)前對象的引用計數(shù)。這將導(dǎo)致任何一個shared_ptr計數(shù)為 0 時提前釋放了對象,后續(xù)操作這個釋放的對象都會導(dǎo)致程序異常。
此時就需要引入shared_from_this。對象繼承了enable_shared_from_this后,可以通過shared_from_this()獲取當(dāng)前對象的shared_ptr指針。
class?A:?public?std::enable_shared_from_this<A> { //上述代碼省略public:void?new_D(){//錯誤方式,用this指針重新構(gòu)造shared_ptr,將導(dǎo)致二次釋放當(dāng)前對象std::shared_ptr<A>?this_shared_ptr1(this);std::unique_ptr<D>?d1(new?D(this_shared_ptr1));//正確方式std::shared_ptr<A>?this_shared_ptr2?=?shared_from_this();std::unique_ptr<D>?d2(new?D(this_shared_ptr2));} };3.2 智能指針的錯誤用法
智能指針的使用時有較多常見的錯誤用法,可能會導(dǎo)致程序異常。下面我會列舉這些錯誤用法,開發(fā)時需要避免。
1、使用智能指針托管的對象,盡量不要在再使用原生指針
很多開發(fā)同學(xué)(包括我在內(nèi))在最開始使用智能指針的時候,對同一個對象會混用智能指針和原生指針,導(dǎo)致程序異常。
void?incorrect_smart_pointer1() {A?*a=?new?A();std::unique_ptr<A>?unique_ptr_a(a);//?此處將導(dǎo)致對象的二次釋放delete?a; }2、不要把一個原生指針交給多個智能指針管理
如果將一個原生指針交個多個智能指針,這些智能指針釋放對象時會產(chǎn)生對象的多次銷毀
void?incorrect_smart_pointer2() {A?*a=?new?A();std::unique_ptr<A>?unique_ptr_a1(a);std::unique_ptr<A>?unique_ptr_a2(a);//?此處將導(dǎo)致對象的二次釋放 }3、盡量不要使用 get()獲取原生指針
void?incorrect_smart_pointer3() {std::shared_ptr<A>?shared_ptr_a1?=?std::make_shared<A>();A?*a=?shared_ptr_a1.get();std::shared_ptr<A>?shared_ptr_a2(a);//?此處將導(dǎo)致對象的二次釋放delete?a;//?此處也將導(dǎo)致對象的二次釋放 }4、不要將 this 指針直接托管智能指針
class?E {void?use_this(){//錯誤方式,用this指針重新構(gòu)造shared_ptr,將導(dǎo)致二次釋放當(dāng)前對象std::shared_ptr<E>?this_shared_ptr1(this);} };std::shared_ptr<E>?e?=?std::make_shared<E>();5、智能指針只能管理堆對象,不能管理?xiàng)I蠈ο?/p>
棧上對象本身在出棧時就會被自動銷毀,如果將其指針交給智能指針,會造成對象的二次銷毀
void?incorrect_smart_pointer5() {int?int_num?=?3;std::unique_ptr<int>?int_unique_ptr(&int_num); }3.3 解決多線程下對象析構(gòu)問題
有了智能指針之后,我們就可以使用智能指針解決多線程下的對象析構(gòu)問題。
我們使用shared_ptr管理ReportClass。并將 weak_ptr傳給子線程,子線程會判斷外部的ReportClass是否已經(jīng)被銷毀,如果沒有被銷毀會通過weak_ptr換取shared_ptr,否則線程退出。解決了外部對象銷毀,內(nèi)部線程使用外部對象的野指針的問題。
//?日志上報Class class?ReportClass:?public?std::enable_shared_from_this<ReportClass> {//...private:static?void?workThread(std::weak_ptr<ReportClass>?weak_report_ptr);private:static?std::shared_ptr<ReportClass>?instance_;static?std::mutex?static_mutex_;public:static?std::shared_ptr<ReportClass>?GetInstance();static?void?ReleaseInstance(); };std::mutex?ReportClass::static_mutex_; std::shared_ptr<ReportClass>?ReportClass::instance_;std::shared_ptr<ReportClass>?ReportClass::GetInstance() {//?單例簡單實(shí)現(xiàn),非本文重點(diǎn)std::lock_guard<std::mutex>?lock(static_mutex_);if?(!instance_)?{instance_?=?std::shared_ptr<ReportClass>(new?ReportClass());instance_->addWorkThread();}return?instance_; }void?ReportClass::ReleaseInstance() {std::lock_guard<std::mutex>?lock(static_mutex_);if(instance_){instance_.reset();} }//?輪詢上報線程 void?ReportClass::workThread(std::weak_ptr<ReportClass>?weak_report_ptr) {while(true){std::shared_ptr<ReportClass>?shared_report_ptr?=?weak_report_ptr.lock();if(!shared_report_ptr){return;}std::unique_lock<std::mutex>(shared_report_ptr->mutex_);if(shared_report_ptr->count_?>?0){shared_report_ptr->count_--;}usleep(1000*1000);} }//?創(chuàng)建任務(wù)線程 void?ReportClass::addWorkThread() {std::weak_ptr<ReportClass>?weak_report_ptr?=?shared_from_this();std::thread?work_thread(workThread,?weak_report_ptr);work_thread.detach(); }//?外部調(diào)用 void?ReportClass::pushEvent(std::string?event) {std::unique_lock<std::mutex>?lock(mutex_);this->count_++; }并且在多個線程使用的時候,由于采用shared_ptr管理,因此只要有shared_ptr持有對象,就不會銷毀對象,因此不會出現(xiàn)多個線程使用時對象被析構(gòu)的情況。只有該對象的所有shared_ptr都被銷毀的時候,對象的內(nèi)存才會被釋放,保證的對象析構(gòu)的安全。
四、智能指針源碼解析
在介紹智能指針源碼前,需要明確的是,智能指針本身是一個棧上分配的對象。根據(jù)棧上分配的特性,在離開作用域后,會自動調(diào)用其析構(gòu)方法。智能指針根據(jù)這個特性實(shí)現(xiàn)了對象內(nèi)存的管理和自動釋放。
本文所分析的智能指針源碼基于 Android ndk-16b 中 llvm-libc++的 memory 文件。
4.1 unique_ptr
先看下 unique_ptr的聲明。unique_ptr有兩個模板參數(shù),分別為_Tp和_Dp。
_Tp表示原生指針的類型。
_Dp則表示析構(gòu)器,開發(fā)者可以自定義指針銷毀的代碼。其擁有一個默認(rèn)值default_delete<_Tp>,其實(shí)就是標(biāo)準(zhǔn)的delete函數(shù)。
函數(shù)聲明中typename __pointer_type<_Tp, deleter_type>::type可以簡單理解為_Tp*,即原生指針類型。
template <class _Tp, class _Dp = default_delete<_Tp> > class _LIBCPP_TEMPLATE_VIS unique_ptr { public:typedef _Tp element_type;typedef _Dp deleter_type;typedef typename __pointer_type<_Tp, deleter_type>::type pointer;//... }unique_ptr中唯一的數(shù)據(jù)成員就是原生指針和析構(gòu)器的 pair。
private:__compressed_pair<pointer,?deleter_type>?__ptr_;下面看下unique_ptr的構(gòu)造函數(shù)。
template?<class?_Tp,?class?_Dp?=?default_delete<_Tp>?> class?_LIBCPP_TEMPLATE_VIS?unique_ptr?{public://?默認(rèn)構(gòu)造函數(shù),用pointer的默認(rèn)構(gòu)造函數(shù)初始化__ptr_constexpr?unique_ptr()?noexcept?:?__ptr_(pointer())?{}//?空指針的構(gòu)造函數(shù),同上constexpr?unique_ptr(nullptr_t)?noexcept?:?__ptr_(pointer())?{}//?原生指針的構(gòu)造函數(shù),用原生指針初始化__ptr_explicit?unique_ptr(pointer?__p)?noexcept?:?__ptr_(__p)?{}//?原生指針和析構(gòu)器的構(gòu)造函數(shù),用這兩個參數(shù)初始化__ptr_,當(dāng)前析構(gòu)器為左值引用unique_ptr(pointer?__p,?_LValRefType<_Dummy>?__d)?noexcept:?__ptr_(__p,?__d)?{}//?原生指針和析構(gòu)器的構(gòu)造函數(shù),析構(gòu)器使用轉(zhuǎn)移語義進(jìn)行轉(zhuǎn)移unique_ptr(pointer?__p,?_GoodRValRefType<_Dummy>?__d)?noexcept:?__ptr_(__p,?_VSTD::move(__d))?{static_assert(!is_reference<deleter_type>::value,"rvalue?deleter?bound?to?reference");}//?移動構(gòu)造函數(shù),取出原有unique_ptr的指針和析構(gòu)器進(jìn)行構(gòu)造unique_ptr(unique_ptr&&?__u)?noexcept:?__ptr_(__u.release(),?_VSTD::forward<deleter_type>(__u.get_deleter()))?{}//?移動賦值函數(shù),取出原有unique_ptr的指針和析構(gòu)器進(jìn)行構(gòu)造unique_ptr&?operator=(unique_ptr&&?__u)?_NOEXCEPT?{reset(__u.release());__ptr_.second()?=?_VSTD::forward<deleter_type>(__u.get_deleter());return?*this;}}再看下unique_ptr幾個常用函數(shù)的實(shí)現(xiàn)。
template?<class?_Tp,?class?_Dp?=?default_delete<_Tp>?> class?_LIBCPP_TEMPLATE_VIS?unique_ptr?{//?返回原生指針 pointer?get()?const?_NOEXCEPT?{return?__ptr_.first(); }//?判斷原生指針是否為空 _LIBCPP_EXPLICIT?operator?bool()?const?_NOEXCEPT?{return?__ptr_.first()?!=?nullptr; }//?將__ptr置空,并返回原有的指針 pointer?release()?_NOEXCEPT?{pointer?__t?=?__ptr_.first();__ptr_.first()?=?pointer();return?__t; }//?重置原有的指針為新的指針,如果原有指針不為空,對原有指針?biāo)笇ο筮M(jìn)行銷毀 void?reset(pointer?__p?=?pointer())?_NOEXCEPT?{pointer?__tmp?=?__ptr_.first();__ptr_.first()?=?__p;if?(__tmp)__ptr_.second()(__tmp); } }再看下unique_ptr指針特性的兩個方法。
//?返回原生指針的引用 typename?add_lvalue_reference<_Tp>::type operator*()?const?{return?*__ptr_.first(); } //?返回原生指針 pointer?operator->()?const?_NOEXCEPT?{return?__ptr_.first(); }最后再看下unique_ptr的析構(gòu)函數(shù)。
//?通過reset()方法進(jìn)行對象的銷毀 ~unique_ptr()?{?reset();?}4.2 shared_ptr
shared_ptr 與unique_ptr最核心的區(qū)別就是比unique_ptr多了一個引用計數(shù),并由于引用計數(shù)的加入,可以支持拷貝。
先看下shared_ptr的聲明。shared_ptr主要有兩個成員變量,一個是原生指針,一個是控制塊的指針,用來存儲這個原生指針的shared_ptr和weak_ptr的數(shù)量。
template<class?_Tp> class?shared_ptr { public:typedef?_Tp?element_type;private:element_type*??????__ptr_;__shared_weak_count*?__cntrl_;//... }我們重點(diǎn)看下__shared_weak_count的定義。
//?共享計數(shù)類 class?__shared_count {__shared_count(const?__shared_count&);__shared_count&?operator=(const?__shared_count&);protected://?共享計數(shù)long?__shared_owners_;virtual?~__shared_count(); private://?引用計數(shù)變?yōu)?的回調(diào),一般是進(jìn)行內(nèi)存釋放virtual?void?__on_zero_shared()?_NOEXCEPT?=?0;public://?構(gòu)造函數(shù),需要注意內(nèi)部存儲的引用計數(shù)是從0開始,外部看到的引用計數(shù)其實(shí)為1explicit?__shared_count(long?__refs?=?0)?_NOEXCEPT:?__shared_owners_(__refs)?{}//?增加共享計數(shù)void?__add_shared()?_NOEXCEPT?{__libcpp_atomic_refcount_increment(__shared_owners_);}//?釋放共享計數(shù),如果共享計數(shù)為0(內(nèi)部為-1),則調(diào)用__on_zero_shared進(jìn)行內(nèi)存釋放bool?__release_shared()?_NOEXCEPT?{if?(__libcpp_atomic_refcount_decrement(__shared_owners_)?==?-1)?{__on_zero_shared();return?true;}return?false;}//?返回引用計數(shù),需要對內(nèi)部存儲的引用計數(shù)+1處理long?use_count()?const?_NOEXCEPT?{return?__libcpp_relaxed_load(&__shared_owners_)?+?1;} };class?__shared_weak_count:?private?__shared_count {//?weak?ptr計數(shù)long?__shared_weak_owners_;public://?內(nèi)部共享計數(shù)和weak計數(shù)都為0explicit?__shared_weak_count(long?__refs?=?0)?_NOEXCEPT:?__shared_count(__refs),__shared_weak_owners_(__refs)?{} protected:virtual?~__shared_weak_count();public://?調(diào)用通過父類的__add_shared,增加共享引用計數(shù)void?__add_shared()?_NOEXCEPT?{__shared_count::__add_shared();}//?增加weak引用計數(shù)void?__add_weak()?_NOEXCEPT?{__libcpp_atomic_refcount_increment(__shared_weak_owners_);}//?調(diào)用父類的__release_shared,如果釋放了原生指針的內(nèi)存,還需要調(diào)用__release_weak,因?yàn)閮?nèi)部weak計數(shù)默認(rèn)為0void?__release_shared()?_NOEXCEPT?{if?(__shared_count::__release_shared())__release_weak();}//?weak引用計數(shù)減1void?__release_weak()?_NOEXCEPT;//?獲取共享計數(shù)long?use_count()?const?_NOEXCEPT?{return?__shared_count::use_count();}__shared_weak_count*?lock()?_NOEXCEPT;private://?weak計數(shù)為0的處理virtual?void?__on_zero_shared_weak()?_NOEXCEPT?=?0; };其實(shí)__shared_weak_count也是虛類,具體使用的是__shared_ptr_pointer。__shared_ptr_pointer中有一個成員變量__data_,用于存儲原生指針、析構(gòu)器、分配器。__shared_ptr_pointer繼承了__shared_weak_count,因此它就主要負(fù)責(zé)內(nèi)存的分配、銷毀,引用計數(shù)。
class?__shared_ptr_pointer:?public?__shared_weak_count {__compressed_pair<__compressed_pair<_Tp,?_Dp>,?_Alloc>?__data_; public:_LIBCPP_INLINE_VISIBILITY__shared_ptr_pointer(_Tp?__p,?_Dp?__d,?_Alloc?__a):??__data_(__compressed_pair<_Tp,?_Dp>(__p,?_VSTD::move(__d)),?_VSTD::move(__a))?{}#ifndef?_LIBCPP_NO_RTTIvirtual?const?void*?__get_deleter(const?type_info&)?const?_NOEXCEPT; #endifprivate:virtual?void?__on_zero_shared()?_NOEXCEPT;virtual?void?__on_zero_shared_weak()?_NOEXCEPT; };了解了引用計數(shù)的基本原理后,再看下shared_ptr的實(shí)現(xiàn)。
//?使用原生指針構(gòu)造shared_ptr時,會構(gòu)建__shared_ptr_pointer的控制塊 shared_ptr<_Tp>::shared_ptr(_Yp*?__p,typename?enable_if<is_convertible<_Yp*,?element_type*>::value,?__nat>::type):?__ptr_(__p) {unique_ptr<_Yp>?__hold(__p);typedef?typename?__shared_ptr_default_allocator<_Yp>::type?_AllocT;typedef?__shared_ptr_pointer<_Yp*,?default_delete<_Yp>,?_AllocT?>?_CntrlBlk;__cntrl_?=?new?_CntrlBlk(__p,?default_delete<_Yp>(),?_AllocT());__hold.release();__enable_weak_this(__p,?__p); }//?如果進(jìn)行shared_ptr的拷貝,會增加引用計數(shù) template<class?_Tp> inline shared_ptr<_Tp>::shared_ptr(const?shared_ptr&?__r)?_NOEXCEPT:?__ptr_(__r.__ptr_),__cntrl_(__r.__cntrl_) {if?(__cntrl_)__cntrl_->__add_shared(); }//?銷毀shared_ptr時,會使共享引用計數(shù)減1,如果減到0會銷毀內(nèi)存 template<class?_Tp> shared_ptr<_Tp>::~shared_ptr() {if?(__cntrl_)__cntrl_->__release_shared(); }4.3 weak_ptr
了解完shared_ptr,weak_ptr也就比較簡單了。weak_ptr也包括兩個對象,一個是原生指針,一個是控制塊。雖然weak_ptr內(nèi)存儲了原生指針,不過由于未實(shí)現(xiàn)operator->因此不能直接使用。
class?_LIBCPP_TEMPLATE_VIS?weak_ptr { public:typedef?_Tp?element_type; private:element_type*????????__ptr_;__shared_weak_count*?__cntrl_;}//?通過shared_ptr構(gòu)造weak_ptr。會將shared_ptr的成員變量地址進(jìn)行復(fù)制。增加weak引用計數(shù) weak_ptr<_Tp>::weak_ptr(shared_ptr<_Yp>?const&?__r,typename?enable_if<is_convertible<_Yp*,?_Tp*>::value,?__nat*>::type)_NOEXCEPT:?__ptr_(__r.__ptr_),__cntrl_(__r.__cntrl_) {if?(__cntrl_)__cntrl_->__add_weak(); }//?weak_ptr析構(gòu)器 template<class?_Tp> weak_ptr<_Tp>::~weak_ptr() {if?(__cntrl_)__cntrl_->__release_weak(); }最近熱文:
淺談 K8s 網(wǎng)絡(luò)模型CNI協(xié)議
提速 30%!騰訊TQUIC 網(wǎng)絡(luò)傳輸協(xié)議
大牛書單 | 消息隊(duì)列方向的好書
總結(jié)
以上是生活随笔為你收集整理的C++ 智能指针最佳实践源码分析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 基于 iframe 的全新微前端方案
- 下一篇: C++ 学习笔记