CUJ:標準庫:容納指標的容器 (轉)
The Standard Librarian: I/O and Function s:
Containers of Pointers:namespace prefix = o ns = "urn:schemas--com::office" />
Matthew Austern
http://www.cuj.com/experts/1910/austern.htm?topic=experts
-----------------------------------------------------------------------------------------------------------------------
和標準C++執行庫中的絕大部分東西一樣,標準容器類是用型別來引數化的:你能建立一個std::vector
不幸地是,雖然是常見技術,容納指標的容器對新手來說也是造成混亂的最常見的根源之一。幾乎沒有哪個星期在C++新聞組中不出現這樣的貼子的:為什麼這樣的程式碼導致洩漏:
{
std::vector
for (int i = 0; i < N; ++i)
v.insert(new my_type(i));
...
} // v is destroyed here
這個記憶體洩漏是的嗎?std::vector的析構不是會銷燬v的元素的嗎?
如果你仔細想過std::vector
這篇文章解釋了容納指標的容器的行為是怎麼樣的,什麼時候容納指標的容器會有用,和在需要比標準容器在記憶體管理上的預設行更多的任務時,該做些什麼。
容器和所有權
標準容器使用值語義。舉例來說,當你向一個vector附加一個變數x時:
v.push_back(x)
你實際正在做的是附加x的一個複製。這個語句了x的值(的一個複製),而不是x的地址。在你將x加入一個vector後,你能對x做如何想做的事--比如賦給它一個新值或讓它離開生存域而銷燬--而不影響vector中的複製。一個容器中的元素不能是另外一個容器的元素(兩個容器的元素必須是不同的物件,即使這些元素碰巧相等),並且將一個元素從容器中移除將會銷燬這個元素(雖然具有相同的值的另外一個物件可能存在於別處)。最後,容器“擁有”它的元素:當一個容器銷燬時,其中的所以元素都隨它一起銷燬了。
這些特性與平常的內建陣列很相似,並且可能是太明顯了而不值一提。我列出它們以清楚顯示容器和陣列有多麼相似。新手使用標準容器時發生的最常見的概念是認為容器“在幕後”做了比實際上更多的事。
值語義不總是你所需要的:有時你需要在容器中儲存物件的地址而不是複製物件的值。你能以和陣列相同的方式,用容器實現引用語義:藉由顯式要求。你能將任何型別的物件放入容器[注1],而指標自己就是非常好的物件。指標佔用記憶體;能被賦值;自己有地址;有能被複製的值。如果你需要儲存物件的地址,就使用容納指標的容器。不再是寫:
std::vector
my_type x;
...
v.push_back(x);
你能寫:
std::vector
my_type x;
...
v.push_back(&x);
感覺上,沒有任何變化。你仍然正在建立一個std::vector
擁有指標和擁有指標所指的東西之間的區別就象是vector與陣列或區域性變數。假如你寫:
{
my_type* p = new my_type;
}
當離開程式碼域時,指標p將會消失,但它所指向的物件,*p,不會消失。如果你想銷燬這物件並釋放其記憶體,你需要自己來完成,顯式地寫delete p或用其它等價的方法。 同樣,在std::vector
你可能奇怪為什麼std::vector和其它標準容器沒有設計得對指標做些特別的動作。首先,當然,有一個簡單的一致性因素:理解有一致語義的庫比理解有許多特例的庫容易。如果存在特例,很難劃出分界線。你將iterator或使用者自定義的handle型別等同於指標嗎?如果在通用規則上對vector
第二,並且更重要的是:如果std::vector
想像一個特別的例子:
l 你正在維護一個任務連結串列,某些任務當前是活動的,某些被掛起。你用一個std::list
l 你的有一個字串表:std::vector
l 你正在做I/O multiplexing,並且將一個std::vector<:istream>傳給一個函式。input stream是在別處開啟的,將於別處關閉,並且,也許其中之一就是&std::cin。
如果容納指標的容器多手多腳地delete了所指向的物件,上面的用法沒一個能成為可能。
擁有所指向的物件
如果你建立了一個容納指標的容器,原因通常應該是所指向的物件由別處建立和銷燬的。有沒有情況是有理由獲得一個容器,它擁有指標本身,還擁有所指向的物件?有的。我知道的唯一一個好的理由,但也是很重要的一個理由:多型。
C++中的多型是和指標/引用語義繫結在一起的。假如,舉例來說,那個task不只是一個類,而且它是一個繼承體系的基類。如果p是一個task *,那麼p可能指向一個task物件或任何一個從task派生的類的物件。當你透過p呼叫task的一個虛擬函式,將會在執行期根據p所指向的實際型別呼叫相應的函式。
不幸地是,將task作為多型的基類意味著你不能使用vector
物件導向的設計通常意味著在物件被建立到物件被銷燬之間,你透過指標或引用來訪問物件。如果你想擁有一組物件,除了容納指標的容器外,你幾乎沒有選擇[注2]。管理這樣的容器的最好的方法是什麼?
如果你正使用容納指標的容器來擁有一組物件,關鍵是確保所有的物件都被銷燬。最明顯的解決方法,可能也是最常見的,是在銷燬容器前,遍歷它,併為每個元素呼叫delete語句。如果手寫這個迴圈太麻煩,很容易作一個包裝:
template
class my_vector : private std::vector
{
typedef std::vector
public:
using Base::iterator;
using Base::begin;
using Base::end;
...
public:
~my_vector() {
for (iterator i = begin(); i != end(); ++i)
delete *i;
}
};
這個技巧能工作,但是它比看起來有更多的限制和要求。
問題是,只改解構函式是不夠的。如果你有一個列出所有的正要被銷燬的物件的容器,那麼你最好確保只要指標離開了容器那個物件就要被銷燬,並且一個指標絕不在容器中出現兩次。當你用erase()或clear()移除指標時,必須要小心,但是你也需要小心容器的賦值和透過iterator的賦值:象v1 = v2,和v[n] = p這樣的操作是危險的。標準泛型演算法,有很多會執行透過iterator的賦值的,這是另外一個危險。你顯然不能使用std::copy()和std::replace()這樣的泛型演算法;稍微不太明顯地,你也不能使用std::remove()、std::remove_if(),和std::unique()[注3]。
象my_vector這樣的包裝類能夠解決其中一些問題,但不是全部。很難看出如何阻止使用者以危險的方式使用賦值,除非你禁止所有的賦值--而那時,你所得到的就不怎麼象容器了。
問題是每個元素都必須被單獨追蹤,所以,也許解決方法是包裝指標而不是包裝整個容器。
標準執行庫定義了一個對指標包裝的類std::auto_ptr
這是很自然的主意,但它是錯誤的。原因呢,再一次,是因為值語義。容器類假設它們能複製自己的元素。舉例來說,如果你有一個vector
T t2(t1)
並且得到一個t1的複製t2。
形式上,按C++標準中的說法,T要是Assignable的和CopyConstructible的。指標滿足這些要求--你能得到指標的一個複製--但auto_ptr不滿足。auto_ptr的賣點是它維護強所用權,所以不允許複製。有一個形式上是複製建構函式的東西,但auto_ptr的“複製建構函式”實際上並不進行複製。如果t1是一個 std::auto_ptr
std::auto_ptr
然後t2將不是t1的一個複製。不是進行複製,而是發生了所有權轉移--t2得到了t1曾經有著的值,而t1被改成一個NULL指標。auto_ptr 的物件是脆弱的:你只不過看了它一下就能改變它的值。
在某些實作上,當你試圖建立一個vector
取代auto_ptr,你應該使用一個不同的“智慧指標”,引用計數的指標類。帶引用計數的指標跟蹤多少個指標指向相同的物件。當你構造了一個引用計數指標的複製時,計數加1;當你銷燬一個引用計數指標時,計數減1。當計數變成0時,指標所指的物件被自動銷燬。
寫一個引用計數的指標類不是特別困難,但也不是幾乎什麼都不用做;達到執行緒安全需要特別的技巧。幸運地是,使用引用計數並不意味著你需要寫一個自己的引用計數指標類;幾種這樣的類已經存在並可免費使用。比如,你能使用Boost的shared_ptr類[注5]。我期望 shared_ptr或其它類似的東西將成為C++標準的下個修訂版本的組成部分。
當然,引用計數只是一種特別的垃圾回收。像所有形式的垃圾回收,它自動銷燬你不再需要的物件。引用計數指標的主要優勢是它們易於加入現有:share_ptr這樣的機制只是一個小的單獨的類,你能只在一個較大系統中某個部分中使用它。另一方面,引用計數是垃圾回收的一種最低效的形式(每個指標的賦值和複製都需要一些相關的複雜處理),最沒有柔性的形式(在兩個資料結構擁有互指指標時,你必須小心)。其它形式的垃圾回收在C++程式中工作得同樣好。特別地,Boehm conservative garbage collector[注6]是免費的、可移植的,並被很好測試過。
如果你使用一個保守的垃圾回收器,你只需要將它連結入程式就可以了。你不需要使用任何特別的指標包裝類;只需要分配記憶體而不用關心delete它。特別地,如果你建立一個vector
垃圾回收的優勢--無論是引用計數還是保守的垃圾回收器,或其它方法--是它讓你將物件的生存期處理得完全不確定:你不須要掌握在某個特定時間程式的哪個部份引用了這個物件。另一方面,垃圾回收的缺點也正是這一點!有時你確實知道物件的生存期,或至少知道在程式的某個特定狀態結束後物件不應該繼續存在。舉例來說,你可能建立了一個複雜的分析樹;也許它填滿了多型物件,也許它是太複雜而無法單獨掌握每個節點,但是你能確定在分析完後就將不再需要它們中的任何一個。
從手工管理的vetor
如果你的程式確實有明確定義的狀態,那麼你可能有理由期望在某個狀態結束時銷燬一組物件。代替垃圾回收,另外一個可選方法是透過一個arena(WQ注:字典上為“競技場、舞臺”之意,譯不好,不譯)來分配物件:維護一個物件列表,以便能一次就銷燬所有物件。
你可能想知道這個技術和前面提到的技術(遍歷容器併為每個元素呼叫destory())有多大差異。有實質性差異嗎?如果沒有,讓我花了這麼多時間的那些危險和限制,又怎麼說?
差異很小,但很重要:arena儲存了一個物件集,目的是為了在以後delete它們,並且沒有其它目的。它可以以標準容器的形式實現,但是它不暴露容器介面。你在vector
arena是個通用的主意。arena可能簡單地是個容器,只要記住別使用不安全的成員函式,或者它可以是個包裝類以試圖執行得更安全。很多這樣的包裝類已經被寫出來了[注7]。Listing 1是一個arena類的簡化例子,使用了一個實現上的技巧以使得不必為每個指標型別使用一個不同的arena類。舉例來說,你可能寫:
arena a;
...
std::vector
v.push_back(a.create
...
a.destroy_all();
我們幾乎回到了起點:使用一個容納指標的vector,所指向的物件由別處擁有和管理。
總結
指標在C++程式中很常見,標準容器同樣如此;不用驚奇,它們的組合物,容納指標容器同樣很常見。
新手最大的困難是容納指標的容器時的所有權問題:應該什麼時候delete所指向的物件?處理容納指標的容器的絕大部分技術都可以歸結到一個原則:如果你有一個容納指標的容器,所指向的物件應該由其它地方所擁有。
l 如果正在處理非多型物件集(型別為my_type),你應該將物件的值存入容器,比如list
l 不要試圖將auto_ptr放入標準容器。
l 如果有一組多型物件,你需要用容納指標的容器來管理它們。(但是,那些指標可以被包裝在某種handle類或智慧指標類中。)當物件生存期不能預知時,或不重要時,最容易的方法是使用垃圾回收。垃圾回收的兩個最簡單的選擇是引用計數指標類和保守的垃圾回收器。對你來說哪個是最佳選擇取決於工具的可用性。
l 如果你有一組多型物件,並需要控制它們的生存期,最簡單的方法是使用arena。一個簡單的arena類例子展示於Listing 1。
Listing 1: A simple arena class
#include
class arena {
private:
struct holder {
virtual ~holder() { }
};
template
struct obj_holder : public holder {
T obj;
template
obj_holder(Arg1 arg1)
: obj(arg1) { }
};
std::vector
private:
arena(const arena&);
void operator=(const arena&);
public:
arena() { }
~arena() {
destroy_all();
}
template
T* create(Arg1 arg1) {
obj_holder
owned.push_back(p);
return &p->obj;
}
void destroy_all() {
std::vector
while (i < owned.size()) {
delete owned[i];
++i;
}
owned.clear();
}
};
注
[1] Well, almost any: there are some restrictions — which will be discussed later — on the objects you put in a container. Most reasonable types confoto those restrictions; pointers certainly do.
[2] There is one sense in which you have a choice: you can simulate value semantics by hiding polymorphic pointers ins a non-polymorphic wrapper class. See, for example, James Coplien, Advanced C++ Programming Styles and Idioms (Addison-Wesley, 1991), for a discussion of this "envelope and letter" idiom. See also chapter 14 of Andrew Koenig and Barbara Moo's Accelerated C++ (Addison-Wesley, 2000), for a family of generic handle classes. However, while the envelope-and-letter idiom is useful, it is also fairly heavyweight and it will affect many ects of your design. Unless you have other reasons for using an envelope-and-letter design, it would be silly to turn to it just for the sake of container classes.
[3] See Harald Nowak, "A remove_if for vector
[4] ?0.4.5, paragraph 3.
[5] <>.
[6] Hans-J. Boehm, "A garbage collector for C and C++," <>.
[7] See, for example, Andrew Koenig, "Allocating C++ objects in clusters," Journal of Object-Oriented Programming, 6(3), 1993.
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752019/viewspace-979933/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- CUJ:標準庫:容納不完全型別的容器 (轉)型別
- CUJ:標準庫:基於檔案的容器 (轉)
- CUJ:標準庫:bitset和bit vector (轉)
- CUJ:標準庫:標準庫中的搜尋演算法 (轉)演算法
- CUJ:標準庫:定義iterator和const iterator (轉)
- CUJ:高效使用標準庫:STL中的unary predicate (轉)
- 指標問題的一點體會(區別 [指向指標的指標] 與 [指標的指標] .) (轉)指標
- 指向指標的指標指標
- 詳解c++指標的指標和指標的引用C++指標
- CUJ:高效使用標準庫:set的iterator是mutable的還是immutable的? (轉)
- 如何理解指向指標的指標?指標
- 好的北極星指標的六個制定標準指標
- 關於指標傳遞和指標的指標指標
- C/C++指向指標的指標C++指標
- C++標準模板庫------容器C++
- 建立存放指標的容器並讀出指標
- 指標常量和常量指標的區別指標
- STL標準模組庫:容器string模組
- C++標準庫有四種智慧指標C++指標
- JavaScript 獲取滑鼠指標的座標JavaScript指標
- 標準模板庫STL (轉)
- c++標準程式庫:STL容器之mapC++
- 【C++系列】指標物件和物件指標的區別C++指標物件
- 標準模板庫介紹(轉)
- C++ 指標陣列與陣列指標的區別C++指標陣列
- 指標的理解指標
- 指標的用法指標
- STL 簡介,標準模板庫(轉)
- 指標 (轉)指標
- 戴爾將Linux納入工業標準Linux
- 人力資源指標分析庫(轉載)指標
- 空指標的救星指標
- 準確率評價指標指標
- STL 簡介,標準模板庫[1] (轉)
- Object-C 指標 和 C 指標的相互轉換 與ARC 並驗證__bridge關鍵字的作用(轉)Object指標
- 補充內容:C++語言中陣列指標和指標陣列徹底分析 (轉)C++陣列指標
- 成員變數/函式指標的用法 (轉)變數函式指標
- CUJ:普及知識:typeint (轉)