標簽(空格分隔): STL
運用STL柴淘,可以充分利用該庫的設(shè)計迫淹,讓我為簡單而直接的問題設(shè)計出簡單而直接的解決方案,也能幫助你為更復(fù)雜的問題設(shè)計出優(yōu)雅的解決方案为严。我將指出一些常見的STL錯誤用法敛熬,并指出該如何避免這樣的錯誤。這能幫助你避免產(chǎn)生資源泄漏第股、寫出不能移植的代碼应民,以及出現(xiàn)不確定的行為。我還將討論如何對你的代碼進行優(yōu)化夕吻,從而可以讓STL執(zhí)行得更快诲锹、更流暢,就像你所期待的那樣涉馅。
STL并沒有一個官方的正式定義归园,不同的人使用這個詞的時候,它有不同的含義稚矿。在本書中庸诱,STL表示C++標準庫中與迭代器一起工作的那部分,其中包括標準容器(包含string)晤揣、iostream庫的一部分桥爽,函數(shù)對象和各種算法。它排除了標準容器配接器(stack昧识、queue和priority_queue)以及容器bitset和valarray钠四,因為它們?nèi)鄙賹Φ鞯闹С帧?shù)組也不包括在其中滞诺。不錯形导,數(shù)組支持指針形式的迭代器环疼,但數(shù)組是C++語言的一部分习霹。而不是STL庫的一部分。
第一章 容器
沒錯炫隶,STL中有迭代器(iterator)淋叶、算法(algorithm)和 函數(shù)對象(function object),但是 對于大多數(shù)C++程序員來說,最值得注意的還是容器伪阶。容器比數(shù)組功能更強大煞檩、更靈活处嫌。它們可以動態(tài)增長(和縮減),可以自己管理內(nèi)存斟湃,可以記住自己包含了多少對象熏迹。它們限定了自己所支持的操作的復(fù)雜性。
問題1:如何進行動態(tài)增長(和縮減)
A 如何就我所面臨的具體制約條件選擇適當?shù)娜萜黝愋停?/p>
B 對于容器中的對象凝赛,復(fù)制操作的重要性注暗;
C 當指針或auto_ptr被存放在容器中時會有什么樣的困難;
D 刪除操作的細節(jié)墓猎;
E 用定制的分配只能做什么以及不能做什么捆昏;
F 使程序獲得最高效率的竅門;
G 在多線程環(huán)境中使用容器時的一些考慮毙沾;
標準STL序列容器: vector骗卜、string、deque 和 list左胞。
標準STL關(guān)聯(lián)容器: set寇仓、multiset、map 和 multimap烤宙。
非標準序列容器: slist 和 rope焚刺。slist是一個單向鏈表,rope本質(zhì)上是一 "重型" string 门烂。
非標準關(guān)聯(lián)容器: hash_set乳愉、hash_multiset、hash_map 和
hash_multimap屯远。在第25條中蔓姚,我們分析了這些基于散列表的、標準關(guān)聯(lián)容器的變體(它們通常是廣泛可用的)慨丐。
vector<char> 作為 string 的替代坡脐。在一些情況下,這種替代是有意義的房揭。
vector作為標準關(guān)聯(lián)容器的替代备闲。正如第23條所闡明的,有時vector在運行時間和空間上都要優(yōu)于標準關(guān)聯(lián)容器捅暴。
幾種標準的非STL容器恬砂,包括數(shù)組、bitset蓬痒、valarray泻骤、stack、queue和priority_queue。因為它們不是STL容器狱掂,后面解釋了為什么bitset比vector<bool>要好演痒。值得記住的是,數(shù)組也可以被用于STL算法趋惨,因為指針可以被用作數(shù)組的迭代器鸟顺。
C++標準就 "如何在vector、deque 和 list 中做出選擇" 提供了如下建議:
vector器虾、list和deque為程序員提供了不同的復(fù)雜性诊沪,vector是默認應(yīng)使用的序列類型;當需要頻繁地在序列中間做插入和刪除操作時曾撤,應(yīng)使用list端姚;當大多數(shù)插入和刪除操作發(fā)生在序列的頭部和尾部時,deque是應(yīng)考慮的數(shù)據(jù)結(jié)構(gòu)挤悉。
對STL容器的一種分類方法渐裸,該方法沒有得到應(yīng)有的重視。這就是對連續(xù)內(nèi)存容器和基于節(jié)點的容器的區(qū)分装悲。
連續(xù)內(nèi)存容器(或稱為基于數(shù)組的容器昏鹃,array-based container)把它的元素存放在一塊或多塊(動態(tài)分配的)內(nèi)存中,每塊內(nèi)存中存有多個元素诀诊。當有新元素插入或已有的元素被刪除時洞渤,同一內(nèi)存塊中的其他元素要向前或向后移動,以便為新元素讓出空間属瓣,或者填充被刪除元素所留下的空隙载迄。這種移動影響到效率和異常安全性。標準的連續(xù)內(nèi)存容器有vector抡蛙、string和deque护昧。非標準的rope也是一個連續(xù)內(nèi)存容器。
基于節(jié)點的容器在每一個(動態(tài)分配的)內(nèi)存塊中只存放一個元素粗截。容器中元素的插入或刪除只影響到指向節(jié)點的指針惋耙,而不影響節(jié)點本身的內(nèi)容,所以熊昌,當有插入或刪除操作時绽榛,元素的值不需要移動。表示鏈表的容器婿屹,如List 和 slist灭美,是基于節(jié)點的;所以標準的關(guān)聯(lián)容器也是如此(通常的實現(xiàn)方式是平衡樹)选泻。非標準的散列容器使用不同的基于節(jié)點的實現(xiàn))
你是否需要在容器的任意位置插入新元素冲粤? 如果需要,就選擇序列容器页眯;關(guān)聯(lián)容器時不行的梯捕。
你是否關(guān)心容器中的元素是排序的? 如果不關(guān)心窝撵,則散列容器是 一個可行的選擇方案傀顾;否則,你要避免散列容器碌奉。
對插入和刪除操作短曾,你需要事務(wù)語義嗎?也就是說赐劣,在插入和刪除操作失敗時嫉拐,你需要回滾的能力嗎?如果需要魁兼,你就要使用基于節(jié)點的容器婉徘。如果對多個元素的插入操作(即針對一個區(qū)間的形式)需要事務(wù)語義,則你需要選擇list咐汞,因為在標準容器中盖呼,只有l(wèi)ist對多個元素的插入操作提供了事務(wù)語義。對那些希望編寫異常安全代碼的程序員化撕,事務(wù)語義顯得尤為重要几晤。(使用連續(xù)內(nèi)存容器也可以獲得事務(wù)語義,但是要付出性能上的代價植阴,而且代價也顯得不那么直接了當蟹瘾。
確保容器中的對象副本正確而高效
容器中保存了對象,但并不是你提供給容器的那些對象掠手。而當從容器中取出一個對象時陈莽,你所取出的也并不是容器中所保存的那份。當向容器中加入對象時(通過insert或Push_back之類的操作)轴合,存入容器的是你所指定的對象的副本晓殊。當(通過如front或back之類的操作)從容器中取出一個對象時,你所得到的是容器中所保存的對象的副本魁衙。進去的是副本报腔,出來的也是副本。這就是STL的工作方式剖淀。
一旦一個對象被保存到容器中纯蛾,它經(jīng)常會進一步被復(fù)制。當對vector纵隔、string或deque進行元素的插入或刪除操作時翻诉,現(xiàn)有元素的位置通常會被移動(復(fù)制)炮姨。如果你使用下列任何操作--排序算法,next_permutation 或 previous_permutation碰煌,remove舒岸,unique或類似的操作,rotate或reverse芦圾,等等. 那么對象將會被移動蛾派。沒錯,復(fù)制對象是STL的工作方式个少。
復(fù)制動作是通過一個對象的復(fù)制成員函數(shù)就可以很方便地復(fù)制該對象洪乍,特別是對象的賦值構(gòu)造函數(shù)(copy constructor)和復(fù)制賦值操作符(copy assignment operator)。內(nèi)置類型(built-in type)(如整型夜焦、指針類型等)的實現(xiàn)總是簡單地按位賦值壳澳。如果你向容器中填充對象,而對象的復(fù)制操作又很費時茫经,那么向容器中填充對象這一簡單的操作將會成為程序的性能"瓶頸"钾埂。放入容器中的對象越多,而且科平,如果這些對象的“副本”有特殊的含義褥紫,那么把它們放入容器時將不可避免地會產(chǎn)生錯誤。
當然瞪慧,在存在繼承關(guān)系的情況下髓考,復(fù)制動作會導(dǎo)致剝離(slicing).也就是說,如果你創(chuàng)建了一個存放基類對象的容器弃酌,卻向其中插入派生類的對象氨菇,那么在派生類對象(通過基類的復(fù)制構(gòu)造函數(shù))被復(fù)制進容器時,它所特有的部分(即派生類中的信息)將會丟失:
vector<Widget> vw;
class SpecialWidget: public Widget{...}; //SpecialWidget 繼承于上面的Widget
SpecialWidget sw;
vw.push_back(sw); // sw作為基類對象唄復(fù)制進vw中妓湘,它的派生類特有部分在復(fù)制時被丟掉了
“剝離” 問題意味著向基類對象的容器中插入派生類對象幾乎總是錯誤的查蓉。如果你希望插入后的對象仍然表現(xiàn)得像派生類對象一樣,例如調(diào)用派生類的虛函數(shù)等榜贴,那么這種期望是錯誤的豌研。(關(guān)于剝離問題的更多知識,請參見Effective C++的第22條唬党。關(guān)于STL中剝離問題的另一個例子)
使復(fù)制動作高效鹃共、正確,并防止剝離問題發(fā)生的一個簡單辦法是使容器包含指針而不是對象驶拱。也就是說霜浴,使用Widget*的容器,而不是Widget的容器蓝纲。復(fù)制指針的速度非骋趺希快晌纫,并且總是會按你期望的方式進行(它復(fù)制構(gòu)成指針的每一位),而且當它被復(fù)制時不會有任何剝離現(xiàn)象發(fā)生永丝。如果你想避開這些使人頭疼的問題锹漱,同時又想避免效率、正確性和剝離這些問題类溢,你可能會發(fā)現(xiàn)智能指針(small pointer)是一個誘人的選擇凌蔬。
STL做了很多副本露懒,但它總的設(shè)計思想是為了避免不必要的復(fù)制闯冷。事實上,它總體的設(shè)計目標是為了避免創(chuàng)建不必要的對象懈词。把它跟C和C++僅有的內(nèi)置容器(即數(shù)組)的行為做比較:
Widget w[maxNumWidgets]; //創(chuàng)建了有maxNumWidgets個Widget的數(shù)組蛇耀,//每個對象都使用默認構(gòu)造函數(shù)來創(chuàng)建
如果用STL,則我們可以使用vector坎弯,當需要的時候它會增長纺涤;
我們也可以創(chuàng)建一個空的vector,它包含足夠的空間來容納 maxNumWidgets個Widget對象抠忘,但并沒有創(chuàng)建任何一個Widget對象:
vector<Widget> vw;
vw.reserver(maxNumWidgets);
調(diào)用empty而不是檢查size() 是否為0.
對于任一容器c撩炊,代碼:if(c.size() == 0)...
本質(zhì)上與if(c.empty())...
是等價的。既然如此崎脉,還是應(yīng)該使用empty()拧咳,理由是:empty對所有的標準容器都是常數(shù)時間操作,而對一些list實現(xiàn)囚灼,size耗費線性時間骆膝。到底是什么使list這么討厭呢?為什么它不也提供常數(shù)時間的size呢灶体?答案在于list所獨有的鏈接(splice)操作阅签。考慮如下代碼:
list<int> list1;
list<int> list2;
...
list1.splice(list1.end(),list2,find(list2.begin(),list2.end(),5),find(list2.rbegin(),list2.rend(),10).base());
// 把list2中從第一個含5的節(jié)點到最后一個含10的所有節(jié)點移動到list1的末尾蝎抽。
鏈接后的list1中有多少個元素政钟?很明顯,鏈接后的list1中的元素個數(shù)是它鏈接前的元素個數(shù)加上鏈接過來的元素個數(shù)樟结。但有多少個元素被鏈接過來了呢锥涕?應(yīng)該與 find(list2.begin(),list2.end(),5),find(list2.rbegin(),list2.rend(),10).base()
所定義的區(qū)間中的元素個數(shù)一樣多。究竟有多少個狭吼,如果不遍歷該區(qū)間是無法知道的层坠。通常,list或splice——必須做出讓步刁笙。其中的一個可以成為常數(shù)時間操作破花,但不可能二者都是谦趣。
不同的鏈表實現(xiàn)通過不同的方式解決這一沖突,具體方式取決于作者選擇把size還是splice實現(xiàn)得最為高效座每。如果你使用的list實現(xiàn)恰好是把splice的常數(shù)時間操作放在第一位前鹅,那么你使用empty而不是size會更好些,因為empty操作總是花費常數(shù)時間峭梳。即使現(xiàn)在你使用的list實現(xiàn)不是這種方式舰绘,將來你也可能會發(fā)現(xiàn)自己在使用這樣的實現(xiàn)。比如葱椭,你可能把自己的代碼移植到不同的平臺上捂寿,不管發(fā)生了什么,調(diào)用empty而不是檢查size==0是否成立總是沒錯的孵运。所以秦陋,如果你想知道容器中是否含有零個元素,請調(diào)用empty治笨。
區(qū)間成員函數(shù)優(yōu)先于與之對應(yīng)的單元素成員函數(shù)驳概。
給定v1和v2兩個矢量(vector),使v1的內(nèi)容和v2的后半部分相同的最簡單操作是什么?
不必為了當v2含有奇數(shù)個元素時"一半"的定義而煞費苦心旷赖。只要做到合理即可顺又。
v1.assign(v2.begin() + v2.size()/2, v2.end());
assign這么一個使用及其方便,卻為許多程序員所忽略的成員函數(shù)等孵。對所有的標準序列容器(vector,string,deque 和list)稚照,它都存在。當你需要完全替換一個容器的內(nèi)容時流济,你應(yīng)該想到賦值(assignment)如果你想把一個容器復(fù)制到相同類型的另一個容器锐锣,那么operator=是可選擇的賦值函數(shù),但正如該例子所揭示的那樣绳瘟,當你想給容器一組全新的值時雕憔,你可以使用assign,而operator=則不能滿足你的要求糖声。
這里同時也揭示了為什么區(qū)間成員函數(shù)(range member function)優(yōu)先于與之對應(yīng)的單元素成員函數(shù)斤彼。區(qū)間成員函數(shù)是指這樣的一類成員函數(shù),它們像STL算法一樣蘸泻,使用兩個迭代器參數(shù)來確定該成員操作所執(zhí)行的區(qū)間琉苇。如果不使用區(qū)間成員函數(shù)來解決問題,就得寫一個顯式的循環(huán)悦施,或許像這樣:
vector<Widget>v1, v2;
v1.clear(); //clear 和 erase 的區(qū)別
for(vector<Widget>::conset_iterator ci = v2.begin() + v2.size()/2; ci != v2.end(); ++ci)
v1.push_back(*ci);
這樣寫顯式的循環(huán)并扇,其實比調(diào)用assign多做了很多工作。這樣的循環(huán)在一定程度上影響了效率抡诞,后面會接著討論這個問題穷蛹。避免循環(huán)的一種方法是遵從第43條的建議土陪,使用一個算法:
v1.clear();
copy(v2.begin() + v2.size()/2, v2.end(), back_inserter(v1));
同調(diào)用assign相比,所做的工作還是多了些肴熏。而且鬼雀,盡管上面的代碼中沒有循環(huán),但copy中肯定有蛙吏。結(jié)果是源哩,影響效率的因素仍然存在。幾乎所有通過利用插入迭代器的方式(即利用inserter鸦做、back_inserter 或 front_inserter)來限定目標區(qū)間的copy調(diào)用励烦,其實都可以(也應(yīng)該)被替換為堆區(qū)間成員函數(shù)的調(diào)用。比如馁龟,在這里崩侠,對copy的調(diào)用可以被替換為利用區(qū)間的Insert版本:
v1.insert(v1.end(), v2.begin() + v2.size()/2, v2.end());
同調(diào)用copy相比漆魔,敲鍵盤的工作稍少了些坷檩,但它更加直截了當?shù)卣f明了所發(fā)生的事情:數(shù)據(jù)被插入到V1中。對copy的調(diào)用也說明了這一點改抡,但沒有這么直接矢炼,而是把重點放在了不合適的地方。對這里所發(fā)生的事情阿纤,有意義的不是元素被復(fù)制句灌,而是有新的數(shù)據(jù)被插入到了v1中。Insert成員函數(shù)很清晰地表明了這一點欠拾,使用copy則把這一點掩蓋了胰锌。太多的STL程序員濫用了copy,所以我剛才給出的建議值得再重復(fù)一下:通過利用插入迭代器的方式來限定目標區(qū)間的copy 調(diào)用藐窄,幾乎都應(yīng)該被替換為對區(qū)間成員函數(shù)的調(diào)用资昧。
太多的STL程序員濫用了copy,所以我剛才給出的建議值得再重復(fù)一下:通過利用插入迭代器的方式來限定目標區(qū)間的copy調(diào)用荆忍,幾乎都應(yīng)該被替換為對區(qū)間成員函數(shù)的調(diào)用格带。
現(xiàn)在回到assign的例子。我們已經(jīng)給出了使用區(qū)間成員函數(shù)而不是其相應(yīng)的單元素成員函數(shù)的原因:
- 通過使用區(qū)間成員函數(shù)刹枉,通尺闯可以少寫一些代碼。
- 使用區(qū)間成員函數(shù)通常會得到意圖清晰和更加直接的代碼微宝。
對于標準的序列容器棺亭,我們又一個標準:效率。當處理標準序列容器時蟋软,為了取得同樣的效果镶摘,使用單元素的成員函數(shù)比使用區(qū)間成員函數(shù)需要更多地調(diào)用內(nèi)存分配子专甩,更頻繁地復(fù)制對象,而且 / 或者做冗余的操作钉稍。
比如涤躲,假定你要把一個int數(shù)組復(fù)制到一個vector的前端。(首先贡未,數(shù)據(jù)可能來自數(shù)組而不是vector种樱,因為數(shù)據(jù)來自遺留的C代碼。關(guān)于STL容器和C API混合使用時導(dǎo)致的問題俊卤。使用vector的區(qū)間insert函數(shù)嫩挤,非常簡單:
// 假定numValues 在別處定義
int data[numValues];
vector<int> v;
...
v.insert(v.begin(),data,data+numValues); //把整數(shù)插入到v的前端
而通過顯式地循環(huán)調(diào)用insert,或多或少可能像這樣:
請注意消恍,我們必須記得把insert的返回值記下來供下次進入循環(huán)時使用岂昭。如果在每次插入操作后不更新insertLoc,我們會遇到兩個問題狠怨。首先约啊,第一次迭代后的所有循環(huán)迭代都將導(dǎo)致不可預(yù)料的行為(undefined behavior),因為每次調(diào)用insert都會使insertLoc無效佣赖。其次恰矩,即使insertLoc仍然有效,插入總是發(fā)生在vector的最前面(即在v.begin()處)憎蛤,結(jié)果這組整數(shù)被以相反的順序復(fù)制到v當中外傅。
如果遵從第43條,把循環(huán)替換為對copy的調(diào)用俩檬,我們得到如下代碼:
當copy模板被實例化之后萎胰,基于copy的代碼和使用循環(huán)的代碼幾乎是相同的,所以棚辽,為了分析效率技竟。我們將注意力集中在顯式循環(huán)上,但要記住晚胡,對于使用copy的代碼灵奖,下列分析同樣有效。分析顯式循環(huán)將更易于理解“哪些地方影響了效率”估盘。對瓷患,右多個地方影響了效率,使用單元素版本的insert總共在三個方面影響了效率遣妥,而如果使用區(qū)間版本的insert擅编,則這三種影響都不復(fù)存在。
第一種影響是不必要的函數(shù)調(diào)用。把numValues個元素逐個插入到v中導(dǎo)致了對insert的numValues次調(diào)用爱态。而使用區(qū)間形式的insert谭贪,則只做了一次函數(shù)調(diào)用,當然锦担,使用內(nèi)聯(lián)(inlining)可能會避免這樣的影響俭识,但是,實際中不見得會使用內(nèi)聯(lián)洞渔。只要一點是肯定的:使用區(qū)間形式的insert套媚,肯定不會有這樣的影響。
內(nèi)聯(lián)無法避免第二種影響磁椒,即把v中已有的元素頻繁地移動到插入后它們所處的位置堤瘤。每次調(diào)用insert把新元素插入到v中時,插入點后的每個元素都要向后移動一個位置浆熔,以便為新元素騰出空間本辐。所以,位置p的元素必須被移動到位置p+1医增,等等慎皱。在我們的例子中,我們向v的前端插入numValues個元素调窍,這意味著v中插入點之后的每個元素都要向后移動numValues個位置宝冕。每次調(diào)用insert時张遭,每個元素需向后移動一個位置邓萨,所以每個元素將移動numValues次。如果插入前v中有n個元素菊卷,就會有nnumValues次移動缔恳。在這個例子中,v中存儲的是int類型洁闰,每次移動最終可能會歸為調(diào)用memmove歉甚,可是如果v中存儲的是Widget這樣的用戶自定義類型,則每次一定會導(dǎo)致調(diào)用該類型的賦值操作符或復(fù)制構(gòu)造函數(shù)扑眉。(大多數(shù)情況下會調(diào)用賦值操作符纸泄,但每次vector中的最后一個元素被移動時,將會調(diào)用該元素的復(fù)制構(gòu)造函數(shù)腰素。)所以在通常情況下聘裁,把numValues個元素逐個插入到含有n個元素的vector<Widget>的前端將會有nnumValues次函數(shù)調(diào)用的代價:(n-1)*numValues次調(diào)用Widget的賦值操作符和numValues次調(diào)用Widget的復(fù)制構(gòu)造函數(shù)。即使這些調(diào)用時內(nèi)聯(lián)的弓千,你仍然需要把v中的元素移動numValues次衡便。
與此不同的是,C++標準要求區(qū)間insert函數(shù)把現(xiàn)有容器中的元素直接移動到它們最終的位置上,即只需付出每個元素移動一次的代價镣陕∏床停總的代價包括n次移動,numValues次調(diào)用該容器中元素類型的復(fù)制構(gòu)造函數(shù)呆抑,以及調(diào)用該類型的賦值操作符岂嗓。同每次插入一個元素的策略相比較,區(qū)間insert減少了n*(numValues-1)次移動鹊碍。細算下來摄闸,這意味著如果numValues是100,那么區(qū)間形式的insert比重復(fù)調(diào)用單元素形式的insert減少了99%的移動妹萨。
在講述單元素形式的成員函數(shù)和與其對應(yīng)的區(qū)間成員函數(shù)相比較所存在的第三個效率問題之前年枕,我需要做一個小小的更在。區(qū)間insert函數(shù)僅當能確定兩個迭代器之間的距離而不會失去它們的位置時乎完,才可以一次就把元素移動到其最終位置上熏兄。這幾乎是可能的,因為所有的前向迭代器都提供這樣的功能树姨,而前向迭代器幾乎無處不在摩桶。標準容器的所有迭代器都提供了前向迭代器的功能。非標準散列容器的迭代器也是如此(見第25條)帽揪。指針作為數(shù)組的迭代器也提供了這一功能硝清。實際上,不提供這一功能的標準迭代器僅有輸入和輸出迭代器转晰。所以芦拿,所說的是正確的,除非傳入?yún)^(qū)間形式insert的是輸入迭代器(如istream_iterator查邢,見第6條)蔗崎。僅在這樣的情況下,區(qū)間insert也必須把元素一步步移動到其最終位置上扰藕,因而它的優(yōu)勢就喪失了缓苛。(對于輸出迭代器不會產(chǎn)生這個問題,因為輸出迭代器不能用來標明一個區(qū)間邓深。)
不明智地使用重復(fù)的單元素插入操作而不是一次區(qū)間插入操作未桥,這樣所帶來的最后一個性能問題跟內(nèi)存分配有關(guān),盡管它同時還伴有討厭的復(fù)制問題芥备。在第14條將會指出冬耿,如果試圖把一個元素插入到vector中,而它的內(nèi)存已滿门躯,那么vector將分配具有更大容量(capacity)的新內(nèi)存淆党,把它的元素從舊內(nèi)存賦值到新內(nèi)存中,銷毀舊內(nèi)存中的元素,并釋放舊內(nèi)存染乌。然后它把要插入的元素加入進來山孔。第14條還解釋了多數(shù)vector實現(xiàn)每次在內(nèi)存耗盡時,會把容量加倍荷憋,因此台颠,插入numValues個新元素最多可導(dǎo)致log2numValues次新的內(nèi)存分配。
第14條指出勒庄,表現(xiàn)出這種行為的vector實現(xiàn)是存在的串前,因此,把1000個元素逐個插入可能會導(dǎo)致10次新的內(nèi)存分配(包括低效的元素復(fù)制)实蔽。與之對應(yīng)(而且荡碾,到現(xiàn)在為止也可以預(yù)見),使用區(qū)間插入的方法局装,在開始插入前可以知道自己需要多少新內(nèi)存(假定給它的是前向迭代器)坛吁,所以不必多次重新分配vector的內(nèi)存☆砩校可以想見拨脉,這一節(jié)省是很客觀的。
剛才所做的分析是針對vector的宣增,但該論證過程對string同樣有效玫膀。對于deque,論證過程與之類似爹脾,但deque管理內(nèi)存的方式與vector和string都不同帖旨。但是,關(guān)于把元素不必要地移動很多次的論斷依然是成立的誉简。
在標準的序列容器中碉就,現(xiàn)在只剩下list,對此使用區(qū)間形式而不是單元素形式的insert也有其效率上的優(yōu)勢闷串。關(guān)于重復(fù)函數(shù)調(diào)用的論斷當然繼續(xù)生效,由于鏈表工作的方式筋量,對list中某些節(jié)點的next和prev指針的重復(fù)的烹吵,多余的賦值操作。
每當有元素加入到鏈表中時桨武,含有這一元素的節(jié)點必須設(shè)定它的next和prev指針肋拔,當然新節(jié)點前面的節(jié)點(我們稱之為B,代表“前面”)必須設(shè)置自己的next指針呀酸,而新節(jié)點后面的節(jié)點則必須設(shè)定自己的prev指針:
當通過調(diào)用list的單元素insert把一系列節(jié)點逐個加入進來時凉蜂,除了最后一個新節(jié)點,其余所有的節(jié)點都要把其next指針賦值兩次:一次指向A,另一次指向在它之后插入的節(jié)點窿吩。每次有新節(jié)點再A前面插入時茎杂,A會把其prev指針指向新指針。如果A前面插入了numValues個指針纫雁,對所插入的節(jié)點的next指針會有numValues-1次多余的賦值,對A的prev指針也會有numValues-1次賦值¢购牛總共就會有2*(numValues-1)次不必要的指針賦值举庶。
而避免這一代價的答案是使用區(qū)間形式的Insert。因為這一函數(shù)知道最終將插入多少節(jié)點忌愚,可以避免不必要的指針賦值曲管,而只使用一次賦值將每個指針設(shè)為插入后的值。
下面了解哪些成員函數(shù)支持區(qū)間硕糊,在下面的函數(shù)原型中翘地,參數(shù)類型iterator按字母意義理解為容器的迭代器類型,即container:: iterator癌幕。另一方面衙耕,參數(shù)類型 InputIterator表示任何類型的輸入迭代器都是可接受的。
區(qū)間創(chuàng)建:所有的標準容器都提供了如下形式的構(gòu)造函數(shù):
當傳給這種構(gòu)造函數(shù)的迭代器是istream_iterator或者istreambuf_iterator時(見第29條)勺远,你可能會遇到C++最煩人的分析機制橙喘,它使編譯器把這條語句解釋為函數(shù)聲明,而不是定義新的容器對象胶逢。第6條將向你解釋這一分析的細節(jié)厅瞎,包括你如何避免這一問題。
區(qū)間插入:所有的標準序列容器都提供了如下形式的insert:
關(guān)聯(lián)容器利用比較函數(shù)來決定元素該插入何處初坠,它們提供了一個省去position參數(shù)的函數(shù)原型:
在尋找區(qū)間形式的insert來代替單元素版本時和簸,不要忘了一些單元素的變體使用了不同的函數(shù)名稱,從而把自己給掩蓋了碟刺。比如锁保,push_front和push_back都向同其中插入單一元素,盡管它們不叫insert半沽。當你看到使用push_front或push_back的循環(huán)調(diào)用爽柒,或者front_inserter或back_inserter被作為參數(shù)傳遞給copy函數(shù)時,你會發(fā)現(xiàn)在這里區(qū)間形式的insert可能是更好的選擇者填。
區(qū)間刪除浩村。所有的標準容器都提供了區(qū)間形式的刪除(erase)操作,但對于序列和關(guān)聯(lián)容器占哟,其返回值有所不同心墅。
為何會有這樣的區(qū)別呢酿矢,據(jù)說使用關(guān)聯(lián)容器版本的erase返回一個迭代器(指向被刪除元素之后的元素)將導(dǎo)致不可接受的性能負擔。包括我在內(nèi)的很多人都對這種說法表示懷疑怎燥,可是C++標準畢竟是C++標準瘫筐。
本條款中關(guān)于insert 的效率分析對erase也類似。對vector和string的論斷中刺覆,有一條對erase不適用严肪,那就是內(nèi)存的反復(fù)分配。這是因為vector和string的內(nèi)存會自動增長以容納新元素谦屑,但當元素數(shù)目減少時內(nèi)存卻不會自動減少驳糯。(第17條將指出怎樣減少vector或string所占用的多余內(nèi)存)
區(qū)間賦值。正如我在本條款開頭所指出的氢橙,所有的標準容器都提供了區(qū)間形式的assign:
第六條:當心C++編譯器最煩人的分析機制酝枢。
假設(shè)你有一個存有整數(shù)(int)的文件,你想把這些整數(shù)復(fù)制到一個list中悍手。下面是很合理的一種做法:
這種做法的思路是帘睦,把一對istream_iterator傳入到list的區(qū)間構(gòu)造函數(shù)中(見第5條),從而把文件中的整數(shù)復(fù)制到list中坦康。這段代碼可以通過編譯竣付,但是在運行時,它什么也不會做滞欠。它不會從文件中讀取任何數(shù)據(jù)古胆,它不會創(chuàng)建list。這是因為第二條語句并沒有聲明一個list筛璧,也沒有調(diào)用構(gòu)造函數(shù)逸绎。
【這里是當做聲明了一個函數(shù),參數(shù)為 istream_iterator<int>】
下面從最基本的說起夭谤,下面這行代碼聲明了一個帶double參數(shù)并返回int的函數(shù):
int f(double d);
下面這行也一樣棺牧,參數(shù)d兩邊的括號是多余的,會被忽略:
int f(double (d)); //同上朗儒;d兩邊的括號被忽略
下面這行聲明了同樣的函數(shù)颊乘,只是它省略了參數(shù)名稱:
int f(double);
這三種形式的聲明你應(yīng)當很熟悉,盡管以前你可能不知道可以給參數(shù)名加上圓括號采蚀。
現(xiàn)在再看三個函數(shù)聲明疲牵。第一個聲明了一個函數(shù)g,它的參數(shù)是一個指向不帶任何參數(shù)的函數(shù)的指針榆鼠,該函數(shù)返回double值:
有另外一種方式可表明同樣的意思。唯一的區(qū)別是亥鸠,pf用非指針的形式來聲明(這種形式在C和C++中都有效):
跟通常一樣妆够,參數(shù)名稱可以省略识啦,因此下面是g的第三種聲明,其中參數(shù)名pf被省略了:
請注意圍繞參數(shù)名的括號(int f(double (d)); //同上神妹;d兩邊的括號被忽略)
與獨立的括號的區(qū)別颓哮。圍繞參數(shù)名的括號被忽略,而獨立的括號則表明參數(shù)列表的存在鸵荠;它們說明存在一個函數(shù)指針參數(shù)冕茅。
在熟悉了對f和g的聲明后,我們開始研究本條款開始時提出的問題蛹找。它是這樣的:
請你注意了姨伤。這聲明了一個函數(shù)data,其返回值是list<int>庸疾。這個data函數(shù)有兩個參數(shù):
- 第一個參數(shù)的名稱是dataFile乍楚。它的類型是istream_iterator<int>。dataFile兩邊的括號是多余的届慈,會被忽略徒溪。
- 第二個參數(shù)沒有名稱。它的類型是指向不帶參數(shù)的函數(shù)的指針金顿,該函數(shù)返回一個istream_iterator<int>臊泌。
這一切都與C++中的一條普通規(guī)律相符,即盡可能地解釋為函數(shù)聲明揍拆。曾經(jīng)多少次見到過下面這種錯誤渠概?
它沒有聲明名為w 的Widget,而是聲明了一個名為w的函數(shù)礁凡,該函數(shù)不帶任何參數(shù)高氮,并返回一個Widget。學(xué)會識別這一類言不達意是稱為C++程序員的必經(jīng)之路顷牌。
所有這些都很有意思(通過它自己的歪曲的方式)剪芍,但這并不能幫助我們做自己想做的事情。我們想用文件的內(nèi)容初始化list<int> 對象】呃叮現(xiàn)在我們已經(jīng)知道必須繞過某一種分析機制罪裹,剩下的事情就簡單了。把形式參數(shù)的聲明用括號括起來是非法的运挫,但給函數(shù)參數(shù)加上括號卻是合法的状共,所以通過增加一對括號,我們強迫編譯器按我們的方式來工作:
這是聲明data的正確方式谁帕,在使用istream_iterator和區(qū)間構(gòu)造函數(shù)時(同樣峡继,見第5條),注意到這一點是有益的。
不幸的是匈挖,并不是所有的編譯器都知道這一點碾牌。幾乎有一半測試的編譯器中康愤,拒絕接受data的上述聲明方式,除非它被錯誤地用不帶括號的形式來聲明舶吗。更好地方式是在對data的聲明中避免使用匿名的istream_iterator對象(盡管使用匿名對象是一種趨勢)征冷,而是給這些迭代器一個名稱。下面的代碼應(yīng)該總是可以工作的:
使用命名的迭代器對象與通常的STL程序風(fēng)格相違背誓琼,但你或許覺得為了使代碼對所有編譯器都沒有二義性检激,并且使維護代碼的人理解起來更容易,這一代價是值得的腹侣。
Copy 函數(shù)
所謂變易算法就是一組能夠修改容器元素數(shù)據(jù)的模板函數(shù)叔收,可進行序列數(shù)據(jù)的復(fù)制,變換等筐带。其中copy就是其中一個元素復(fù)制算法copy今穿。該算法主要用于容器之間元素的拷貝,即將迭代器區(qū)間[first, last] 的元素復(fù)制到由復(fù)制目標result給定的區(qū)間[result, result+(last-first)]中伦籍。函數(shù)原型為:
template<class InputIterator, class OutputIterator>
OutputIterator copy(
InputIterator _First,
InputIterator _Last,
OutputIterator _DestBeg
);
參數(shù):
_First, _Last 指出被復(fù)制的元素的區(qū)間范圍[_First, _Last].
_DestBeg 指出復(fù)制到的目標區(qū)間起始位置
返回值:
返回一個迭代器蓝晒,指出已被復(fù)制元素區(qū)間的最后一個位置
程序示例:
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
int main ()
{
int myints[] = {10, 20, 30, 40, 50, 60, 70};
vector<int> myvector;
vector<int>::iterator it;
myvector.resize(7); // 為容器myvector分配空間
//copy用法一:
//將數(shù)組myints中的七個元素復(fù)制到myvector容器中
copy ( myints, myints+7, myvector.begin() );
cout << "myvector contains: ";
for ( it = myvector.begin(); it != myvector.end(); ++it )
{
cout << " " << *it;
}
cout << endl;
//copy用法二:
//將數(shù)組myints中的元素向左移動一位
copy(myints + 1, myints + 7, myints);
cout << "myints contains: ";
for ( size_t i = 0; i < 7; ++i )
{
cout << " " << myints[i];
}
cout << endl;
return 0;
}
從上例中我們看出copy算法可以很簡單地將一個容器里面的元素復(fù)制至另一個目標容器中,上例中代碼特別要注意一點就是myvector.resize(7);這行代碼帖鸦,在這里一定要先為vector分配空間芝薇,否則程序會崩,這是初學(xué)者經(jīng)常犯的一個錯誤作儿。
其實copy函數(shù)最大的威力是結(jié)合標準輸入輸出迭代器的時候洛二,我們通過下面這個示例就可以看出它的威力了。
#include <iostream>
#include <algorithm>
#include <vector>
#include <iterator>
#include <string>
using namespace std;
int main ()
{
typedef vector<int> IntVector;
typedef istream_iterator<int> IstreamItr;
typedef ostream_iterator<int> OstreamItr;
typedef back_insert_iterator< IntVector > BackInsItr;
IntVector myvector;
// 從標準輸入設(shè)備讀入整數(shù)
// 直到輸入的是非整型數(shù)據(jù)為止 請輸入整數(shù)序列攻锰,按任意非數(shù)字鍵并回車結(jié)束輸入
cout << "Please input element:" << endl;
copy(IstreamItr(cin), IstreamItr(), BackInsItr(myvector));
//輸出容器里的所有元素晾嘶,元素之間用空格隔開
cout << "Output : " << endl;
copy(myvector.begin(), myvector.end(), OstreamItr(cout, " "));
cout << endl;
return 0;
}
對于 vector 的clear() 和erase() 的區(qū)別
vector :: erase(): 從指定容器刪除指定位置的元素或某段范圍內(nèi)的元素
vector :: erase() 方法有兩種重載形式
如下:
iterator erase(iterator _Where);
iterator erase(iterator _First, iterator _Last); // 但是這里不會刪除掉_Last 所對應(yīng)的數(shù)據(jù)
如果是刪除指定位置的元素時,返回值是一個迭代器娶吞,指向刪除元素下一個元素垒迂;如果是刪除某范圍內(nèi)的元素時:返回值也表示一個迭代器,指向最后一個刪除元素的下一個元素妒蛇;如何一個容器里有多個相同的元素机断,要怎么刪除呢?
for(Iter = v1.begin(); Iter != v1.end(); Iter++)
{
if(*Iter == 10)
{
Iter = v1.erase(Iter);//Iter為刪除元素的下一個元素的迭代器
//即第一次這段語句后Iter 會是20绣夺,大家可以通過debug調(diào)試出來查看下數(shù)值
}
if(Iter == v1.end()) //要控制迭代器不能超過整個容器
{
break;
}
}
但是這里就算調(diào)用了erase 其實好像也只是將后面的數(shù)移位覆蓋了前面的數(shù)吏奸,但是整個size還是沒有改變。
而對于vector陶耍,clear() 并沒有真正釋放內(nèi)存(這是為優(yōu)化效率所做的事)奋蔚,clear實際所做的是為vector中所保存的所有對象調(diào)用析構(gòu)函數(shù)(如果有的話),然后初始化size這些東西烈钞,讓你覺得把所有的對象清除了旺拉。但是總的來講vector并沒有出現(xiàn)內(nèi)存泄漏产上。在《effective STL》中的“條款17" 已經(jīng)指出了:
當vector棵磷、string大量插入數(shù)據(jù)后蛾狗,即使刪除了大量數(shù)據(jù)(或者全部都刪除,即clear)并沒有改變?nèi)萜鞯娜萘浚╟apacity)仪媒,所以仍然會占用著內(nèi)存沉桌。為了避免這種情況,我們應(yīng)該想辦法改變?nèi)萜鞯娜萘渴怪M可能小的符合當前數(shù)據(jù)所需(shrink to fit)
《Effective STL》給出的解決方案是:
vector<type> v;
//.... 這里添加許多元素給v
//.... 這里刪除v中的許多元素
vector<type>(v).swap(v);
//此時v的容量已經(jīng)盡可能的符合其當前包含的元素數(shù)量
//對于string則可能像下面這樣
string(s).swap(s);
即先創(chuàng)建一個臨時拷貝與原先的vector一致算吩,值得注意的是留凭,此時的拷貝 其容量是盡可能小的符合所需數(shù)據(jù)的。緊接著該拷貝與原先的vector v 進行交換偎巢。好了此時蔼夜,執(zhí)行交換后,臨時變量會被銷毀压昼,內(nèi)存得到釋放求冷。此時的v即為原先的臨時拷貝,而交換后的臨時拷貝則為容量非常大的vector(不過已經(jīng)被銷毀)
為了證明這一點窍霞,我寫了一個程序匠题,如下:
#include <iostream>
#include <vector>
using namespace std;
vector <string> v;
char ch;
int main ()
{
for(int i=0; i<1000000; i++)
v.push_back("abcdefghijklmn");
cin >> ch;
// 此時檢查內(nèi)存情況 占用54M
v.clear();
cin >> ch;
// 此時再次檢查, 仍然占用54M
cout << "Vector 的 容量為" << v.capacity() << endl;
// 此時容量為 1048576
vector<string>(v).swap(v);
cout << "Vector 的 容量為" << v.capacity() << endl;
// 此時容量為0
cin >> ch;
// 檢查內(nèi)存但金,釋放了 10M+ 即為數(shù)據(jù)內(nèi)存
return 0;
}
在創(chuàng)建一個vector后韭山,vector的實際容量一般會比所給數(shù)據(jù)要大,這樣做應(yīng)該是避免過多的重新分配內(nèi)存的吧冷溃。
當然钱磅,上面這種方法雖然釋放了內(nèi)存,但是同時也增加了拷貝數(shù)據(jù)的時間消耗似枕。不過一般需要重新調(diào)整容量的情況都是 vector 本身元素較少的情況盖淡,所以,時間消耗可以忽略不計菠净。
再回過來看vector內(nèi)存釋放機制
vector 中的內(nèi)建有內(nèi)存管理禁舷,當vector離開它的生存期的時候,它的析構(gòu)函數(shù)會把vector 中的元素銷毀毅往,并釋放它們所占用的空間牵咙,所以用 vector 一般不用顯式釋放 ——不過,如果你 vector 中存放的是指針攀唯,那么當 vector 銷毀時洁桌,那些指針指向的對象不會被銷毀,那些內(nèi)存不會被釋放侯嘀。 其實這也是一種比較常見的內(nèi)存泄漏另凌。
vector的工作原理是系統(tǒng)預(yù)先分配一塊capacity大小的空間谱轨,當插入的數(shù)據(jù)超過這個空間的時候,這塊空間會以某種方式擴展吠谢,但是你刪除數(shù)據(jù)的時候土童,它卻不會縮小。vector為了防止大量分配連續(xù)內(nèi)存的開銷工坊,保持一塊默認的尺寸献汗,clear只是清數(shù)據(jù)了,為清內(nèi)存王污,因為vector的capacity容量未變化罢吃,系統(tǒng)維護一個的默認值。
有什么方法可以釋放掉vector中占用的全部內(nèi)存呢昭齐?
標準的解決方法如下
template<class T>
void ClearVector(vector<T>& vt)
{
vector<T> vtTemp;
vtTemp.swap(vt);
}
----------------------------------------
舉例1:
vector<int> vec(100);
cout<< vec.capacity()<<endl;
vector<int>().swap(vec);
cout<< vec.capacity()<<endl;
-----------------------------------------
利用vector釋放指針:
#include<vector>
using namespace std;
vector<void*> v;
每次new之后調(diào)用v.push_back()該指針尿招,
在程序退出時或其它你認為適當?shù)臅r候,執(zhí)行如下代碼:
//這里就是通過一輪循環(huán)阱驾,然后將每一個節(jié)點的指針給銷毀了就谜,再調(diào)用clear()函數(shù)。
for (vector<void *>::iterator it = g_vPtrManager.begin(); it != g_vPtrManager.end(); it ++)
{
if (NULL != *it)
{
delete *it;
*it = NULL;
}
}
v.clear();
// remark: 若你的程序是多線程的啊易,注意以下線程安全問題吁伺,必要時加個臨界區(qū)控制一下。
總結(jié):
vector與deque不同租谈,其內(nèi)存占用空間只會增長篮奄,不會減小。比如你首先分配了10,000個字節(jié)割去,然后erase掉后面9,999個窟却,則雖然有效元素只有一個,但是內(nèi)存占用仍為10,000個呻逆。所有空間在vector析構(gòu)時回收夸赫。empty()是用來檢測容器是否為空的,clear()可以清空所有元素咖城。但是即使clear()茬腿,所占用的內(nèi)存空間依然如故。如果你需要空間動態(tài)縮小宜雀,可以考慮使用deque切平。如果非要用vector,這里有一個方法:
vector<int>().swap(nums); //nums.swap(vector<int>());
vector<int>().swap(nums)辐董;
{
std::vector<int> tmp = nums;
nums.swap(tmp);
}
// 加一對大括號是可以讓tmp退出{}的時候自動析構(gòu)悴品。
// swap技法就是通過交換函數(shù)swap(),使得vector離開其自身的作用域,從而強制釋放vector所占的內(nèi)存空間苔严。
assign的設(shè)計
assign的函數(shù)的好處定枷,應(yīng)該很好理解就是在不能使用賦值符“=”的情況下,可以將一個容器中的部分元素通過迭代器傳遞賦值到另一個容器中届氢,但是在assign的使用過///程中欠窒,有一點需要特別注意,就是調(diào)用assign()函數(shù)的容器必須有足夠的空間來容納復(fù)制過來的元素悼沈。
函數(shù)原型:
void assign(const_iterator first, const_iterator last);
void assign(size_type n, const T&x = T());
功能:
將區(qū)間(first,last)的元素賦值到當前的vector容器中贱迟,或者賦n個值為x的元素到vector容器中,這個容器會清除掉vector容器中以前的內(nèi)容絮供。
#include <vector>
#include <iostream>
int main( )
{
using namespace std;
vector<int> v1, v2, v3;
vector<int>::iterator iter;
v1.push_back(10); v1.push_back(20); v1.push_back(30); v1.push_back(40); v1.push_back(50); v2.push_back(1); v2.push_back(2);
v2.assign(v1.begin(), v1.end());
cout << "v2 = ";
for (iter = v2.begin(); iter != v2.end(); iter++)
cout << *iter << " ";
cout << endl;
v3.assign(7, 3) ;
cout << "v3 = ";
for (iter = v3.begin(); iter != v3.end(); iter++)
cout << *iter << " ";
cout << endl;
return 0;
}
說到安全問題,我還想問茶敏,vector是不是線程安全的壤靶?http://book.51cto.com/art/201305/394132.htm
標準 C++的世界相當狹小和古舊。在這個純凈的世界中惊搏,所有的可執(zhí)行程序都是靜態(tài)鏈接的贮乳。不存在內(nèi)存映像文件或共享內(nèi)存。沒有窗口系統(tǒng)恬惯,沒有網(wǎng)絡(luò)向拆,沒有數(shù)據(jù)庫,也沒有其他進程酪耳∨遥考慮到這一點,當你得知 C++標準對線程只字未提時碗暗,你不應(yīng)該感到驚訝颈将。于是,你對STL的線程安全性的第一個期望應(yīng)該是言疗,它會因不同實現(xiàn)而異晴圾。
當然,多線程程序是很普遍的噪奄,所以多數(shù)STL提供商會盡量使自己的實現(xiàn)可在多線程環(huán)境下工作死姚。然而,即使他們在這一方面做得不錯勤篮,多數(shù)負擔仍然在你的肩膀上都毒。理解為什么會這樣是很重要的。STL提供商對解決多線程問題只能做很有限的工作叙谨。
在STL容器中支持多線程的標準(這是多數(shù)提供商們所希望的)已經(jīng)為SGI所確定温鸽,并在它們的STL Web站點上發(fā)布。概括來說,它指出涤垫,對一個 STL實現(xiàn)你最多只能期望:
多個線程讀是安全的姑尺。多個線程可以同時讀同一個容器的內(nèi)容,并且保證是正確的蝠猬。自然地切蟋,在讀的過程中,不能對容器有任何寫入操作榆芦。
多個線程對不同的容器做寫入操作是安全的柄粹。多個線程可以同時對不同的容器做寫入操作。
就這些匆绣。我必須指明驻右,這是你所能期待的,而不是你所能依賴的崎淳。有些實現(xiàn)提供了這些保證堪夭,有些則沒有。
寫多線程的代碼并不容易拣凹,許多程序員希望 STL的實現(xiàn)能提供完全的線程安全性森爽。如果是這樣的話,程序員可以不必再考慮自己做同步控制嚣镜。這無疑是很方便的爬迟,但要做到這一點將會很困難【漳洌考慮當一個庫試圖實現(xiàn)完全的容器線程安全性時可能采取的方式:
對容器成員函數(shù)的每次調(diào)用付呕,都鎖住容器直到調(diào)用結(jié)束。
在容器所返回的每個迭代器的生存期結(jié)束前捧请,都鎖住容器(比如通過 begin 或 end 調(diào)用)凡涩。
對于作用于容器的每個算法,都鎖住該容器疹蛉,直到算法結(jié)束活箕。
(實際上這樣做沒有意義。因為可款,算法無法知道它們所操作的容器育韩。盡管如此,在這里我們?nèi)砸懻撨@一選擇闺鲸。因為即使這是可能的筋讨,我們也會發(fā)現(xiàn)這種做法仍不能實現(xiàn)線程安全性,這對于我們的討論是有益的)摸恍。
現(xiàn)在考慮下面的代碼悉罕。它在一個vector<int>中查找值為5的第一個元素赤屋,如果找到了,就把該元素置為0.
vector<int> v;
...
vector<int>:: iterator first5(find(v.begin(),v.end(),5)); // 第 1 行
if (first5 != v.end()){ //第 2行
*first5 =0; //第 3行
}
在一個多線程環(huán)境中壁袄,可能在第一行剛剛完成后类早,另一個不同的線程會更改 v中的數(shù)據(jù)。如果這種更改真的發(fā)生了嗜逻,那么第2行對 first5 和 v.end是否相等的檢查將會變得沒有
意義涩僻,因為v的值將會與在第一行結(jié)束時不同。事實上栈顷,這一檢查會產(chǎn)生不確定的行為逆日,因為另外一個線程可能會夾在第1行和第2行中間,使 first5 變得無效萄凤,這第二個線程或許會執(zhí)行一個插入操作使得vector 重新分配它的內(nèi)存室抽。類似地,第 3行對*first5的賦值也是不安全的蛙卤,因為另一個線程可能在第 2行和第 3行之間執(zhí)行狠半,該線程可能會使 first5無效,例如可能會刪除它所指向的元素(或者至少是曾經(jīng)指向過的元素)颤难。
要做到線程安全, v必須從第 1行到第 3行始終保持在鎖住狀態(tài)已维,很難想象一個 STL實現(xiàn)能自動推斷出這一點行嗤。考慮到同步原語(例如信號量垛耳、互斥體等)通常會有較高的開銷栅屏,這就更難想象,一個 STL實現(xiàn)如何既能夠做到這一點堂鲜,同時又不會對那些在第 1行和第 3行之間本來就不會有另外線程來訪問 v的程序(假設(shè)程序就是這樣設(shè)計的)造成顯著的效率影響栈雳。
這樣的考慮說明了為什么你不能指望任何 STL 實現(xiàn)來解決你的線程難題。相反缔莲,在這種情況下哥纫,必須手工做同步控制。在這個例子中痴奏,或許可以這樣做:
vector<int> v;
...
getMutexFor(v);
vector<int>::iterator first5(find(v.begin(), v.end(), 5));
if (first5 != v.end()){
*first5 = 0;
}
releaseMutexFor(v);
更為面向?qū)ο蟮姆桨甘莿?chuàng)建一個Lock類蛀骇,它在構(gòu)造函數(shù)中獲得一個互斥體,在析構(gòu)函數(shù)中釋放它读拆,從而盡可能地減少 getMutexFor 調(diào)用沒有相對應(yīng)的 releaseMutexFox調(diào)用的可能性擅憔。這樣的類(實際上是一個類模板)看起來大概像下面這樣:
template<typename Container> //一個為容器獲取和釋放互斥體的模板
class Lock{
public:
Lock( const Container& container): c(container){
getMutexFox(c); // 在構(gòu)造函數(shù)中獲取互斥體
}
~Lock(){
releaseMutexFor(c); // 在析構(gòu)函數(shù)中釋放它
}
private:
const Container& c;
};
使用類(如 Lock)來管理資源的生存期的思想通常被稱為 "獲得資源時即初始化",你可以在任何一本全面介紹C++的書中找到這種思想檐晕。
vector<int> v;
{ Lock<vector<int> > lock(v); //獲取互斥體
vector<int>::iterator first5(find(v.begin(), v.end(), 5)); if (first5 != v.end()) { *first5 = 0; }
} //代碼塊結(jié)束暑诸,自動釋放互斥體
因為 Lock對象在其析構(gòu)函數(shù)中釋放容器的互斥體,所以很重要的一點是,當互斥體應(yīng)該被釋放時个榕,Lock就要被析構(gòu)篡石。為了做到這一點,我們創(chuàng)建了一個新的代碼塊(block)笛洛,在其中定義了Lock夏志,當不再需要互斥體時就結(jié)束該代碼塊】寥茫看起來好像是我們把“調(diào)用releaseMutexFor”這一任務(wù)換成了“結(jié)束代碼塊”沟蔑,事實上這種說法是不確切的。如果我們忘了為Lock創(chuàng)建新的代碼塊狱杰,則互斥體仍然會被釋放瘦材,只不過會晚一些——當控制到達包含 Lock的代碼塊末尾時。而如果我們忘記了調(diào)用releaseMutexFor仿畸,那么我們永遠也不會釋放互斥體食棕。
而且,基于 Lock的方案在有異常發(fā)生時也是強壯的错沽。C++保證簿晓,如果有異常被拋出,局部對象會被析構(gòu)千埃,所以憔儿,即便在我們使用 Lock對象的過程中有異常拋出, Lock仍會釋放它所擁有的互斥體放可。
如果我們依賴于手工調(diào)用 getMutexFor和releaseMutexFor谒臼,那么,當在調(diào)用 getMutexFor之后而在調(diào)用 releaseMutexFor之前有異常被拋出時耀里,我們將永遠也無法釋放互斥體蜈缤。
異常和資源管理雖然很重要,但它們不是本條款的主題冯挎。本條款是講述STL中的線程安全性的底哥。當涉及STL容器和線程安全性時,你可以指望一個STL庫允許多個線程同時讀一個容器织堂,以及多個線程對不同的容器做寫入操作叠艳。你不能指望 STL庫會把你從手工同步控制中解脫出來,而且你不能依賴于任何線程支持易阳。
已經(jīng)證實存在一個漏洞附较。如果該異常根本沒有被捕獲到,那么程序?qū)⒔K止潦俺。在這種情況下拒课,局部對象(如 lock)可能還沒有讓它們的析構(gòu)函數(shù)被調(diào)用到徐勃。有些編譯器會調(diào)用它們,有些編譯器不會早像。這兩種情況都是有效的僻肖。