More Effective C++ 條款28(上) (轉)

worldblog發表於2007-12-09
More Effective C++ 條款28(上) (轉)[@more@]

 

條款28:靈巧(smart)指標(上):namespace prefix = o ns = "urn:schemas--com::office" />

靈巧指標是一種外觀和行為都被設計成與內建指標相類似的,不過它能提供更多的功能。它們有許多應用的領域,包括資源管理(參見條款9、10、25和31)和重複程式碼任務的自動化(參見條款17和29)

當你使用靈巧指標替代C++的內建指標(也就是dumb pointer),你就能控制下面這些方面的指標的行為:

構造和析構。你可以決定建立靈巧指標時應該怎麼做。通常賦給靈巧指標預設值0,避免出現令人頭疼的未初始化的指標。當指向某一物件的最後一個靈巧指標被釋放時,一些靈巧指標負責刪除它們指向的物件。這樣做對防止資源洩漏很有幫助。

複製和賦值。你能對複製靈巧指標或設計靈巧指標的賦值操作進行控制。對於一些型別的靈巧指標來說,期望的行為是自動複製它們所指向的物件或用對這些物件進行賦值操作,也就是進行deep copy(深層複製)。對於其它的一些靈巧指標來說,僅僅複製指標本身或對指標進行賦值操作。還有一部分型別的靈巧指標根本就不允許這些操作。無論你認為應該如何去做,靈巧指標始終受你的控制。

Dereferencing(取出指標所指東西的內容)。當客戶端引用被靈巧指標所指的物件,會發生什麼事情呢?你可以自行決定。例如你可以用靈巧指標實現條款17提到的lazy fetching 方法。

靈巧指標從模板中生成,因為要與內建指標類似,必須是strongly typed(強型別)的;模板引數確定指向物件的型別。大多數靈巧指標模板看起來都象這樣:

template  //靈巧指標物件模板

class SmartPtr { 

public:

  SmartPtr(T* realPtr = 0);  // 建立一個靈巧指標

  // 指向dumb pointer所指的

  // 物件。未初始化的指標

  // 預設值為0(null)

 

  SmartPtr(const SmartPtr& rhs);  // 複製一個靈巧指標

 

  ~SmartPtr();   // 釋放靈巧指標

 

  // make an assignment to a smart ptr

  SmartPtr& operator=(const SmartPtr& rhs);

 

  T* operator->() const;  // dereference一個靈巧指標

  // 以訪問所指物件的成員

 

  T& operator*() const;  // dereference 靈巧指標

 

private:

  T *pointee;  // 靈巧指標所指的物件

}; 

複製構造和賦值運算子都被展現在這裡。對於靈巧指標類來說,不能允許進行複製和賦值操作,它們應該被宣告為private(參見Effective C++條款27)。兩個dereference運算子被宣告為const,是因為dereference一個指標時不能對指標進行修改(儘管可以修改指標所指的物件)。最後,每個指向T物件的靈巧指標包含一個指向T的dumb pointer。這個dumb pointer指向的物件才是靈巧指標指向的真正物件。

進入靈巧指標實作的細節之前,應該研究一下客戶端如何使用靈巧指標。考慮一下,存在一個分散式(即其上的物件一些在本地,一些在)。相對於訪問遠端物件,訪問本地物件通常總是又簡單而且速度又快,因為遠端訪問需要遠端過程(RPC),或其它一些聯絡遠距離的方法。

對於編寫程式碼的客戶端來說,採用不同的方法分別處理本地物件與遠端物件是一件很煩人的事情。讓所有的物件都位於一個地方會更方便。靈巧指標可以讓程式庫實現這樣的夢想。

template  // 指向位於分散式 ()

class DBPtr {  // 中物件的靈巧指標模板

public:  //

 

  DBPtr(T *realPtr = 0);  // 建立靈巧指標,指向

   // 由一個本地dumb pointer

  // 給出的DB 物件

 

 

  DBPtr(DataBaseID id);  // 建立靈巧指標,

  // 指向一個DB物件,

  // 具有惟一的DB識別符

 

  ...   // 其它靈巧指標函式

};  //同上

 

class Tuple {  // 資料庫元組類

public: 

  ...

  void displayEditDialog();  // 顯示一個圖形對話方塊,

  // 允許編輯元組。

  // user to edit the tuple

 

  bool isValid() const;  // 返回*this是否透過了

};  // 合法性驗證

 

// 這個類别範本用於在修改T物件時進行日誌登記。

// 有關細節參見下面的敘述:

template

class LogEntry {

public:

  LogEntry(const T& ToBeModified);

  ~LogEntry();

};

 

void editTuple(DBPtr& pt)

{

  LogEntry entry(*pt);  // 為這個編輯操作登記日誌

  // 有關細節參見下面的敘述

 

  // 重複顯示編輯對話方塊,直到提供了合法的數值。

  do {

  pt->displayEditDialog();

  } while (pt->isValid() == false);

}

在editTuple中被編輯的元組物理上可以位於本地也可以位於遠端,但是編寫editTuple的程式設計師不用關心這些事情。靈巧指標類隱藏了系統的這些方面。程式設計師只需關心透過物件進行訪問的元組,而不用關心如何宣告它們,其行為就像一個內建指標。

注意在editTuple中LogEntry物件的用法。一種更傳統的設計是在呼叫displayEditDialog前開始日誌記錄,呼叫後結束日誌記錄。在這裡使用的方法是讓LogEntry的建構函式啟動日誌記錄,解構函式結束日誌記錄。正如條款9所解釋的,當面對異常時,讓物件自己開始和結束日誌記錄比顯示地呼叫函式可以使的程式更健壯。而且建立一個LogEntry物件比每次都呼叫開始記錄和結束記錄函式更容易。

正如你所看到的,使用靈巧指標與使用dump pointer沒有很大的差別。這表明了封裝是非常有效的。靈巧指標的客戶端可以象使用dumb pointer一樣使用靈巧指標。正如我們將看到的,有時這種替代會更透明化。

靈巧指標的構造、賦值和析構

靈巧指標的的析構通常很簡單:找到指向的物件(一般由靈巧指標建構函式的引數給出),讓靈巧指標的內部成員dumb pointer指向它。如果沒有找到物件,把內部指標設為0或發出一個錯誤訊號(可以是丟擲一個異常)。

靈巧指標複製建構函式、賦值運算子函式和解構函式的實作由於所有權的問題所以有些複雜。如果一個靈巧指標擁有它指向的物件,當它被釋放時必須負責刪除這個物件。這裡假設靈巧指標指向的的物件是動態分配的。這種假設在靈巧指標中是常見的(有關確定這種假設是真實的方法,參見條款27)。

看一下標準C++類庫中auto_ptr模板。這如條款9所解釋的,一個auto_ptr物件是一個指向堆物件的靈巧指標,直到auto_ptr被釋放。auto_ptr的解構函式刪除其指向的物件時,會發生什麼事情呢?auto_ptr模板的實作如下:

template

class auto_ptr {

public:

  auto_ptr(T *ptr = 0): pointee(ptr) {}

  ~auto_ptr() { delete pointee; }

  ...

 

private:

  T *pointee;

};

假如auto_ptr擁有物件時,它可以正常執行。但是當auto_ptr被複製或被賦值時,會發生什麼情況呢?

auto_ptr ptn1(new TreeNode);

 

auto_ptr ptn2 = ptn1;  // 呼叫複製建構函式

  //會發生什麼情況?

 

auto_ptr ptn3;

 

ptn3 = ptn2;  // 呼叫 operator=;

   // 會發生什麼情況?

如果我們只複製內部的dumb pointer,會導致兩個auto_ptr指向一個相同的物件。這是一個災難,因為當釋放quto_ptr時每個auto_ptr都會刪除它們所指的物件。這意味著一個物件會被我們刪除兩次。這種兩次刪除的結果將是不可預測的(通常是災難性的)。

另一種方法是透過呼叫new,建立一個所指物件的新複製。這確保了不會有許多指向同一個物件的auto_ptr,但是建立(以後還得釋放)新物件會造成不可接受的損耗。並且我們不知道要建立什麼型別的物件,因為auto_ptr物件不用必須指向型別為T的物件,它也可以指向T的派生型別物件。虛擬建構函式(參見條款25)可能幫助我們解決這個問題,但是好象不能把它們用在auto_ptr這樣的通用類中。

如果quto_ptr禁止複製和賦值,就可以消除這個問題,但是採用“當auto_ptr被複製和賦值時,物件所有權隨之被傳遞”的方法,是一個更具靈活性的解決方案:

template

class auto_ptr {

public:

  ...

 

  auto_ptr(auto_ptr& rhs);  // 複製建構函式

 

  auto_ptr&  // 賦值

  operator=(auto_ptr& rhs);  // 運算子

 

  ...

};

 

template

auto_ptr::auto_ptr(auto_ptr& rhs)

{

  pointee = rhs.pointee;  // 把*pointee的所有權

   // 傳遞到 *this

 

  rhs.pointee = 0;  // rhs不再擁有

}  // 任何東西

 

template

auto_ptr& auto_ptr::operator=(auto_ptr& rhs)

{

  if (this == &rhs)  // 如果這個物件自我賦值

  return *this;  // 什麼也不要做

 

 

  delete pointee;  // 刪除現在擁有的物件

 

 

  pointee = rhs.pointee;  // 把*pointee的所有權

  rhs.pointee = 0;   // 從 rhs 傳遞到 *this

 

  return *this;

}

注意賦值運算子在接受新物件的所有權以前必須刪除原來擁有的物件。如果不這樣做,原來擁有的物件將永遠不會被刪除。記住,除了auto_ptr物件,沒有人擁有auto_ptr指向的物件。

因為當呼叫auto_ptr的複製建構函式時,物件的所有權被傳遞出去,所以透過傳值方式傳遞auto_ptr物件是一個很糟糕的方法。因為:

// 這個函式通常會導致災難發生

void printTreeNode(ostream& s, auto_ptr p)

{ s << *p; }

 

int main()

{

  auto_ptr ptn(new TreeNode);

 

  ...

 

  printTreeNode(cout, ptn);  //透過傳值方式傳遞auto_ptr

 

  ...

 

}

當printTreeNode的引數p被初始化時(呼叫auto_ptr的複製建構函式),ptn指向物件的所有權被傳遞到給了p。當printTreeNode結束後,p離開了作用域,它的解構函式刪除它指向的物件(就是原來ptr指向的物件)。然而ptr不再指向任何物件(它的dumb pointer是null),所以呼叫printTreeNode以後任何試圖使用它的操作都將產生不可定義的行為。只有在你確實想把物件的所有權傳遞給一個臨時的函式引數時,才能透過傳值方式傳遞auto_ptr。這種情況很少見。

這不是說你不能把auto_ptr做為引數傳遞,這隻意味著不能使用傳值的方法。透過const引用傳遞(Pass-by-reference-to-const)的方法是這樣的:

// 這個函式的行為更直觀一些

void printTreeNode(ostream& s,

  const auto_ptr& p)

{ s << *p; }

在函式里,p是一個引用,而不是一個物件,所以不會呼叫複製建構函式初始化p。當ptn被傳遞到上面這個printTreeNode時,它還保留著所指物件的所有權,呼叫printTreeNode以後還可以地使用ptn。從而透過const引用傳遞auto_ptr可以避免傳值所產生的風險。(“引用傳遞”替代“傳值”的其他原因參見Effective C++條款22)。

在複製和賦值中,把物件的所有權從一個靈巧指標傳遞到另一箇中去,這種思想很有趣,而且你可能已經注意到複製建構函式和賦值運算子不同尋常的宣告方法同樣也很有趣。這些函式同上會帶有const引數,但是上面這些函式則沒有。實際上在複製和賦值中上述這些程式碼修改了這些引數。也就是說,如果auto_ptr物件被複製或做為賦值操作的資料來源,就會修改auto_ptr物件!

是的,就是這樣。C++是如此靈活能讓你這樣去做,真是太好了。如果語言要求複製建構函式和賦值運算子必須帶有const引數,你必須去掉引數的const屬性(參見Effective C++條款21)或用其他方法實現所有權的轉移。準確地說:當複製一個物件或這個物件做為賦值的資料來源,就會修改該物件。這可能有些不直觀,但是它是簡單的,直接的,在這種情況下也是準確的。

如果你發現研究這些auto_ptr成員函式很有趣,你可能希望看看完整的實作。在291頁至294頁上有(只原書頁碼),在那裡你也能看到在標準C++庫中auto_ptr模板有比這裡所描述的更靈活的複製建構函式和賦值運算子。在標準C++庫中,這些函式是成員函式模板,不只是成員函式。(在本條款的後面會講述成員函式模板。也可以閱讀Effective C++條款25)。

靈巧指標的解構函式通常是這樣的:

template

SmartPtr::~SmartPtr()

{

  if (*this owns *pointee) {

  delete pointee;

  }

}

有時刪除前不需要進行測試,例如在一個auto_ptr總是擁有它指向的物件時。而在另一些時候,測試會更為複雜。一個使用了引用計數(參見條款29)靈巧指標必須在判斷是否有權刪除所指物件前調整引用計數值。當然還有一些靈巧指標象dumb pointer一樣,當它們被刪除時,對所指物件沒有任何影響。

實作Dereference 運算子

讓我們把注意力轉向靈巧指標的核心部分,the operator*  和 operator-> 函式。前者返回所指的物件。理論上,這很簡單:
template


T& SmartPtr::operator*() const


{


  perfo"smart pointer" processing;


 


  return *pointee;


}


首先無論函式做什麼,必須先初始化指標或使pointee合法。例如,如果使用lazy fetch(參見條款17),函式必須為pointee建立一個新物件。一旦pointee合法了,operator*函式就返回其所指物件的一個引用。

注意返回型別是一個引用。如果返回物件,儘管允許這麼做,這也將會導致災難性後果。必須時刻牢記:pointee不用必須指向T型別物件;它也可以指向T的派生類物件。如果在這種情況下operator*函式返回的是T型別物件而不是派生類物件的引用,你的函式實際上返回的是一個錯誤型別的物件!(這是一個slicing問題,參見Effective C++條款22和本書條款13)。在返回的這種物件上呼叫虛擬函式,不會觸發與所指物件的動態型別相符的函式。實際上就是說你的靈巧指標不能支援虛擬函式,象這樣的指標再靈巧也沒有用。而返回一個引用還能夠具有更高的(不需要構造一個臨時物件,參見條款19)。能夠兼顧正確與效率當然是一件好事。

如果你是一個急性子的人,你可能會想如果一些人在null靈巧指標上呼叫operator*,也就是說靈巧指標的dumb pointer是null。放鬆。隨便做什麼都行。dereference一個空指標的結果是未定義的,所以這不是一個“錯誤”的行為。想排除一個異常麼?可以,丟擲吧。想呼叫abort函式(可能被assert在失敗時呼叫)?好的,呼叫吧。想遍歷把每個位元組都設成你生日與256模數麼?當然也可以。雖說這樣做沒有什麼好處,但是就語言本身而言,你完全是自由的。

operator->的情況與operator*是相同的,但是在分析operator->之前,讓我們先回憶一下這個函式呼叫的與眾不同的含義。再考慮editTuple函式,其使用一個指向Tuple物件的靈巧指標:

void editTuple(DBPtr& pt)


{


  LogEntry entry(*pt);


 


  do {


  pt->displayEditDialog();


  } while (pt->isValid() == false);


}


語句

pt->displayEditDialog();

被編譯器解釋為:
(pt.operator->())->displayEditDialog();

這意味著不論operator->返回什麼,它必須使用member-ion operator(成員選擇運算子)(->)。因此operator->僅能返回兩種東西:一個指向某物件的dumb pointer或另一個靈巧指標。多數情況下,你想返回一個普通dumb pointer。在此情況下,你這樣實作operator-> :

template


T* SmartPtr::operator->() const


{


  perform "smart pointer" processing;


 


  return pointee;


}


這樣做執行良好。因為該函式返回一個指標,透過operator->呼叫虛擬函式,其行為也是正確的。

對於很多程式來說,這就是你需要了解靈巧指標的全部東西。條款29的引用計數程式碼並沒有比這裡更多的功能。但是如果你想更深入地瞭解靈巧指標,你必須知道更多的有關dumb pointer的知識和靈巧指標如何能或不能進行模擬。如果你的座右銘是“Most people stop at the Z-but not me(多數人淺嘗而止,但我不能夠這樣) ”,下面講述的內容正適合你。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-990657/,如需轉載,請註明出處,否則將追究法律責任。

相關文章