More Effective C++ 條款27(下) (轉)

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

條款27:要求或禁止在堆中產生(下)

到目前為止,這種邏輯很正確,但是不夠深入。最根本的問題是物件可以被分配在三個地方,而不是兩個。是的,棧和堆能夠容納物件,但是我們忘了靜態物件。靜態物件是那些在執行時僅能初始化一次的物件。靜態物件不僅僅包括顯示地宣告為static的物件,也包括在全域性和名稱空間裡的物件(參見條款47)。這些物件肯定位於某些地方,而這些地方既不是棧也不是堆。:namespace prefix = o ns = "urn:schemas--com::office" />

它們的位置是依據而定的,但是在很多棧和堆相向擴充套件的系統裡,它們位於堆的底端。先前管理的圖片到講述的是事實,不過是很多系統都具有的事實,但是沒有告訴我們這些系統全部的事實,加上靜態變數後,這幅圖片如下所示:

onHeap不能工作的原因立刻變得很清楚了,不能辨別堆物件與靜態物件的區別:

void allocateSomes()

{

  char *pc = new char;  // 堆物件: onHeap(pc)

   // 將返回true

 

  char c;  // 棧物件: onHeap(&c)

  // 將返回false

 

  static char sc;  // 靜態物件: onHeap(&sc)

  // 將返回true

  ...

 

}

現在你可能不顧一切地尋找區分堆物件與棧物件的方法,在走頭無路時你想在可移植性上打主意,但是你會這麼孤注一擲地進行一個不能獲得正確結果的交易麼?絕對不會。我知道你會拒絕使用這種雖然誘人但是不可靠的“地址比對”技巧。

令人傷心的是不僅沒有一種可移植的方法來判斷物件是否在堆上,而且連能在多數時間正常工作的“準可移植”的方法也沒有。如果你實在非得必須判斷一個地址是否在堆上,你必須使用完全不可移植的方法,其實現依賴於系統,只能這樣做了。因此你最好重新設計你的,以便你可以不需要判斷物件是否在堆中。

如果你發現自己實在為物件是否在堆中這個問題所困擾,一個可能的原因是你想知道物件是否能在其上呼叫delete。這種刪除經常採用“delete this”這種宣告狼籍的形式。不過知道“是否能安全刪除一個指標”與“只簡單地知道一個指標是否指向堆中的事物”不一樣,因為不是所有在堆中的事物都能被安全地delete。再考慮包含UPNumber物件的Asset物件:

class Asset {

private:

  UPNumber value;

  ...

 

};

 

Asset *pa = new Asset;

很明顯*pa(包括它的成員value)在堆上。同樣很明顯在指向pa->value上呼叫delete是不安全的,因為該指標不是被new返回的。

幸運的是“判斷是否能夠刪除一個指標”比“判斷一個指標指向的事物是否在堆上”要容易。因為對於前者我們只需要一個operator new返回的地址集合。因為我們能自己編寫operator new(參見Effective C++條款8—條款10),所以構建這樣一個集合很容易。如下所示,我們這樣解決這個問題:

void *operator new(size_t size)

{

  void *p = getMemory(size);  //呼叫一些函式來分配記憶體,

  //處理記憶體不夠的情況

 

  把 p加入到一個被分配地址的集合;

 

  return p;

 

}

 

void operator delete(void *ptr)

{

  releaseMemory(ptr);  // return memory to

   // free store

 

  從被分配地址的集合中移去ptr;

}

 

bool isSafeToDelete(const void *address)

{

  返回address是否在被分配地址的集合中;

}

這很簡單,operator new在地址分配集合里加入一個元素,operator delete從集合中移去專案,isSafeToDelete在集合中查詢並確定某個地址是否在集合中。如果operator new 和 operator delete函式在全域性作用域中,它就能適用於所有的型別,甚至是內建型別。

在實際當中,有三種因素制約著對這種設計方式的使用。第一是我們極不願意在全域性域定義任何東西,特別是那些已經具有某種含義的函式,象operator new和operator delete正如我們所知,只有一個全域性域,只有一種具有正常特徵形式(也就是引數型別)的operator new和operator delete 。這樣做會使得我們的軟體與其它也實現全域性版本的operator new 和operator delete的軟體(例如許多物件導向系統)不相容。

 

我們考慮的第二個因素是:如果我們不需要這些,為什麼還要為跟蹤返回的地址而負擔額外的開銷呢?

最後一點可能有些平常,但是很重要。實現isSafeToDelete讓它總能夠正常工作是不可能的。難點是多繼承下來的類或繼承自虛基類的類有多個地址,所以無法保證傳給isSafeToDelete的地址與operator new 返回的地址相同,即使物件在堆中建立。有關細節參見條款24和條款31。

我們希望這些函式提供這些功能時能夠不汙染全域性名稱空間,沒有額外的開銷,沒有正確性問題。幸運的是C++使用一種抽象mixin基類滿足了我們的需要。

抽象基類是不能被例項化的基類,也就是至少具有一個純虛擬函式的基類。mixin(mix in)類提供某一特定的功能,並可以與其繼承類提供的其它功能相相容(參見Effective C++條款7)。這種類幾乎都是抽象類。因此我們能夠使用抽象混合(mixin)基類給派生類提供判斷指標指向的記憶體是否由operator new分配的能力。該類如下所示:

class HeapTracked {  // 混合類; 跟蹤

public:   // 從operator new返回的ptr

 

  class MissingAddress{};  // 異常類,見下面程式碼

 

  virtual ~HeapTracked() = 0;

 

  static void *operator new(size_t size);

  static void operator delete(void *ptr);

 

  bool isOnHeap() const;

 

private:

  typedef const void* RawAddress;

  static list addresses;

};

這個類使用了list(連結串列)資料結構跟蹤從operator new返回的所有指標,list標準C++庫的一部分(參見Effective C++條款49和本書條款35)。operator new函式分配記憶體並把地址加入到list中;operator delete用來釋放記憶體並從list中移去地址元素。isOnHeap判斷一個物件的地址是否在list中。

HeapTracked類的實作(我覺得把implementation翻譯成“實作”更好 譯者注)很簡單,呼叫全域性的operator new和operator delete函式來完成記憶體的分配與釋放,list類裡的函式進行插入操作和刪除操作,並進行單語句的查詢操作。以下是HeapTracked的全部實作:

// mandatory definition of static class member

list HeapTracked::addresses;

 

// HeapTracked的解構函式是純虛擬函式,使得該類變為抽象類。

// (參見Effective C++條款14). 然而解構函式必須被定義,

//所以我們做了一個空定義。.

HeapTracked::~HeapTracked() {}

 

 

 

void * HeapTracked::operator new(size_t size)

{

  void *memPtr = ::operator new(size);  // 獲得記憶體

 

  addresses.push_front(memPtr);  // 把地址放到list的前端

  return memPtr;

}

 

void HeapTracked::operator delete(void *ptr)

{

  //得到一個 "iterator",用來識別list元素包含的ptr;

  //有關細節參見條款35

  list::iterator it =

  find(addresses.begin(), addresses.end(), ptr);

 

  if (it != addresses.end()) {  // 如果發現一個元素

  addresses.erase(it);   //則刪除該元素

  ::operator delete(ptr);  // 釋放記憶體

  } else {  // 否則

  throw MissingAddress();  // ptr就不是用operator new

  }  // 分配的,所以丟擲一個異常

}

 

bool HeapTracked::isOnHeap() const

{

  // 得到一個指標,指向*this佔據的記憶體空間的起始處,

  // 有關細節參見下面的討論

  const void *rawAddress = dynamic_cast(this);

 

  // 在operator new返回的地址list中查到指標

  list::iterator it =

  find(addresses.begin(), addresses.end(), rawAddress);

 

  return it != addresses.end();  // 返回it是否被找到

}

儘管你可能對list類和標準C++庫的其它部分不很熟悉,程式碼還是很一目瞭然。條款35將解釋這裡的每件東西,不過程式碼裡的註釋已經能夠解釋這個例子是如何執行的。

只有一個地方可能讓你感到困惑,就是這個語句(在isOnHeap函式中)

const void *rawAddress = dynamic_cast(this);

我前面說過帶有多繼承或虛基類的物件會有幾個地址,這導致編寫全域性函式isSafeToDelete會很複雜。這個問題在isOnHeap中仍然會遇到,但是因為isOnHeap僅僅用於HeapTracked物件中,我們能使用dynamic_cast運算子的一種特殊的特性來消除這個問題。只需簡單地放入dynamic_cast,把一個指標dynamic_cast成void*型別(或const void*或volatile void* 。。。。。),生成的指標指向“原指標指向物件記憶體”的開始處。但是dynamic_cast只能用於“指向至少具有一個虛擬函式的物件”的指標上。我們該死的isSafeToDelete函式可以用於指向任何型別的指標,所以dynamic_cast也不能幫助它。isOnHeap更具有選擇性(它只能測試指向HeapTracked物件的指標),所以能把this指標dynamic_cast成const void*,變成一個指向當前物件起始地址的指標。如果HeapTracked::operator new為當前物件分配記憶體,這個指標就是HeapTracked::operator new返回的指標。如果你的支援dynamic_cast 運算子,這個技巧是完全可移植的。

使用這個類,即使是最初級的程式設計師也可以在類中加入跟蹤堆中指標的功能。他們所需要做的就是讓他們的類從HeapTracked繼承下來。例如我們想判斷Assert物件指標指向的是否是堆物件:

class Asset: public HeapTracked {

private:

  UPNumber value;

  ...

};

我們能夠這樣查詢Assert*指標,如下所示:

void inventoryAsset(const Asset *ap)

{

  if (ap->isOnHeap()) {

  ap is a heap-based asset — inventory it as such;

  }

  else {

  ap is a non-heap-based asset — record it that way;

  }

}

象HeapTracked這樣的混合類有一個缺點,它不能用於內建型別,因為象int和char這樣的型別不能繼承自其它型別。不過使用象HeapTracked的原因一般都是要判斷是否可以呼叫”delete this”,你不可能在內建型別上呼叫它,因為內建型別沒有this指標。

禁止堆物件

判斷物件是否在堆中的測試到現在就結束了。與此相反的領域是“禁止在堆中建立物件”。通常物件的建立這樣三種情況:物件被直接例項化;物件做為派生類的基類被例項化;物件被嵌入到其它物件內。我們將按順序地討論它們。

禁止客戶端直接例項化物件很簡單,因為總是呼叫new來建立這種物件,你能夠禁止客戶端呼叫new。你不能影響new運算子的可用性(這是內嵌於語言的),但是你能夠利用new運算子總是呼叫operator new函式這點(參見條款8),來達到目的。你可以自己宣告這個函式,而且你可以把它宣告為private.。例如,如果你想不想讓客戶端在堆中建立UPNumber物件,你可以這樣編寫:

class UPNumber {

private:

  static void *operator new(size_t size);

  static void operator delete(void *ptr);

  ...

};

現在客戶端僅僅可以做允許它們做的事情:

UPNumber n1;  // okay

 

static UPNumber n2;  // also okay

 

UPNumber *p = new UPNumber;  // error! attempt to call

  // private operator new

把operator new宣告為private就足夠了,但是把operator new宣告為private,而把iperator delete宣告為public,這樣做有些怪異,所以除非有絕對需要的原因,否則不要把它們分開宣告,最好在類的一個部分裡宣告它們。如果你也想禁止UPNumber堆物件陣列,可以把operator new[]和operator delete[](參見條款8)也宣告為private。(operator new和operator delete之間的聯絡比大多數人所想象的要強得多。有關它們之間關係的鮮為人知的一面,可以參見我的文章counting objects裡的sbar部分。)

有趣的是,把operator new宣告為private經常會阻礙UPNumber物件做為一個位於堆中的派生類物件的基類被例項化。因為如果operator new和operator delete沒有在派生類中被宣告為public,它們就會被繼承下來,繼承了基類private函式的類,如下所示:

class UPNumber { ... };  // 同上

 

class NonNegativeUPNumber:  //假設這個類

  public UPNumber {  //沒有宣告operator new

  ...

};

 

NonNegativeUPNumber n1;  // 正確

 

static NonNegativeUPNumber n2;  // 也正確

 

NonNegativeUPNumber *p =  // 錯誤! 試圖呼叫

  new NonNegativeUPNumber;  // private operator new

如果派生類宣告它自己的operator new,當在堆中分配派生物件時,就會呼叫這個函式,必須得找到一種不同的方法防止UPNumber基類部分纏繞在這裡。同樣,UPNumber的operator new是private這一點,不會對分配包含做為成員的UPNumber物件的物件產生任何影響:

class Asset {

public:

  Asset(int initValue);

  ...

 

private:

  UPNumber value;

};

 

Asset *pa = new Asset(100);  // 正確, 呼叫

  // Asset::operator new 或

   // ::operator new, 不是

  // UPNumber::operator new

實際上,我們又回到了這個問題上來,即“如果UPNumber物件沒有被構造在堆中,我們想丟擲一個異常”。當然這次的問題是“如果物件在堆中,我們想丟擲異常”。正像沒有可移植的方法來判斷地址是否在堆中一樣,也沒有可移植的方法判斷地址是否不在堆中,所以我們很不走運,不過這也絲毫不奇怪,畢竟如果我們能辨別出某個地址在堆上,我們也就能辨別出某個地址不在堆上。但是我們什麼都不能辨別出來。


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

相關文章