vector<int>(v)使用v創建一個臨時變量,v中空余的內存將不會被拷貝到這個臨時變量的空間中,再利用swap將這個臨時變量與v進行交換,相當於去除掉了v中的多余內存。
由於STL實現的多樣行,swap的方式並不能保證去掉所有的多余容量,但它將盡量將空間壓縮到其實現的最小程度。
利用swap的交換容器的值的好處在於可以保證容器中元素的迭代器、指針和引用在交換后依然有效。
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的實現可能將鍵值放在只讀的內存區域當中。
相對安全(而低效)的方式來修改關聯容器中的元素
- 找到希望修改的元素。
- 將要被修改的元素做一份拷貝。(注意拷貝的Map的key值不要聲明為const)
- 修改拷貝的值。
- 從容器中刪除元素。(erase 見第九條)
- 插入拷貝的那個元素。如果位置不變或鄰近,可以使用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] Picture2_thumb[7]](/image/aHR0cHM6Ly9pbWFnZXMuY25ibG9ncy5jb20vY25ibG9nc19jb20vYXJ0aHVybGl1LzIwMTEwOC8yMDExMDgyMDEzMjk1NDM1MjgucG5n.png)
容器的插入、刪除和修改操作都是基於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函數對象按值傳遞的特性,所以在設計函數對象時要:
- 將函數對象要盡可能的小,以減少拷貝的開銷。
- 函數對象盡量是單態的(不要使用虛函數),以避免剝離問題。
對於復雜的設計而言,具有包含很多信息的和含有繼承關系的函數對象也可能難以避免,這時可以采用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_fun、mem_fun和mem_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可以求得符合條件的元素的個數。
下表總結了在什么情況下使用什么樣的算法或成員函數

對於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/
第三十條: 確保目標區間足夠大
下面例子中,希望將一個容器中的內容添加到另一個容器的尾部
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的元素的值,並一一填補空下來的元素的空間,對於最后三個元素,並沒有其他的元素去覆蓋他們的值,所以仍然保留原值。

上圖可以看出,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());
上面的代碼可能會造成內存泄漏

避免內存泄漏的方式有兩種,第一種是先將需要被刪除的元素的指針刪除並設置為空,然后再刪除容器中的空指針。第二種方式更為簡單而且直觀,就是使用智能指針。
方案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不一定需要排序的區間,但一般來說只有針對排序的區間才能刪除所有的重復數據,否則只是保留相鄰的重復數據中的第一個。
針對一個區間的進行多次算法的操作,要保證這些算法的排序方式是一致的。(比如都是升序或都是降序)
第三十五條: 通過mismatch和lexicographical_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();