條款1:仔細選擇你的容器
標準的連續(xù)內(nèi)存容器是vector、string和deque。非標準的rope也是連續(xù)內(nèi)存容器。
基于節(jié)點的容器是list和slist以及所有關(guān)聯(lián)容器。
● 你需要“可以在容器的任意位置插入一個新元素”的能力嗎?如果是,你需要序列容器,關(guān)聯(lián)容器做
不到。
● 你關(guān)心元素在容器中的順序嗎?如果不,散列容器就是可行的選擇。否則,你要避免使用散列容器。
● 必須使用標準C++中的容器嗎?如果是,就可以除去散列容器、slist和rope。
● 你需要哪一類迭代器?如果必須是隨機訪問迭代器,在技術(shù)上你就只能限于vector、deque和string,但
你也可能會考慮rope(關(guān)于rope的更多信息在條款50)。如果需要雙向迭代器,你就用不了slist(參見
條款50)和散列容器的一般實現(xiàn)(參見條款25)。
● 當(dāng)插入或者刪除數(shù)據(jù)時,是否非常在意容器內(nèi)現(xiàn)有元素的移動?如果是,你就必須放棄連續(xù)內(nèi)存容器
(參見條款5)。
● 容器中的數(shù)據(jù)的內(nèi)存布局需要兼容C嗎?如果是,你就只能用vector(參見條款16)。
● 查找速度很重要嗎?如果是,你就應(yīng)該看看散列容器(參見條款25),排序的vector(參見條款23)和
標準的關(guān)聯(lián)容器——大概是這個順序。
● 你介意如果容器的底層使用了引用計數(shù)嗎?如果是,你就得避開string,因為很多string的實現(xiàn)是用引
用計數(shù)(參見條款13)。你也不能用rope,因為權(quán)威的rope實現(xiàn)是基于引用計數(shù)的(參見條款50)。
于是你得重新審核你的string,你可以考慮使用vector<char>。
● 你需要插入和刪除的事務(wù)性語義嗎?也就是說,你需要有可靠地回退插入和刪除的能力嗎?如果是,
你就需要使用基于節(jié)點的容器。如果你需要多元素插入(比如,以范圍的方式——參見條款5)的事
務(wù)性語義,你就應(yīng)該選擇list,因為list是唯一提供多元素插入事務(wù)性語義的標準容器。事務(wù)性語義對
于有興趣寫異常安全代碼的程序員來說非常重要。(事務(wù)性語義也可以在連續(xù)內(nèi)存容器上實現(xiàn),但會
有一個性能開銷,而且代碼不那么直觀。要了解這方面的知識,請參考Sutter的《Exceptional C++》的
條款17[8]。)
● 你要把迭代器、指針和引用的失效次數(shù)減到最少嗎?如果是,你就應(yīng)該使用基于節(jié)點的容器,因為在
這些容器上進行插入和刪除不會使迭代器、指針和引用失效(除非它們指向你刪除的元素)。一般來
說,在連續(xù)內(nèi)存容器上插入和刪除會使所有指向容器的迭代器、指針和引用失效。
● 你需要具有有以下特性的序列容器嗎:1)可以使用隨機訪問迭代器;2)只要沒有刪除而且插入只發(fā)生在容器結(jié)尾,指針和引用的數(shù)據(jù)就不會失效?這個一個非常特殊的情況,但如果你遇到這種情況,
deque就是你夢想的容器。(有趣的是,當(dāng)插入只在容器結(jié)尾時,deque的迭代器也可能會失效,deque
是唯一一個“在迭代器失效時不會使它的指針和引用失效”的標準STL容器。)
條款2:小心對“容器無關(guān)代碼”的幻想
不同的容器是不同的,而且它們的優(yōu)點和缺點有重大不同。
它們并不被設(shè)計成可互換的,而且你做不了什么包裝的工作。
既然有了要一次次的改變?nèi)萜黝愋偷谋厝恍?,你可以用這個常用的方法讓改變得以簡化:使用封裝,封裝,
再封裝。其中一種最簡單的方法是通過自由地對容器和迭代器類型使用typedef。
要限制如果用一個容器類型替換了另一個容器可能需要修改的代碼,就需要在類中隱藏那個容器,而且要通
過類的接口限制容器特殊信息可見性的數(shù)量。比如,如果你需要建立一個客戶列表,請不要直接用list。取而
代之的是,建立一個CustomerList類,把list隱藏在它的private區(qū)域。你寫不出容器無關(guān)性代碼,但他們可能可以。
條款3:使容器里對象的拷貝操作輕量而正確
拷貝對象是STL的方式。
一個使拷貝更高效、正確而且對分割問題免疫的簡單的方式是建立指針的容器而不是對象的容器。
我們需要知道STL容器使用了拷貝,但是別忘了一個事實:比起數(shù)組它們?nèi)匀皇且粋€進步。
條款4:用empty來代替檢查size()是否為0
你應(yīng)該首選empty的構(gòu)造,而且理由很簡單:對于所有的標準容器,empty是一個常數(shù)時間的操作,但對于一
些list實現(xiàn),size花費線性時間。
條款5:盡量使用區(qū)間成員函數(shù)代替它們的單元素兄弟
太多的STL程序員過度使用copy,所以我重復(fù)一遍我的建議:幾乎所有目標區(qū)間被插入迭代器指定的copy的使用都可以
用調(diào)用的區(qū)間成員函數(shù)的來代替。
● 一般來說使用區(qū)間成員函數(shù)可以輸入更少的代碼。
● 區(qū)間成員函數(shù)會導(dǎo)致代碼更清晰更直接了當(dāng)。
我們有一個:效率。當(dāng)處理標準序列容器時,應(yīng)用單元素成員函數(shù)比完成同樣目的
的區(qū)間成員函數(shù)需要更多地內(nèi)存分配,更頻繁地拷貝對象,而且/或者造成多余操作。
● 區(qū)間構(gòu)造。所有標準容器都提供這種形式的構(gòu)造函數(shù):
container::container(InputIterator begin, // 區(qū)間的起點
InputIterator end); // 區(qū)間的終點
● 區(qū)間插入。所有標準序列容器都提供這種形式的insert:
void container::insert(iterator position, // 區(qū)間插入的位置
InputIterator begin, // 插入?yún)^(qū)間的起點
InputIterator end); // 插入?yún)^(qū)間的終點
關(guān)聯(lián)容器使用它們的比較函數(shù)來決定元素要放在哪里,所以它們了省略position參數(shù)。
void container::insert(lnputIterator begin, InputIterator end);
● 區(qū)間刪除。每個標準容器都提供了一個區(qū)間形式的erase,但是序列和關(guān)聯(lián)容器的返回類型不同。序列容器提供
了這個:
iterator container::erase(iterator begin, iterator end);
而關(guān)聯(lián)容器提供這個:
void container::erase(iterator begin, iterator end);
● 區(qū)間賦值。就像我在這個條款的一開始提到的,所有標準列容器都提供了區(qū)間形式的assign:
void container::assign(InputIterator begin, InputIterator end);
所以現(xiàn)在我們明白了,盡量使用區(qū)間成員函數(shù)來代替單元素兄弟的三個可靠的論點。區(qū)間成員函數(shù)更容易寫,它們更清
楚地表達你的意圖,而且它們提供了更高的性能。那是很難打敗的三駕馬車。
條款6:警惕C++最令人惱怒的解析
假設(shè)你有一個int的文件,你想要把那些int拷貝到一個list中。這看起來像是一個合理的方式:
ifstream dataFile("ints.dat");
list<int> data(istream_iterator<int>(dataFile), // 警告!這完成的并不
istream_iterator<int>()); // 是像你想象的那樣
用括號包圍一個
實參的聲明是不合法的,但用括號包圍一個函數(shù)調(diào)用的觀點是合法的,所以通過增加一對括號,我們強迫編譯器以我們的方式看事情:
list<int> data((istream_iterator<int>(dataFile)), // 注意在list構(gòu)造函數(shù)
istream_iterator<int>()); // 的第一個實參左右的
// 新括號
一個更好的解決辦法是在數(shù)據(jù)聲明中從時髦地使用匿名istream_iterator對象后退一步,僅僅給那些迭代器名
字。以下代碼到哪里都能工作:
ifstream dataFile("ints.dat");
istream_iterator<int> dataBegin(dataFile);
istream_iterator<int> dataEnd;
list<int> data(dataBegin, dataEnd);
條款7:當(dāng)使用new得指針的容器時,記得在銷毀容器前delete那些指針
我們需要記住的所有事情就是STL容器很智能,但它們沒有智能到知道是否應(yīng)該刪除它們所包含的指針。當(dāng)你要刪除指針的容器時要避免資源泄漏,你必須用智能引用計數(shù)指針對象(比如Boost的shared_ptr)來代替指針,或者你必須在容器銷毀前手動刪除容器中的每個指針。
條款8:永不建立auto_ptr的容器
拷貝一個auto_ptr將改變它的值
條款9:在刪除選項中仔細選擇
如果我們觀察在本條款中提到的所有東西,我們得出下列結(jié)論:
● 去除一個容器中有特定值的所有對象:
如果容器是vector、string或deque,使用erase-remove慣用法。
如果容器是list,使用list::remove。
如果容器是標準關(guān)聯(lián)容器,使用它的erase成員函數(shù)。
● 去除一個容器中滿足一個特定判定式的所有對象:
如果容器是vector、string或deque,使用erase-remove_if慣用法。
如果容器是list,使用list::remove_if。
如果容器是標準關(guān)聯(lián)容器,使用remove_copy_if和swap,或?qū)懸粋€循環(huán)來遍歷容器元素,當(dāng)你把迭代
器傳給erase時記得后置遞增它。
● 在循環(huán)內(nèi)做某些事情(除了刪除對象之外):
如果容器是標準序列容器,寫一個循環(huán)來遍歷容器元素,每當(dāng)調(diào)用erase時記得都用它的返回值更新你
的迭代器。
如果容器是標準關(guān)聯(lián)容器,寫一個循環(huán)來遍歷容器元素,當(dāng)你把迭代器傳給erase時記得后置遞增它。
條款10:注意分配器的協(xié)定和約束
因此,如果你想要寫自定義分配器,讓我們總結(jié)你需要記得的事情。
● 把你的分配器做成一個模板,帶有模板參數(shù)T,代表你要分配內(nèi)存的對象類型。
● 提供pointer和reference的typedef,但是總是讓pointer是T*,reference是T&。
● 決不要給你的分配器每對象狀態(tài)。通常,分配器不能有非靜態(tài)的數(shù)據(jù)成員。
● 記得應(yīng)該傳給分配器的allocate成員函數(shù)需要分配的對象個數(shù)而不是字節(jié)數(shù)。也應(yīng)該記得這些函數(shù)返回
T*指針(通過pointer typedef),即使還沒有T對象被構(gòu)造。
● 一定要提供標準容器依賴的內(nèi)嵌rebind模板。
條款11:理解自定義分配器的正確用法
分配器在許多情況里有用。只要你遵循相同類型的所有分配器都一定等價的限制條
件,你將毫不費力地使用自定義分配器來控制一般內(nèi)存管理策略,群集關(guān)系和使用共享內(nèi)存以及其他特殊的堆。
條款12:對STL容器線程安全性的期待現(xiàn)實一些
當(dāng)涉及到線程安全和STL容器時,你可以確定庫實現(xiàn)允許在一個容器上的多讀取者和不同容器上的多寫入者。你不能希望庫消除對手工并行控制的需要,而且你完全不能依賴于任何線程支持。
條款13:盡量使用vector和string來代替動態(tài)分配的數(shù)組
如果你用到的string實現(xiàn)是引用計數(shù)的,而你現(xiàn)在已經(jīng)確定string的引用計數(shù)支持是一個性能問題的多線程環(huán)境中運行,你至少有三個合理的選擇,而且沒有一個放棄了STL。第一,看看你的庫實現(xiàn)是否可以關(guān)閉引用計數(shù),通常是通過改變預(yù)處理變量的值。當(dāng)然那是不可移植的,但使工作變得可能,值得研究。第二,尋找或開發(fā)一個不使用引用計數(shù)的string實現(xiàn)(或部分實現(xiàn))替代品。第三,考慮使用vector<char>來代替string,vector實現(xiàn)不允許使用引用計數(shù),所以隱藏的多線程性能問題不會出現(xiàn)了。當(dāng)然,如果你選擇了vector<char>,你就放棄了string的專用成員函數(shù),但大部分功能仍然可以通過STL算法得到,所以你從一種語法切換到另一種不會失去很多功能。
所有的結(jié)果都是簡單的。如果你在使用動態(tài)分配數(shù)組,你可能比需要的做更多的工作。要減輕你的負擔(dān),就使用vector或string來代替。
條款14:使用reserve來避免不必要的重新分配
回到本條款的主旨,通常有兩情況使用reserve來避免不必要的重新分配。第一個可用的情況是當(dāng)你確切或者
大約知道有多少元素將最后出現(xiàn)在容器中。那樣的話,就像上面的vector代碼,你只是提前reserve適當(dāng)數(shù)量的
空間。第二種情況是保留你可能需要的最大的空間,然后,一旦你添加完全部數(shù)據(jù),修整掉任何多余的容
量。這里要加上一個注意點:調(diào)用reserve不改變?nèi)萜髦袑ο蟮膫€數(shù)。
條款15:小心string實現(xiàn)的多樣性
● 字符串值可能是或可能不是引用計數(shù)的。默認情況下,很多實現(xiàn)的確是用了引用計數(shù),但它們通常提供了關(guān)閉的方法,一般是通過預(yù)處理
器宏。條款13給了一個你可能要關(guān)閉的特殊環(huán)境的例子,但你也可能因為其他原因而要那么做。比如,引用計數(shù)只對頻繁拷貝的字符串有
幫助,而有些程序不經(jīng)??截愖址詻]有那個開銷。
● string對象的大小可能從1到至少7倍char*指針的大小。
● 新字符串值的建立可能需要0、1或2次動態(tài)分配。
● string對象可能是或可能不共享字符串的大小和容量信息。
● string可能是或可能不支持每對象配置器。
● 不同實現(xiàn)對于最小化字符緩沖區(qū)的配置器有不同策略。
條款16: 如何將vector和string的數(shù)據(jù)傳給遺留的API
vector v;
void doSomething(const int* pints, size_t numInts);
if (!v.empty()) doSomething(&v[0], v.size()); // 傳遞數(shù)據(jù)到API
string s;
void doSomething(const char *pString);
doSomething(s.c_str());
條款17:使用“交換技巧”來修整過剩容量
class Contestant {...};
vector<Contestant> contestants;
vector<Contestant>(contestants).swap(contestants);
同樣的技巧可以應(yīng)用于string:
string s;
... // 使s變大,然后刪除所有它的字符
string(s).swap(s); // 在s上進行“收縮到合適”
另外,交換技巧的變體可以用于清除容器和減少它的容量到你的實現(xiàn)提供的最小值。你可以簡單地和一個默
認構(gòu)造的臨時vector或string做個交換:
vector<Contestant> v;
string s;
... // 使用v和s
vector<Contestant>().swap(v); // 清除v而且最小化它的容量
string().swap(s); // 清除s而且最小化它的容量
條款18:避免使用vector<bool>
因為vector<bool>是一個偽容器,并不保存真正的bool,而是打包bool以節(jié)省空間。在一個典
型的實現(xiàn)中,每個保存在“vector”中的“bool”占用一個單獨的比特,而一個8比特的字節(jié)將容納8
個“bool”。在內(nèi)部,vector<bool>使用了與位域(bitfield)等價的思想來表示它假裝容納的bool。
條款19:了解相等和等價的區(qū)別
總之,通過只使用一個比較函數(shù)并使用等價作為兩個值“相等”的意義的仲裁者,標準關(guān)聯(lián)容器避開了很多
會由允許兩個比較函數(shù)而引發(fā)的困難。一開始行為可能看起來有些奇怪(特別是當(dāng)你發(fā)現(xiàn)成員和非成員find
可能返回不同結(jié)果),但最后,它避免了會由在標準關(guān)聯(lián)容器中混用相等和等價造成的混亂。
條款20:為指針的關(guān)聯(lián)容器指定比較類型
無論何時你建立一個指針的標準關(guān)聯(lián)容器,你必須記住容器會以指針的值排序。這基本上不是你想要的,所以你幾乎
總是需要建立自己的仿函數(shù)類作為比較類型。
struct DereferenceLess {
template <typename PtrType>
bool operator()(PtrType pT1, // 參數(shù)是值傳遞的,
PtrType pT2) const // 因為我們希望它們
{ // 是(或行為像)指針
return *pT1 < *pT2;
}
};
本條款是關(guān)于指針的關(guān)聯(lián)容器,但它也可以應(yīng)用于表現(xiàn)為指針的對象的容器,例如,智能
指針和迭代器。如果你有一個智能指針或迭代器的關(guān)聯(lián)容器,那也得為它指定比較類型。幸運的是,指針的
這個解決方案也可以用于類似指針的對象。正如DereferenceLess適合作為T*的關(guān)聯(lián)容器的比較類型一樣,它也
可以作為T對象的迭代器和智能指針容器的比較類型。
條款21: 永遠讓比較函數(shù)對相等的值返回false
因為相等不是等價
條款22:避免原地修改set和multiset的鍵
本條款的動機很容易理解。正如所有標準關(guān)聯(lián)容器,set和multiset保持它們的元素有序,這些容器的正確行為
依賴于它們保持有序。 如果你改了關(guān)聯(lián)容器里的一個元素的值(例如,把10變?yōu)?000),新值可能不在正確
的位置,而且那將破壞容器的有序性。
你將原諒我以這種方法放它,但要記得關(guān)鍵的事情是對于set和multiset,如果你進行任何容器元素的原地修
改,你有責(zé)任確保容器保持有序。
條款23:考慮用有序vector代替關(guān)聯(lián)容器
在有序vector中存儲數(shù)據(jù)很有可能比在標準關(guān)聯(lián)容器中保存相同的數(shù)據(jù)消耗更少的內(nèi)存;當(dāng)頁面錯誤
值得重視的時候,在有序vector中通過二分法查找可能比在一個標準關(guān)聯(lián)容器中查找更快。
階段性操作:
1. 建立。通過插入很多元素建立一個新的數(shù)據(jù)結(jié)構(gòu)。在這個階段,幾乎所有的操作都是插入和刪除。幾
乎沒有或根本沒有查找。
2. 查找。在數(shù)據(jù)結(jié)構(gòu)中查找指定的信息片。在這個階段,幾乎所有的操作都是查找。幾乎沒有或根本沒
有插入和刪除。
3. 重組。修改數(shù)據(jù)結(jié)構(gòu)的內(nèi)容,也許通過刪除所有現(xiàn)有數(shù)據(jù)和在原地插入新數(shù)據(jù)。從動作上說,這個階
段等價于階段1。一旦這個階段完成,應(yīng)用程序返回階段2。
如果你的程序不是按照階段的方式操作數(shù)據(jù)結(jié)構(gòu),那么使用有序vector代替標準關(guān)聯(lián)容器幾乎可以確定是在
浪費時間。