智能指針Boost.smart_ptr學習2--scoped_ptr

 頭文件: "boost/scoped_ptr.hpp"

boost::scoped_ptr 用於確保動態分配的對象能夠被正確地刪除。scoped_ptr 有着與std::auto_ptr類似的特性,而最大的區別在於它不能轉讓所有權而auto_ptr可以。事實上,scoped_ptr永遠不能被複制或被賦值!scoped_ptr 擁有它所指向的資源的所有權,並永遠不會放棄這個所有權。scoped_ptr的這種特性提升了我們的代碼的表現,我們可以根據需要選擇最合適的智能指針(scoped_ptr 或 auto_ptr)。

要決定使用std::auto_ptr還是boost::scoped_ptr, 就要考慮轉移所有權是不是你想要的智能指針的一個特性。如果不是,就用scoped_ptr. 它是一種輕量級的智能指針;使用它不會使你的程序變大或變慢。它只會讓你的代碼更安全,更好維護。

下面是scoped_ptr的摘要,以及其成員的簡要描述:

namespace boost {

  template<typename T> class scoped_ptr : noncopyable {
  public:
    explicit scoped_ptr(T* p = 0);
    ~scoped_ptr();

    void reset(T* p = 0);

    T& operator*() const;
    T* operator->() const;
    T* get() const;
  
    void swap(scoped_ptr& b);
  };

  template<typename T>
    void swap(scoped_ptr<T> & a, scoped_ptr<T> & b);
}

成員函數

explicit scoped_ptr(T* p=0);

構造函數,存儲p的一份拷貝。注意,p 必須是用operator new分配的,或者是null. 在構造的時候,不要求T必須是一個完整的類型。當指針p是調用某個分配函數的結果而不是直接調用new得到的時候很有用:因爲這個類型不必是完整的,只需要類型T的一個前向聲明就可以了。這個構造函數不會拋出異常。

~scoped_ptr();

刪除被指物。類型T在被銷燬時必須是一個完整的類型。如果scoped_ptr在它被析構時並沒有保存資源,它就什麼都不做。這個析構函數不會拋出異常。

void reset(T* p=0);

重置一個 scoped_ptr 就是刪除它已保存的指針,如果它有的話,並重新保存 p. 通常,資源的生存期管理應該完全由scoped_ptr自己處理,但是在極少數時候,資源需要在scoped_ptr的析構之前釋放,或者scoped_ptr要處理它原有資源之外的另外一個資源。這時,就可以用reset,但一定要儘量少用它。(過多地使用它通常表示有設計方面的問題) 這個函數不會拋出異常。

T& operator*() const;

返回一個到被保存指針指向的對象的引用。由於不允許空的引用,所以解引用一個擁有空指針的scoped_ptr將導致未定義行爲。如果不能肯定所含指針是否有效,就用函數get替代解引用。這個函數不會拋出異常。

T* operator->() const;

返回保存的指針。如果保存的指針爲空,則調用這個函數會導致未定義行爲。如果不能肯定指針是否空的,最好使用函數get。這個函數不會拋出異常。

T* get() const;

返回保存的指針。應該小心地使用get,因爲它可以直接操作裸指針。但是,get使得你可以測試保存的指針是否爲空。這個函數不會拋出異常。get通常在調用那些需要裸指針的函數時使用。

operator unspecified_bool_type() const

返回scoped_ptr是否爲非空。返回值的類型是未指明的,但這個類型可被用於Boolean的上下文中。在if語句中最好使用這個類型轉換函數,而不要用get去測試scoped_ptr的有效性

void swap(scoped_ptr& b)

交換兩個scoped_ptr的內容。這個函數不會拋出異常。

普通函數

template<typename T> void swap(scoped_ptr<T>& a,scoped_ptr<T>& b)

這個函數提供了交換兩個scoped pointer的內容的更好的方法。之所以說它更好,是因爲 swap(scoped1,scoped2) 可以更廣泛地用於很多指針類型,包括裸指針和第三方的智能指針。scoped1.swap(scoped2) 則只能用於它的定義所在的智能指針,而不能用於裸指針。


用法

scoped_ptr的用法與普通的指針沒什麼區別;最大的差別在於你不必再記得在指針上調用delete,還有複製是不允許的。典型的指針操作(operator* 和 operator->)都被重載了,並提供了和裸指針一樣的語法。用scoped_ptr和用裸指針一樣快,也沒有大小上的增加,因此它們可以廣泛使用。使用boost::scoped_ptr時,包含頭文件"boost/scoped_ptr.hpp". 在聲明一個scoped_ptr時,用被指物的類型來指定類模板的參數。例如,以下是一個包含std::string指針的scoped_ptr:

boost::scoped_ptr<std::string> p(new std::string("Hello"));

當scoped_ptr被銷燬時,它對它所擁有的指針調用delete 。

不需要手工刪除

讓我們看一個程序,它使用scoped_ptr來管理std::string指針。注意這裏沒有對delete的調用,因爲scoped_ptr是一個自動變量,它會在離開作用域時被銷燬。

#include "boost/scoped_ptr.hpp"
#include <string>
#include <iostream>

int main() {
  {
  boost::scoped_ptr<std::string>
  p(new std::string("Use scoped_ptr often."));

  // 打印字符串的值
  if (p)
    std::cout << *p << '\n';
   
  // 獲取字符串的大小
  size_t i=p->size();

  // 給字符串賦新值
  *p="Acts just like a pointer";
 
  } // 這裏p被銷燬,並刪除std::string
}

這段代碼中有幾個地方值得註明一下。首先,scoped_ptr可以測試其有效性,就象一個普通指針那樣,因爲它提供了隱式轉換到一個可用於布爾表達式的類型的方法。其次,可以象使用裸指針那樣調用被指物的成員函數,因爲重載了operator->. 第三,也可以和裸指針一樣解引用scoped_ptr,這歸功於operator*的重載。這些特性正是scoped_ptr和其它智能指針的用處所在,因爲它們和裸指針的不同之處在於對生存期管理的語義上,而不在於語法上。

和auto_ptr幾乎一樣

scoped_ptr 與 auto_ptr間的區別主要在於對擁有權的處理。auto_ptr在複製時會從源auto_ptr自動交出擁有權,而scoped_ptr則不允許被複制。看看下面這段程序,它把scoped_ptr 和 auto_ptr放在一起,你可以清楚地看到它們有什麼不同。

 

void scoped_vs_auto() {

  using boost::scoped_ptr;
  using std::auto_ptr;

  scoped_ptr<std::string> p_scoped(new std::string("Hello"));
  auto_ptr<std::string> p_auto(new std::string("Hello"));

  p_scoped->size();
  p_auto->size();

  scoped_ptr<std::string> p_another_scoped=p_scoped;
  auto_ptr<std::string> p_another_auto=p_auto;

  p_another_auto->size();
  (*p_auto).size();
}

這個例子不能通過編譯,因爲scoped_ptr不能被複制構造或被賦值。auto_ptr既可以複製構造也可以賦值,但這們同時也意味着它把所有權從p_auto 轉移給了 p_another_auto, 在賦值後p_auto將只剩下一個空指針。這可能會導致令人不快的驚訝,就象你試圖把auto_ptr放入容器內時所發生的那樣。如果我們刪掉對p_another_scoped的賦值,程序就可以編譯了,但它的運行結果是不可預測的,因爲它解引用了p_auto裏的空指針(*p_auto).

由於scoped_ptr::get會返回一個裸指針,所以就有可能對scoped_ptr做一些有害的事情,其中有兩件是你尤其要避免的。第一,不要刪除這個裸指針。因爲它會在scoped_ptr被銷燬時再一次被刪除。第二,不要把這個裸指針保存到另一個scoped_ptr (或其它任何的智能指針)裏。因爲這樣也會兩次刪除這個指針,每個scoped_ptr一次。簡單地說,儘量少用get, 除非你要使用那些要求你傳送裸指針的遺留代碼!

scoped_ptr 和Pimpl用法

scoped_ptr可以很好地用於許多以前使用裸指針或auto_ptr的地方,如在實現pimpl用法時。pimpl 用法背後的思想是把客戶與所有關於類的私有部分的知識分隔開。由於客戶是依賴於類的頭文件的,頭文件中的任何變化都會影響客戶,即使僅是對私有節或保護節的修改。pimpl用法隱藏了這些細節,方法是將私有數據和函數放入一個單獨的類中,並保存在一個實現文件中,然後在頭文件中對這個類進行前向聲明並保存一個指向該實現類的指針。類的構造函數分配這個pimpl類,而析構函數則釋放它。這樣可以消除頭文件與實現細節的相關性。我們來構造一個實現pimpl 用法的類,然後用智能指針讓它更爲安全。

// pimpl_sample.hpp

#if !defined (PIMPL_SAMPLE)
#define PIMPL_SAMPLE

class pimpl_sample {
  struct impl;  // 譯者注:原文中這句在class之外,與下文的實現代碼有矛盾
  impl* pimpl_;
public:
  pimpl_sample();
  ~pimpl_sample();
  void do_something();
};

#endif

這是pimpl_sample類的接口。struct impl 是一個前向聲明,它把所有私有成員和函數放在另一個實現文件中。這樣做的效果是使客戶與pimpl_sample類的內部細節完全隔離開來。

// pimpl_sample.cpp

#include "pimpl_sample.hpp"
#include <string>
#include <iostream>

struct pimpl_sample::impl {
  void do_something_() {
    std::cout << s_ << "\n";
  }

  std::string s_;
};

pimpl_sample::pimpl_sample()
  : pimpl_(new impl) {
  pimpl_->s_ = "This is the pimpl idiom";
}

pimpl_sample::~pimpl_sample() {
  delete pimpl_;
}

void pimpl_sample::do_something() {
  pimpl_->do_something_();
}

看起來很完美,但並不是的。這個實現不是異常安全的!原因是pimpl_sample的構造函數有可能在pimpl被構造後拋出一個異常。在構造函數中拋出異常意味着已構造的對象並不存在,因此在棧展開時將不會調用它的析構函數。這樣就意味着分配給pimpl_指針的內存將泄漏。然而,有一樣簡單的解決方法:用scoped_ptr來解救!

class pimpl_sample {
  struct impl;
  boost::scoped_ptr<impl> pimpl_;
  ...
};

讓scoped_ptr來處理隱藏類impl的生存期管理,並從析構函數中去掉對impl的刪除(它不再需要,這要感謝scoped_ptr),這樣就做完了。但是,你必須記住要手工定義析構函數;原因是在編譯器生成隱式析構函數時,類impl還是不完整的,所以它的析構函數不能被調用。如果你用auto_ptr來保存impl, 你可以編譯,但也還是有這個問題,但如果用scoped_ptr, 你將收到一個錯誤提示。

要注意的是,如果你使用scoped_ptr作爲一個類的成員,你就必須手工定義這個類的複製構造函數和賦值操作符。原因是scoped_ptr是不能複製的,因此聚集了它的類也變得不能複製了。

最後一點值得注意的是,如果pimpl實例可以安全地被多個封裝類(在這裏是pimpl_sample)的實例所共享,那麼用boost::shared_ptr來管理pimpl的生存期纔是正確的選擇。用shared_ptr比用scoped_ptr的優勢在於,不需要手工去定義複製構造函數和賦值操作符,而且可以定義空的析構函數,shared_ptr被設計爲可以正確地用於未完成的類。

scoped_ptr 不同於 const auto_ptr

留心的讀者可能已經注意到auto_ptr可以幾乎象scoped_ptr一樣地工作,只要把auto_ptr聲明爲const:

const auto_ptr<A> no_transfer_of_ownership(new A);

它們很接近,但不是一樣。最大的區別在於scoped_ptr可以被reset, 在需要時可以刪除並替換被指物。而對於const auto_ptr這是不可能的。另一個小一點的區別是,它們的名字不同:儘管const auto_ptr意思上和scoped_ptr一樣,但它更冗長,也更不明顯。當你的詞典裏有了scoped_ptr,你就應該使用它,因爲它可以更清楚地表明你的意圖。如果你想說一個資源是要被限制在作用域裏的,並且不應該有辦法可以放棄它的所有權,你就應該用 boost::scoped_ptr.

總結

使用裸指針來寫異常安全和無錯誤的代碼是很複雜的。使用智能指針來自動地把動態分配對象的生存期限制在一個明確的範圍之內,是解決這種問題的一個有效方法,並且提高了代碼的可讀性、可維護性和質量。scoped_ptr 明確地表示被指物不能被共享和轉移。正如你所看到的,std::auto_ptr可以從另一個auto_ptr那裏竊取被指物,那怕是無意的,這被認爲是auto_ptr的最大缺點。正是這個缺點使得scoped_ptr成爲auto_ptr最好的補充。當一個動態分配的對象被傳送給scoped_ptr, 它就成爲了這個對象的唯一的擁有者。因爲scoped_ptr幾乎總是以自動變量或數據成員來分配的,因此它可以在離開作用域時正確地銷燬對象,從而在執行流由於返回語句或異常拋出而離開作用域時,也總能釋放它所管理的內存。

在以下情況時使用 scoped_ptr :

  • 在可能有異常拋出的作用域裏使用指針
  • 函數裏有幾條控制路徑
  • 動態分配對象的生存期應被限制於特定的作用域內
  • 異常安全非常重要時(總應如此!)

 

發佈了13 篇原創文章 · 獲贊 6 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章