1. 程式人生 > >【C++學習筆記】詳解C++中的三種智慧指標

【C++學習筆記】詳解C++中的三種智慧指標

一、簡介

  • 由於 C++ 語言沒有垃圾回收機制,程式設計師每次 new出來的記憶體都要手動 delete。程式設計師忘記 delete,有可能就會造成記憶體洩漏,程式崩潰等嚴重的後果。用智慧指標便可以有效緩解這類問題,本文主要講解常見的智慧指標的用法。包括:std::auto_ptr、boost::scoped_ptr、boost::shared_ptr、boost::weak_ptr。

二、具體使用

1、總括

  • 對於編譯器來說,智慧指標實際上是一個棧物件,並非指標型別,在棧物件生命期即將結束時,智慧指標通過解構函式釋放有它管理的堆記憶體。所有智慧指標都過載了“operator->”操作符,直接返回物件的引用,用以操作物件。訪問智慧指標原來的方法則使用“.”操作符。
    訪問智慧指標包含的裸指標則可以用 get() 函式。由於智慧指標是一個物件,所以if (my_smart_object)永遠為真,要判斷智慧指標的裸指標是否為空,需要這樣判斷:if (my_smart_object.get())。
    智慧指標包含了 reset() 方法,如果不傳遞引數(或者傳遞 NULL),則智慧指標會釋放當前管理的記憶體。如果傳遞一個物件,則智慧指標會釋放當前物件,來管理新傳入的物件。

2、std::auto_ptr

  • std::auto_ptr 屬於 STL,當然在 namespace std 中,包含標頭檔案 #include 便可以使用。std::auto_ptr 能夠方便的管理單個堆記憶體物件。

我們從程式碼開始分析:

void TestAutoPtr() 
{

std::auto_ptr<Simple> my_memory(new Simple(1));   // 建立物件,輸出:Simple:1

if (my_memory.get())
 {                            // 判斷智慧指標是否為空

    my_memory->
PrintSomething(); // 使用 operator-> 呼叫智慧指標物件中的函式 my_memory.get()->info_extend = "Addition"; // 使用 get() 返回裸指標,然後給內部物件賦值 my_memory->PrintSomething(); // 再次列印,表明上述賦值成功 (*my_memory).info_extend += " other"; // 使用 operator* 返回智慧指標內部物件,然後用“.”呼叫智慧指標物件中的函式
my_memory->PrintSomething(); // 再次列印,表明上述賦值成功 } } // my_memory 棧物件即將結束生命期,析構堆物件 Simple(1)

執行結果為:

Simple: 1

PrintSomething:

PrintSomething: Addition

PrintSomething: Addition other

~Simple: 1

上述為正常使用 std::auto_ptr 的程式碼,一切似乎都良好,無論如何不用我們顯示使用該死的 delete 了。

其實好景不長,我們看看如下的另一個例子:

void TestAutoPtr2() 
{

  std::auto_ptr<Simple> my_memory(new Simple(1));

  if (my_memory.get())
   {

    std::auto_ptr<Simple> my_memory2;   // 建立一個新的 my_memory2 物件

    my_memory2 = my_memory;             // 複製舊的 my_memory 給 my_memory2

    my_memory2->PrintSomething();       // 輸出資訊,複製成功

    my_memory->PrintSomething();        // 崩潰

  }

}

再來看另外一段程式碼:

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


int main()
{
    auto_ptr<int> ap1(new int);
    *ap1 = 10;

    if (true)
    {
        auto_ptr<int> ap2(ap1);
        *ap2 = 20;
    }

    *ap1 = 100;

    return 0;
}

最終上面兩段程式碼都會導致崩潰,跟進 std::auto_ptr 的原始碼後,我們看到,第一段程式碼罪魁禍首是“my_memory2 = my_memory”,這行程式碼,my_memory2 完全奪取了 my_memory 的記憶體管理所有權,導致 my_memory 懸空,最後使用時導致崩潰。第二段程式碼為什麼會崩潰呢?原因是因為,雖然有兩個指標,但由於ap2是根據ap1拷貝構造出來的,所以他們只有一塊記憶體空間,並且這塊記憶體空間的所有者為ap2,ap1只有使用權,沒有銷燬權(即呼叫ap1的解構函式並不會釋放空間),在上面的程式碼中,出了ap2的作用域後,就會呼叫ap2的解構函式釋放空間,但是由於ap1並不知道空間已經釋放,所有此時ap1就為“野指標”,再次訪問就會造成程式崩潰。

所以,使用 std::auto_ptr 時,絕對不能使用“operator=”操作符及,拷貝構造。作為一個庫,不允許使用者使用,確沒有明確拒絕[1],多少會覺得有點出乎預料。

看完 std::auto_ptr 好景不長的第一個例子後,讓我們再來看一個:

void TestAutoPtr3() {

  std::auto_ptr<Simple> my_memory(new Simple(1));



  if (my_memory.get()) {

    my_memory.release();

  }

}

執行結果為:

Simple: 1

看到什麼異常了嗎?我們創建出來的物件沒有被析構,沒有輸出“~Simple: 1”,導致記憶體洩露。當我們不想讓 my_memory 繼續生存下去,我們呼叫 release() 函式釋放記憶體,結果卻導致記憶體洩露(在記憶體受限系統中,如果my_memory佔用太多記憶體,我們會考慮在使用完成後,立刻歸還,而不是等到 my_memory 結束生命期後才歸還)。

正確的程式碼應該為:

void TestAutoPtr3() {

  std::auto_ptr<Simple> my_memory(new Simple(1));

  if (my_memory.get()) {

    Simple* temp_memory = my_memory.release();

    delete temp_memory;

  }

}

原來 std::auto_ptr 的 release() 函式只是讓出記憶體所有權,這顯然也不符合 C++ 程式設計思想。

總結: std::auto_ptr 可用來管理單個物件的對記憶體,但是,請注意如下幾點:

(1)    儘量不要使用“operator=”以及拷貝構造。如果使用了,請不要再使用先前物件。

(2)    記住 release() 函式不會釋放物件,僅僅歸還所有權。

(3)    std::auto_ptr 最好不要當成引數傳遞(因為出了函式作用域,空間就會銷燬,實參就成為野指標)。

(4)    由於 std::auto_ptr 的“operator=”問題,有其管理的物件不能放入 std::vector 等容器中。

(5)    ……

使用一個 std::auto_ptr 的限制還真多,還不能用來管理堆記憶體陣列,這應該是你目前在想的事情吧,我也覺得限制挺多的,哪天一個不小心,就導致問題了。

由於 std::auto_ptr 引發了諸多問題,一些設計並不是非常符合 C++ 程式設計思想,所以引發了下面 boost 的智慧指標,boost 智慧指標可以解決如上問題。
讓我們繼續向下看。

3、boost::scoped_ptr

  • boost::scoped _ptr 屬於 boost 庫,(在STL中叫unique_ptr,原理相同)定義在 namespace boost 中,boost::scoped_ptr 跟 std::auto_ptr 一樣,可以方便的管理單個堆記憶體物件,特別的是,boost::scoped_ptr 獨享所有權,避免了 std::auto_ptr 惱人的幾個問題。
    boost::scoped_ptr 也可以像 auto_ptr 一樣正常使用。但其沒有 release() 函式,不會導致先前的記憶體洩露問題。其次,由於 boost::scoped_ptr 是獨享所有權的,所以明確拒絕使用者寫“my_memory2 = my_memory”之類的語句,可以緩解 std::auto_ptr 幾個惱人的問題。
  • 由於 boost::scoped_ptr 獨享所有權,當我們真真需要複製智慧指標時,需求便滿足不了了,如此我們再引入一個智慧指標,專門用於處理複製,引數傳遞的情況,這便是如下的 boost::shared_ptr。

4、shared_ptr

  • shared_ptr 屬於 boost 庫(STL中也叫做shared_ptr),定義在 namespace boost 中,在上面我們看到boost::scoped_ptr 獨享所有權,不允許賦值、拷貝,boost::shared_ptr 是專門用於共享所有權的,由於要共享所有權,其在內部使用了引用計數。boost::shared_ptr 也是用於管理單個堆記憶體物件的。
void TestShared(shared_ptr<int> spt1)
{
    cout << "usecount:" << spt1.use_count() << endl;

    *spt1 = 10;
    cout << "*spt1 = " << *spt1 << endl;


    shared_ptr<int> spt2(spt1);
    *spt2 = 20;
    cout << "*spt2 = " << *spt2 << endl;
    cout << "usecount:" << spt2.use_count() << endl;

    shared_ptr<int> spt3 = spt1;
    *spt3 = 30;
    cout << "*spt3 = " << *spt3 << endl;
    cout << "usecount:" << spt3.use_count() << endl;

    return;
}
int main()
{
    shared_ptr<int> spt1(new int);
    cout << "usecount:" << spt1.use_count() << endl;

    TestShared(spt1);

    cout << "usecount:" << spt1.use_count() << endl;
    return 0;
}
  • boost::shared_ptr 也可以很方便的使用。並且沒有 release() 函式。關鍵的一點,boost::shared_ptr 內部維護了一個引用計數,由此可以支援複製、引數傳遞等。boost::shared_ptr 提供了一個函式 use_count() ,此函式返回 boost::shared_ptr 內部的引用計數。檢視執行結果,當我們需要使用一個共享物件的時候,boost::shared_ptr 是再好不過的了。

迴圈引用:
但是,如果僅僅只是這樣,我們想想有可能還會出現下面的問題!
假設,有這樣一個場景,p1,p2,分別指向雙向連結串列的兩個節點,在節點內,還有兩個指標,pNext、pPre,這些指標的型別全都是shared_ptr管理,那麼同一個節點,就會有兩個shared_ptr管理,也就是說,p1和p2的引用計數都為2,假設函式Test()結束時,p1物件和p2物件就會銷燬,但是由於此時還有p2指向的節點中的pPre指標指向p1指向的節點,即引用計數為1,所以並不能銷燬,同理,p2指向的的節點也不能銷燬,這樣就會造成記憶體洩漏,那麼,這樣的問題該怎麼解決呢?

struct ListNode
{
    ListNode()
    : pNext(NULL)
    , pPre(NULL)
    {}

    shared_ptr<ListNode> pNext;
    shared_ptr<ListNode> pPre;
    int data;
};


void Test()
{
    shared_ptr<ListNode> p1(new ListNode);
    shared_ptr<ListNode> p2(new ListNode);

    p1->pNext = p2;
    p2->pPre = p1;

    cout << "p1 usecount: " <<p1.use_count() << endl;
    cout << "p2 usecount: " <<p2.use_count() << endl;

    return;
}
int main()
{
    Test();

    return 0;
}

這裡就引出了另一個指標weak_ptr,weak_ptr不對shared_ptr引用計數進行影響的“弱”指標,可以和shared_ptr搭配使用以解決資源的迴圈引用問題。當shared_ptr因引用計數為0而被釋放時,相關的weak_ptr自動標記為無效。weak_ptr不能單獨管理空間。