1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        智能指針-使用、避坑和實(shí)現(xiàn)

        共 27008字,需瀏覽 55分鐘

         ·

        2022-08-03 11:05

        大家好,今天借助本文,從實(shí)踐、避坑實(shí)現(xiàn)原理三個角度分析下C++中的智能指針。

        本文主要內(nèi)容如下圖所示:

        1. 智能指針的由來
        2. auto_ptr為什么被廢棄
        3. unique_ptr的使用、特點(diǎn)以及實(shí)現(xiàn)
        4. shared_ptr的使用、特點(diǎn)以及實(shí)現(xiàn)
        5. weak_ptr的使用、特點(diǎn)以及實(shí)現(xiàn)
        6. 介紹筆者在工作中遇到的一些職能指針相關(guān)的坑,并給出一些建議

        背景

        內(nèi)存的分配與回收都是由開發(fā)人員在編寫代碼時主動完成的,好處是內(nèi)存管理的開銷較小,程序擁有更高的執(zhí)行效率;弊端是依賴于開發(fā)者的水平,隨著代碼規(guī)模的擴(kuò)大,極容易遺漏釋放內(nèi)存的步驟,或者一些不規(guī)范的編程可能會使程序具有安全隱患。如果對內(nèi)存管理不當(dāng),可能導(dǎo)致程序中存在內(nèi)存缺陷,甚至?xí)谶\(yùn)行時產(chǎn)生內(nèi)存故障錯誤。換句話說,開發(fā)者自己管理內(nèi)存,最容易發(fā)生下面兩種情況:

        • 申請了內(nèi)存卻沒有釋放,造成內(nèi)存泄漏
        • 使用已經(jīng)釋放的內(nèi)存,造成segment fault

        所以,為了在保證性能的前提下,又能使得開發(fā)者不需要關(guān)心內(nèi)存的釋放,進(jìn)而使得開發(fā)者能夠?qū)⒏嗟木ν度氲綐I(yè)務(wù)上,自C++11開始,STL正式引入了智能指針。

        所有權(quán)

        智能指針一個很關(guān)鍵的一個點(diǎn)就是是否擁有一個對象的所有權(quán),當(dāng)我們通過std::make_xxx或者new一個對象,那么就擁有了這個對象的所有權(quán)。

        所有權(quán)分為獨(dú)占所有權(quán)共享所有權(quán)以及弱共享所有權(quán)三種。

        獨(dú)占所有權(quán)

        顧名思義,獨(dú)占該對象。獨(dú)占的意思就是不共享,所有權(quán)可以轉(zhuǎn)移,但是轉(zhuǎn)移之后,所有權(quán)也是獨(dú)占。auto_ptr和unique_ptr就是一種獨(dú)占所有權(quán)方式的智能指針。

        假設(shè)有個Object對象,如果A擁有該對象的話,就需要保證其在不使用該對象的時候,將該對象釋放;而此時如果B也想擁有Object對象,那么就必須先讓A放棄該對象所有權(quán),然后B獨(dú)享該對象,那么該對象的使用和釋放就只歸B所有,跟A沒有關(guān)系了。

        獨(dú)占所有權(quán)具有以下幾個特點(diǎn):

        • 如果創(chuàng)建或者復(fù)制了某個對象,就擁有了該對象
        • 如果沒有創(chuàng)建對象,而是將對象保留使用,同樣擁有該對象的所有權(quán)
        • 如果你擁有了某個對象的所有權(quán),在不需要某一個對象時,需要釋放它們

        共享所有權(quán)

        共享所有權(quán),與獨(dú)占所有權(quán)正好相反,對某個對象的所有權(quán)可以共享。shared_ptr就是一種共享所有權(quán)方式的智能指針。

        假設(shè)此時A擁有對象Object,在沒有其它擁有該對對象的情況下,對象的釋放由A來負(fù)責(zé);如果此時B也想擁有該對象,那么對象的釋放由最后一個擁有它的來負(fù)責(zé)。

        舉一個我們經(jīng)常遇到的例子,socket連接,多個發(fā)送端(sender)可以使用其發(fā)送和接收數(shù)據(jù)。

        弱共享所有權(quán)

        弱共享所有權(quán),指的是可以使用該對象,但是沒有所有權(quán),由真正擁有其所有權(quán)的來負(fù)責(zé)釋放。weak_ptr就是一種弱共享所有權(quán)方式的智能指針。

        分類

        在C++11中,有unique_ptr、shared_ptr以及weak_ptr三種,auto_ptr因?yàn)樽陨?code style="overflow-wrap: break-word;margin-right: 2px;margin-left: 2px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(53, 148, 247);background: rgba(59, 170, 250, 0.1);padding-right: 2px;padding-left: 2px;border-radius: 2px;height: 21px;line-height: 22px;">轉(zhuǎn)移所有權(quán)的原因,在C++11中被廢棄(本節(jié)最后,將簡單說下被廢棄的原因)。

        • unique_ptr

          • 使用上限制最多的一種智能指針,被用來取代之前的auto_ptr,一個對象只能被一個unique_ptr所擁有,而不能被共享,如果需要將其所擁有的對象轉(zhuǎn)移給其他unique_ptr,則需要使用move語義
        • shared_ptr

          • 與unique_ptr不同的是,unique_ptr是獨(dú)占管理權(quán),而shared_ptr則是共享管理權(quán),即多個shared_ptr可以共用同一塊關(guān)聯(lián)對象,其內(nèi)部采用的是引用計(jì)數(shù),在拷貝的時候,引用計(jì)數(shù)+1,而在某個對象退出作用域或者釋放的時候,引用計(jì)數(shù)-1,當(dāng)引用計(jì)數(shù)為0的時候,會自動釋放其管理的對象。
        • weak_ptr

          • weak_ptr的出現(xiàn),主要是為了解決shared_ptr的循環(huán)引用,其主要是與shared_ptr一起來使用。和shared_ptr不同的地方在于,其并不會擁有資源,也就是說不能訪問對象所提供的成員函數(shù),不過,可以通過weak_ptr.lock()來產(chǎn)生一個擁有訪問權(quán)限的shared_ptr。

        auto_ptr

        auto_ptr自C++98被引入,因?yàn)槠浯嬖谳^多問題,所以在c++11中被廢棄,自C++17開始正式從STL中移除。

        首先我們看下auto_ptr的簡單實(shí)現(xiàn)(為了方便閱讀,進(jìn)行了修改,基本功能類似于std::auto_ptr):

        template<class T
        class auto_ptr 
        {
         
            T* p; 
        public
            auto_ptr(T* s) :p(s) {} 
            ~auto_ptr() { delete p; } 
             
            auto_ptr(auto_ptr& a) { 
              p = a.p; 
              a.p = NULL
            } 
            auto_ptroperator=(auto_ptr& a) { 
              delete p; 
              p=a.p; 
              a.p = NULL
              return *this
            } 
         
            T& operator*() const { return *p; } 
            T* operator->() const { return p; } 
        }; 

        從上面代碼可以看出,auto_ptr采用copy語義來轉(zhuǎn)移所有權(quán),轉(zhuǎn)移之后,其關(guān)聯(lián)的資源指針設(shè)置為NULL,而這跟我們理解上copy行為不一致。

        在<< Effective STL >>第8條,作者提出永不建立auto_ptr的容器,并以一個例子來說明原因,感興趣的可以去看看這本書,還是不錯的。

        實(shí)際上,auto_ptr被廢棄的直接原因是拷貝造成所有權(quán)轉(zhuǎn)移,如下代碼:

        auto_ptr<ClassA> a(new ClassA);
        auto_ptr<ClassA> b = a;
        a->Method();

        在上述代碼中,因?yàn)?code style="overflow-wrap: break-word;margin-right: 2px;margin-left: 2px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(53, 148, 247);background: rgba(59, 170, 250, 0.1);padding-right: 2px;padding-left: 2px;border-radius: 2px;height: 21px;line-height: 22px;">b = a導(dǎo)致所有權(quán)被轉(zhuǎn)移,即a關(guān)聯(lián)的對象為NULL,如果再調(diào)用a的成員函數(shù),顯然會造成coredump。

        正是因?yàn)?code style="overflow-wrap: break-word;margin-right: 2px;margin-left: 2px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(53, 148, 247);background: rgba(59, 170, 250, 0.1);padding-right: 2px;padding-left: 2px;border-radius: 2px;height: 21px;line-height: 22px;">拷貝導(dǎo)致所有權(quán)被轉(zhuǎn)移,所以auto_ptr使用上有很多限制:

        • 不能在STL容器中使用,因?yàn)閺?fù)制將導(dǎo)致數(shù)據(jù)無效
        • 一些STL算法也可能導(dǎo)致auto_ptr失效,比如std::sort算法
        • 不能作為函數(shù)參數(shù),因?yàn)檫@會導(dǎo)致復(fù)制,并且在調(diào)用后,導(dǎo)致原數(shù)據(jù)無效
        • 如果作為類的成員變量,需要注意在類拷貝時候?qū)е碌臄?shù)據(jù)無效

        正是因?yàn)閍uto_ptr的諸多限制,所以自C++11起,廢棄了auto_ptr,引入unique_ptr。

        unique_ptr

        unique_ptr是C++11提供的用于防止內(nèi)存泄漏的智能指針中的一種實(shí)現(xiàn)(用來替代auto_ptr),獨(dú)享被管理對象指針?biāo)袡?quán)的智能指針。

        unique_ptr對象包裝一個原始指針,并負(fù)責(zé)其生命周期。當(dāng)該對象被銷毀時,會在其析構(gòu)函數(shù)中刪除關(guān)聯(lián)的原始指針。具有->和*運(yùn)算符重載符,因此它可以像普通指針一樣使用。

        分類

        unique_ptr分為以下兩種:

        • 指向單個對象
        std::unique_ptr<Type> p1; // p1關(guān)聯(lián)Type對象
        • 指向一個數(shù)組
        unique_ptr<Type[]> p2; // p2關(guān)聯(lián)Type對象數(shù)組

        特點(diǎn)

        在前面的內(nèi)容中,我們已經(jīng)提到了unique_ptr的特點(diǎn),主要具有以下:

        • 獨(dú)享所有權(quán),在作用域結(jié)束時候,自動釋放所關(guān)聯(lián)的對象
        void fun() {
          unique_ptr<inta(new int(1));
        }
        • 無法進(jìn)行拷貝與賦值操作
        unique_ptr<intptr(new int(1));
        unique_ptr<intptr1(ptr) // error
        unique_ptr<int> ptr2 = ptr; //error
        • 顯示的所有權(quán)轉(zhuǎn)移(通過move語義)
        unique_ptr<intptr(new int(1));
        unique_ptr<int> ptr1 = std::move(ptr) ; // ok
        • 作為容器元素存儲在容器中
        unique_ptr<intptr(new int(1));
        std::vector<unique_ptr<int>> v;

        v.push_back(ptr); // error
        v.push_back(std::move(ptr)); // ok

        std::cout << *ptr << std::endl;// error

        需要注意的是,自c++14起,可以使用下面的方式對unique_ptr進(jìn)行初始化:

        auto p1 = std::make_unique<double>(3.14);
        auto p2 = std::make_unique<double[]>(n);

        如果在c++11中使用上述方法進(jìn)行初始化,會得到下面的錯誤提示:

        error: ‘make_unique’ is not a member of ‘std’

        因此,如果為了使得c++11也可以使用std::make_unique,我們可以自己進(jìn)行封裝,如下:

        namespace details {

        #if __cplusplus >= 201402L // C++14及以后使用STL實(shí)現(xiàn)的
        using std::make_unique;
        #else
        template<typename T, typename... Args>
        std::unique_ptr<T> make_unique(Args &&... args)
        {
            return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
        }
        #endif
        // namespace details

        使用

        為了盡可能了解unique_ptr的使用姿勢,我們使用以下代碼為例:

        #include <memory>
        #include <utility> // std::move

        void fun1(double *);
        void fun2(std::unique<double> *);
        void fun3(std::unique<double> &);
        void fun4(std::unique<double> );

        int main() {
          std::unique_ptr<doublep(new double(3.14));
          
          fun1(p.get());
          fun2(&p);
          fun3(p);
          
          if (p) {
            std::cout << "is valid" << std::endl;
          }
          auto p2(p.release())// 轉(zhuǎn)移所有權(quán)
          auto p2.reset(new double(1.0));
          fun4(std::move(p2));
          
          return 0;
        }

        上述代碼,基本覆蓋了常見的unique_ptr用法:

        • 第10行,通過new創(chuàng)建一個unique_ptr對象
        • 第11行,通過get()函數(shù)獲取其關(guān)聯(lián)的原生指針
        • 第12行,通過unique_ptr對象的指針進(jìn)行訪問
        • 第13行,通過unique_ptr對象的引用進(jìn)行訪問
        • 第16行,通過if(p)來判斷其是否有效
        • 第18行,通過release函數(shù)釋放所有權(quán),并將所有權(quán)進(jìn)行轉(zhuǎn)移
        • 第19行,通過reset釋放之前的原生指針,并重新關(guān)聯(lián)一個新的指針
        • 第20行,通過std::move轉(zhuǎn)移所有權(quán)

        簡單實(shí)現(xiàn)

        本部分只是基于源碼的一些思路,便于理解,實(shí)現(xiàn)的一個簡單方案,如果想要閱讀源碼,請點(diǎn)擊unique_ptr查看。

        基本代碼如下:

        template<class T
        class unique_ptr 
        {
         
           T* p; 
        public
           unique_ptr() :p() {} 
           unique_ptr(T* s) :p(s) {} 
           ~unique_ptr() { delete p; } 
          
            unique_ptr(const unique_ptr&) = delete;
             unique_ptroperator=(const unique_ptr&) = delete;
         
           unique_ptr(unique_ptr&& s) :p(s.p) { s.p = nullptr } 
         
           unique_ptroperator=(unique_ptr s) 
           { delete p; p = s.p; s.p=nullptrreturn *this; } 
         
           T* operator->() const { return p; } 
           T& operator*() const { return *p; } 
        }; 

        從上面代碼基本可以看出,unique_ptr的控制權(quán)轉(zhuǎn)移是通過move語義來實(shí)現(xiàn)的,相比于auto_ptr的拷貝語義轉(zhuǎn)移所有權(quán),更為合理。

        shared_ptr

        unique_ptr因?yàn)槠渚窒扌?獨(dú)享所有權(quán)),一般很少用于多線程操作。在多線程操作的時候,既可以共享資源,又可以自動釋放資源,這就引入了shared_ptr。

        shared_ptr為了支持跨線程訪問,其內(nèi)部有一個引用計(jì)數(shù)(線程安全),用來記錄當(dāng)前使用該資源的shared_ptr個數(shù),在結(jié)束使用的時候,引用計(jì)數(shù)為-1,當(dāng)引用計(jì)數(shù)為0時,會自動釋放其關(guān)聯(lián)的資源。

        特點(diǎn)

        相對于unique_ptr的獨(dú)享所有權(quán),shared_ptr可以共享所有權(quán)。其內(nèi)部有一個引用計(jì)數(shù),用來記錄共享該資源的shared_ptr個數(shù),當(dāng)共享數(shù)為0的時候,會自動釋放其關(guān)聯(lián)的資源。

        shared_ptr不支持?jǐn)?shù)組,所以,如果用shared_ptr指向一個數(shù)組的話,需要自己手動實(shí)現(xiàn)deleter,如下所示:

        std::shared_ptr<intp(new int[8], [](int *ptr){delete []ptr;});

        使用

        仍然以一段代碼來說明,畢竟代碼更有說服力。

        #include <iostream>
        #include  <memory> 

        int main() {
            // 創(chuàng)建shared_ptr對象
            std::shared_ptr<int> p1 = std::make_shared<int>();
            *p1 = 78;
            std::cout << "p1 = " << *p1 << std::endl;
            // 打印引用計(jì)數(shù)
            std::cout << "p1 Reference count = " << p1.use_count() << std::endl;
            
            std::shared_ptr<intp2(p1);
            // 打印引用計(jì)數(shù)
            std::cout << "p2 Reference count = " << p2.use_count() << std::endl;
            std::cout << "p1 Reference count = " << p1.use_count() << std::endl;
            
            if (p1 == p2)
            {
                std::cout << "p1 and p2 are pointing to same pointer\n";
            }
            std::cout<<"Reset p1 "<<std::endl;
            // 引用計(jì)數(shù)-1
            p1.reset();
            
            std::cout << "p1 Reference Count = " << p1.use_count() << std::endl;
            
            // 重置
            p1.reset(new int(11));
            std::cout << "p1  Reference Count = " << p1.use_count() << std::endl;
            
            p1 = nullptr;
            std::cout << "p1  Reference Count = " << p1.use_count() << std::endl;
            if (!p1) // 通過此種方式來判斷關(guān)聯(lián)的資源是否為空
            {
                std::cout << "p1 is NULL" << std::endl;
            }
            return 0;
        }

        輸出如下:

        p1 = 78
        p1 Reference count = 1
        p2 Reference count = 2
        p1 Reference count = 2
        p1 and p2 are pointing to same pointer
        Reset p1 
        p1 Reference Count = 0
        p1  Reference Count = 1
        p1  Reference Count = 0
        p1 is NULL

        上面代碼基本羅列了shared_ptr的常用方法,對于其他方法,可以參考源碼或者官網(wǎng)。

        線程安全

        可能很多人都對shared_ptr是否線程安全存在疑惑,借助本節(jié),對線程安全方面的問題進(jìn)行分析和解釋。

        shared_ptr的線程安全問題主要有以下兩種:

        • 引用計(jì)數(shù)的加減操作是否線程安全
        • shared_ptr修改指向時是否線程安全

        引用計(jì)數(shù)

        shared_ptr中有兩個指針,一個指向所管理數(shù)據(jù)的地址,另一個一個指向執(zhí)行控制塊的地址。

        執(zhí)行控制塊包括對關(guān)聯(lián)資源的引用計(jì)數(shù)以及弱引用計(jì)數(shù)等。在前面我們提到shared_ptr支持跨線程操作,引用計(jì)數(shù)變量是存儲在堆上的,那么在多線程的情況下,指向同一數(shù)據(jù)的多個shared_ptr在進(jìn)行計(jì)數(shù)的++或--時是否線程安全呢?

        引用計(jì)數(shù)在STL中的定義如下:

        _Atomic_word  _M_use_count;     // #shared
        _Atomic_word  _M_weak_count;    // #weak + (#shared != 0)

        當(dāng)對shared_ptr進(jìn)行拷貝時,引入計(jì)數(shù)增加,實(shí)現(xiàn)如下:

        template<> 
          inline void
         _Sp_counted_base<_S_atomic>::
         _M_add_ref_lock() {
               // Perform lock-free add-if-not-zero operation.
               _Atomic_word __count;
               do
             {
               __count = _M_use_count;
               if (__count == 0)
                 __throw_bad_weak_ptr(); 
             }
               while (!__sync_bool_compare_and_swap(&_M_use_count, __count,
                                __count + 1));
             }

        最終,計(jì)數(shù)的增加,是調(diào)用__sync_bool_compare_and_swap實(shí)現(xiàn)的,而該函數(shù)是線程安全的,因此我們可以得出結(jié)論:在多線程環(huán)境下,管理同一個數(shù)據(jù)的shared_ptr在進(jìn)行計(jì)數(shù)的增加或減少的時候是線程安全的,這是一波原子操作。

        修改指向

        修改指向分為操作同一個對象和操作不同對象兩種。

        同一對象

        以下面代碼為例:

        void fun(shared_ptr<Type> &p) {
          if (...) {
            p = p1;
          } else {
            p = p2;
          }
        }

        當(dāng)在多線程場景下調(diào)用該函數(shù)時候,p之前的引用計(jì)數(shù)要進(jìn)行-1操作,而p1對象的引用計(jì)數(shù)要進(jìn)行+1操作,雖然這倆的引用計(jì)數(shù)操作都是線程安全的,但是對這倆對象的引用計(jì)數(shù)的操作在一起時候卻不是線程安全的。這是因?yàn)楫?dāng)對p1的引用計(jì)數(shù)進(jìn)行+1時候,恰恰前一時刻,p1的對象被釋放,后面再進(jìn)行+1操作,會導(dǎo)致segment fault。

        不同對象

        代碼如下:

        void fun1(std::shared_ptr<Type> &p) {
          p = p1;
        }

        void fun2(std::shared_ptr<Type> &p) {
          p = p2;
        }

        int main() {
          std::shared_ptr<Type> p = std::make_shared<Type>();
          auto p1 = p;
          auto p2 = p;
          std::thread t1(fun1, p1);
          std::thread t2(fun2, p2);
          
          t1.join();
          t2.join();
          
          return 0;
        }

        在上述代碼中,p、p1、p2指向同一個資源,分別有兩個線程操作不同的shared_ptr對象(雖然關(guān)聯(lián)的底層資源是同一個),這樣在多線程下,只對p1和p2的引用計(jì)數(shù)進(jìn)行操作,不會引起segment fault,所以是線程安全的。

        ?

        同一個shared_ptr被多個線程同時讀是安全的

        同一個shared_ptr被多個線程同時讀寫是不安全的

        ?

        簡單實(shí)現(xiàn)

        本部分只是基于源碼的一些思路,便于理解,實(shí)現(xiàn)的一個簡單方案,如果想要閱讀源碼,請點(diǎn)擊shared_ptr查看。

        記得之前看過一個問題為什么引用計(jì)數(shù)要new,這個問題我在面試的時候也問過,很少有人能夠回答出來,其實(shí),很簡單,因?yàn)?code style="overflow-wrap: break-word;margin-right: 2px;margin-left: 2px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(53, 148, 247);background: rgba(59, 170, 250, 0.1);padding-right: 2px;padding-left: 2px;border-radius: 2px;height: 21px;line-height: 22px;">要支持多線程訪問,所以只能要new呀??。

        代碼如下:

        template <class T>
        class weak_ptr;


        class Counter {
         public:
          Counter() = default;
          int s_ = 0// shared_ptr的計(jì)數(shù)
          int w_ = 0// weak_ptr的計(jì)數(shù)
        };

        template <class T>
        class shared_ptr {

         public:
          shared_ptr(T *p = 0) : ptr_(p) {
           cnt_ = new Counter();
           if (p) {
             cnt_->s_ = 1;
           }
         }

          ~shared_ptr() {
            release();
          }

          shared_ptr(shared_ptr<T> const &s) {
            ptr_ = s.ptr_;
            (s.cnt)->s_++;
            cnt_ = s.cnt_;
          }

          shared_ptr(weakptr_<T> const &w) {
            ptr_ = w.ptr_;
            (w.cnt_)->s_++;
            cnt_ = w.cnt_;
          }

          shared_ptr<T> &operator=(shared_ptr<T> &s) {
            if (this != &s) {
              release();
              (s.cnt_)->s_++;
              cnt_ = s.cnt_;
              ptr_ = s.ptr_;
            }
              return *this;
          }

          T &operator*() {
            return *ptr_;
          }

          T *operator->() {
            return ptr_;
          }

          friend class weak_ptr<T>;

         protected:
          void release() {
            cnt_->s_--;
            if (cnt_->s_ < 1)
            {
              delete ptr_;
              if (cnt_->w_ < 1)
              {
                  delete cnt_;
                  cnt_ = NULL;
              }
            }
          }

        private:
          T *ptr_;
          Counter *cnt_;
        };

        weak_ptr

        在三個智能指針中,weak_ptr是存在感最低的一個,也是最容易被大家忽略的一個智能指針。它的引入是為了解決shared_ptr存在的一個問題循環(huán)引用。

        特點(diǎn)

        1. 不具有普通指針的行為,沒有重載operator*和operator->
        2. 沒有共享資源,它的構(gòu)造不會引起引用計(jì)數(shù)增加
        3. 用于協(xié)助shared_ptr來解決循環(huán)引用問題
        4. 可以從一個shared_ptr或者另外一個weak_ptr對象構(gòu)造,進(jìn)而可以間接獲取資源的弱共享權(quán)。

        使用

        int main() {
            std::shared_ptr<int> p1 = std::make_shared<Entity>(14);
            {
                std::weak_ptr<int> weak = p1;
                std::shared_ptr<Entity> new_shared = weak.lock();
         
                shared_e1 = nullptr;
               
                new_shared = nullptr;
                if (weak.expired()) {
                    std::cout << "weak pointer is expired" << std::endl;
                }
                
                new_shared = weak.lock();
                std::cout << new_shared << std::endl;
           }
          
          return 0;
        }

        上述代碼輸出如下:

        weak pointer is expired
        0
        1. 使用成員函數(shù)use_count()和expired()來獲取資源的引用計(jì)數(shù),如果返回為0或者false,則表示關(guān)聯(lián)的資源不存在
        2. 使用lock()成員函數(shù)獲得一個可用的shared_ptr對象,進(jìn)而操作資源
        3. 當(dāng)expired()為true的時候,lock()函數(shù)將返回一個空的shared_ptr

        簡單實(shí)現(xiàn)

        template <class T>
        class weak_ptr
        {

         public:
          weak_ptr() = default;

          weak_ptr(shared_ptr<T> &s) : ptr_(s.ptr_), cnt(s.cnt_) {
            cnt_->w_++;
          }

          weak_ptr(weak_ptr<T> &w) : ptr_(w.ptr_), cnt_(w.cnt_) {
            cnt_->w_++;
          }
          ~weak_ptr() {
            release();
          }
          weak_ptr<T> &operator=(weak_ptr<T> &w) {
            if (this != &w) {
              release();
              cnt_ = w.cnt_;
              cnt_->w_++;
              ptr_ = w.ptr_;
            }
            return *this;
          }
          weak_ptr<T> &operator=(shared_ptr<T> &s)
          {
            release();
            cnt_ = s.cnt_;
            cnt_->w_++;
            ptr_ = s.ptr_;
            return *this;
          }

          shared_ptr<T> lock() {
            return shared_ptr<T>(*this);
          }

          bool expired() {
            if (cnt) {
              if (cnt->s_ > 0) {
                return false;
              }
            }
            return true;
          }

          friend class shared_ptr<T>;

        protected:
          void release() {
            if (cnt_) {
              cnt_->w_--;
              if (cnt_->w_ < 1 && cnt_->s_ < 1) {
                cnt_ = nullptr;
              }
            }
          }

        private:
            T *ptr_ = nullptr;
            Counter *cnt_ = nullptr;
        };

        循環(huán)引用

        在之前的文章內(nèi)存泄漏-原因、避免以及定位中,我們講到使用weak_ptr來配合shared_ptr使用來解決循環(huán)引用的問題,借助本文,我們深入說明下如何來解決循環(huán)引用的問題。

        代碼如下:

        class Controller {
         public:
          Controller() = default;

          ~Controller() {
            std::cout << "in ~Controller" << std::endl;
          }

          class SubController {
           public:
            SubController() = default;

            ~SubController() {
              std::cout << "in ~SubController" << std::endl;
            }

            std::shared_ptr<Controller> controller_;
          };

          std::shared_ptr<SubController> sub_controller_;
        };

        在上述代碼中,因?yàn)閏ontroller和sub_controller之間都有一個指向?qū)Ψ降膕hared_ptr,這樣就導(dǎo)致任意一個都因?yàn)閷Ψ接幸粋€指向自己的對象,進(jìn)而引用計(jì)數(shù)不能為0。

        為了解決std::shared_ptr循環(huán)引用導(dǎo)致的內(nèi)存泄漏,我們可以使用std::weak_ptr來單面去除上圖中的循環(huán)。

        class Controller {
         public:
          Controller() = default;

          ~Controller() {
            std::cout << "in ~Controller" << std::endl;
          }

          class SubController {
           public:
            SubController() = default;

            ~SubController() {
              std::cout << "in ~SubController" << std::endl;
            }

            std::weak_ptr<Controller> controller_;
          };

          std::shared_ptr<SubController> sub_controller_;
        };

        在上述代碼中,我們將SubController類中controller_的類型從std::shared_ptr變成std::weak_ptr。

        那么,為什么將SubController中的shared_ptr換成weak_ptr就能解決這個問題呢?我們看下源碼:

        template<typename _Tp1>
                 __weak_ptr&
                 operator=(const __shared_ptr<_Tp1, _Lp>& __r) // never throws
                 {
               _M_ptr = __r._M_ptr;
               _M_refcount = __r._M_refcount;
               return *this;
             }

        在上面代碼中,我們可以看到,將一個shared_ptr賦值給weak_ptr的時候,其引用計(jì)數(shù)并沒有+1,所以也就解決了循環(huán)引用的問題。

        那么,如果我們想要使用shared_ptr關(guān)聯(lián)的對象進(jìn)行操作時候,該怎么做呢?使用weak_ptr::lock()函數(shù)來實(shí)現(xiàn),源碼如下:

         __shared_ptr<_Tp, _Lp>
         lock() const 
        {
           return expired() ? __shared_ptr<element_type, _Lp>() : __shared_ptr<element_type, _Lp>(*this);
         }

        從上面代碼可看出,使用lock()函數(shù)生成一個shared_ptr供使用,如果之前的shared_ptr已經(jīng)被釋放,那么就返回一個空shared_ptr對象,否則生成shared_ptr對象的拷貝(這樣即使之前的釋放也不會存在問題)。

        經(jīng)驗(yàn)之談

        不要混用

        指針之間的混用,有時候會造成不可預(yù)知的錯誤,所以建議盡量不要混用。包括裸指針和智能指針以及智能指針之間的混用

        裸指針和智能指針混用

        代碼如下:

        void fun() {
          auto ptr = new Type;
          std::shared_ptr<Type> t(ptr);
          
          delete ptr;
        }

        在上述代碼中,將ptr所有權(quán)歸shared_ptr所擁有,所以在出fun()函數(shù)作用域的時候,會自動釋放ptr指針,而在函數(shù)末尾又主動調(diào)用delete來釋放,這就會造成double delete,會造成segment fault。

        智能指針混用

        代碼如下:

        void fun() {
          std::unique_ptr<Type> t(new Type);
          std::shared_ptr<Type> t1(t.get());
        }

        在上述代碼中,將t關(guān)聯(lián)的對象又給了t1,也就是說同一個對象被兩個智能指針?biāo)鶕碛?,所以在出fun()函數(shù)作用域的時候,二者都會釋放其關(guān)聯(lián)的對象,這就會造成double delete,會造成segment fault。

        需要注意的是,下面代碼在STL中是支持的:

        void fun() {
          std::unique_ptr<Type> t(new Type);
          std::shared_ptr<Type> t1(std::move(t));
        }

        不要管理同一個裸指針

        代碼如下:

        void fun() {
          auto ptr = new Type;
          std::unique_ptr<Type> t(ptr);
          std::shared_ptr<Type> t1(ptr);
        }

        在上述代碼中,ptr所有權(quán)同時給了t和t1,也就是說同一個對象被兩個智能指針?biāo)鶕碛校栽诔鰂un()函數(shù)作用域的時候,二者都會釋放其關(guān)聯(lián)的對象,這就會造成double delete,會造成segment fault。

        避免使用get()獲取原生指針

        void fun(){
          auto ptr = std::make_shared<Type>();

          auto a= ptr.get();

          std::shared_ptr<Type> t(a);
          delete a;
        }

        一般情況下,生成的指針都要顯式調(diào)用delete來進(jìn)行釋放,而上述這種,很容易稍不注意就調(diào)用delete;非必要不要使用get()獲取原生指針

        不要管理this指針

        class Type {
         private:
          void fun() {
            std::shared_ptr<Type> t(this);
          }
        };

        在上述代碼中,如果Type在棧上,則會導(dǎo)致segment fault,堆上視實(shí)際情況(如果在對象在堆上生成,那么使用合理的話,是允許的)。

        只管理堆上的對象

        void fun() {
           Type t;
           std::shared_ptr<Type> ptr(&t);
        };

        在上述代碼中,t在棧上進(jìn)行分配,在出作用域的時候,會自動釋放。而ptr在出作用域的時候,也會調(diào)用delete釋放t,而t本身在棧上,delete一個棧上的地址,會造成segment fault。

        優(yōu)先使用unique_ptr

        根據(jù)業(yè)務(wù)場景,如果需要資源獨(dú)占,那么建議使用unique_ptr而不是shared_ptr,原因如下:

        • 性能優(yōu)于shared_ptr
          • 因?yàn)閟hared_ptr在拷貝或者釋放時候,都需要操作引用計(jì)數(shù)
        • 內(nèi)存占用上小于shared_ptr
          • shared_ptr需要維護(hù)它指向的對象的線程安全引用計(jì)數(shù)和一個控制塊,這使得它比unique_ptr更重量級

        使用make_shared初始化

        我們看下常用的初始化shared_ptr兩種方式,代碼如下:

        std::shared_ptr<Type> p1 = new Type;
        std::shared_ptr<Type> p2 = std::make_shared<Type>();

        那么,上述兩種方法孰優(yōu)孰劣呢?我們且從源碼的角度進(jìn)行分析。

        第一種初始化方法,有兩次內(nèi)存分配:

        • new Type分配對象
        • 為p1分配控制塊(control block),控制塊用于存放引用計(jì)數(shù)等信息

        我們再看下make_shared源碼:

        template<class _Ty,
          class... _Typesinline
            shared_ptr<_Ty> make_shared(_Types&&... _Args)
          {
          // make a shared_ptr
          _Ref_count_obj<_Ty> *_Rx =
            new _Ref_count_obj<_Ty>(_STD forward<_Types>(_Args)...);

          shared_ptr<_Ty> _Ret;
          _Ret._Resetp0(_Rx->_Getptr(), _Rx);
          return (_Ret);
          }

        這里的_Ref_count_obj類包含成員變量:

        • 控制塊
        • 一個內(nèi)存塊,用于存放智能指針管理的資源對象

        再看看_Ref_count_obj的構(gòu)造函數(shù):

        template<class... _Types>
          _Ref_count_obj(_Types&&... _Args)
          :
         _Ref_count_base()
          {  // construct from argument list
          ::new ((void *)&_Storage) _Ty(_STD forward<_Types>(_Args)...);
          }

        此處雖然也有一個new操作,但是此處是placement new,所以不存在內(nèi)存申請。

        從上面分析我們可以看出,第一種初始化方式(new方式)共有兩次內(nèi)存分配操作,而第二種初始化方式(make_shared)只有一次內(nèi)存申請,所以建議使用make_shared方式進(jìn)行初始化。

        結(jié)語

        智能指針的出現(xiàn),能夠使得開發(fā)者不需要關(guān)心內(nèi)存的釋放,進(jìn)而使得開發(fā)者能夠?qū)⒏嗟木ν度氲綐I(yè)務(wù)上。但是,因?yàn)橹悄苤羔槺旧硪灿衅渚窒扌?,如果使用不?dāng),會造成意想不到的后果,所以,在使用之前,需要做一些必要的檢查,為了更好地用好智能指針,建議看下源碼實(shí)現(xiàn),還是比較簡單的??。

        好了,今天的文章就到這,我們下期見。

        參考

        https://docs.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-2012/hh279676(v=vs.110)?redirectedfrom=MSDN

        https://rufflewind.com/2016-03-05/unique-ptr

        https://www.nextptr.com/tutorial/ta1450413058/unique_ptr-shared_ptr-weak_ptr-or-reference_wrapper-for-class-relationships

        https://gcc.gnu.org/onlinedocs/gcc-4.6.3/libstdc++/api/a01099_source.html

        https://gcc.gnu.org/onlinedocs/libstdc++/libstdc++-html-USERS-4.4/a01327.html

        https://www.nextptr.com/tutorial/ta1358374985/shared_ptr-basics-and-internals-with-examples

        瀏覽 34
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評論
        圖片
        表情
        推薦
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            黄色片一级片 | 用力挺进她的花苞啊h视频 | 午夜福利视频日本 | 亚洲电影网站 | 内射无套内射国产精品视频 | 五月丁香激情四射 | 被老师摁在教室cao到爽的作文 | 91丨九色丨国产 在线 | 亚洲三级视频 | 日韩一区二区三区不卡 |