《Effective STL》學習筆記


http://www.cnblogs.com/arthurliu/archive/2011/08/07/2108386.html

作者:咆哮的馬甲 
出處:http://www.cnblogs.com/arthurliu/ 
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接。 
轉載請保持文檔的完整性,嚴禁用於任何商業用途,否則保留追究法律責任的權利。

第一條: 慎重選擇容器類型

C++所提供的容器類型有如下幾種:

  • 標准STL序列容器 vector string deque list
  • 標准STL關聯容器 set multiset map multimap
  • 非標准序列容器 slist rope
  • 非標准關聯容器 hash_set hash_multiset hash_map hash_multimap
  • vector<char>作為string的替代
  • vector作為標准關聯容器的替代
  • 非標准的STL容器 array bitset valarray stack queue priority_queue

標准容器中的vector string和list比較熟悉。
deque是double ended queue,提供了與vector一樣的隨機訪問功能,但同時對頭尾元素的增刪操作提供了優化。
set和multiset中的數據都是順序排列的,數據值本身就是鍵值,set中的數據必須唯一而multiset沒有這樣的限制。
map和multimap中的數據對按照鍵值順序排列,map中不允許出現重復的key,而multimap中可以用相同的key對應不同的value。
slist是single linked list,與STL中標准的list之間的區別就在於slist的iterator是單向的,而list的iterator是雙向的。
rope用於處理大規模的字符串。
hash_set hash_multiset hash_map hash_multimap利用hash算法對相對應的關聯容器進行了優化。
bitset是專門用來存儲bit的容器。
valarray主要用於對一系列的數字進行高速運算。 
priority_queue類似於heap,可以高效的獲取最高priority的元素。

 

  • 連續內存容器,動態申請一塊或多塊內存,每塊內存中存儲多個容器中的元素,當發生插入或刪除操作時,要對該內存中的其他元素進行新移動操作,這會降低效率。vector,string,rope都是連續內存容器。
  • 基於節點的容器,為容器中的每一元素申請單獨的內存,元素中有指針指向其他的元素,插入和刪除的操作只需要改變指針的指向。缺點在於占用內存相對連續內存容器較大。list, slist, 關聯容器以及hash容器都是基於節點的容器。
如何選擇合適的STL容器
如何選擇合適的STL容器.png
 
 
 
第二條: 不要編寫試圖獨立於容器的代碼
 
  • 數組被泛化為以其所包含對象的類型為參數的容器
  • 函數被泛化為以其使用的迭代器的類型為參數的算法
  • 指針被泛化為以其所指向的對象的類型為參數的迭代器
考慮到以后可能會使用其他的容器替換現有的容器,為了使修改的部分最小化,最好采用如下的方式
復制代碼
 1 class Widget{...};
2 template<typename T>
3 SpecialAllocator{...};
4 typedef vector<Widget, SpecialAllocator<Widget>> WidgetContainer;
5 typedef WidgetContainer::iterator WCIterator;
6
7 WidgetContainer wc;
8 Widget widget;
9 ...
10 WCIterator i = find(wc.begin(), wc.end(), widget);
復制代碼
 

使用Class將自定義的容器封裝起來,可以更好的實現修改部分最小化,同時達到了安全修改的目的
復制代碼
 1 class CustomizedContainer
2 {
3 private:
4
5 typedef vector<Widget> InternalContainer;
6 typedef InternalContainer::Iterator ICIterator;
7
8 InternalContainer container;
9
10 public:
11 ...
12 };
復制代碼

 

 

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


STL的工作方式是Copy In, Copy Out,也就是說在STL容器中的插入對象和讀取對象,使用的都是對象的拷貝。


在存放基類對象的容器中存放子類的對象,當容器內的對象發生拷貝時,會發生截斷(剝離 slicing)。

復制代碼
1 vector<Widget> vw;
2
3 class SpecialWidget : public Widget
4 {
5 ...
6 };
7
8 SpecialWidget sw;
9 vw.push_back(sw);
復制代碼


正確的方法是使容器包含指針而非對象。

復制代碼
1 vector<Widget*> vw;
2
3 class SpecialWidget : public Widget
4 {
5 ...
6 };
7
8 SpecialWidget sw;
9 vw.push_back(&sw);
復制代碼

  

容器與數組在數據拷貝方面的對比:

當創建一個包含某類型對象的一個數組的時候,總是調用了次數等於數組長度的該類型的構造函數。盡管這個初始值之后會被覆蓋掉

Widget w[maxNumWidgets]; //maxNumWidgets 次的Widget構造函數


如果使用vecor,效率會有所提升。

復制代碼
vector<widget> w;     //既不調用構造函數也不調用拷貝構造函數

vector<widget> w(5); //1次構造 5次拷貝構造

vector<widget> w; //既不調用構造函數也不調用拷貝構造函數
w.reserve(5); //既不調用構造函數也不調用拷貝構造函數
vector<widget> w(5);  //1次構造 5次拷貝構造
w.reserve(6); //需要移動位置,調用5次拷貝構造
復制代碼

 

 

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


empty()對於所有標准容器都是常數時間,而對list操作,size()耗費線性時間。


list具有常數時間的Splice操作,如果在兩個list之間做鏈接的時候需要記錄被鏈接到當前list的節點的個數,那么Splice操作將會變成線性時間。對於list而言,用戶對Splice效率的要求高於取得list長度的要求,所以list的size()需要耗費線性的時間去遍歷整個list。所以,調用empty()是判斷list是否為空的最高效方法。

 

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


區間成員函數在效率方面的開銷要小於循環調用單元素的成員函數,以insert為例

  1. 避免不必要的函數調用
  2. 避免頻繁的元素移動
  3. 避免多次進行內存分配


區間創建

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


區間插入

1 void container::insert(Iterator position, InputIterator begin, InputIterator end);
2 void associatedContainer::insert(InputIterator begin, InputIterator end);


區間刪除

1 Iterator container::erase(Iterator begin, Interator end);
2 void associatedContainer:erase(Iterator begin, Iterator end);


區間賦值

1 void container::assign(InputIterator begin, InputIterator end);


 

第六條:當心C++編譯器最煩人的分析機制


C++會盡可能的將一條語句解釋為函數聲明。


下列語句都聲明了一個函數返回值為int類型的函數f,其參數是double類型。

1 int f(double(d));
2 int f(double d);
3 int f(double);  


下列語句都聲明了一個返回值為int類型的函數g,它的參數是返回值為double類型且無參的函數指針

1 int g(double(*pf)());
2 int g(double pf());
3 int g(double ()); //注意與int g(double (f))的區別

  

對於如下語句,編譯器會做出這樣的解釋:聲明了一個返回值為list<int>的函數data,該函數有兩個參數,一個是istream_iterator<int>類型的變量,另一個是返回值為istream_iterator<int>類型的無參函數指針。

1 ifstream dataFile("ints.dat");
2 list<int> data(istream_iterator<int>(dataFile),istream_iterator<int>());


如果希望構造一個list<int>類型的變量data,最好的方式是使用命名的迭代器。盡管這與通常的STL風格相違背,但是消除了編譯器的二義性而且增強了程序的可讀性。

1 ifstream dataFile("ints.dat");
2 istream_iterator dataBegin(dataFile);
3 istream_iterator dataEnd;
4 list<int> data(dataBegin,dataEnd); 



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


STL容器在析構之前,會將其所包含的對象進行析構。

復制代碼
 1 class widget
2 {
3 ...
4 };
5
6 doSth()
7 {
8 widget w; //一次構造函數
9 vector<widget> v;
10 v.push_back(w); //一次拷貝構造函數
11 } // 兩次析構函數
復制代碼


但如果容器中包含的是指針的話,一旦沒有特別將指針delete掉將會發生內存泄漏

復制代碼
 1 class widget
2 {
3 ...
4 };
5
6 doSth()
7 {
8 widget* w = new widget();
9 vector<widget*> v;
10 v.push_back(w);
11 } // memory leak!!!
復制代碼

  

最為方便並且能夠保證異常安全的做法是將容器所保存的對象定義為帶有引用計數的智能指針

復制代碼
 1 class widget
2 {
3 ...
4 };
5
6 doSth()
7 {
8 shared_ptr<widget> w(new widget()); //構造函數一次
9 vector<shared_ptr<widget>> v;
10 v.push_back(w);
11 } //析構函數一次 沒有內存泄漏
復制代碼

 

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


由於auto_ptr對於其"裸指針"必須具有獨占性,當將一個auto_ptr的指針賦給另一個auto_ptr時,其值將被置空。

1 auto_ptr<int> p1(new int(1)); // p1 = 1
2 auto_ptr<int> p2(new int(2)); // p2 = 2
3
4 p2 = p1; // p2 = 1 p1 = emtpy;


第三條提到STL容器中的插入對象和讀取對象,使用的都是對象的拷貝,並且基於STL容器的算法也通常需要進行對象的copy,所以,創建包含auto_ptr的容器是不明智的。


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


要刪除容器中特定值的所有對象

復制代碼
1 //對於vector、string、deque 使用erase-remove方法
2 container.erase(remove(container.begin(),container.end(),value),container.end());
3
4 //對於list 使用remove方法
5 list.remove(value);
6
7 //對於標准關聯容器 使用erase方法
8 associatedContainer.erase(value);
復制代碼


要刪除容器中滿足特定條件的所有對象

復制代碼
 1 bool condition(int );
2
3 //對於vector、string、deque 使用erase-remove_if方法
4 container.erase(remove_if(container.begin(),container.end(),condition),container.end());
5
6 //對於list 使用remove_if方法
7 list.remove_if(condition);
8
9 //對於標准關聯容器 第一種方法是結合remove_copy_if和swap方法
10 associatedContainer.remove_copy_if(associatedContainer.begin(),
11 associatedContainer.end(),
12 insert(tempAssocContainer,tempAssocContainer.end()),
13 condition);
14
15 //另外一種方法是遍歷容器內容並在erase元素之前將迭代器進行后綴遞增
16 for(assocIt = associatedContainer.begin(); assocIt != associatedContainer.end())
17 {
18 if(condition(*assoIt))
19 {
20 ///當關聯容器中的一個元素被刪除掉時,所有指向該元素的迭代器都被設為無效,所以要提前將迭代器向后遞增
21 associatedContainer.erase(assoIt++);
22 }
23 else
24 {
25 assocIt++;
26 }
27 }
復制代碼


如果出了在刪除容器內對象的同時還需要進行額外的操作時

復制代碼
 1 bool condition(int );
2 void dosth();
3
4 //對於標准序列容器,循環遍歷容器內容,利用erase的返回值更新迭代器
5 for(containerIt = container.begin(); containerIt != container.end())
6 {
7 if(condition(*containerIt))
8 {
9 doSth();
10 //當標准容器中的一個元素被刪除掉時,所有指向該元素以及該元素之后的迭代器都被設為無效,所以要利用erase的返回值
11 containerIt = container.erase(containerIt++);
12 }
13 else
14 {
15 containerIt++;
16 }
17 }
18
19 //對於標准關聯容器,循環遍歷容器內容,並在erase之前后綴遞增迭代器
20 for(assocIt = associatedContainer.begin(); assocIt != associatedContainer.end())
21 {
22 if(condition(*assoIt))
23 {
24 dosth();
25 associatedContainer.erase(assoIt++);
26 }
27 else
28 {
29 assocIt++;
30 }
31 }
復制代碼


我覺得第三種情況下可以用第二種情況的實現代替,我們需要做的僅是將額外做的事情和判斷條件包裝在一個函數內,並用這個函數替代原有的判斷條件。 

復制代碼
 1 bool condition_doSth(int i)
2 {
3 bool ret = condition(i);
4 if(ret)
5 {
6 doSth();
7 }
8
9 return ret;
10 }
復制代碼



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

如果需要編寫自定義的分配子,有以下幾點需要注意

  • 當分配子是一個模板,模板參數T代表你為其分配內存的對象的類型
  • 提供類型定義pointer和reference,始終讓pointer為T*而reference為T&
  • 不要讓分配子擁有隨對象而不同的狀態,通常,分配子不應該有非靜態數據成員
  • 傳遞給allocator的是要創建元素的個數而不是申請的字節數,該函數返回T*,盡管此時還沒有T對象構造出來
  • 必須提供rebind模板,因為標准容器依賴於該模板


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

如果需要在共享的內存空間中手動的管理內存分配,下列代碼提供了一定的參考
復制代碼
 1 //用戶自定義的管理共享內存的malloc和free
2 void* mallocShared(size_t bytesNeeded);
3 void* freeShared(void* ptr);
4
5 template<typename T>
6 class sharedMemoryAllocator
7 {
8 public:
9 ...
10
11 point allocator(size_type numObjects, const void* localityHint=0)
12 {
13 return static_cast<pointer>(mallocShared(numObjects*sizeof(T)));
14 }
15
16 void deallocate(pointer ptrMemory, size_type numObjects)
17 {
18 freeShared(ptrMemory);
19 }
20
21 ...
22 }
復制代碼

  
如果不僅僅是將容器的元素放在共享內存,而且要將容器對象本身也放在共享內存中,參考如下代碼

復制代碼
1 void* ptrVecMemory = mallocShared(sizeof(SharedDoubleVec));
2 SharedDoubleVec* sharedVec = new(ptrVecMemory) SharedDoubleVec;
3
4 ...
5
6 sharedVec->~SharedDoubleVec();
7 freeShared(sharedVec);
復制代碼



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

  • 對於STL容器的多線程讀是安全的
  • 對於多個不同的STL容器

采用面向對象的方式對STL容器進行加鎖和解鎖

復制代碼
 1 template<typename Container>
2 class lock
3 {
4 public:
5
6 Lock(const Container& container):c(container)
7 {
8 getMutexFor(c);
9 }
10
11 ~Lock()
12 {
13 releaseMutex(c);
14 }
15
16 private:
17
18 Container& c;
19 },
20
21
22
23 vector<int> v;
24 ...
25
26 {
27 Lock<vector<int>> lock(v); //構造lock,加鎖v
28 doSthSync(v); //對v進行多線程的操作
29 } //析構lock,解鎖v
復制代碼
 
作者:咆哮的馬甲 
出處:http://www.cnblogs.com/arthurliu/ 
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接。 
轉載請保持文檔的完整性,嚴禁用於任何商業用途,否則保留追究法律責任的權利。

第十三條: vector和string優先於動態分配的數組

如果使用new來動態分配內存,使用者必須承擔以下的責任

  • 確保之后調用delete將內存釋放
  • 確保使用的是正確的delete形式,對於單個對象要用delete,對於數組對象需要用delete[]
  • 確保對於一個對象只delete一次
vector、string自動管理其所包含元素的構造與析構,並有一系列的STL算法支持,同時vector也能夠保證和老代碼的兼容。

使用了引用計數的string可以避免不必要的內存分配和字符串拷貝(COW- copy on write),但是在多線程環境里,對這個string進行線程同步的開銷遠大於COW的開銷。此時,可以考慮使用vector<char>或動態數組。

 

第十四條: 使用reserve來避免不必要的內存分配

對於STL容器而言,當他們的容量不足以放下一個新元素的時候,會自動增長以便容納新的數據。(只要不超過max_size)
  1. 分配一塊兒原內存大小數倍的新內存,對於vector和string而言,通常是兩倍。
  2. 將原來容器中的元素拷貝到新內存中
  3. 析構舊內存中的對象
  4. 釋放舊內存
 
reserve以及與resever相關的幾個函數
  • size() 容器中現有的元素的個數
  • capacity() 容器在不重新分配內存的情況下可容納元素的總個數
  • resize(Container::size_type n) 將容器的size強制改變為n  
    • n>size 將現有容器中的元素拷貝到新內存,並將空余部分用默認構造的新函數填滿
    • n<size 將尾部的元素全部析構掉
  • reserve(Container::size_type n)將容器的size改變至少為n
    • n>size 將現有容器中的元素拷貝到新內存,多余部分的內存仍然空置
    • n<size 對容器沒有影響

通常有兩種方式使用reserve避免不必要的內存分配
  1. 預測大致所需的內存,並在構造容器之后就調用reserve預留內存
  2. 先用reserve分配足夠大的內存,將所有元素都加入到容器之后再去除多余內存。
     

第十五條: string實現的多樣性

實現A  在該實現中,包含默認Allocator的string是一個指針大小的4倍。對於有自定義的Allocator的string,他的大小將更大
實現A

實現B 在使用默認的Allocator的情況下,string對象的大小與指針的大小相等。當使用自定義的Allocator時,string對象將加上對應的自定義Allocator的對象
Other部分用來在多線程條件下進行同步控制,其大小通常為指針大小的6倍。
實現B

實現C string對象的大小總與指針大小相同,沒有對單個對象的Allocator的支持。X包含一些與值的可共享性相關的數據
實現C

實現D 對於使用默認Allocator的string,其大小等於指針大小的7倍。不使用引用計數,string內部包含一塊內存可容納15個字符的字符串。
實現D

總結string的多種實現
  • string的值可能會被引用計數(實現A 實現B 實現C)也可能不會(實現D)
  • string對象的大小可能在char*指針的1倍到7倍之間
  • 創建一個新的字符串值可能需要0次(實現D capacity<=15)、1次(實現A、實現C、實現D capacity>15)或2次(實現B)動態的內存分配
  • string對象可能共享(實現B、實現C)也可能不共享(實現A 實現D)其大小和容量信息
  • string可能支持(實現A 實現B 實現D)也可能不支持(實現C)單個對象的分配子
  • 不同的實現對字符內存的最小分配單位有不同的策略

 

第十六條: 了解如何把vector和string數據傳給舊的API

將vector傳遞給接受數組指針的函數,要注意vector為空的情況。 迭代器並不等價於指針,所以不要將迭代器傳遞給參數為指針的函數。
復制代碼
1 void foo(const int* ptr, size_t size);
2
3
4 vector<int> v;
5 ...
6 foo(v.empty() ? NULL : &v[0], v.size());
復制代碼


將string傳遞給接受字符串指針的函數。該方法還適用於s為空或包含"\0"的情況

復制代碼
1 void foo(const char* ptr);
2
3
4 string s;
5 ...
6 foo(s.c_str());
復制代碼


使用初始化數組的方式初始化vector

復制代碼
1 //向數組中填入數據
2 size_t fillArray(int* ptr, size_t size);
3
4 int maxSize = 10;
5 vector<int> v(maxSize);
6 v.resize(fillArray(&v[0],v.size()));
復制代碼

 

借助vector與數組內存布局的一致性,我們可以使用vector作為中介,將數組中的內容拷貝到其他STL容器之中或將其他STL容器中的內容拷貝到數組中

復制代碼
 1 //向數組中填入數據
2 size_t fillArray(int* ptr, size_t size);
3
4 vector<int> v(maxSize);
5 v.resize(fillArray(&v[0],v.size()));
6 set<int> s(v.begin(),v.end());
7
8
9 void foo(const int* ptr, size_t size);
10
11
12 list<int> l();
13 ...
14
15 vector<int> v(l.begin(),l.end());
16 foo(v.empty()? NULL : &v[0],v.size());
復制代碼

 

第十七條: 使用swap去除多余容量
 
1 vector<int>(v).swap(v);

vector<int>(v)使用v創建一個臨時變量,v中空余的內存將不會被拷貝到這個臨時變量的空間中,再利用swap將這個臨時變量與v進行交換,相當於去除掉了v中的多余內存。

由於STL實現的多樣行,swap的方式並不能保證去掉所有的多余容量,但它將盡量將空間壓縮到其實現的最小程度。

利用swap的交換容器的值的好處在於可以保證容器中元素的迭代器、指針和引用在交換后依然有效。 

復制代碼
1 vector<int> v1;
2 v1.push_back(1);
3 vector<int>::iterator i = v1.begin();
4
5 vector<int> v2(v1);
6 v2.swap(v1);
7 cout<<*i<<endl; //output 1 iterator指向v2的begin
復制代碼


但是在使用基於臨時變量的swap要當心iterator失效的情況  

復制代碼
1 vector<int> v1;
2 v1.push_back(1);
3 vector<int>::iterator i = v1.begin();
4
5 vector<int>(v1).swap(v1);
6 cout<<*i<<endl; //crash here
復制代碼

原因在於第5行構造的臨時變量在該行結束后就被析構了。



第十八條: 避免使用vector<bool>

vector<bool>並不是一個真正的容器,也並不存儲真正的bool類型,為了節省空間,它存儲的是bool的緊湊表示,通常是一個bit。 
由於指向單個bit的指針或引用都是不被允許的,vector<bool>采用代理對象模擬指針指向單個bit。 
復制代碼
1 vector<bool> v;
2 //...
3
4 bool *pb = &v[0]; // compile error
5
6 vector<bool>::reference *pr = &v[0]; // OK
復制代碼

  

可以考慮兩種方式替代vector<bool>

  • deque<bool> 但是要注意deque的內存布局與數組並不一致
  • bitset bitset不是STL容器所以不支持迭代器,其大小在編譯器就已經確定,bool也是緊湊的存儲在內存中。

    第十九條: 理解相等(equality)和等價(equivalence)的區別  

    • 相等的概念是基於operator==的,也就是取決於operator==的實現
    • 等價關系是基於元素在容器中的排列順序的,如果兩個元素誰也不能排列在另一個的前面,那么這兩個元素是等價的。
    標准關聯容器需要保證內部元素的有序排列,所以標准容器的實現是基於等價的。標准關聯容器的使用者要為所使用的容器指定一個比較函數(默認為less),用來決定元素的排列順序。

    非成員的函數(通常為STL算法)大部分是基於相等的。下列代碼可能會返回不同的結果
    復制代碼
     1 struct CIStringCompare:
    2 public binary_function<string, string, bool> {
    3 bool operator()(const string& lhs,
    4 const string& rhs) const
    5 {
    6 int i = stricmp(lhs.c_str(),rhs.c_str());
    7 if(i < 0)
    8 return true;
    9 else
    10 return false;
    11 }
    12 };
    14
    15
    16 set<string,CIStringCompare> s; //set的第二個參數是類型而不是函數
    17 s.insert("A");
    18
    19 if(s.find("a") != s.end()) //true
    20 {
    21 cout<<"a";
    22 }
    23
    24 if(find(s.begin(),s.end(),"a") != s.end()) //false
    25 {
    26 cout<<"a";
    27 }
    復制代碼



    第二十條: 為包含指針的關聯容器指定比較類型  

     

    下面的程序通常不會得到用戶期望的結果。
    復制代碼
    1 set<string*> s;
    2 s.insert(new string("A"));
    3 s.insert(new string("C"));
    4 s.insert(new string("B"));
    5
    6 for(set<string*>::iterator i = s.begin(); i != s.end(); i++)
    7 {
    8 cout<<**i; //輸出一定會是ABC么?
    9 }
    復制代碼

    因為set中存儲的是指針類型,而它也僅僅會對指針所處的位置大小進行排序,與指針所指向的內容無關。

    當關聯容器中存儲指針或迭代器類型的時候,往往需要用戶自定義一個比較函數來替換默認的比較函數。

    復制代碼
     1 struct CustomedStringCompare:
    2 public binary_function<string*, string*, bool> {
    3 bool operator()(const string* lhs,
    4 const string* rhs) const
    5 {
    6 return *lhs < *rhs;
    7 }
    8 };
    9
    10
    11 set<string*,CustomedStringCompare> s;
    12 s.insert(new string("A"));
    13 s.insert(new string("C"));
    14 s.insert(new string("B"));
    15
    16 for(set<string*, CustomedStringCompare>::iterator i = s.begin(); i != s.end(); i++)
    17 {
    18 cout<<**i; //ABC
    19 }
    復制代碼

      
    可以更進一步的實現一個通用的解引用比較類型

    復制代碼
    1 struct DerefenceLess{
    2 template<typename PtrType>
    3 bool operator()(PtrType ptr1, PtrType ptr2) const
    4 {
    5 return *ptr1 < *ptr2;
    6 }
    7 };
    8
    9 set<string*,DerefenceLess> s;
    復制代碼

      


     

    如果用less_equal來實現關聯容器中的比較函數,那么對於連續插入兩個相等的元素則有
    1 set<int,less_equal<int>> s;
    2 s.insert(1);
    3 s.insert(1);

    因為關聯容器是依據等價來實現的,所以判斷兩個1是否等價!

    !(1<=1) && !(1<=1) // false 不等價

    所以這兩個1都被存儲在set中,從而破壞了set中不能有重復數據的約定. 


    比較函數的返回值表明元素按照該函數定義的順序排列,一個值是否在另一個之前。相等的值不會有前后順序,所以,對於相等的值,比較函數應該返回false。


    對於multiset又如何呢?multiset應該可以存儲兩個相等的元素吧? 答案也是否定的。對於下面的操作:
    復制代碼
    1 multiset<int,less_equal> s;
    2 s.insert(1);
    3 s.insert(1);
    4
    5 pair<multiset<int,less_equal>::iterator,multiset<int,less_equal>::iterator> ret = s.equal_range(1);
    復制代碼

      

    返回的結果並不是所期望的兩個1。因為equal_range的實現(lower_bound:第一個不小於參數值的元素(基於比較函數的小於), upper_bound:第一個大於參數值的元素)是基於等價的,而這兩個1基於less_equal是不等價的,所以返回值中比不存在1。

    事實上,上面的代碼在執行時會產生錯誤。VC9編譯器Debug環境會在第3行出錯,Release環境會在之后用到ret的地方發生難以預測的錯誤。

      

    第二十二條: 切勿直接修改set或multiset的鍵  
     

    set、multiset、map、multimap都會按照一定的順序存儲其中的元素,但如果修改了其中用於排序的鍵值,則將會破壞容器的有序性。

    對於map和multimap而言,其存儲元素的類型為pair<const key, value>,修改map中的key值將不能通過編譯(除非使用const_cast)。
    對於set和multiset,其存儲的鍵值並不是const的,在修改其中元素的時候,要小心不要修改到鍵值。
    復制代碼
     1 class Employee
    2 {
    3 public:
    4 int id;
    5 string title;
    6 };
    7
    8 struct compare:
    9 public binary_function<Employee&, Employee&, bool> {
    10 bool operator()(const Employee& lhs,
    11 const Employee& rhs) const
    12 {
    13 return lhs.id < rhs.id;
    14 }
    15 };
    16
    17
    18 set<Employee,compare> s;
    19
    20 Employee e1,e2;
    21
    22 e1.id = 2;
    23 e1.title = "QA";
    24
    25 e2.id = 1;
    26 e2.title = "Developer";
    27
    28 s.insert(e1);
    29 s.insert(e2);
    30
    31 set<Employee,compare>::iterator i = s.begin();
    32 i->title = "Manager"; //OK to update non-key value
    33 i->id = 3; // 破壞了有序性
    復制代碼

      
     有些STL的實現將set<T>::iterator的operator*返回一個const T&,用來保護容器中的值不被修改,在這種情況下,如果希望修改非鍵值,必須通過const_case。

    復制代碼
    1 set<Employee,compare>::iterator i = s.begin();
    2 const_cast<Employee&>(*i).title = "Manager"; //OK
    3 const_cast<Employee*>(&*i).title = "Arch"; //OK
    4 const_cast<Employee>(*i).title = "Director"; // Bad 僅僅就修改了臨時變量的值 set中的值沒有發生改變
    復制代碼

      
    對於map和multimap而言,盡量不要修改鍵值,即使是通過const_cast的方式,因為STL的實現可能將鍵值放在只讀的內存區域當中。

    相對安全(而低效)的方式來修改關聯容器中的元素

    1. 找到希望修改的元素。
    2. 將要被修改的元素做一份拷貝。(注意拷貝的Map的key值不要聲明為const)
    3. 修改拷貝的值。
    4. 從容器中刪除元素。(erase 見第九條)
    5. 插入拷貝的那個元素。如果位置不變或鄰近,可以使用hint方式的insert從而將插入的效率從對數時間提高到常數時間。
    復制代碼
     1 set<Employee,compare> s;
    2
    3 Employee e1,e2;
    4
    5 e1.id = 2;
    6 e1.title = "QA";
    7
    8 e2.id = 1;
    9 e2.title = "Developer";
    10
    11 s.insert(e1);
    12 s.insert(e2);
    13
    14 set<Employee,compare>::iterator i = s.begin();
    15 Employee e(*i);
    16 e.title = "Manager";
    17
    18 s.erase(i++);
    19 s.insert(i,e);
    復制代碼

     

    第二十三條: 考慮使用排序的vector替代關聯容器  
     

    哈希容器大部分情況下可以提供常數時間的查找效率,標准容器也可以達到對數時間的查找效率。

    標准容器通常基於平衡二叉樹實現, 這種實現對於插入、刪除和查找的混合操作提供了優化。但是對於3步式的操作(首先進行插入操作,再進行查找操作,再修改元素或刪除元素),排序的vector能夠提供更好的性能。
    因為相對於vector,關聯容器需要更大的存儲空間。在排序的vector中存儲數據比在關聯容器中存儲數據消耗更少的內存,考慮到頁面錯誤的因素,通過二分搜索進行查找,排序的vector效率更高一些。

    如果使用排序的vector替換map,需要實現一個自定義的排序類型,該排序類型依照鍵值進行排序。
     

    第二十四條: 當效率至關重要時,請在map:operator[]和map:insert之間謹慎作出選擇 
     

    從效率方面的考慮,當向map中添加元素時,應該使用insert,當需要修改一個元素的值的時候,需要使用operator[]

    如果使用operator[]添加元素

    復制代碼
    1 class Widget{
    2 };
    3
    4
    5 map<int,Widget> m;
    6 Widget w;
    7
    8 m[0] = w;
    9 //Widget構造函數被調用兩次 
    復制代碼

    對於第8行,如果m[0]沒有對應的值,則會通過默認的構造函數生成一個widget對象,然后再用operator=將w的值賦給這個widget對象。 使用insert可以避免創建這個中間對象。

    1 map<int,Widget> m;
    2 Widget w;
    3
    4 m.insert(map<int,Widget>::value_type(0,w)); //沒有調用構造函數

      
    如果使用insert修改元素的值(當然,不會有人這樣做)

    復制代碼
     1 map<int,Widget> m;
    2 Widget w(1);
    3 m.insert(map<int,Widget>::value_type(0,w));
    4
    5 Widget w2(2);
    6
    7 m.insert(map<int,Widget>::value_type(0,w2)).first->second = w2; //構造了一個pair對象
    8
    9 // 上面這段代碼比較晦澀
    10 // map::insert(const value_type& x)的返回值為pair<iterator,bool>
    11 // 當insert的值已經存在時,iterator指向這個已經存在的值,bool值為false。
    12 // 反之,指向新插入的值,bool值為true。
    復制代碼

     

    使用operator[]則輕便且高效的多

    復制代碼
    1 map<int,Widget> m;
    2 Widget w(1);
    3 m.insert(map<int,Widget>::value_type(0,w));
    4
    5 Widget w2(2);
    6
    7 m[0] = w2;
    復制代碼

     
    一個通用的添加和修改map中元素的方法

    復制代碼
     1 template<typename MapType,
    2 typename KeyType,
    3 typename ValueType>
    4 typename MapType::iterator InsertOrUpdate(MapType& map,const KeyType& k, const ValueType& v) // 注意typename的用法 從屬類型前一定要使用typename
    5 {
    6 typename MapType::iterator i = map.lower_bound(k); // 如果i!=map.end(),則i->first不小於k
    7
    8 if(i!=map.end() && !map.key_comp()(k,i->first)) // k不小於i->first 等價!
    9 {
    10 i->second = v;
    11 return i;
    12 }
    13
    14 else
    15 {
    16 return map.insert(i,pair<const KeyType, ValueType>(k,v));
    17 }
    18 };
    19
    20
    21 map<int,Widget> m;
    22 Widget w(1);
    23
    24 map<int,Widget>::iterator i = InsertOrUpdate<map<int,Widget>,int,Widget>(m,0,w);
    復制代碼

     

    第二十五條: 熟悉非標准的哈希容器
     

    如果你和我一樣對於hash容器僅僅停留在知道的層次,這篇文章是我看到的國內對於hash_map講解的最為認真的文章,建議參考一下。

    常見的hash容器的實現有SGI和Dinkumware,SGI的hashset的聲明類似於

    復制代碼
    1 template<typename T,
    2 typename HashFunction = hash<T>,
    3 typename CompareFunction = equal_to<T>,
    4 typename Allocator = allocator<T>>
    5 class hashSet;
    復制代碼

     

      
    Dinkumware的hash_set聲明

    復制代碼
    1 template<typename T,
    2 typename CompareFunction>
    3 class hash_compare;
    4
    5 template<typename T,
    6 typename HashingInfo = hash_compare<T,less<T>>,
    7 typename Allocator = allocator<T>>
    8 class hash_set;
    復制代碼

      
    SGI使用傳統的開放式哈希策略,由指向元素的單向鏈表的指針數組(桶)構成。Dinkumware同樣使用開放式哈希策略,由指向元素的雙向鏈表的迭代器數組(桶)組成。從內存的角度上講,SGI的設計要節省一些

第二十六條: iterator優先於const_iterator, reverse_iterator以及const_reverse_iterator

對於容器類container<T>而言,

  • iterator的功效相當與T*
  • const_iterator的功效相當於 const T*
  • reverse_iterator與const_reverse_iterator與前兩者類似,只是按照反向遍歷

它們之間相互轉換的關系如圖


從iterator到const_iterator和reverse_iterator存在隱式轉換,從reverse_iterator到const_iterator也存在隱式轉換。

通過base()可以將reverse_iterator轉換為iterator,同樣可以將const_reversse_iterator轉換為const_iterator,但是轉換后的結果並不指向同一元素(有一個偏移量)


第二十七條: 使用distance和advance將容器的const_iterator轉換成iterator
 

對於大多數的容器,const_cast並不能將const_iterator轉換為iterator。即使在某些編譯器上可以將vector和string的const_iterator轉換為iterator,但存在移植性的問題 

通過distance和advance將const_iterator轉換為iterator的方法

復制代碼
 1 vector<Widget> v;
2
3 typedef vector<Widget>::const_iterator ConstIter;
4 typedef vector<Widget>::iterator Iter;
5
6 ConstIter ci;
7
8 ... //使ci指向v中的元素
9 Iter i = v.begin();
10 advance(i,distance<ConstIter>(i,ci));
復制代碼

  



第二十八條: 正確理解由reverse_iterator的base()成員函數所產生的iterator的用法

使用reverse_iterator的base()成員函數所產生的iterator和原來的reverse_iterator之間有一個元素的偏移量。

Picture2_thumb[7]


容器的插入、刪除和修改操作都是基於iterator的,所以對於reverse_iterator,必須通過base()成員函數轉換為iterator之后才能進行增刪改的操作。

  • 對於插入操作而言,新插入的元素都在3和4之間,所以可以直接使用insert(ri.base(),xxx)
  • 對於修改和刪除操作,由於ri和ri.base()並不指向同一元素,所以在修改和刪除前,必須修正偏移量

修正ri和ri.base()偏移量的做法

復制代碼
 1 set<Widget> s;
2
3 typedef set<Widget>::reverse_iterator RIter;
4
5 RIter ri;
6
7 ... //使ri指向v中的元素
8
9 s.erase(--ri.base()); //直接修改函數返回的指針不能被直接修改。 如果iterator是基於指針實現的,代碼將不具有可以執行。
10
11 s.erase((++ri).base()); //具備可移植行的代碼
復制代碼

  


第二十九條: 對於逐個字符的輸入請考慮使用istreambuf_iterator

 

常用的istream_iterator內部使用的operator>>實際上執行了格式化的輸入,每一次的operator>>操作都有很多的附加操作

  • 一個內部sentry對象的構造和析構(設置和清理行為的對象)
  • 檢查可能影響行為的流標志(比如skipws)
  • 檢查可能發生的讀取錯誤
  • 出現錯誤時檢查流的異常屏蔽標志以決定是否拋出異常


對於istreambuf_iterator,它直接從流的緩沖區中讀取下一個字符,不存在任何的格式化,所以效率相對istream_iterator要高得多。

對於非格式化的輸出,也可以考慮使用ostreambuf_iterator代替ostream_iterator。(損失了格式化輸出的靈活性)

第三十八條 遵循按值傳遞的原則來設計函數子類

c和C++中 以函數指針為參數的例子,函數指針是按值傳遞的

1 void qsort(void* base, size_t nmemb, size_t size,
2
3 int(*cmpfcn)(const void *, const void *));

  

STL函數對象是對函數指針的抽象形式,在STL中函數對象在函數中的傳遞也是按值傳遞的。

for_each算法的返回值就是一個函數對象,它的第三個參數也是函數對象。

1 template<class InputIterator,
2 class Function>
3 Function //按值返回
4 for_each(InputIterator first, InputIterator second, Function f); //按值傳遞

  

因為STL函數對象按值傳遞的特性,所以在設計函數對象時要:

  1. 將函數對象要盡可能的小,以減少拷貝的開銷。
  2. 函數對象盡量是單態的(不要使用虛函數),以避免剝離問題。


對於復雜的設計而言,具有包含很多信息的和含有繼承關系的函數對象也可能難以避免,這時可以采用Bridge Pattern來實現

 

復制代碼
 1 template<typename T>
2 class functorImp :
3 public unary_function<T,void> {
4 private :
5 Widget w;
6 int x;
7
8 public :
9 virtual ~functorImp();
10 virtual void operator() (const T& val) const;
11 friend class functor<T>;
12 };
13
14 template<typename T>
15 class functor :
16 public unary_function<T,void> {
17 private:
18 functorImp<T> *pImp; //唯一的一個數據成員
19
20 public:
21 void operator() (const T& val) const
22 {
23 pImp->operator()(val); //調用重載的operator
24 }
25 };
復制代碼


函數對象本身只包含一個指針,而且是不含虛函數的單態對象。真正的數據和操作都是由指針所指向的對象完成的。

對於這個實現,要注意的是在函數對象拷貝的過程中,如何維護這個指針成員。既能避免內存泄漏而且可以保證指針有效性的智能指針是個不錯的選擇。

1 shared_ptr<functorImp<T> *> pImp;



第三十九條
 確保判別式是純函數
 

判別式的一些基本概念:

  •  判別式 - 返回值為bool類型或者可以隱式轉換為bool類型的函數
  •  純函數 - 返回值僅與函數的參數相關的函數
  •  判別式類 – operator()函數是判別式的函數子類。 STL中凡是能接受判別式的地方,就可以接受一個判別式類的對象。 


對於判別式不是純函數的一個反例

復制代碼
 1 class Remove3rdElement 
2 : public unary_function<int,bool> {
3 public:
4
5 Remove3rdElement():i(0){}
6
7 bool operator() (const int&)
8 {
9 return ++i == 3;
10 }
11
12 int i;
13 };
14 ...
15 vector<int> myvector;
16 vector<int>::iterator it;
17
18 myvector.push_back(1);
19 myvector.push_back(2);
20 myvector.push_back(3);
21 myvector.push_back(4);
22 myvector.push_back(5);
23 myvector.push_back(6);
24 myvector.push_back(7);
25 myvector.erase(remove_if(myvector.begin(), myvector.end(), Remove3rdElement()),myvector.end()); // 1,2,4,5,7 remove_if之后的結果為 1,2,4,5,7,6,7。 返回值指向的是第六個元素。
復制代碼



第四十條
 如果一個類是函數子,應該使它可配接
 

STL中四個標准的函數配接器(not1, not2, bind1st, bind2nd)要求其使用的函數對象包含一些特殊的類型定義,包含這些類型定義的函數對象稱作是可配接的函數對象。下面的代碼無法通過編譯:

復制代碼
1 bool isWanted(const int i);
2
3 ...
4
5 vector<int> myvector;
6
7 vector<int>::iterator it = find_if(myvector.begin(), myvector.end(), not1(isWanted)); // error C2955: 'std::unary_function' : use of class template requires template argument list
復制代碼


從上面的錯誤可以看出,這個isWanted函數指針不能被not1使用,因為缺少了一些模板參數列表。ptr_fun的作用就在於給予這個函數指針所需要的類型定義從而使之可配接。

1 vector<int>::iterator it = find_if(myvector.begin(), myvector.end(), not1(ptr_fun(isWanted)));


這些特殊的類型定義包括: argument_type first_argument_type second_argument_type result_type,提供這些類型定義最簡單的方式是是函數對象的類從特定的模板繼承。

如果函數子類的operator方法只有一個實參,那么應該從unary_function繼承;如果有兩個實參,應該從binary_function繼承。

對於unary_function和binary_function,必須指定參數類型和返回值類型。

復制代碼
 1 template<typename T>
2 class functor : public unary_function<int, bool>
3 {
4 public :
5 bool operator()(int);
6 };
7
8 template<typename T>
9 class functor2 : public binary_function<int, double, bool>
10 {
11 public :
12 bool operator()(int, double, bool);
13 };
復制代碼

 

對於operator方法的參數:

  • operator的參數如果是非指針類型的,傳遞給unary_function和binary_function的參數需要去掉const和引用&符號
  •  operator的參數如果是指針類型的,傳遞給unary_function和binary_function的參數要與operator的參數完全一致。



第四十一條
 理解ptr_funmem_funmem_fun_reference的來由
 

對於ptr_fun在第40條已經有了一些介紹,它可以用在任何的函數指針上來使其可配接。

下面的例子,希望在myvector和myvector2的每一個元素上調用元素的成員函數。

復制代碼
 1 class Widget
2 {
3 public :
4 void test();
5 };
6
7 ...
8
9 vector<Widget> myvector;
10 vector<Widget*> myvector2;
11
12 ...
13
14 for_each(myvector.begin(),myvector.end(), &Widget::test); // 編譯錯誤
15 for_each(myvector2.begin(),myvector2.end(), &Widget::test); //編譯錯誤
復制代碼


而for_each的實現可能是這樣的

復制代碼
1 template<typename InputIterator, typename Function>
2 Function for_each(InputIterator begin, InputIterator end, Function f)
3 {
4 while (begin != end)
5 f(*begin++);
6 } 
復制代碼


對於mem_fun和mem_fun_reference, 就是要使成員方法可以作為合法的函數指針傳遞

復制代碼
1 for_each(myvector.begin(),myvector.end(), mem_fun_ref(&Widget::test)); // 當容器中的元素為對象時使用mem_fun_ref
2
3 for_each(myvector2.begin(),myvector2.end(), mem_fun(&Widget::test)); // 當容器中的元素為指針時,使用mem_fun
復制代碼


那么mem_fun是如何實現的呢?

1 template<typename R, typename C>
2 mem_fun_t<R,C>
3 mem_fun(R(C::*pmf)());


mem_fun接受一個返回值為R且不帶參數的C類型的成員函數,並返回一個mem_fun_t類型的對象。mem_fun_t是一個函數子類,擁有成員函數的指針,並提供了operator()接口。operator中調用了通過參數傳遞進來的對象上的成員函數。



第四十二條
 確保less<T>operator<具有相同的語義
 

STL規定,less總是等價於operator<, operator<是less的默認實現。

應當盡量避免修改less的行為,而且要確保它與operator<具有相同的意義。如果希望以一種特殊的方式來排序對象,那么就去創建一個新的函數子類,它的名字不能是less.

第四十三條:算法調用優先於手寫的循環
 

算法往往作用於一對迭代器所指定的區間中的每一個元素上,所以算法的內部實現是基於循環的。雖然說類似於find和find_if的算法可能不會遍歷所有的元素就返回了結果,但是在極端情況下,還是需要遍歷全部的元素。

從以下幾點分析,算法調用是優於手寫的循環的

  • 效率
  • 正確性
  • 可維護性



第四十四條:容器的成員函數優於同名的算法
 

  • 成員函數速度優於同名算法
  • 成員函數與容器的聯系更加緊密

對於關聯容器請看下面的例子:

復制代碼
1 set<int> s;
2
3 set<int>::iterator i1 = s.find(727);
4
5 set<int>::iterator i2 = find(s.begin(), s.end(), 727);
復制代碼

對於set而言,它的find成員函數的時間復雜度是log(n),而算法find的時間復雜度是線性的n。明顯,成員函數的效率要遠高於算法。

另外,算法是基於相等性而關聯容器基於等價性,在這種情況下,調用成員函數和調用算法可能會得到不同的結果。(參見第19條)

對於map以及multimap,成員函數之針基於key進行操作,而算法基於key-value pair進行操作。

對於list而言,成員函數相對於算法的優勢更加明顯。算法是基於元素的拷貝的,而list成員函數可能只需要修改指針的指向。

還有之前所提到的list的remove成員函數,同時起到了remove和erase的作用。

有些算法,例如sort並不能應用在list上,因為sort是基於隨機訪問迭代器的。還有merge算法,它要求不能修改源區間,而merge成員函數總是在修改源鏈表的元素的指針指向。



第四十五條:
正確區分count、find、binary_search、lower_bound、upper_bound和equal_range
 

  • count: 區間內是否存在某個特定的值,如果存在的話,這個值有多少個拷貝。
  • find:  區間內時候存在某個特定的值,如果存在的話,第一個符合條件的值在哪里。
  • binary_search:一個排序的區間內是否存在一個特定的值。
  • lower_bound:返回一個迭代器,或者指向第一個滿足條件的元素,或者指向適合於該值插入的位置。切記lower_bound是基於等價性的,用相等性來比較lower_bound的返回值和目標元素是存在潛在風險的。
  • upper_bound:返回一個迭代器,指向最后一個滿足條件元素的后面一個元素。
  • equal_range:返回一對迭代器,第一個指向lower_bound的返回值,第二個指向upper_bound的返回值。如果兩個返回值指向同一位置,則說明沒有符合條件的元素。Lower_bound與upper_bound的distance可以求得符合條件的元素的個數。

下表總結了在什么情況下使用什么樣的算法或成員函數

clip_image002


對於multi容器來說,find並不能保證找出的元素是第一個具有此值的元素。如果希望找到第一個元素,必須通過lower_bound,然后在通過等價性的驗證。Equal_range是另外一種方式,而且可以避免等價性測試,只是equal_range的開銷要大於lower_bound。



第四十六條:
考慮使用函數對象而不是函數作為STL算法的參數


函數對象優於函數的第一個原因在於函數對象的operator方法可以被優化為內聯函數,從而使的函數調用的開銷在編譯器被消化。而編譯器並沒有將函數指針的間接調用在編譯器進行優化,也就是說,函數作為STL算法的參數相對於函數對象而言,具有函數調用的開銷。

第二個理由是某些編譯器對於函數作為STL的參數支持的並不好。

第三個理由是有助於避免一些微妙的、語言本身的缺陷。比如說實例化一個函數模板,可能會與其他已經預定義的函數產生沖突。



第四十七條:
避免產生“直寫型”(write-only)的代碼


根據以往的經驗,代碼被閱讀的次數要遠遠多於被編寫的次數,所以要有意識的寫出具備可讀性的代碼。對於STL而言,則是盡量避免“直寫型”的代碼。

直寫型的代碼是這樣的,對於程序的編寫者而言,它顯得非常的直接,並且每一步都符合當初設計的邏輯。但是對於程序的閱讀者來說,在沒有全面了解程序編寫者動機的前提下,這樣的代碼往往讓人一頭霧水。

1 v.erase(remove_if(find_if(v.rbegin(),v.rend(),bind2nd(greater_equaql<int>(),y)).base()),v.end(),bind2nd(less<int>(),x));

  

比較易讀的寫法最好是這樣的

復制代碼
// 初始化range_begin,使它指向v中大於等於y的最后一個元素之后的那個元素
// 如果不存在這樣的元素,則rangeBegin被初始化為v.begin()
// 如果這個元素恰好是v的最后一個元素,則range_begin將被初始化為v.end()
VecIt rangeBegin = find_if(v.rbegin(),v.rend(),bind2nd(greater_equal<int>(),y)).base();

// 從rangeBegin到v.end()的區間中,刪除所有小於x的值
v.erase(remove_if(rangeBegin,v.end(),bind2nd(less<int>(),x)),v.end());
復制代碼

  


第四十八條 總是include正確的頭文件


與STL頭文件相關的一些總結

  • 幾乎所有的STL容器都被聲明在與之同名的頭文件之中
  • 除了accumulate、inner_product、adjacent_difference和partial_sum被聲明在<numeric>中之外,其他都所有算法都聲明在<algorithm>中
  • 特殊類型的迭代器,例如isteam_iterator和istreambuf_iterator,都被聲明在<iterator>中
  • 標准的函數子,比如less<T>,和函數子配接器,比如not1、bind2nd都被聲明在<functional>中。



第四十九條 
學會分析與STL相關的編譯器診斷信息


STL的編譯錯誤信息往往冗長而且難以閱讀,通過文本替換將復雜的容器名稱替換為簡單的代號,可以使得錯誤信息得到簡化。

例如,將std::basic_string<char, std::char_traits<char>, std::allocator<char>>替換為可讀性更強的string。


下面列舉一些常見的STL錯誤,以及可能的出錯原因

  • Vector和string的迭代器通常就是指針,當錯誤的使用iterator的時候,編譯器的錯誤信息中可能會包含指針類型的錯誤。
  • 如果診斷信息提到了back_insert_iterator, front_insert_iterator和insert_iterator,則幾乎意味着程序中直接或間接地調用了back_inserter, front_inserter或者是inserter。
  • 輸出迭代器以及inserter函數返回的迭代器在賦值操作符內部完成輸入或者插入操作,如果有賦值操作符有關的錯誤信息,可以關注這些迭代器。
  • 如果錯誤信息來自於算法的內部實現,往往意味着傳遞給算法的對象使用了錯誤的類型。
  • 如果在使用一個常見的STL組件,但編譯器卻不認知,可能是沒有包含合適的頭文件。



第五十條
  熟悉與STL相關的Web站點


SGI STL http://www.sgi.com/tech/stl

STLport http://www.stlport.org

Boost http://www.boost.org

另外個人推薦一個中文站點http://stlchina.huhoo.net/

Effective STL 讀書筆記》 第五章 算法

第三十條: 確保目標區間足夠大
 

下面例子中,希望將一個容器中的內容添加到另一個容器的尾部

復制代碼
1 int transformogrify(int x); //將x值做一些處理,返回一個新的值
2
3 vector<int> values;
4
5 vector<int> results;
6
7 ... //初始化values
8
9 transform(values.begin(),values.end(),results.end(),transformogrify);
復制代碼

由於results.end()返回的迭代器指向一段未初始化的內存,上面的代碼在運行時會導致無效對象的賦值操作。

可以通過back_inserter或者front_inserter來實現在頭尾插入另一個容器中的元素。因為front_inserter的實現是基於push_front操作(vector和string不支持push_front),所以通過front_inserter插入的元素與他們在原來容器中的順序正好相反,這個時候可以使用reverse_iterator。

復制代碼
 1 int transformogrify(int x); //將x值做一些處理,返回一個新的值
2
3 vector<int> values;
4
5 vector<int> results;
6
7 ... //初始化values
8
9 transform(values.begin(),values.end(),back_inserter(results),transformogrify);
10
11 int transformogrify(int x); //將x值做一些處理,返回一個新的值
12
13 deque<int> values;
14
15 deque<int> results;
16
17 ... //初始化values
18
19 transform(values.rbegin(),values.rend(),front_inserter(results),transformogrify);
復制代碼

  

另外可以使用inserter在results的任意位置插入元素

復制代碼
1 int transformogrify(int x); //將x值做一些處理,返回一個新的值
2
3 vector<int> values;
4
5 vector<int> results;
6
7 ... //初始化values
8
9 transform(values.begin(),values.end(),inserter(results,results.begin()+results.size()/2),transformogrify); //插入中間 
復制代碼

書中提到“但是,如果該算法執行的是插入操作,則第五條中建議的方案(使用區間成員函數)並不適用”,不知是翻譯的問題還是理解不到位,為什么插入操作不能用區間成員函數替換? 在我看來是因為區間成員函數並不支持自定義的函數對象,而這又跟插入操作有什么關系呢?莫非刪除可以???

如果插入操作的目標容器是vector或string,可以通過reserve操作來避免不必要的容器內存重新分配。

復制代碼
 1 int transformogrify(int x); //將x值做一些處理,返回一個新的值
2
3 vector<int> values;
4
5 vector<int> results;
6
7 //... //初始化values
8
9 results.reserve(values.size()+results.size()); //預留results和values的空間
10
11 transform(values.begin(),values.end(),back_inserter(results),transformogrify);
復制代碼

  

如果操作的結果不是插入而是替換目標容器中的元素,可以采用下面的兩種方式

復制代碼
 1 int transformogrify(int x); //將x值做一些處理,返回一個新的值
2
3 vector<int> values;
4
5 vector<int> results;
6
7 //... //初始化values
8
9 results.resize(values.size()); //想想對於results.size() > values.size() 和results.size() < values.size()兩種情況
10
11 transform(values.begin(),values.end(),results.begin(),transformogrify);
12
13 int transformogrify(int x); //將x值做一些處理,返回一個新的值
14
15 vector<int> values;
16
17 vector<int> results;
18
19 //... //初始化values
20
21 results.clear(); //results.size()為,results.capacity()不變
22
23 results.reserve(values.size()); //相對於上一種方式,如果values.size()小於原來的results.size(),那么會空余出一些元素的內存。
24
25 transform(values.begin(),values.end(),results.begin(),transformogrify);
復制代碼



第三十一條:
 了解各種與排序有關的選擇
 

對vector、string、deque或數組中的元素執行一次完全排序,可以使用sort或stable_sort

復制代碼
 1 vector<int> values;
2
3 values.push_back(4);
4
5 values.push_back(1);
6
7 values.push_back(2);
8
9 values.push_back(5);
10
11 values.push_back(3);
12
13 sort(values.begin(),values.end()); // 1,2,3,4,5
復制代碼

  

對vector、string、deque或數組中的元素選出前n個進行並對這n個元素進行排序,可以使用partial_sort

1 partial_sort(values.begin(),values.begin()+2,values.end()); // 1,2,4,5,3 注意第二個參數是一個開區間

  

對vector、string、deque或數組中的元素,要求找到按順序排在第n個位置上的元素,或者找到排名前n的數據,但並不需要對這n個數據進行排序,這時可以使用nth_element

1 nth_element(values.begin(),values.begin()+1,values.end()); // 1,2,3,4,5 注意第二個參數是一個閉區間

這個返回的結果跟我期望的有些差距,期望的返回值應該是1,2,4,5,3。VC10編譯器


對於標准序列容器(這回包含了list),如果要將其中元素按照是否滿足某種特定的條件區分開來,可以使用partition或stable_partition

復制代碼
1 vector<int>::iterator firstIteratorNotLessThan3 = partition(values.begin(),values.end(),lessThan3); //返回值為 2,1,4,5,3
2
3 vector<int>::iterator firstIteratorNotLessThan3 = stable_partition(values.begin(),values.end(),lessThan3); //返回值為 1,2,4,5,3
復制代碼

  

對於list而言,它的成員函數sort保證了可以stable的對list中元素進行排序。對於nth_element和partition操作,有三種替代方案:

  • 將list中的元素拷貝到提供隨機訪問迭代器的容器中,然后執行相應的算法
  • 創建一個list::iterator的容器,在對容器執行相應的算法
  • 利用一個包含迭代器的有序容器的信息,反復調用splice成員函數,將list中的成員調整到相應的位置。



第三十二條: 如果確實要刪除元素,請確保在remove這一類算法以后調用erase
 

remove算法接受兩個迭代器作為參數,這兩個迭代器指定了需要進行操作的區間。Remove並不知道它所操作的容器,所以並不能真正的將容器中的元素刪除掉。

復制代碼
 1 vector<int> values;
2
3 for(int i=0; i<10; i++)
4
5 {
6
7 values.push_back(i);
8
9 }
10
11 values[3] = values[5] = values[9] = 99;
12
13 remove(values.begin(),values.end(),99); // 0,1,2,4,6,7,8,7,8,99
復制代碼

  

從上面的代碼可見,remove並沒有刪除所有值為99的元素,只不過是用后面元素的值覆蓋了需要被remove的元素的值,並一一填補空下來的元素的空間,對於最后三個元素,並沒有其他的元素去覆蓋他們的值,所以仍然保留原值。

clip_image002


上圖可以看出,remove只不過是用后面的值填補了空缺的值,但並沒有將容器中的元素刪除,所以在remove之后,要調用erase將不需要的元素刪除掉。

1 values.erase(remove(values.begin(),values.end(),99),values.end()); // 0,1,2,4,6,7,8

  

類似於remove的算法還有remove_if和unique, 這些算法都沒有真正的刪除元素,習慣用法是將它們作為容器erase成員函數的第一個參數。

List是容器中的一個例外,它有remove和unique成員函數,而且可以從容器中直接刪除不需要的元素。



第三十三條:
 對於包含指針的容器使用remove這一類算法時要特別小心
 

復制代碼
 1 class Widget{
2 public:
3 ...
4 bool isCertified() const;
5 ...
6
7 };
8
9 vector<Widget*> v;
10
11 for(int i=0; i<10; i++)
12 {
13 v.push_back(new Widget());
14 }
15
16 v.erase(remove_if(v.begin(),v.end(),not1(mem_fun(&Widget::isCertified))),v.end());
復制代碼

  

上面的代碼可能會造成內存泄漏

clip_image004


避免內存泄漏的方式有兩種,第一種是先將需要被刪除的元素的指針刪除並設置為空,然后再刪除容器中的空指針。第二種方式更為簡單而且直觀,就是使用智能指針。

方案1

復制代碼
 1 void delAndNullifyUncertified(Widget*& pWidget)
2 {
3 if(!pWidget->isCertified())
4 {
5 delete pWidget;
6 pWidget = 0;
7 }
8 }
9
10 vector<Widget*> v;
11
12 for(int i=0; i<10; i++)
13 {
14 v.push_back(new Widget());
15 }
16
17 for_each(v.begin(),v.end(),delAndNullifyUncertified);
18
19 v.erase(remove(v.begin(),v.end(),static_cast<Widget*>(0)),v.end());
復制代碼

  

方案2

復制代碼
 1 template<typename T>
2 class RCSP{...}; // Reference counting smart pointer
3
4 typedef RSCP<Widget> RSCPW;
5
6 vector<RSCPW> v;
7
8 for(int i=0; i<10; i++)
9 {
10 v.push_back(RSCPW(new Widget()));
11 }
12
13 v.erase(remove_if(v.begin(),v.end(),not1(mem_fun(&Widget::isCertified))),v.end());
復制代碼



第三十四條:
 了解哪些算法要求使用排序的區間作為參數
 

  • 用於查找的算法binary_search, lower_bound, upper_bound和equal_range采用二分法查找數據,所以數據必須是事先排好序的。對於隨機訪問迭代器,這些算法可以保證對數時間的查找效率,對於雙向迭代器,需要線性時間
  • set_union, set_intersection, set_difference和set_symmetric_difference提供了線性時間的集合操作。排序的元素是線性效率的前提。
  • merge和inplace_merge實現了合並和排序的聯合操作。讀入兩個排序的區間,合並成一個新的排序區間。具有線性時間的性能。
  • includes,判斷一個區間中的元素是否都在另一個區間之中。具有線性的時間性能。
  • unique和unique_copy不一定需要排序的區間,但一般來說只有針對排序的區間才能刪除所有的重復數據,否則只是保留相鄰的重復數據中的第一個。

針對一個區間的進行多次算法的操作,要保證這些算法的排序方式是一致的。(比如都是升序或都是降序)



第三十五條:
 通過mismatchlexicographical_compare實現簡單的忽略大小寫的字符串比較
 

Mismatch的作用在於找出兩個區間中第一個對應值不同的位置。 要實現忽略大小寫的字符串比較,可以先找到兩個字符串中第一個不同的字符,然后通過比較這兩個字符的大小。

復制代碼
 1 int ciStringCompareImpl(const string& s1, const string& s2)
2 {
3 typedef pair<string::const_iterator, string::const_iterator> PSCI; //pair of string::const_iterator
4
5 PSCI p = mismatch(s1.begin(),s1.end(),s2.begin(),not2(ptr_fun(ciCharCompare)));
6
7 if(p.first == s1.end())
8 {
9 if(p.second == s2.end()) return 0;
10 else return -1;
11 }
12
13 return ciCharCompare(*p.first,*p.second);
14 }
復制代碼

  

Lexicograghical_compare是strcmp的一個泛化的版本,strcmp只能與字符數組一起工作,而lexicograghical_compare可以與任何類型的值區間一起工作。

復制代碼
1 bool charLess(char c1, char c2);
2
3 bool ciStringCompair(const string& s1, const string& s2)
4 {
5 return lexicographical_compare(s1.begin(),s1.end(),s2.begin(),s2.end(),charLess);
6 }
復制代碼



第三十六條:
 理解copy_if算法的正確實現
 

標准的STL中並不存在copy_if算法,正確的copy_if算法的實現如下所示:

復制代碼
 1 template<typename InputIterator,
2 typename OutputIterator,
3 typename Predicate>
4 OutputIterator copy_if(InputIterator begin,
5 InputIterator end,
6 OutputIterator destBegin,
7 Predicate p)
8 {
9 while(begin != end)
10 {
11 if(p(*begin))
12 {
13 *destBegin++ = *begin;
14 ++begin;
15 }
16
17 return destBegin;
18 }
19 }
復制代碼



第三十七條:
 使用accumulate或者for_each進行區間統計
 

 

accumulate有兩種形式

第一種接受兩個迭代器和一個初始值,返回結果是初始值與兩個迭代器區間的元素的總和。

1 vector<int> v;
2 ...
3 accumulate(v.begin(),v.end(),0);

  

第二種方式加了一個統計函數,使得accumulate函數變得更加通用。

1 vector<string> v;
2 ...
3 accumulate(v.begin(),v.end(),static_cast<string::size_type>(0), StringLegthSum);

  

accumulate的一個限制是不能產生任何的副作用,這時,for_each就是一個很好的補充。For_each接受三個參數,兩個迭代器確定的一個區間,以及統計函數。For_each的返回值是一個函數對象,必須通過調用函數對象中的方法才能夠取得統計的值。

復制代碼
 1 struct Point
2 {
3 Point(double _x, double _y):x(_x),y(_y)
4 {
5 }
6
7 double x,y;
8 }
9
10 class PointAverge : public unary_function<Point,void>
11 {
12 public:
13 PointAverage(): sum_x(0.0), sum_y(0.0),sum(0)
14 {
15 }
16
17 void operator()(const Point& p) //可以產生副作用
18 {
19 sum++;
20 sum_x += p.x;
21 sum_y += p.y;
22 }
23
24 Point GetResult() //用於返回統計結果
25 {
26 return Point(sum_x/sum, sum_y/sum);
27 }
28
29 private:
30
31 double sum_x, sum_y;
32 nt sum;
33 }
34
35 vector<Point> v;
36 ...
37 Point result = for_each(v.begin(),v.end(),PointAverage()).GetResult();

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM