More Effective C++ 條款27(下) (轉)
條款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
};
這個類使用了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的解構函式是純虛擬函式,使得該類變為抽象類。
// (參見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
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
// 在operator new返回的地址list中查到指標
list
find(addresses.begin(), addresses.end(), rawAddress);
return it != addresses.end(); // 返回it是否被找到
}
儘管你可能對list類和標準C++庫的其它部分不很熟悉,程式碼還是很一目瞭然。條款35將解釋這裡的每件東西,不過程式碼裡的註釋已經能夠解釋這個例子是如何執行的。
只有一個地方可能讓你感到困惑,就是這個語句(在isOnHeap函式中)
const void *rawAddress = dynamic_cast
我前面說過帶有多繼承或虛基類的物件會有幾個地址,這導致編寫全域性函式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/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- More effective c++ 條款10(下) (轉)C++
- More Effective C++ 條款28(下) (轉)C++
- More Effective C++ 條款26(下) (轉)C++
- More Effective C++ 條款4 (轉)C++
- More Effective C++ 條款19 (轉)C++
- More Effective C++ 條款6 (轉)C++
- More effective C++ 條款14 (轉)C++
- More Effective C++ 條款15 (轉)C++
- More Effective C++ 條款2 (轉)C++
- More Effective C++ 條款3 (轉)C++
- More Effective C++ 條款11 (轉)C++
- More effective C++ 條款13 (轉)C++
- More effective C++ 條款12 (轉)C++
- More Effective C++ 條款5 (轉)C++
- More Effective C++ 條款一 (轉)C++
- More Effective C++ 條款17 (轉)C++
- More Effective C++ 條款18 (轉)C++
- More Effective C++ 條款20 (轉)C++
- More Effective C++ 條款21 (轉)C++
- More Effective C++ 條款22 (轉)C++
- More Effective C++ 條款23 (轉)C++
- More Effective C++ 條款24 (轉)C++
- More Effective C++ 條款25 (轉)C++
- More Effective C++ 條款7 (轉)C++
- More Effective C++ 條款8 (轉)C++
- More Effective C++ 條款28(中) (轉)C++
- More effective c++ 條款10(上) (轉)C++
- More Effective C++ 條款28(上) (轉)C++
- Effective Modern C++ 系列之 條款2: autoC++
- Effective C++ 條款08_不止於此C++
- Guru of the Week 條款27:轉呼叫函式 (轉)函式
- Effective c++條款11:在operator=中處理“自我賦值”C++賦值
- [讀書筆記][effective C++]條款30-inline的原理筆記C++inline
- Effective C++: Item 32 (轉)C++
- Effective C++: Item 21 (轉)C++
- Effective C++: Item 24 (轉)C++
- more effective entity bean(新的改進entity bean的效能的七條(EJB2.0版)) (轉)Bean
- Effective STL之條款2:謹防容器無關程式碼的假象 (轉)