shared_ptr理解

shared_ptr是一種智能指針(smart pointer)。shared_ptr的作用有如同指針,但會記錄有多少個shared_ptrs共同指向一個對象。

shared_ptr是C++非常重要的一個防止內存泄露的設計

作用:

這便是所謂的引用計數(reference counting)。一旦最後一個這樣的指針被銷燬,也就是一旦某個對象的引用計數變爲0,這個對象會被自動刪除。這在非環形數據結構中防止資源泄露很有幫助。
auto_ptr由於它的破壞性複製語義,無法滿足標準容器對元素的要求,因而不能放在標準容器中;如果我們希望當容器析構時能自動把它容納的指針元素所指的對象刪除時,通常採用一些間接的方式來實現,顯得比較繁瑣。boost庫中提供了一種新型的智能指針shared_ptr,它解決了在多個指針間共享對象所有權的問題,同時也滿足容器對元素的要求,因而可以安全地放入容器中。
用法:

1.1刪除共享對象

使用shared_ptr解決的主要問題是知道刪除一個被多個客戶共享的資源的正確時機。下面是一個簡單易懂的例子,有兩個類 A和 B, 它們共享一個int實例。使用 boost::shared_ptr, 你必須包含"boost/shared_ptr.hpp".
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include "boost/shared_ptr.hpp"
#include <cassert>
class A {
    boost::shared_ptr<int> no_;
public:
    A(boost::shared_ptr<int> no) : no_(no) {}
    void value(int i) {
        *no_=i;
    }
};
class B {
    boost::shared_ptr<int> no_;
public:
    B(boost::shared_ptr<int> no) : no_(no) {}
    int value() const {
        return *no_;
    }
};
int main() {
    boost::shared_ptr<int> temp(new int(14));
    A a(temp);
    B b(temp);
    a.value(28);
    assert(b.value()==28);
類 A和 B都保存了一個 shared_ptr<int>. 在創建 A和 B的實例時,shared_ptr temp被傳送到它們的構造函數。這意味着共有三個 shared_ptr:a, b, 和 temp,它們都引向同一個int實例。如果我們用指針來實現對一個的共享,A和 B必須能夠在某個時間指出這個int要被刪除。在這個例子中,直到main的結束,引用計數爲3,當所有 shared_ptr離開了作用域,計數將達到0,而最後一個智能指針將負責刪除共享的 int.

1.2標準容器

把對象直接存入容器中有時會有些麻煩。以值的方式保存對象意味着使用者將獲得容器中的元素的拷貝,對於那些複製是一種昂貴的操作的類型來說可能會有性能的問題。此外,有些容器,特別是 std::vector, 當你加入元素時可能會複製所有元素,這更加重了性能的問題。最後,傳值的語義意味着沒有多態的行爲。如果你需要在容器中存放多態的對象而且你不想切割它們,你必須用指針。如果你用裸指針,維護元素的完整性會非常複雜。從容器中刪除元素時,你必須知道容器的使用者是否還在引用那些要刪除的元素,不用擔心多個使用者使用同一個元素。這些問題都可以用shared_ptr來解決。
下面是如何把共享指針存入標準庫容器的例子。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include "boost/shared_ptr.hpp"
#include <vector>
#include <iostream>
class A {
public:
    virtual void sing()=0;
    protected:
    virtual ~A() {};
};
class B : public A {
public:
    virtual void sing() {
        std::cout << "Do re mi fa so la";
    }
};
boost::shared_ptr<A> createA() {
    boost::shared_ptr<A> p(new B());
    return p;
}
int main() {
    typedef std::vector<boost::shared_ptr<A> > container_type;
    typedef container_type::iterator iterator;
    container_type container;
    for (int i=0;i<10;++i) {
        container.push_back(createA());
    }
    std::cout << "The choir is gathered: \n";
    iterator end=container.end();
    for (iterator it=container.begin();it!=end;++it) {
        (*it)->sing();
    }
這裏有兩個類, A和 B, 各有一個虛擬成員函數 sing. B從 A公有繼承而來,並且如你所見,工廠函數 createA返回一個動態分配的B的實例,包裝在shared_ptr<A>裏。在 main裏, 一個包含shared_ptr<A>的 std::vector被放入10個元素,最後對每個元素調用sing。如果我們用裸指針作爲元素,那些對象需要被手工刪除。而在這個例子裏,刪除是自動的,因爲在vector的生存期中,每個shared_ptr的引用計數都保持爲1;當 vector被銷燬,所有引用計數器都將變爲零,所有對象都被刪除。有趣的是,即使 A的析構函數沒有聲明爲 virtual, shared_ptr也會正確調用 B的析構函數!
上面的例子示範了一個強有力的技術,它涉及A裏面的protected析構函數。因爲函數 createA返回的是 shared_ptr<A>, 因此不可能對shared_ptr::get返回的指針調用 delete。這意味着如果爲了向某個需要裸指針的函數傳送裸指針而從shared_ptr中取出裸指針的話,它不會由於意外地被刪除而導致災難。那麼,又是如何允許 shared_ptr刪除它的對象的呢? 這是因爲指針指向的真正類型是 B; 而B的析構函數不是protected的。這是非常有用的方法,用於給shared_ptr中的對象增加額外的安全性
RAII

現在考慮如下一個資源從創建到銷燬的過程:
{
int* pInt = new int(14);
...... // 此處有一系列代碼
delete pInt;
}
  如果程序在......這些代碼的執行中跳出了異常。這樣導致程序不會運行到delete pInt2這裏,這時資源泄露了。

  如何防止這種情況呢? 由此我們聯想到類的構造和和類離開作用域時的自動析構.這就是資源獲取即初始化技術。資源獲取即初始化(RAII, Resource Acquisition Is Initialization)是指,當你

獲得一個資源的時候,不管這個資源是對象、內存、文件句柄或者其它什麼,你都要在一個對象的構造函數中獲得它, 並且在該對象的析構函數中釋放它。實現這種功能的類,我們就說它採用了"資源

獲取即初始化(RAII)"的方式。這樣的類常常被稱爲封裝類。

下面爲一個最簡單的RAII:

class CMySingleLock 
{
public:
CMySingleLock()
{
     InitializeCriticalSection(&m_CritSec);
     EnterCriticalSection(&m_CritSec);
}

~CMySingleLock (void) 
{
     LeaveCriticalSection(&m_CritSec);
     DeleteCriticalSection(&m_CritSec);
}

private:
CRITICAL_SECTION     m_CritSec;
};

這樣就可以這樣調用:

{

CMySingleLock mySingleLock;

....處理變量

} // 此處mySingleLock已經離開作用域,自動解鎖

  很多程序用到了該技術,如mutex, criticalSection. 這樣,當你創建資源的時候就立即將它放入封裝類對象構造函數(new出來的指針立即放入shared_ptr析造函數裏), 當該對象離開作用域時,

對象析構函數會自動銷燬資源(shared_ptr對象離開作用域時,會自動銷燬指向的資源).

1.2
  shared_ptr是典型的資源獲取即初始化技術。不過shared_ptr採用了更復雜一點RAII方式,因爲它實現了引用計數。當創建資源的時候將它放入一個shared_ptr, shared_ptr內部記錄對這種資源的引用次數爲1次。當這個shared_ptr對象離開作用域時,引用次數減1,shared_ptr對象析構時,檢查到引用次數爲0了,就銷燬資源:

{
boost::shared_ptr pInt(new int(14));
assert(pInt.use_count() == 1); // new int(14)這個指針被引用1次
...... // 此處有一系列代碼

}    //pInt離開作用域, 所以new int(14)被引用次數爲0. 指針被銷燬。防止了

  此處及以下assert代碼都會判斷成功。如果......跳出異常。那麼pInt也離開了作用域,指針照常還是被銷燬,所以智能指針可以有效防止資源泄露。

考慮更多次的引用情況:

{

boost::shared_ptr pInt2;
assert(pInt2.use_count() == 0);   // temp2還沒有引用指針

{

    boost::shared_ptr pInt1(new int(14));
    assert(pInt1.use_count() == 1); // new int(14)這個指針被引用1次

    pInt2 = pInt1;
    assert(pInt1.use_count() == 2); // new int(14)這個指針被引用2次
    assert(pInt2.use_count() == 2);
} //pInt1離開作用域, 所以new int(14)被引用次數-1

assert(pInt2.use_count() == 1);
} // pInt2離開作用域,引用次數-1,現在new int(14)被引用0次,所以銷燬它

不管資源曾經被多少次引用。當它被引用0次時就會銷燬。

1.3

  在shard_ptr使用中經常會發現,一個對象會有兩次被析構的情況。其實這種是因爲那個對象指針被兩次當成shard_ptr構造函數裏的參數。一定要避免這種現象。考慮如下代碼:

{
int* pInt = new int(14);
boost::shared_ptr temp1(pInt);
assert(temp1.use_count() == 1);      // 用一個指針初始化temp1,temp1認爲pInt只被它擁有。所以這個指針被引用1次

boost::shared_ptr temp2(pInt); // 用一個指針初始化temp2,temp2認爲pInt只被它擁有。所以這個指針被引用1次
assert(temp2.use_count() == 1);

}   // temp1,temp2都離開作用域,它們都銷燬pInt. pInt被銷燬了兩次!系統終於崩潰了 -_-


正確的做法是將原始指針賦給智能指針後,以後的操作都要針對智能指針了.

{
boost::shared_ptr temp1(new int(14)); // 資源獲取即初始化
assert(temp1.use_count() == 1);

boost::shared_ptr temp2(temp1);
assert(temp2.use_count() == 2);

}   // temp1,temp2都離開作用域,引用次數變爲0,指針被銷燬

1.4
如果資源的創建銷燬不是以new,delete的方式創建銷燬怎麼辦?shared_ptr也可以指定刪除器:

// FileCloser.h FileCloser刪除器  
class FileCloser
{
public:
void operator()(FILE *pf)
{
    if (pf)
    {
      fclose(pf);
    }
}
};

// 某實現文件
{
boost::shared_ptr fp(fopen(pszConfigFile, "r"), FileCloser());    // 指定調用FileCloser函數對象銷燬資源
}


1.5
  shared_ptr已經被即將到來的標準庫技術報告所採納,將成爲tr1中的一員。爲了以後更好的移植現有代碼到C++新標準中。可以使用一個namespace的一個小技巧,在頭文件StdAfx.h中聲明(參考

Effective C++ Item 54):
namespace std

{

namespace tr1 = ::boost;           // namespace std::tr1 is an alias

}                                   // for namespace boost

這樣就可以用如下方法寫代碼:

std::tr1::shared_ptr pInt(new int(14));



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