Effective STL第一部分: 容器

第一條:慎重地選擇容器

C++提供了以下一些容器:

n  標準STL序列容器:vector、string、deque和list。

n  標準STL關聯容器:set、multiset、map和multimap。

n  非標準序列容器:slist和rope。slist是一個單向鏈表,rope本質上是一個“重型”string。

n  非標準關聯容器:hash_set、hash_multiset、hash_map和hash_multimap。

n  vector<char>作爲string的代替

n  vector作爲標準關聯容器的代替

n  幾種標準的非STL容器,包括數組、bitset、valarry、stack、queue和priority_queue。

 

在選擇容器時考慮但不限於如下因素:

n  你是否需要在容器的任意位置插入新元素?如果需要,選擇序列容器。

n  你是否關心容器中的元素是排序的? 如果關心,你要避免使用哈希容器。

n  你選擇的容器必須是C++的一部分嗎?如果是,就排除了非標準的容器(哈希容器、slist、rope)。

n  你需要哪種類型的迭代器?vector、deque、string提供隨機訪問迭代器。

n  當發生元素插入或刪除操作時,避免移動容器中原來的元素是否很重要?如果是,避免使用連續內存的容器。

n  容器中數據的佈局是否要和c兼容?如果是,只能選擇vector。

n  元素的查找速度是否是關鍵的考慮因素?如果是,就要考慮哈希容器,排序的vector和標準關聯容器----或許這就是優先順序。

n  如果容器內部使用引用計數,你是否介意?如果是,則避免使用string,因爲string的實現都使用了引用計數。Rope也需要避免。可以考慮使用vector<char>代替string

n  對於插入和刪除操作,你需要事務語意嗎?也就是插入和刪除失敗時,需要回滾的能力嗎?如果需要,最好使用基於節點的容器。如果需要對多個元素的插入操作需要事務語意,則需要選擇list,因爲在標準容器中,只有list提供這功能。連續內存的容器也可以獲得事務語意,但是要付出性能上的代價,而且代碼也顯得不那麼直截了當。

n  你需要使迭代器、指針和引用變成無效的次數最小嗎?如果是這樣,就需要使用基於節點的容器

n  如果在容器上使用swap,使得迭代器、指針和引用變成無效,你會在意嗎?如果在意,避免使用string,因爲string是在swap過程中使迭代器、指針和引用變成無效的唯一容器。

n 如果序列容器的迭代器是隨機訪問類型,而且只要沒有發生刪除操作,且插入操作只是發生在容器的末尾,則指向數據的指針和引用就不會變成無效,這樣的容器是否對你有幫助?這是很特別的情形,如果是,deque是你希望的容器。deque是唯一的,迭代器可能變成無效而指針和引用不會變得無效的stl標準容器。



第二條:不要試圖編寫獨立於容器類型的代碼

容器類型被泛化爲序列容器和標準容器,類似的容器被賦予相似的功能。

n 標準的連續內存容器提供了隨機訪問迭代器

n 標準的基於節點的容器提供了雙向迭代器。

n 序列容器支持push_front和/或push_back操作,而關聯容器沒有

n 關聯容器提供對數時間的lower_bound、upper_bound和equal_range成員函數,但是序列容器卻沒有。

因此試圖編寫對各容器試用的代碼,你的程序只能試用他們的交集。這意味着要放棄所有容器獨特的優勢,因此沒有太大意義。



第三條確保容器中的對象拷貝正確而高效

(1)內置類型的實現總是簡單的按位拷貝

(2)往容器中填充對象是不明智的,原因如下:

n 對象的拷貝操作費時,放入對象越多,佔用內存和時間就越多。

n 往基類對象容器拷貝派生類對象時,會發生剝離,派生類特有的信息會丟失。

n 解決這個問題的一個簡單方法是是容器包含指針,智能指針是很好的選擇。



第四條:調用empty而不是檢查size()是否爲0

原因很簡單:empty對於所有標準容器都是常數時間操作,而對一些list實現,size耗費線性時間。 

這條好簡單啊!


第五條:區間成員函數優先於與之對應的單元素成員函數

太多的STL程序員濫用了copy,copy中肯定會有循環,效率會低下。通過利用插入迭代器的方式來限定目標區間的copy調用,幾乎都應該換成對區間成員函數的調用。原因:

n  通過使用區間成員函數,通常可以少寫一些代碼。

n  使用區間成員函數通常會得到意圖清晰和更加直接的代碼

一句話,區間成員函數使代碼更加易懂易寫。

例如:

V1.clear();

Copy(v2.begin(),  v2.end(), back_inserter(v1));

可以替換成:

V1.insert(v1.end,  v2.begin(), v2.end());

 

單元素的insert對於區間insert在三個方面影響效率:

第一種是不必要的函數調用。把n個元素逐個插入到v中導致對insert的n次調用。而區間形式的insert,只做一次調用。

第二種是把容器內原有的元素頻繁地移動到新元素插入後他們所處的位置,且插入n個元素,最多會導致log2n次的內存分配。而區間形式的insert將原有元素直接移動到它們的最終位置,即只需要付出每個元素移動一次的代價。

 

總結一下區間形式的操作:

區間創建:

所有標準容器提供如下形式的構造函數:

Container::container(InputIterator  begin, InputIterator end);

當傳入這種構造函數的迭代器是istream_iterator和istreambuf_iterator時,可能遇到c++最煩人的parse機制,編譯器把這條語句解釋爲函數聲明,而不是定義新的容器對象。

區間插入:

所有標準序列容器提供如下形式的insert:

Void container::insert(Iterator  position,    //在何處插入區間、

InputIterator  begin,      //區間開始   

InputIterator end);         //區間結束

關聯容器利用比較函數來決定元素插入何處,提供一個省去position的函數原型。

Void container::insert(InputIterator  begin, InputIterator end);

區間刪除:

所有標準容器都提供了區間形式的刪除操作,但對於序列和關聯容器,其返回值有所不同。序列容器提供了這樣的形式。

Iterator container::erase(Iterator begin,  Iterator end);

關聯容器提供瞭如下形式,據說是因爲返回iterator會影響效率:

void container::erase(Iterator begin,  Iterator end);

         要特別注意區間刪除的erase-remove用法。

區間賦值:

所有標準序列容器提供如下形式的區間賦值:

Void container::assign(InputIterator begin, InputIterator end);


 

第六條:當心c++最煩人的分析機制

假設你有一個存有整數的文件,想把這些整數複製到一個list中。下面是很合理的一種做法:

Ifstream datefile(“ints.dat”);

list<int> date(istream_iterator<int>(datafile),  istream_iterator<int>());

這種做法的思路是,把一對istream_iterator傳入list的區間構造函數中,見第五條,從而將文件中的整數複製到list中。

 

注意,結果並不是你想像的那樣。這裏會被解釋爲聲明一個函數data,其返回值是list<int>。這個data函數有兩個參數:

n  第一個參數的名稱是datafile。它的類型是istream_iterator<int>。datafile兩邊的括號是多餘的。

n  第二個參數沒有名稱。它的類型是指向不帶參數的函數的指針,該函數返回一個istream_iterator<int>。

非常令人喫驚!但是卻和c++的一條普遍規律符合,儘可能的解釋爲函數聲明。

 

正確的做法:在對data的聲明中避免使用匿名的istream_iterator對象(儘管使用匿名對象是一種趨勢),而是給這些迭代器一個名稱。下面的代碼應該總是可以工作的:

Ifstream datefile(“ints.dat”);

istream_iterator<int>  dataBegin(datafile);

istream_iterator<int>  dataEnd;

list<int> date(dataBegin, dataEnd);

使用命名的迭代器對象與通常的STL程序風格相違背。但是爲了保證代碼對所有編譯器都沒有二義性,且使得代碼維護人員理解更容易,這一代價是值得的。


 

第七條:如果容器中包含了通過new操作創建的指針,切記在容器析構前將指針delete掉。

這篇講的比較複雜,總結起來主要有如下兩個方面:

1.stl中的容器刪除時,能夠正確地刪除對象,但是不能正確地刪除new的方式分配的指針。指針容器在析構時會析構包含的每個元素,但指針的析構函數不作任何事情!他當然不會調用delete。因此會造成內存泄露。

2.effective c++第十三款和這條意思差不多:即用對象來管理資源。用智能指針來替代普通指針放入容器。容器在刪除元素時,智能指針會調用析構函數並刪除資源。



第八條切勿創建包含auto_ptr的容器對象

原因如下:

n  當auto_ptr對象被拷貝時,它所指向的對象的所有權會被移交到複製的auto_ptr上,它自身被置爲NULL。你可以理解爲:拷貝一個auto_ptr對象意味着改變它的值。

n 假設容器包含auto_ptr對象。容器進行一次排序操作,將對象的值拷貝到一個臨時對象,即意味着原來對象被置爲0,其他操作也會有類似的情況。

這是一個令人討厭的陷阱,因此,千萬別創建包含auto_ptr的容器對象。



第九條 慎重選擇刪除元素的方法

(1)要刪除容器中所有特定值的元素

n  如果是連續內存的容器(例如vector、deque或string),最好的方法是使用erase-remove習慣用法。

n  如果是list,則使用list::remove

n  如果是標準關聯容器,如果任何名爲remove的操作都是完全錯誤的。這樣的容器沒有名爲remove的成員函數,使用remove算法可能會覆蓋容器的值。正確的方法是調用erase

 

(2)要刪除容器中所有滿足特定判別式(條件)的元素

n  vector、deque或string,最好的方法是使用erase-remove習慣用法。

n  如果是list,則使用list::remove_if

n  如果是標準關聯容器,正確的方法是調用remove_copy_ifswap或者寫一個循環來遍歷,記住把迭代器傳給erase時,要對它進行後綴遞增(類似c.erase(it++))。

 

(3)要在循環內部做某些(除了刪除對象之外的)操作


n 如果容器是一個標準序列容器,記住每次調用erase時,要用它的返回值更新迭代器

n 如果容器時一個關聯序列容器,記得當把迭代器傳給erase時,要對它進行後綴遞增


第十條瞭解分配子的約定和限制

分配子這個概念現在只需要簡單瞭解一下,在stl源碼剖析中會有詳細的介紹。

主要記住以下分配子的特性:

n  你的分配子是一個模板,模板參數T代表你爲它分配內存的對象的類型。

n  提供類型定義pointer和reference,但是始終讓pointer爲T*,reference爲T&。

n  千萬別讓你的分配子擁有雖對象而不同的狀態(per-object state)通常,分配子不應該有非靜態的數據成員。

n  記住,傳給分配子的allocate成員函數的是那些要求內存是的對象的個數,而不是所需的字節數。同時要記住,這些函數返回T*指針(通過pointer類型定義),及時尚未有T對象被構造出來。

n  一定要提供嵌套的rebind模板,因爲標準容器依賴該模板.



第十一條理解自定義分配子的合理用法

考慮使用自定義分配子的幾種情況:

n  Stl自己的內存管理器(即allocate<T>)太慢,或者浪費內存,你相信可以實現地更好。

n  你發現allocate是線程安全的,而你所感興趣的是在單線程環境下運行,不願爲多線程同步付出不必要的代價。

n  你知道容器中的對象通常是一起使用的,所以你想把它們放在一個特殊堆中的相鄰位置上,以便於儘可能地做到引用局部化。

n  你想建立一個與共享內存相對應的特殊的堆,然後在這塊內存中存放一個或多個容器,以便使其它進程可以共享這些容器。



第十二條 切勿對stl容器的線程安全性有不切實際的依賴

對一個stl你只能期望:

n  多個線程讀是安全的。

n  多個線程對不同的容器做寫入操作是安全的

考慮當一個庫要實現完全的容器線程安全性時可能採取的方式:

n  對容器成員函數的每次調用,都鎖住容器直到調用結束。

n  容器所返回的每個迭代器的生存週期結束前,都鎖住容器。

n  對作用於容器的每個算法,都鎖住容器直到算法結束。

可以考慮使用autolock類,在構造函數中獲得一個互斥體,在析構函數中釋放它

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章