《Effective STL》学习笔记(第一部分)
(1) 容器:占12個條款,主要介紹了所有容器的共同指導法則
(2) vector和string:占6個條款,介紹了最常用的兩種容器的一些使用經驗
(3)?關聯容器:占7個條款,介紹了關聯容器(*map,*set)的使用經驗
(4) 迭代器:占12個條款,介紹了迭代器的一些使用技巧
(5) 算法:占8個條款,介紹STL算法的正確使用方法和提高效率的技巧
(6) 仿函數、仿函數類、函數等:占5個條款,介紹仿函數的使用經驗
(7) 使用STL編程:占8個條款,介紹怎樣在程序中使用由容器、迭代器、算法和函數對象組成的STL
在這些條款中,有的是一些編程經驗,即告訴你,如果想避免錯誤,應該這樣編寫程序,應該使用這個函數而不是另一個,還有一些是告訴你怎樣選擇最高效的函數以提高你程序的效率,總結如下:
{1} 介紹編程經驗:
條款1:仔細選擇你的容器
條款2:小心對“容器無關代碼”的幻想
條款6:警惕C++最令人惱怒的解析
條款7:當使用new得指針的容器時,記得在銷毀容器前delete那些指針
條款8:永不建立auto_ptr的容器
條款9:在刪除選項中仔細選擇
條款12:對STL容器線程安全性的期待現實一些
條款13:盡量使用vector和string來代替動態分配的數組
條款16: 如何將vector和string的數據傳給遺留的API
條款17:使用“交換技巧”來修整過剩容量
條款18:避免使用vector<bool>
條款19:了解相等和等價的區別
條款20:為指針的關聯容器指定比較類型
條款21: 永遠讓比較函數對“相等的值”返回false
條款22:避免原地修改set和multiset的鍵
條款26:盡量用iterator代替const_iterator,reverse_iterator和const_reverse_iterator
條款27:用distance和advance把const_iterator轉化成iterator
條款28:了解如何通過reverse_iterator的base得到iterator
條款30:確保目標區間足夠大
……
{2}提高程序效率:
條款3:使容器里對象的拷貝操作輕量而正確
條款4:用empty來代替檢查size()是否為0
條款5:盡量使用區間成員函數代替它們的單元素兄弟
條款14:使用reserve來避免不必要的重新分配
條款15:小心string實現的多樣性
條款23:考慮用有序vector代替關聯容器
條款24:當關乎效率時應該在map::operator[]和map-insert之間仔細選擇
條款25:熟悉非標準散列容器
條款29:需要一個一個字符輸入時考慮使用istreambuf_iterator
條款31:了解你的排序選擇
條款44:盡量用成員函數代替同名的算法
條款46:考慮使用函數對象代替函數作算法的參數
關于《Effective STL》學習筆記分為四部分,本文是第一部分(對應書中“容器”一節),第二、三、四部分分別為:《Effective STL》學習筆記(第二部分)(對應書中“vector和string”,“關聯容器”兩小節),《Effective STL》學習筆記(第三部分)(對應書中“迭代器”和“算法”兩小節),《Effective STL》學習筆記(第四部分)(對應書中“仿函數、仿函數類、函數等”和“使用STL”兩小節)。
1、 容器
本章關注的是可以適用于所有STL容器的指導方針。后面的章節則專注于特殊的容器類型。
條款1:仔細選擇你的容器
C++提供了很多可供程序員使用的容器:
(1) 標準STL序列容器:vector,string,deque和list
(2) 標準STL關聯容器:set,multiset,map和multimap
(3) 非標準序列容器slist(單鏈表)和rope(重型字符串)
(4) 非標準關聯容器hash_set,hash_multiset,hash_map和hash_multimap
(5) vector<char>可以作為string的替代品
(6) vector作為標準關聯容器的替代品
(7) 幾種標準非STL容器,包括數組、bitset、valarray、stack、queue和priority_queue
不同容器有不同的優缺點,用戶需要根據實際應用的特點綜合決定使用哪種容器,如:vector是一種可以默認使用的序列類型,當很頻繁地對序列中部進行插入和刪除時應該用list,當大部分插入和刪除發生在序列的頭或尾時可以選擇deque這種數據結構。
條款2:小心對“容器無關代碼”的幻想
本條款要告誡程序員:編寫與容器無關的代碼是沒有必要的。
有人想編寫這樣的程序,剛開始時使用vector存儲,之后由于需求的變化,將vector改為deque或者list,其他代碼不變。實際上,這基本上是做不到的。這是因為:不同的序列容器所對應了不同的迭代器、指針和引用的失效規則,此外,不同的容器支持的操作也不相同,如:vector支持reserve()和capacity(),而deque和list不支持;即使是相同的操作,復雜度也不一樣(如:insert),這會讓你的系統產生意想不到的瓶頸。
此外,鼓勵程序員在聲明容器和迭代器的時候使用typedef進行重命名,這能夠對你的程序進行封轉,從而使用起來更簡單,如有下面一個map容器:
map<string,vectorWidget>::iterator,CIStringCompare>;
如要用const_iterator遍歷這個map,你需不止一次地寫下:
map<string, vectorWidget>::iterator, CIStringCompare>::const_iterator
如果使用typedef,會快速方便很多。
條款3:使容器里對象的拷貝操作輕量而正確
容器容納了對象,但不是你給它們的那個對象。當你向容器中插入一個對象時,你插入的是該對象的拷貝而不是它本身;當你從容器中獲取一個對象時,你獲取的是容器中對象的拷貝。
拷貝是STL的基本工作方式。當你刪除或者插入某個對象時,現有容器中的元素會移動(拷貝);當你使用了排序算法,remove、uniquer或者他們的同類,rotate或者reverse,對象會移動(拷貝)。
一個使拷貝更高效、正確的方式是建立指針的容器而不是對象的容器,即保存對象的指針而不是對象,然而,指針的容器有它們自己STL相關的頭疼問題,改進的方法是采用智能指針。
條款4:用empty來代替檢查size()是否為0
對于任意容器c,寫下
if (c.size() == 0)…
本質上等價于寫下
if (c.empty())…
但是為什么第一種方式比第二種優呢?理由很簡單:對于所有的標準容器,empty是一個常數時間的操作,但對于一些list實現,size花費線性時間。
這什么造成list這么麻煩?為什么不能也提供一個常數時間的size?如果size是一個常數時間操作,當進行增加/刪除操作時每個list成員函數必須更新list的大小,也包括了splice,這會造成splice的效率降低(現在的splice是常量級的),反之,如果splice不必修改list大小,那么它就是常量級地,而size則變為線性復雜度,因此,設計者需要權衡這兩個操作的算法:一個或者另一個可以是常數時間操作。
條款5:盡量使用區間成員函數代替單元素操作
給定兩個vector,v1和v2,怎樣使v1的內容和v2的后半部分一樣?
可行的解決方案有:
(1) 使用區間函數assign:
| 1 | v1.assign(v2.begin() + v2.size() / 2, v2.end()); |
(2) 使用單元素操作:
| 1 2 3 4 5 6 7 | vector<Widget>::const_iterator ci = v2.begin() + v2.size() / 2; ci != v2.end(); ++ci) v1.push_back(*ci); |
(3) 使用copy區間函數
| 1 2 3 | v1.clear(); copy(v2.begin() + v2.size() / 2, v2.end(), back_inserter(v1)); |
(4) 使用insert區間函數
| 1 | v1.insert(v1.end(), v2.begin() + v2.size() / 2, v2.end()); |
最優的方案是assign方案,理由如下:
首先,使用區間函數的好處是:
● 一般來說使用區間成員函數可以輸入更少的代碼。
● 區間成員函數會導致代碼更清晰更直接了當。
使用copy區間函數存在的問題是:
【1】 需要編寫更多的代碼,比如:v1.clear(),這個與insert區間函數類似
【2】 copy沒有表現出循環,但是在copy中的確存在一個循環,這會降低性能
使用insert單元素版本的代碼對你征收了三種不同的性能稅,分別為:
【1】 沒有必要的函數調用;
【2】 無效率地把v中的現有元素移動到它們最終插入后的位置的開銷;
【3】 重復使用單元素插入而不是一個區間插入必須處理內存分配。
下面進行總結:
說明:參數類型iterator表示容器的迭代器類型,也就是container::iterator,參數類型InputIterator表示可以接受任何輸入迭代器。
【1】 區間構造
所有標準容器都提供這種形式的構造函數:
| 1 2 3 | container::container(InputIterator begin, // 區間的起點 InputIterator end); // 區間的終點 |
【2】 區間插入
所有標準序列容器都提供這種形式的insert:
| 1 2 3 4 5 | void container::insert(iterator position, // 區間插入的位置 InputIterator begin, // 插入區間的起點 InputIterator end); // 插入區間的終點 |
關聯容器使用它們的比較函數來決定元素要放在哪里,所以它們了省略position參數。
.
| 1 | void container::insert(lnputIterator begin, InputIterator end); |
【3】 區間刪除
每個標準容器都提供了一個區間形式的erase,但是序列和關聯容器的返回類型不同。序列容器提供了這個:
| 1 | iterator container::erase(iterator begin, iterator end); |
而關聯容器提供這個:
| 1 | void container::erase(iterator begin, iterator end); |
為什么不同?解釋是如果erase的關聯容器版本返回一個迭代器(被刪除的那個元素的下一個)會招致一個無法接受的性能下降.
【4】 區間賦值
所有標準列容器都提供了區間形式的assign:
| 1 | void container::assign(InputIterator begin, InputIterator end); |
條款6:警惕C++最令人惱怒的解析
假設你有一個int的文件,你想要把那些int拷貝到一個list中。這看起來像是一個合理的方式:
| 1 2 3 4 5 | ifstream dataFile("ints.dat"); list<int> data(istream_iterator<int>(dataFile), // 警告!這完成的并不 istream_iterator<int>()); // 是像你想象的那樣 |
這里的想法是傳一對istream_iterator給list的區間構造函數(參見條款5),因此把int從文件拷貝到list中。
實際上,這段代碼可以編譯通過,但運行時不會產生任何結果。仔細分析后,會發現,你這段代碼實際上是聲明了一個data函數,它的返回值是list<int>,兩個參數均為istream_iterator<int>類型。
解決辦法是在數據聲明中從時髦地使用匿名istream_iterator對象后退一步,僅僅給那些迭代器名字。以下代碼到哪里都能工作:
| 1 2 3 4 5 6 7 | ifstream dataFile("ints.dat"); istream_iterator<int> dataBegin(dataFile); istream_iterator<int> dataEnd; list<int> data(dataBegin, dataEnd); |
條款7:當使用new得指針的容器時,記得在銷毀容器前delete那些指針
條款8:永不建立auto_ptr的容器
條款9:在刪除選項中仔細選擇
(1)假定你有一個標準STL容器,c,容納int,
| 1 | Container<int> c; |
而你想把c中所有值為1963的對象都去,則不同的容器類型采用的方法不同:沒有一種是通用的.
[1] 如果采用連續內存容器(vector、queue和string),最好的方法是erase-remove慣用法:
| 1 2 3 | c.erase(remove(c.begin(), c.end(), 1963), // 當c是vector、string c.end()); // 或deque時,erase-remove慣用法是去除特定值的元素的最佳方法 |
[2] 對于list,最有效的方法是直接使用remove函數:
| 1 | c.remove(1963); |
[3] 對于關聯容器,解決問題的適當方法是調用erase:
| 1 | c.erase(1963); // 當c是標準關聯容器時,erase成員函數是去除特定值的元素的最佳方法 |
(2)讓我們換一下問題:不是從c中除去每個有特定值的元素,而是消除下面判斷式返回真的每個對象:
| 1 | bool badValue(int x); // 返回x是否是“bad” |
[1] 對于序列容器(vector、list、deque和string),只需要將remove換成remove_if即可:
| 1 2 3 4 5 | c.erase(remove_if(c.begin(), c.end(), badValue), // 當c是vector、string c.end()); // 或deque時這是去掉badValue返回真的對象的最佳方法 c.remove_if(badValue); // 當c是list時這是去掉badValue返回真的對象的最佳方法 |
[2] 對于關聯容器,有兩種方法處理該問題,一個更容易編碼,另一個更高效。“更容
易但效率較低”的解決方案用remove_copy_if把我們需要的值拷貝到一個新容器中,然后把原容器的內容和新的交換:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | AssocContainer<int> c; // c現在是一種標準關聯容器 AssocContainer<int> goodValues; // 用于容納不刪除的值的臨時容器 remove_copy_if(c.begin(), c.end(), // 從c拷貝不刪除 inserter(goodValues, // 的值到 goodValues.end()), // goodValues badValue); c.swap(goodValues); // 交換c和goodValues的內容 |
“更高效”的解決方案是直接從原容器刪除元素。不過,因為關聯容器沒有提供類似remove_if的成員函數,所以我們必須寫一個循環來迭代c中的元素,和原來一樣刪除元素:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | AssocContainer<int> c; ... for (AssocContainer<int>::iterator i = c.begin(); // for循環的第三部分 i != c.end(); // 是空的;i現在在下面 /*nothing*/ ){ // 自增 if (badValue(*i)) c.erase(i++); // 對于壞的值,把當前的 else ++i; // i傳給erase,然后 } // 作為副作用增加i;對于好的值,只增加i |
(3)進一步豐富該問題:不僅刪除badValue返回真的每個元素,而且每當一個元素被刪掉時,我們也想把一條消息寫到日志文件中。
[1] 對于關聯容器,只需要對我們剛才開發的循環做一個微不足道的修改就行了:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | ofstream logFile; // 要寫入的日志文件 AssocContainer<int> c; ... for (AssocContainer<int>::iterator i = c.begin(); // 循環條件和前面一樣 i !=c.end();){ if (badValue(*i)){ logFile << "Erasing " << *i <<'\n'; // 寫日志文件 c.erase(i++); // 刪除元素 } else ++i; } |
[2] 對于vector、string、list和deque,必須利用erase的返回值。那個返回值正是我們需要的:一旦刪除完成,它就是指向緊接在被刪元素之后的元素的有效迭代器。換句話說,我們這么寫:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | for (SeqContainer<int>::iterator i = c.begin(); i != c.end();){ if (badValue(*i)){ logFile << "Erasing " << *i << '\n'; i = c.erase(i); // 通過把erase的返回值賦給i來保持i有效 } else ++i; } |
條款10:注意分配器的協定和約束
條款11:理解自定義分配器的正確用法
條款12:對STL容器線程安全性的期待現實一些
當涉及到線程安全和STL容器時,你可以確定庫實現了“允許在一個容器上的多讀者”(在讀取時不能 有任何寫入者操作這個容器。)和“不同容器上的多個寫者”(多線程可以同時寫不同的容器。)。你不能希望庫消除對手工并行控制的需要,且你完全不能依賴于任何線程支持。
原創文章,轉載請注明:?轉載自董的博客
本文鏈接地址:?http://dongxicheng.org/cpp/effective-stl-part1/
總結
以上是生活随笔為你收集整理的《Effective STL》学习笔记(第一部分)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何编写Hadoop调度器
- 下一篇: 《Effective STL》学习笔记(