徹底搞懂之C++智能指針

前言

在現代 c + + 編程中,標準庫包含 智能指針,這些指針用於幫助確保程序不會出現內存和資源泄漏,並具有異常安全。

 

標準庫智能指針分類

auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中後三個是c++11支持,並且第一個已經被c++11棄用。所以我只說後3個。

  1. shared_ptr
    採用引用計數的智能指針。 如果你想要將一個原始指針分配給多個所有者(例如,從容器返回了指針副本又想保留原始指針時),請使用該指針。 直至所有 shared_ptr 所有者超出了範圍或放棄所有權,纔會刪除原始指針。 大小爲兩個指針;一個用於對象,另一個用於包含引用計數的共享控制塊。 頭文件:<memory>。 有關詳細信息,請參閱 如何:創建和使用 Shared_ptr 實例 和 shared_ptr 類。
  2. unique_ptr
    只允許基礎指針的一個所有者。 除非你確信需要 shared_ptr,否則請將該指針用作 POCO 的默認選項。 可以移到新所有者,但不會複製或共享。 替換已棄用的 auto_ptr。 與 boost::scoped_ptr 比較。 unique_ptr 很小且高效;大小是一個指針,它支持用於從 c + + 標準庫集合快速插入和檢索的右值引用。 頭文件:<memory>。 有關詳細信息,請參閱 如何:創建和使用 Unique_ptr 實例 和 unique_ptr 類。
  3. weak_ptr
    結合 shared_ptr 使用的特例智能指針。 weak_ptr 提供對一個或多個 shared_ptr 實例擁有的對象的訪問,但不參與引用計數。 如果你想要觀察某個對象但不需要其保持活動狀態,請使用該實例。 在某些情況下,需要斷開 shared_ptr 實例間的循環引用。 頭文件:<memory>。 有關詳細信息,請參閱 如何:創建和使用 Weak_ptr 實例 和 weak_ptr 類。

 

shared_ptr

shared_ptr 類型是 C++ 標準庫中的一個智能指針,是爲多個所有者可能必須管理對象在內存中的生命週期的方案設計的。 在您初始化一個 shared_ptr 之後,您可複製它,按值將其傳入函數參數,然後將其分配給其他 shared_ptr 實例。 所有實例均指向同一個對象,並共享對一個“控制塊”(每當新的 shared_ptr 添加、超出範圍或重置時增加和減少引用計數)的訪問權限。 當引用計數達到零時,控制塊將刪除內存資源和自身。

下圖顯示了指向一個內存位置的幾個 shared_ptr 實例。

Shared pointer diagram.

 原始用法:

Object * obj = new ChildObject(9);//從heap分配原始父對象,必須手動觸發析構, 但子對象不會釋放
testObject(*obj);
printf("release9 %p \n", obj);
delete obj;

當testObject()出現異常時,delete將不被執行,因此將導致內存泄露。

如何避免這種問題?有人會說,這還不簡單,直接在throw exception(); 在catch中加上delete ps;不就行了。問題是很多人都會忘記在適當的地方加上delete語句,如果你要對一個龐大的工程進行review,看是否有這種潛在的內存泄露問題,那就是一場災難!
這時我們會想:如果指向heap的內存也能像stack變量一樣用完時被自動釋放,那該有多好啊。

這正是 auto_ptr、unique_ptr和shared_ptr這幾個智能指針背後的設計思想。我簡單的總結下就是:將基本類型指針封裝爲類對象指針(這個類肯定是個模板,以適應不同基本類型的需求),並在析構函數裏編寫delete語句刪除指針指向的內存空間。

 

使用shared_ptr:

{
    std::shared_ptr<Object> sObj = std::make_shared<ChildObject>(1);
    testObject(*sObj); //調用父對象
    //自動回收 
}

很簡單對吧~

 

unique_ptr

unique_ptr不共享指針。 它不能複製到另一個 unique_ptr函數,由值傳遞給函數,或在任何需要複製副本的 C++ 標準庫算法中使用。 只能移動 unique_ptr。 這意味着,內存資源所有權將轉移到另一 unique_ptr,並且原始 unique_ptr 不再擁有此資源。 我們建議你將對象限制爲由一個所有者所有,因爲多個所有權會使程序邏輯變得複雜。 因此,當需要純 C++ 對象的智能指針時,請使用make_unique幫助程序函數。

下圖演示了兩個 unique_ptr 實例之間的所有權轉換。

Diagram that shows moving the ownership of a unique pointer.

unique_ptr 在 C++ 標準庫的標頭中 <memory> 定義。 它與原始指針一樣高效,可在 C++ 標準庫容器中使用。 將實例添加到 unique_ptr C++ 標準庫容器是有效的,因爲移動構造函數 unique_ptr 無需複製操作。

unique_ptr 是一個獨享所有權的智能指針,它提供了嚴格意義上的所有權,包括:

1、擁有它指向的對象

2、無法進行復制構造,無法進行復制賦值操作。即無法使兩個unique_ptr指向同一個對象。但是可以進行移動構造和移動賦值操作

3、保存指向某個對象的指針,當它本身被刪除釋放的時候,會使用給定的刪除器釋放它指向的對象

 

用法:

std::unique_ptr<int>p1(new int(5));
std::unique_ptr<int>p2=p1;// 編譯會出錯
std::unique_ptr<int>p3=std::move(p1);// 轉移所有權,那塊內存歸p3所有, p1成爲無效的針.
p3.reset();//釋放內存.
p1.reset();//無效

 

share_ptr和unique_ptr的例子:

#include <iostream>
#include <string>
using namespace std;

namespace Test
{
    #define formatBool(b) ((b) ? "true" : "false")
    class Object
    {
    protected:
        int id;

    public:
        using pointer = std::shared_ptr<Object>;
        virtual std::string version() const {
            return "1.0.0";
        }; 
        Object(int _id):id(_id){
            cout << "\nnew parent Object id:" << id  << endl;
        };
        virtual ~Object(){//釋放時,首先是派生,然後是基類。必須將基類析構函數設爲虛基類, 防止delete 子對象時不會調用父析構函數,導致內存泄露
            delete parent_str_ptr;
            cout << "delete parent Object id:" << id  << endl;
        };
        virtual std::string debug() const
        {
            auto str = std::string( "debug Object id:" + std::to_string(id) );
            return str;
        }

    private:
        std::string *parent_str_ptr = new std::string("parent_str_ptr memory leak");                                          
    };
    class ChildObject : public Object
    {
    public:
        ChildObject(int _id):Object(_id)
        {
            std::cout << "new ChildObject id:" << (id) << "\n";
        }

        ~ChildObject()
        {
            delete str_ptr;
            std::cout << "delete ChildObject id:" << id << "\n";
        }
        virtual std::string version() const {
            return "2.0.0";
        }; 
    private:
        std::string *str_ptr = new std::string("memory leak");  

    };

    void testObject(const Object &obj)
    {
        std::cout << obj.debug() << " version:"<< obj.version() << "\n";
    }

    void testCase()
    {
        {
            std::shared_ptr<Object> sObj = std::make_shared<ChildObject>(1);
            testObject(*sObj); //調用父對象
            //自動回收 
        }

        {
            std::unique_ptr<Object> obj = std::make_unique<ChildObject>(2);
            testObject(*obj);
            auto obj2 = std::move(obj);//轉移所有權到obj2

            printf("obj:%s obj2:%s \n", formatBool(!!obj), formatBool(!!obj2));

            testObject(*obj2);//調用父對象

            obj2.release();//手動釋放後, obj, obj2指向的對象已經被回收, 不會觸發自動回收
            printf("obj2.release, obj:%s obj2:%s \n", formatBool(!!obj), formatBool(!!obj2));
        }

        {
            std::unique_ptr<ChildObject> obj = std::make_unique<ChildObject>(3);// 使用make_unique
            testObject(*obj);
            printf("release3 %s \n", formatBool(!!obj));
        }
        {
            std::unique_ptr<ChildObject> obj(new ChildObject(4));//使用new
            testObject(*obj);
            printf("release4 %s \n", formatBool(!!obj));
        }
        {
            // std::unique_ptr<ChildObject> obj(ChildObject(5));//使用stack對象,這是錯誤的用法, error: no matching constructor for initialization of 'std::unique_ptr<Object>'
            // printf("release5 %d \n", !!obj);
        }
        {
            std::unique_ptr<Object> obj = std::make_unique<ChildObject>(6);//用父對象, 會觸發析構
            testObject(*obj);
            printf("release6 %s \n", formatBool(!!obj));
        }
        {
            ChildObject obj = ChildObject(7);//從stack分配原始對象, 會觸發析構
            testObject(obj);
            printf("release7 %p \n", &obj);
        }
        {
            ChildObject * obj = new ChildObject(8);//從heap分配原始對象, 必須手動觸發析構
            testObject(*obj);
            printf("release8 %p \n", obj);
            delete obj;
        }
        {
            Object * obj = new ChildObject(9);//從heap分配原始父對象,必須手動觸發析構
            testObject(*obj);
            printf("release9 %p \n", obj);
            delete obj;
        }
        {
            Object * obj = new Object(10);//從heap分配原始父對象,必須手動觸發析構
            testObject(*obj);
            printf("release10 %p \n", obj);
            delete obj;
        }
        {
            std::shared_ptr<Object> obj = std::make_unique<ChildObject>(11);//指向父對象, 會釋放子對象
            testObject(*obj);
            printf("release11 %s \n", formatBool(!!obj));
        }
        // {
        //     std::unique_ptr<Object> obj = std::make_shared<ChildObject>(11);//error: no viable conversion from 'shared_ptr<Test::ChildObject>' to 'std::unique_ptr<Object>'
        //     testObject(*obj);
        //     printf("release11 %s \n", formatBool(!!obj));
        // }
    }
}

int main(int argc, char **argv)
{
    Test::testCase();

    return EXIT_SUCCESS;
}
#  c++ -std=c++14 -o a share_ptr.cpp; ./a
new parent Object id:1
new ChildObject id:1
debug Object id:1 version:2.0.0
delete ChildObject id:1
delete parent Object id:1

new parent Object id:2
new ChildObject id:2
debug Object id:2 version:2.0.0
obj:false obj2:true 
debug Object id:2 version:2.0.0
obj2.release, obj:false obj2:false 

new parent Object id:3
new ChildObject id:3
debug Object id:3 version:2.0.0
release3 true 
delete ChildObject id:3
delete parent Object id:3

new parent Object id:4
new ChildObject id:4
debug Object id:4 version:2.0.0
release4 true 
delete ChildObject id:4
delete parent Object id:4

new parent Object id:6
new ChildObject id:6
debug Object id:6 version:2.0.0
release6 true 
delete ChildObject id:6
delete parent Object id:6

new parent Object id:7
new ChildObject id:7
debug Object id:7 version:2.0.0
release7 0x7ff7bfcf3488 
delete ChildObject id:7
delete parent Object id:7

new parent Object id:8
new ChildObject id:8
debug Object id:8 version:2.0.0
release8 0x7fcaef705ba0 
delete ChildObject id:8
delete parent Object id:8

new parent Object id:9
new ChildObject id:9
debug Object id:9 version:2.0.0
release9 0x7fcaef705ba0 
delete ChildObject id:9
delete parent Object id:9

new parent Object id:10
debug Object id:10 version:1.0.0
release10 0x7fcaef705ba0 
delete parent Object id:10

new parent Object id:11
new ChildObject id:11
debug Object id:11 version:2.0.0
release11 true 
delete ChildObject id:11
delete parent Object id:11

weak_ptr

weak_ptr是用來解決shared_ptr相互引用時的死鎖問題,如果說兩個shared_ptr相互引用,那麼這兩個指針的引用計數永遠不可能下降爲0,資源永遠不會釋放。它是對對象的一種弱引用,不會增加對象的引用計數,和shared_ptr之間可以相互轉化,shared_ptr可以直接賦值給它,它可以通過調用lock函數來獲得shared_ptr。

最佳設計是避免在任何時候都能實現指針的共享所有權。 但是,如果您必須有實例的 shared_ptr 共享所有權,請避免它們之間存在循環引用。 如果無法避免循環引用,或者出於某種原因更可取,則使用 weak_ptr 向一個或多個所有者提供對另 shared_ptr 一個的弱引用。 通過使用 weak_ptr ,可以創建一個 shared_ptr 聯接到一組現有相關實例的,但前提是基礎內存資源仍有效。 weak_ptr本身並不參與引用計數,因此它無法阻止引用計數轉到零。 但是,你可以使用 weak_ptr 來嘗試獲取用於初始化的的新副本 shared_ptr 。 如果已刪除內存,則的 bool 運算符將 weak_ptr 返回 false 。 如果內存仍有效,新的共享指針會遞增引用計數,並保證只要 shared_ptr 變量保持在範圍內,內存就有效。weak_ptr是弱智能指針對象,它不控制所指向對象生存期的智能指針,它指向由一個shared_ptr管理的智能指針。將一個weak_ptr綁定到一個shared_ptr對象,不會改變shared_ptr的引用計數。一旦最後一個所指向對象的shared_ptr被銷燬,所指向的對象就會被釋放,即使此時有weak_ptr指向該對象,所指向的對象依然被釋放。

例子:

#include <iostream>
#include <memory>

class A;

class B
{
public:
    ~B()
    {
        std::cout << "B destory, a_ptr use_count:" << a_ptr.use_count() << "\n";
    }

    //    std::shared_ptr<A> a_ptr; //它會造成循環引用
    std::weak_ptr<A> a_ptr;//它不會循環引用
};

class A
{
public:
    ~A()
    {
        std::cout << "A destory, b_ptr use_count:" << b_ptr.use_count() << "\n";
    }

    // std::shared_ptr<B> b_ptr;//它會造成循環引用
    std::weak_ptr<B> b_ptr;//它不會循環引用
};

int main()
{
    std::shared_ptr<A> a(new A());
    std::shared_ptr<B> b(new B());
    a->b_ptr = b;
    b->a_ptr = a;

    std::cout << "A:" << a.use_count() << "\n";
    std::cout << "B:" << b.use_count() << "\n";
}
// * 運行結果:
// A:2
// B:2

 

如何選擇智能指針

(1)如果程序要使用多個指向同一個對象的指針,應選擇shared_ptr。這樣的情況包括:

  • 有一個指針數組,並使用一些輔助指針來標示特定的元素,如最大的元素和最小的元素;
  • 兩個對象包含都指向第三個對象的指針;
  • STL容器包含指針。很多STL算法都支持複製和賦值操作,這些操作可用於shared_ptr,但不能用於unique_ptr(編譯器發出warning)和auto_ptr(行爲不確定)。如果你的編譯器沒有提供shared_ptr,可使用Boost庫提供的shared_ptr。

(2)如果程序不需要多個指向同一個對象的指針,則可使用unique_ptr。如果函數使用new分配內存,並返還指向該內存的指針,將其返回類型聲明爲unique_ptr是不錯的選擇。這樣,所有權轉讓給接受返回值的unique_ptr,而該智能指針將負責調用delete。可將unique_ptr存儲到STL容器在那個,只要不調用將一個unique_ptr複製或賦給另一個算法(如sort())。例如,可在程序中使用類似於下面的代碼段。

  (3) 基於性能考慮:

1、unique_ptr獨佔對象的所有權,由於沒有引用計數,因此性能較好

2、shared_ptr共享對象的所有權,但性能略差

3、weak_ptr配合shared_ptr,解決循環引用的問題

       由於性能問題,那麼可以粗暴的理解:優先使用unique_ptr。但由於unique_ptr不能進行復制,因此部分場景下不能使用的。

 

智能指針的錯誤用法

1、使用智能指針託管的對象,儘量不要在再使用原生指針

很多開發同學(包括我在內)在最開始使用智能指針的時候,對同一個對象會混用智能指針和原生指針,導致程序異常。

void incorrect_smart_pointer1()
{
    A *a= new A();
    std::unique_ptr<A> unique_ptr_a(a);

    // 此處將導致對象的二次釋放
    delete a;
}

2、不要把一個原生指針交給多個智能指針管理

如果將一個原生指針交個多個智能指針,這些智能指針釋放對象時會產生對象的多次銷燬

void incorrect_smart_pointer2()
{
    A *a= new A();
    std::unique_ptr<A> unique_ptr_a1(a);
    std::unique_ptr<A> unique_ptr_a2(a);// 此處將導致對象的二次釋放
}

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);// 此處將導致對象的二次釋放

    delete a;// 此處也將導致對象的二次釋放
}

4、不要將 this 指針直接託管智能指針

class E
{
    void use_this()
    {
        //錯誤方式,用this指針重新構造shared_ptr,將導致二次釋放當前對象
        std::shared_ptr<E> this_shared_ptr1(this);
    }
};

std::shared_ptr<E> e = std::make_shared<E>();

5、智能指針只能管理堆對象,不能管理棧上對象

棧上對象本身在出棧時就會被自動銷燬,如果將其指針交給智能指針,會造成對象的二次銷燬

void incorrect_smart_pointer5()
{
    int int_num = 3;
    std::unique_ptr<int> int_unique_ptr(&int_num);
}

缺點和優化

  1. 內存佔用高
    shared_ptr 的內存佔用是裸指針的兩倍。因爲除了要管理一個裸指針外,還要維護一個引用計數。
    因此相比於 unique_ptr, shared_ptr 的內存佔用更高。 性能要求高時,可以用裸指針。

  2. 原子操作性能低
    考慮到線程安全問題,引用計數的增減必須是原子操作。而原子操作一般情況下都比非原子操作慢。可以引入鎖機制,或者用裸指針。

  3. 使用移動優化性能
    shared_ptr 在性能上固然是低於 unique_ptr。而通常情況,我們也可以儘量避免 shared_ptr 複製。
    如果,一個 shared_ptr 需要將所有權共享給另外一個新的 shared_ptr,而我們確定在之後的代碼中都不再使用這個 shared_ptr,那麼這是一個非常鮮明的移動語義。
    對於此種場景,我們儘量使用 std::move,將 shared_ptr 轉移給新的對象。因爲移動不用增加引用計數,性能比複製更好。

彙總 

智能指針能更安全的回收內存,它能防止:

  1. 忘記delete造成的內存泄露

  2. delete了,又被訪問到了,比如併發時,導致“野指針”的危險情況

  3. delete了,又被delete了,導致重複回收,導致報錯中斷程序

 

總的來說,一般推薦用智能指針,性能要求很高時,可以用裸指針,但要十分小心。

 

參考

https://docs.microsoft.com/zh-cn/cpp/cpp/smart-pointers-modern-cpp?view=msvc-170

https://www.zhihu.com/question/319277442/answer/2384378560

https://www.cyhone.com/articles/right-way-to-use-cpp-smart-pointer/

https://juejin.cn/post/6844904198962675719

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章