標(biāo)簽(空格分隔): STL
運(yùn)用STL,可以充分利用該庫(kù)的設(shè)計(jì),讓我為簡(jiǎn)單而直接的問(wèn)題設(shè)計(jì)出簡(jiǎn)單而直接的解決方案,也能幫助你為更復(fù)雜的問(wèn)題設(shè)計(jì)出優(yōu)雅的解決方案。我將指出一些常見(jiàn)的STL錯(cuò)誤用法,并指出該如何避免這樣的錯(cuò)誤。這能幫助你避免產(chǎn)生資源泄漏、寫出不能移植的代碼,以及出現(xiàn)不確定的行為。我還將討論如何對(duì)你的代碼進(jìn)行優(yōu)化,從而可以讓STL執(zhí)行得更快、更流暢,就像你所期待的那樣。
STL并沒(méi)有一個(gè)官方的正式定義,不同的人使用這個(gè)詞的時(shí)候,它有不同的含義。在本書中,STL表示C++標(biāo)準(zhǔn)庫(kù)中與迭代器一起工作的那部分,其中包括標(biāo)準(zhǔn)容器(包含string)、iostream庫(kù)的一部分,函數(shù)對(duì)象和各種算法。它排除了標(biāo)準(zhǔn)容器配接器(stack、queue和priority_queue)以及容器bitset和valarray,因?yàn)樗鼈內(nèi)鄙賹?duì)迭代器的支持。數(shù)組也不包括在其中。不錯(cuò),數(shù)組支持指針形式的迭代器,但數(shù)組是C++語(yǔ)言的一部分。而不是STL庫(kù)的一部分。
第一章 容器
沒(méi)錯(cuò),STL中有迭代器(iterator)、算法(algorithm)和 函數(shù)對(duì)象(function object),但是 對(duì)于大多數(shù)C++程序員來(lái)說(shuō),最值得注意的還是容器。容器比數(shù)組功能更強(qiáng)大、更靈活。它們可以動(dòng)態(tài)增長(zhǎng)(和縮減),可以自己管理內(nèi)存,可以記住自己包含了多少對(duì)象。它們限定了自己所支持的操作的復(fù)雜性。
問(wèn)題1:如何進(jìn)行動(dòng)態(tài)增長(zhǎng)(和縮減)
A 如何就我所面臨的具體制約條件選擇適當(dāng)?shù)娜萜黝愋停?/p>
B 對(duì)于容器中的對(duì)象,復(fù)制操作的重要性;
C 當(dāng)指針或auto_ptr被存放在容器中時(shí)會(huì)有什么樣的困難;
D 刪除操作的細(xì)節(jié);
E 用定制的分配只能做什么以及不能做什么;
F 使程序獲得最高效率的竅門;
G 在多線程環(huán)境中使用容器時(shí)的一些考慮;
標(biāo)準(zhǔn)STL序列容器: vector、string、deque 和 list。
標(biāo)準(zhǔn)STL關(guān)聯(lián)容器: set、multiset、map 和 multimap。
非標(biāo)準(zhǔn)序列容器: slist 和 rope。slist是一個(gè)單向鏈表,rope本質(zhì)上是一 "重型" string 。
非標(biāo)準(zhǔn)關(guān)聯(lián)容器: hash_set、hash_multiset、hash_map 和
hash_multimap。在第25條中,我們分析了這些基于散列表的、標(biāo)準(zhǔn)關(guān)聯(lián)容器的變體(它們通常是廣泛可用的)。
vector<char> 作為 string 的替代。在一些情況下,這種替代是有意義的。
vector作為標(biāo)準(zhǔn)關(guān)聯(lián)容器的替代。正如第23條所闡明的,有時(shí)vector在運(yùn)行時(shí)間和空間上都要優(yōu)于標(biāo)準(zhǔn)關(guān)聯(lián)容器。
幾種標(biāo)準(zhǔn)的非STL容器,包括數(shù)組、bitset、valarray、stack、queue和priority_queue。因?yàn)樗鼈儾皇荢TL容器,后面解釋了為什么bitset比vector<bool>要好。值得記住的是,數(shù)組也可以被用于STL算法,因?yàn)橹羔樋梢员挥米鲾?shù)組的迭代器。
C++標(biāo)準(zhǔn)就 "如何在vector、deque 和 list 中做出選擇" 提供了如下建議:
vector、list和deque為程序員提供了不同的復(fù)雜性,vector是默認(rèn)應(yīng)使用的序列類型;當(dāng)需要頻繁地在序列中間做插入和刪除操作時(shí),應(yīng)使用list;當(dāng)大多數(shù)插入和刪除操作發(fā)生在序列的頭部和尾部時(shí),deque是應(yīng)考慮的數(shù)據(jù)結(jié)構(gòu)。
對(duì)STL容器的一種分類方法,該方法沒(méi)有得到應(yīng)有的重視。這就是對(duì)連續(xù)內(nèi)存容器和基于節(jié)點(diǎn)的容器的區(qū)分。
連續(xù)內(nèi)存容器(或稱為基于數(shù)組的容器,array-based container)把它的元素存放在一塊或多塊(動(dòng)態(tài)分配的)內(nèi)存中,每塊內(nèi)存中存有多個(gè)元素。當(dāng)有新元素插入或已有的元素被刪除時(shí),同一內(nèi)存塊中的其他元素要向前或向后移動(dòng),以便為新元素讓出空間,或者填充被刪除元素所留下的空隙。這種移動(dòng)影響到效率和異常安全性。標(biāo)準(zhǔn)的連續(xù)內(nèi)存容器有vector、string和deque。非標(biāo)準(zhǔn)的rope也是一個(gè)連續(xù)內(nèi)存容器。
基于節(jié)點(diǎn)的容器在每一個(gè)(動(dòng)態(tài)分配的)內(nèi)存塊中只存放一個(gè)元素。容器中元素的插入或刪除只影響到指向節(jié)點(diǎn)的指針,而不影響節(jié)點(diǎn)本身的內(nèi)容,所以,當(dāng)有插入或刪除操作時(shí),元素的值不需要移動(dòng)。表示鏈表的容器,如List 和 slist,是基于節(jié)點(diǎn)的;所以標(biāo)準(zhǔn)的關(guān)聯(lián)容器也是如此(通常的實(shí)現(xiàn)方式是平衡樹(shù))。非標(biāo)準(zhǔn)的散列容器使用不同的基于節(jié)點(diǎn)的實(shí)現(xiàn))
你是否需要在容器的任意位置插入新元素? 如果需要,就選擇序列容器;關(guān)聯(lián)容器時(shí)不行的。
你是否關(guān)心容器中的元素是排序的? 如果不關(guān)心,則散列容器是 一個(gè)可行的選擇方案;否則,你要避免散列容器。
對(duì)插入和刪除操作,你需要事務(wù)語(yǔ)義嗎?也就是說(shuō),在插入和刪除操作失敗時(shí),你需要回滾的能力嗎?如果需要,你就要使用基于節(jié)點(diǎn)的容器。如果對(duì)多個(gè)元素的插入操作(即針對(duì)一個(gè)區(qū)間的形式)需要事務(wù)語(yǔ)義,則你需要選擇list,因?yàn)樵跇?biāo)準(zhǔn)容器中,只有l(wèi)ist對(duì)多個(gè)元素的插入操作提供了事務(wù)語(yǔ)義。對(duì)那些希望編寫異常安全代碼的程序員,事務(wù)語(yǔ)義顯得尤為重要。(使用連續(xù)內(nèi)存容器也可以獲得事務(wù)語(yǔ)義,但是要付出性能上的代價(jià),而且代價(jià)也顯得不那么直接了當(dāng)。
確保容器中的對(duì)象副本正確而高效
容器中保存了對(duì)象,但并不是你提供給容器的那些對(duì)象。而當(dāng)從容器中取出一個(gè)對(duì)象時(shí),你所取出的也并不是容器中所保存的那份。當(dāng)向容器中加入對(duì)象時(shí)(通過(guò)insert或Push_back之類的操作),存入容器的是你所指定的對(duì)象的副本。當(dāng)(通過(guò)如front或back之類的操作)從容器中取出一個(gè)對(duì)象時(shí),你所得到的是容器中所保存的對(duì)象的副本。進(jìn)去的是副本,出來(lái)的也是副本。這就是STL的工作方式。
一旦一個(gè)對(duì)象被保存到容器中,它經(jīng)常會(huì)進(jìn)一步被復(fù)制。當(dāng)對(duì)vector、string或deque進(jìn)行元素的插入或刪除操作時(shí),現(xiàn)有元素的位置通常會(huì)被移動(dòng)(復(fù)制)。如果你使用下列任何操作--排序算法,next_permutation 或 previous_permutation,remove,unique或類似的操作,rotate或reverse,等等. 那么對(duì)象將會(huì)被移動(dòng)。沒(méi)錯(cuò),復(fù)制對(duì)象是STL的工作方式。
復(fù)制動(dòng)作是通過(guò)一個(gè)對(duì)象的復(fù)制成員函數(shù)就可以很方便地復(fù)制該對(duì)象,特別是對(duì)象的賦值構(gòu)造函數(shù)(copy constructor)和復(fù)制賦值操作符(copy assignment operator)。內(nèi)置類型(built-in type)(如整型、指針類型等)的實(shí)現(xiàn)總是簡(jiǎn)單地按位賦值。如果你向容器中填充對(duì)象,而對(duì)象的復(fù)制操作又很費(fèi)時(shí),那么向容器中填充對(duì)象這一簡(jiǎn)單的操作將會(huì)成為程序的性能"瓶頸"。放入容器中的對(duì)象越多,而且,如果這些對(duì)象的“副本”有特殊的含義,那么把它們放入容器時(shí)將不可避免地會(huì)產(chǎn)生錯(cuò)誤。
當(dāng)然,在存在繼承關(guān)系的情況下,復(fù)制動(dòng)作會(huì)導(dǎo)致剝離(slicing).也就是說(shuō),如果你創(chuàng)建了一個(gè)存放基類對(duì)象的容器,卻向其中插入派生類的對(duì)象,那么在派生類對(duì)象(通過(guò)基類的復(fù)制構(gòu)造函數(shù))被復(fù)制進(jìn)容器時(shí),它所特有的部分(即派生類中的信息)將會(huì)丟失:
vector<Widget> vw;
class SpecialWidget: public Widget{...}; //SpecialWidget 繼承于上面的Widget
SpecialWidget sw;
vw.push_back(sw); // sw作為基類對(duì)象唄復(fù)制進(jìn)vw中,它的派生類特有部分在復(fù)制時(shí)被丟掉了
“剝離” 問(wèn)題意味著向基類對(duì)象的容器中插入派生類對(duì)象幾乎總是錯(cuò)誤的。如果你希望插入后的對(duì)象仍然表現(xiàn)得像派生類對(duì)象一樣,例如調(diào)用派生類的虛函數(shù)等,那么這種期望是錯(cuò)誤的。(關(guān)于剝離問(wèn)題的更多知識(shí),請(qǐng)參見(jiàn)Effective C++的第22條。關(guān)于STL中剝離問(wèn)題的另一個(gè)例子)
使復(fù)制動(dòng)作高效、正確,并防止剝離問(wèn)題發(fā)生的一個(gè)簡(jiǎn)單辦法是使容器包含指針而不是對(duì)象。也就是說(shuō),使用Widget*的容器,而不是Widget的容器。復(fù)制指針的速度非常快,并且總是會(huì)按你期望的方式進(jìn)行(它復(fù)制構(gòu)成指針的每一位),而且當(dāng)它被復(fù)制時(shí)不會(huì)有任何剝離現(xiàn)象發(fā)生。如果你想避開(kāi)這些使人頭疼的問(wèn)題,同時(shí)又想避免效率、正確性和剝離這些問(wèn)題,你可能會(huì)發(fā)現(xiàn)智能指針(small pointer)是一個(gè)誘人的選擇。
STL做了很多副本,但它總的設(shè)計(jì)思想是為了避免不必要的復(fù)制。事實(shí)上,它總體的設(shè)計(jì)目標(biāo)是為了避免創(chuàng)建不必要的對(duì)象。把它跟C和C++僅有的內(nèi)置容器(即數(shù)組)的行為做比較:
Widget w[maxNumWidgets]; //創(chuàng)建了有maxNumWidgets個(gè)Widget的數(shù)組,//每個(gè)對(duì)象都使用默認(rèn)構(gòu)造函數(shù)來(lái)創(chuàng)建
如果用STL,則我們可以使用vector,當(dāng)需要的時(shí)候它會(huì)增長(zhǎng);
我們也可以創(chuàng)建一個(gè)空的vector,它包含足夠的空間來(lái)容納 maxNumWidgets個(gè)Widget對(duì)象,但并沒(méi)有創(chuàng)建任何一個(gè)Widget對(duì)象:
vector<Widget> vw;
vw.reserver(maxNumWidgets);
調(diào)用empty而不是檢查size() 是否為0.
對(duì)于任一容器c,代碼:if(c.size() == 0)...
本質(zhì)上與if(c.empty())...
是等價(jià)的。既然如此,還是應(yīng)該使用empty(),理由是:empty對(duì)所有的標(biāo)準(zhǔn)容器都是常數(shù)時(shí)間操作,而對(duì)一些list實(shí)現(xiàn),size耗費(fèi)線性時(shí)間。到底是什么使list這么討厭呢?為什么它不也提供常數(shù)時(shí)間的size呢?答案在于list所獨(dú)有的鏈接(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中從第一個(gè)含5的節(jié)點(diǎn)到最后一個(gè)含10的所有節(jié)點(diǎn)移動(dòng)到list1的末尾。
鏈接后的list1中有多少個(gè)元素?很明顯,鏈接后的list1中的元素個(gè)數(shù)是它鏈接前的元素個(gè)數(shù)加上鏈接過(guò)來(lái)的元素個(gè)數(shù)。但有多少個(gè)元素被鏈接過(guò)來(lái)了呢?應(yīng)該與 find(list2.begin(),list2.end(),5),find(list2.rbegin(),list2.rend(),10).base()
所定義的區(qū)間中的元素個(gè)數(shù)一樣多。究竟有多少個(gè),如果不遍歷該區(qū)間是無(wú)法知道的。通常,list或splice——必須做出讓步。其中的一個(gè)可以成為常數(shù)時(shí)間操作,但不可能二者都是。
不同的鏈表實(shí)現(xiàn)通過(guò)不同的方式解決這一沖突,具體方式取決于作者選擇把size還是splice實(shí)現(xiàn)得最為高效。如果你使用的list實(shí)現(xiàn)恰好是把splice的常數(shù)時(shí)間操作放在第一位,那么你使用empty而不是size會(huì)更好些,因?yàn)閑mpty操作總是花費(fèi)常數(shù)時(shí)間。即使現(xiàn)在你使用的list實(shí)現(xiàn)不是這種方式,將來(lái)你也可能會(huì)發(fā)現(xiàn)自己在使用這樣的實(shí)現(xiàn)。比如,你可能把自己的代碼移植到不同的平臺(tái)上,不管發(fā)生了什么,調(diào)用empty而不是檢查size==0是否成立總是沒(méi)錯(cuò)的。所以,如果你想知道容器中是否含有零個(gè)元素,請(qǐng)調(diào)用empty。
區(qū)間成員函數(shù)優(yōu)先于與之對(duì)應(yīng)的單元素成員函數(shù)。
給定v1和v2兩個(gè)矢量(vector),使v1的內(nèi)容和v2的后半部分相同的最簡(jiǎn)單操作是什么?
不必為了當(dāng)v2含有奇數(shù)個(gè)元素時(shí)"一半"的定義而煞費(fèi)苦心。只要做到合理即可。
v1.assign(v2.begin() + v2.size()/2, v2.end());
assign這么一個(gè)使用及其方便,卻為許多程序員所忽略的成員函數(shù)。對(duì)所有的標(biāo)準(zhǔn)序列容器(vector,string,deque 和list),它都存在。當(dāng)你需要完全替換一個(gè)容器的內(nèi)容時(shí),你應(yīng)該想到賦值(assignment)如果你想把一個(gè)容器復(fù)制到相同類型的另一個(gè)容器,那么operator=是可選擇的賦值函數(shù),但正如該例子所揭示的那樣,當(dāng)你想給容器一組全新的值時(shí),你可以使用assign,而operator=則不能滿足你的要求。
這里同時(shí)也揭示了為什么區(qū)間成員函數(shù)(range member function)優(yōu)先于與之對(duì)應(yīng)的單元素成員函數(shù)。區(qū)間成員函數(shù)是指這樣的一類成員函數(shù),它們像STL算法一樣,使用兩個(gè)迭代器參數(shù)來(lái)確定該成員操作所執(zhí)行的區(qū)間。如果不使用區(qū)間成員函數(shù)來(lái)解決問(wèn)題,就得寫一個(gè)顯式的循環(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),其實(shí)比調(diào)用assign多做了很多工作。這樣的循環(huán)在一定程度上影響了效率,后面會(huì)接著討論這個(gè)問(wèn)題。避免循環(huán)的一種方法是遵從第43條的建議,使用一個(gè)算法:
v1.clear();
copy(v2.begin() + v2.size()/2, v2.end(), back_inserter(v1));
同調(diào)用assign相比,所做的工作還是多了些。而且,盡管上面的代碼中沒(méi)有循環(huán),但copy中肯定有。結(jié)果是,影響效率的因素仍然存在。幾乎所有通過(guò)利用插入迭代器的方式(即利用inserter、back_inserter 或 front_inserter)來(lái)限定目標(biāo)區(qū)間的copy調(diào)用,其實(shí)都可以(也應(yīng)該)被替換為堆區(qū)間成員函數(shù)的調(diào)用。比如,在這里,對(duì)copy的調(diào)用可以被替換為利用區(qū)間的Insert版本:
v1.insert(v1.end(), v2.begin() + v2.size()/2, v2.end());
同調(diào)用copy相比,敲鍵盤的工作稍少了些,但它更加直截了當(dāng)?shù)卣f(shuō)明了所發(fā)生的事情:數(shù)據(jù)被插入到V1中。對(duì)copy的調(diào)用也說(shuō)明了這一點(diǎn),但沒(méi)有這么直接,而是把重點(diǎn)放在了不合適的地方。對(duì)這里所發(fā)生的事情,有意義的不是元素被復(fù)制,而是有新的數(shù)據(jù)被插入到了v1中。Insert成員函數(shù)很清晰地表明了這一點(diǎn),使用copy則把這一點(diǎn)掩蓋了。太多的STL程序員濫用了copy,所以我剛才給出的建議值得再重復(fù)一下:通過(guò)利用插入迭代器的方式來(lái)限定目標(biāo)區(qū)間的copy 調(diào)用,幾乎都應(yīng)該被替換為對(duì)區(qū)間成員函數(shù)的調(diào)用。
太多的STL程序員濫用了copy,所以我剛才給出的建議值得再重復(fù)一下:通過(guò)利用插入迭代器的方式來(lái)限定目標(biāo)區(qū)間的copy調(diào)用,幾乎都應(yīng)該被替換為對(duì)區(qū)間成員函數(shù)的調(diào)用。
現(xiàn)在回到assign的例子。我們已經(jīng)給出了使用區(qū)間成員函數(shù)而不是其相應(yīng)的單元素成員函數(shù)的原因:
- 通過(guò)使用區(qū)間成員函數(shù),通常可以少寫一些代碼。
- 使用區(qū)間成員函數(shù)通常會(huì)得到意圖清晰和更加直接的代碼。
對(duì)于標(biāo)準(zhǔn)的序列容器,我們又一個(gè)標(biāo)準(zhǔn):效率。當(dāng)處理標(biāo)準(zhǔn)序列容器時(shí),為了取得同樣的效果,使用單元素的成員函數(shù)比使用區(qū)間成員函數(shù)需要更多地調(diào)用內(nèi)存分配子,更頻繁地復(fù)制對(duì)象,而且 / 或者做冗余的操作。
比如,假定你要把一個(gè)int數(shù)組復(fù)制到一個(gè)vector的前端。(首先,數(shù)據(jù)可能來(lái)自數(shù)組而不是vector,因?yàn)閿?shù)據(jù)來(lái)自遺留的C代碼。關(guān)于STL容器和C API混合使用時(shí)導(dǎo)致的問(wèn)題。使用vector的區(qū)間insert函數(shù),非常簡(jiǎn)單:
// 假定numValues 在別處定義
int data[numValues];
vector<int> v;
...
v.insert(v.begin(),data,data+numValues); //把整數(shù)插入到v的前端
而通過(guò)顯式地循環(huán)調(diào)用insert,或多或少可能像這樣:
請(qǐng)注意,我們必須記得把insert的返回值記下來(lái)供下次進(jìn)入循環(huán)時(shí)使用。如果在每次插入操作后不更新insertLoc,我們會(huì)遇到兩個(gè)問(wèn)題。首先,第一次迭代后的所有循環(huán)迭代都將導(dǎo)致不可預(yù)料的行為(undefined behavior),因?yàn)槊看握{(diào)用insert都會(huì)使insertLoc無(wú)效。其次,即使insertLoc仍然有效,插入總是發(fā)生在vector的最前面(即在v.begin()處),結(jié)果這組整數(shù)被以相反的順序復(fù)制到v當(dāng)中。
如果遵從第43條,把循環(huán)替換為對(duì)copy的調(diào)用,我們得到如下代碼:
當(dāng)copy模板被實(shí)例化之后,基于copy的代碼和使用循環(huán)的代碼幾乎是相同的,所以,為了分析效率。我們將注意力集中在顯式循環(huán)上,但要記住,對(duì)于使用copy的代碼,下列分析同樣有效。分析顯式循環(huán)將更易于理解“哪些地方影響了效率”。對(duì),右多個(gè)地方影響了效率,使用單元素版本的insert總共在三個(gè)方面影響了效率,而如果使用區(qū)間版本的insert,則這三種影響都不復(fù)存在。
第一種影響是不必要的函數(shù)調(diào)用。把numValues個(gè)元素逐個(gè)插入到v中導(dǎo)致了對(duì)insert的numValues次調(diào)用。而使用區(qū)間形式的insert,則只做了一次函數(shù)調(diào)用,當(dāng)然,使用內(nèi)聯(lián)(inlining)可能會(huì)避免這樣的影響,但是,實(shí)際中不見(jiàn)得會(huì)使用內(nèi)聯(lián)。只要一點(diǎn)是肯定的:使用區(qū)間形式的insert,肯定不會(huì)有這樣的影響。
內(nèi)聯(lián)無(wú)法避免第二種影響,即把v中已有的元素頻繁地移動(dòng)到插入后它們所處的位置。每次調(diào)用insert把新元素插入到v中時(shí),插入點(diǎn)后的每個(gè)元素都要向后移動(dòng)一個(gè)位置,以便為新元素騰出空間。所以,位置p的元素必須被移動(dòng)到位置p+1,等等。在我們的例子中,我們向v的前端插入numValues個(gè)元素,這意味著v中插入點(diǎn)之后的每個(gè)元素都要向后移動(dòng)numValues個(gè)位置。每次調(diào)用insert時(shí),每個(gè)元素需向后移動(dòng)一個(gè)位置,所以每個(gè)元素將移動(dòng)numValues次。如果插入前v中有n個(gè)元素,就會(huì)有nnumValues次移動(dòng)。在這個(gè)例子中,v中存儲(chǔ)的是int類型,每次移動(dòng)最終可能會(huì)歸為調(diào)用memmove,可是如果v中存儲(chǔ)的是Widget這樣的用戶自定義類型,則每次一定會(huì)導(dǎo)致調(diào)用該類型的賦值操作符或復(fù)制構(gòu)造函數(shù)。(大多數(shù)情況下會(huì)調(diào)用賦值操作符,但每次vector中的最后一個(gè)元素被移動(dòng)時(shí),將會(huì)調(diào)用該元素的復(fù)制構(gòu)造函數(shù)。)所以在通常情況下,把numValues個(gè)元素逐個(gè)插入到含有n個(gè)元素的vector<Widget>的前端將會(huì)有nnumValues次函數(shù)調(diào)用的代價(jià):(n-1)*numValues次調(diào)用Widget的賦值操作符和numValues次調(diào)用Widget的復(fù)制構(gòu)造函數(shù)。即使這些調(diào)用時(shí)內(nèi)聯(lián)的,你仍然需要把v中的元素移動(dòng)numValues次。
與此不同的是,C++標(biāo)準(zhǔn)要求區(qū)間insert函數(shù)把現(xiàn)有容器中的元素直接移動(dòng)到它們最終的位置上,即只需付出每個(gè)元素移動(dòng)一次的代價(jià)。總的代價(jià)包括n次移動(dòng),numValues次調(diào)用該容器中元素類型的復(fù)制構(gòu)造函數(shù),以及調(diào)用該類型的賦值操作符。同每次插入一個(gè)元素的策略相比較,區(qū)間insert減少了n*(numValues-1)次移動(dòng)。細(xì)算下來(lái),這意味著如果numValues是100,那么區(qū)間形式的insert比重復(fù)調(diào)用單元素形式的insert減少了99%的移動(dòng)。
在講述單元素形式的成員函數(shù)和與其對(duì)應(yīng)的區(qū)間成員函數(shù)相比較所存在的第三個(gè)效率問(wèn)題之前,我需要做一個(gè)小小的更在。區(qū)間insert函數(shù)僅當(dāng)能確定兩個(gè)迭代器之間的距離而不會(huì)失去它們的位置時(shí),才可以一次就把元素移動(dòng)到其最終位置上。這幾乎是可能的,因?yàn)樗械那跋虻鞫继峁┻@樣的功能,而前向迭代器幾乎無(wú)處不在。標(biāo)準(zhǔn)容器的所有迭代器都提供了前向迭代器的功能。非標(biāo)準(zhǔn)散列容器的迭代器也是如此(見(jiàn)第25條)。指針作為數(shù)組的迭代器也提供了這一功能。實(shí)際上,不提供這一功能的標(biāo)準(zhǔn)迭代器僅有輸入和輸出迭代器。所以,所說(shuō)的是正確的,除非傳入?yún)^(qū)間形式insert的是輸入迭代器(如istream_iterator,見(jiàn)第6條)。僅在這樣的情況下,區(qū)間insert也必須把元素一步步移動(dòng)到其最終位置上,因而它的優(yōu)勢(shì)就喪失了。(對(duì)于輸出迭代器不會(huì)產(chǎn)生這個(gè)問(wèn)題,因?yàn)檩敵龅鞑荒苡脕?lái)標(biāo)明一個(gè)區(qū)間。)
不明智地使用重復(fù)的單元素插入操作而不是一次區(qū)間插入操作,這樣所帶來(lái)的最后一個(gè)性能問(wèn)題跟內(nèi)存分配有關(guān),盡管它同時(shí)還伴有討厭的復(fù)制問(wèn)題。在第14條將會(huì)指出,如果試圖把一個(gè)元素插入到vector中,而它的內(nèi)存已滿,那么vector將分配具有更大容量(capacity)的新內(nèi)存,把它的元素從舊內(nèi)存賦值到新內(nèi)存中,銷毀舊內(nèi)存中的元素,并釋放舊內(nèi)存。然后它把要插入的元素加入進(jìn)來(lái)。第14條還解釋了多數(shù)vector實(shí)現(xiàn)每次在內(nèi)存耗盡時(shí),會(huì)把容量加倍,因此,插入numValues個(gè)新元素最多可導(dǎo)致log2numValues次新的內(nèi)存分配。
第14條指出,表現(xiàn)出這種行為的vector實(shí)現(xiàn)是存在的,因此,把1000個(gè)元素逐個(gè)插入可能會(huì)導(dǎo)致10次新的內(nèi)存分配(包括低效的元素復(fù)制)。與之對(duì)應(yīng)(而且,到現(xiàn)在為止也可以預(yù)見(jiàn)),使用區(qū)間插入的方法,在開(kāi)始插入前可以知道自己需要多少新內(nèi)存(假定給它的是前向迭代器),所以不必多次重新分配vector的內(nèi)存。可以想見(jiàn),這一節(jié)省是很客觀的。
剛才所做的分析是針對(duì)vector的,但該論證過(guò)程對(duì)string同樣有效。對(duì)于deque,論證過(guò)程與之類似,但deque管理內(nèi)存的方式與vector和string都不同。但是,關(guān)于把元素不必要地移動(dòng)很多次的論斷依然是成立的。
在標(biāo)準(zhǔn)的序列容器中,現(xiàn)在只剩下list,對(duì)此使用區(qū)間形式而不是單元素形式的insert也有其效率上的優(yōu)勢(shì)。關(guān)于重復(fù)函數(shù)調(diào)用的論斷當(dāng)然繼續(xù)生效,由于鏈表工作的方式,對(duì)list中某些節(jié)點(diǎn)的next和prev指針的重復(fù)的,多余的賦值操作。
每當(dāng)有元素加入到鏈表中時(shí),含有這一元素的節(jié)點(diǎn)必須設(shè)定它的next和prev指針,當(dāng)然新節(jié)點(diǎn)前面的節(jié)點(diǎn)(我們稱之為B,代表“前面”)必須設(shè)置自己的next指針,而新節(jié)點(diǎn)后面的節(jié)點(diǎn)則必須設(shè)定自己的prev指針:
當(dāng)通過(guò)調(diào)用list的單元素insert把一系列節(jié)點(diǎn)逐個(gè)加入進(jìn)來(lái)時(shí),除了最后一個(gè)新節(jié)點(diǎn),其余所有的節(jié)點(diǎn)都要把其next指針賦值兩次:一次指向A,另一次指向在它之后插入的節(jié)點(diǎn)。每次有新節(jié)點(diǎn)再A前面插入時(shí),A會(huì)把其prev指針指向新指針。如果A前面插入了numValues個(gè)指針,對(duì)所插入的節(jié)點(diǎn)的next指針會(huì)有numValues-1次多余的賦值,對(duì)A的prev指針也會(huì)有numValues-1次賦值。總共就會(huì)有2*(numValues-1)次不必要的指針賦值。
而避免這一代價(jià)的答案是使用區(qū)間形式的Insert。因?yàn)檫@一函數(shù)知道最終將插入多少節(jié)點(diǎn),可以避免不必要的指針賦值,而只使用一次賦值將每個(gè)指針設(shè)為插入后的值。
下面了解哪些成員函數(shù)支持區(qū)間,在下面的函數(shù)原型中,參數(shù)類型iterator按字母意義理解為容器的迭代器類型,即container:: iterator。另一方面,參數(shù)類型 InputIterator表示任何類型的輸入迭代器都是可接受的。
區(qū)間創(chuàng)建:所有的標(biāo)準(zhǔn)容器都提供了如下形式的構(gòu)造函數(shù):
當(dāng)傳給這種構(gòu)造函數(shù)的迭代器是istream_iterator或者istreambuf_iterator時(shí)(見(jiàn)第29條),你可能會(huì)遇到C++最煩人的分析機(jī)制,它使編譯器把這條語(yǔ)句解釋為函數(shù)聲明,而不是定義新的容器對(duì)象。第6條將向你解釋這一分析的細(xì)節(jié),包括你如何避免這一問(wèn)題。
區(qū)間插入:所有的標(biāo)準(zhǔn)序列容器都提供了如下形式的insert:
關(guān)聯(lián)容器利用比較函數(shù)來(lái)決定元素該插入何處,它們提供了一個(gè)省去position參數(shù)的函數(shù)原型:
在尋找區(qū)間形式的insert來(lái)代替單元素版本時(shí),不要忘了一些單元素的變體使用了不同的函數(shù)名稱,從而把自己給掩蓋了。比如,push_front和push_back都向同其中插入單一元素,盡管它們不叫insert。當(dāng)你看到使用push_front或push_back的循環(huán)調(diào)用,或者front_inserter或back_inserter被作為參數(shù)傳遞給copy函數(shù)時(shí),你會(huì)發(fā)現(xiàn)在這里區(qū)間形式的insert可能是更好的選擇。
區(qū)間刪除。所有的標(biāo)準(zhǔn)容器都提供了區(qū)間形式的刪除(erase)操作,但對(duì)于序列和關(guān)聯(lián)容器,其返回值有所不同。
為何會(huì)有這樣的區(qū)別呢,據(jù)說(shuō)使用關(guān)聯(lián)容器版本的erase返回一個(gè)迭代器(指向被刪除元素之后的元素)將導(dǎo)致不可接受的性能負(fù)擔(dān)。包括我在內(nèi)的很多人都對(duì)這種說(shuō)法表示懷疑,可是C++標(biāo)準(zhǔn)畢竟是C++標(biāo)準(zhǔn)。
本條款中關(guān)于insert 的效率分析對(duì)erase也類似。對(duì)vector和string的論斷中,有一條對(duì)erase不適用,那就是內(nèi)存的反復(fù)分配。這是因?yàn)関ector和string的內(nèi)存會(huì)自動(dòng)增長(zhǎng)以容納新元素,但當(dāng)元素?cái)?shù)目減少時(shí)內(nèi)存卻不會(huì)自動(dòng)減少。(第17條將指出怎樣減少vector或string所占用的多余內(nèi)存)
區(qū)間賦值。正如我在本條款開(kāi)頭所指出的,所有的標(biāo)準(zhǔn)容器都提供了區(qū)間形式的assign:
第六條:當(dāng)心C++編譯器最煩人的分析機(jī)制。
假設(shè)你有一個(gè)存有整數(shù)(int)的文件,你想把這些整數(shù)復(fù)制到一個(gè)list中。下面是很合理的一種做法:
這種做法的思路是,把一對(duì)istream_iterator傳入到list的區(qū)間構(gòu)造函數(shù)中(見(jiàn)第5條),從而把文件中的整數(shù)復(fù)制到list中。這段代碼可以通過(guò)編譯,但是在運(yùn)行時(shí),它什么也不會(huì)做。它不會(huì)從文件中讀取任何數(shù)據(jù),它不會(huì)創(chuàng)建list。這是因?yàn)榈诙l語(yǔ)句并沒(méi)有聲明一個(gè)list,也沒(méi)有調(diào)用構(gòu)造函數(shù)。
【這里是當(dāng)做聲明了一個(gè)函數(shù),參數(shù)為 istream_iterator<int>】
下面從最基本的說(shuō)起,下面這行代碼聲明了一個(gè)帶double參數(shù)并返回int的函數(shù):
int f(double d);
下面這行也一樣,參數(shù)d兩邊的括號(hào)是多余的,會(huì)被忽略:
int f(double (d)); //同上;d兩邊的括號(hào)被忽略
下面這行聲明了同樣的函數(shù),只是它省略了參數(shù)名稱:
int f(double);
這三種形式的聲明你應(yīng)當(dāng)很熟悉,盡管以前你可能不知道可以給參數(shù)名加上圓括號(hào)。
現(xiàn)在再看三個(gè)函數(shù)聲明。第一個(gè)聲明了一個(gè)函數(shù)g,它的參數(shù)是一個(gè)指向不帶任何參數(shù)的函數(shù)的指針,該函數(shù)返回double值:
有另外一種方式可表明同樣的意思。唯一的區(qū)別是,pf用非指針的形式來(lái)聲明(這種形式在C和C++中都有效):
跟通常一樣,參數(shù)名稱可以省略,因此下面是g的第三種聲明,其中參數(shù)名pf被省略了:
請(qǐng)注意圍繞參數(shù)名的括號(hào)(int f(double (d)); //同上;d兩邊的括號(hào)被忽略)
與獨(dú)立的括號(hào)的區(qū)別。圍繞參數(shù)名的括號(hào)被忽略,而獨(dú)立的括號(hào)則表明參數(shù)列表的存在;它們說(shuō)明存在一個(gè)函數(shù)指針參數(shù)。
在熟悉了對(duì)f和g的聲明后,我們開(kāi)始研究本條款開(kāi)始時(shí)提出的問(wèn)題。它是這樣的:
請(qǐng)你注意了。這聲明了一個(gè)函數(shù)data,其返回值是list<int>。這個(gè)data函數(shù)有兩個(gè)參數(shù):
- 第一個(gè)參數(shù)的名稱是dataFile。它的類型是istream_iterator<int>。dataFile兩邊的括號(hào)是多余的,會(huì)被忽略。
- 第二個(gè)參數(shù)沒(méi)有名稱。它的類型是指向不帶參數(shù)的函數(shù)的指針,該函數(shù)返回一個(gè)istream_iterator<int>。
這一切都與C++中的一條普通規(guī)律相符,即盡可能地解釋為函數(shù)聲明。曾經(jīng)多少次見(jiàn)到過(guò)下面這種錯(cuò)誤?
它沒(méi)有聲明名為w 的Widget,而是聲明了一個(gè)名為w的函數(shù),該函數(shù)不帶任何參數(shù),并返回一個(gè)Widget。學(xué)會(huì)識(shí)別這一類言不達(dá)意是稱為C++程序員的必經(jīng)之路。
所有這些都很有意思(通過(guò)它自己的歪曲的方式),但這并不能幫助我們做自己想做的事情。我們想用文件的內(nèi)容初始化list<int> 對(duì)象。現(xiàn)在我們已經(jīng)知道必須繞過(guò)某一種分析機(jī)制,剩下的事情就簡(jiǎn)單了。把形式參數(shù)的聲明用括號(hào)括起來(lái)是非法的,但給函數(shù)參數(shù)加上括號(hào)卻是合法的,所以通過(guò)增加一對(duì)括號(hào),我們強(qiáng)迫編譯器按我們的方式來(lái)工作:
這是聲明data的正確方式,在使用istream_iterator和區(qū)間構(gòu)造函數(shù)時(shí)(同樣,見(jiàn)第5條),注意到這一點(diǎn)是有益的。
不幸的是,并不是所有的編譯器都知道這一點(diǎn)。幾乎有一半測(cè)試的編譯器中,拒絕接受data的上述聲明方式,除非它被錯(cuò)誤地用不帶括號(hào)的形式來(lái)聲明。更好地方式是在對(duì)data的聲明中避免使用匿名的istream_iterator對(duì)象(盡管使用匿名對(duì)象是一種趨勢(shì)),而是給這些迭代器一個(gè)名稱。下面的代碼應(yīng)該總是可以工作的:
使用命名的迭代器對(duì)象與通常的STL程序風(fēng)格相違背,但你或許覺(jué)得為了使代碼對(duì)所有編譯器都沒(méi)有二義性,并且使維護(hù)代碼的人理解起來(lái)更容易,這一代價(jià)是值得的。
Copy 函數(shù)
所謂變易算法就是一組能夠修改容器元素?cái)?shù)據(jù)的模板函數(shù),可進(jìn)行序列數(shù)據(jù)的復(fù)制,變換等。其中copy就是其中一個(gè)元素復(fù)制算法copy。該算法主要用于容器之間元素的拷貝,即將迭代器區(qū)間[first, last] 的元素復(fù)制到由復(fù)制目標(biāo)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ù)制到的目標(biāo)區(qū)間起始位置
返回值:
返回一個(gè)迭代器,指出已被復(fù)制元素區(qū)間的最后一個(gè)位置
程序示例:
#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中的七個(gè)元素復(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中的元素向左移動(dòng)一位
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算法可以很簡(jiǎn)單地將一個(gè)容器里面的元素復(fù)制至另一個(gè)目標(biāo)容器中,上例中代碼特別要注意一點(diǎn)就是myvector.resize(7);這行代碼,在這里一定要先為vector分配空間,否則程序會(huì)崩,這是初學(xué)者經(jīng)常犯的一個(gè)錯(cuò)誤。
其實(shí)copy函數(shù)最大的威力是結(jié)合標(biāo)準(zhǔn)輸入輸出迭代器的時(shí)候,我們通過(guò)下面這個(gè)示例就可以看出它的威力了。
#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;
// 從標(biāo)準(zhǔn)輸入設(shè)備讀入整數(shù)
// 直到輸入的是非整型數(shù)據(jù)為止 請(qǐng)輸入整數(shù)序列,按任意非數(shù)字鍵并回車結(jié)束輸入
cout << "Please input element:" << endl;
copy(IstreamItr(cin), IstreamItr(), BackInsItr(myvector));
//輸出容器里的所有元素,元素之間用空格隔開(kāi)
cout << "Output : " << endl;
copy(myvector.begin(), myvector.end(), OstreamItr(cout, " "));
cout << endl;
return 0;
}
對(duì)于 vector 的clear() 和erase() 的區(qū)別
vector :: erase(): 從指定容器刪除指定位置的元素或某段范圍內(nèi)的元素
vector :: erase() 方法有兩種重載形式
如下:
iterator erase(iterator _Where);
iterator erase(iterator _First, iterator _Last); // 但是這里不會(huì)刪除掉_Last 所對(duì)應(yīng)的數(shù)據(jù)
如果是刪除指定位置的元素時(shí),返回值是一個(gè)迭代器,指向刪除元素下一個(gè)元素;如果是刪除某范圍內(nèi)的元素時(shí):返回值也表示一個(gè)迭代器,指向最后一個(gè)刪除元素的下一個(gè)元素;如何一個(gè)容器里有多個(gè)相同的元素,要怎么刪除呢?
for(Iter = v1.begin(); Iter != v1.end(); Iter++)
{
if(*Iter == 10)
{
Iter = v1.erase(Iter);//Iter為刪除元素的下一個(gè)元素的迭代器
//即第一次這段語(yǔ)句后Iter 會(huì)是20,大家可以通過(guò)debug調(diào)試出來(lái)查看下數(shù)值
}
if(Iter == v1.end()) //要控制迭代器不能超過(guò)整個(gè)容器
{
break;
}
}
但是這里就算調(diào)用了erase 其實(shí)好像也只是將后面的數(shù)移位覆蓋了前面的數(shù),但是整個(gè)size還是沒(méi)有改變。
而對(duì)于vector,clear() 并沒(méi)有真正釋放內(nèi)存(這是為優(yōu)化效率所做的事),clear實(shí)際所做的是為vector中所保存的所有對(duì)象調(diào)用析構(gòu)函數(shù)(如果有的話),然后初始化size這些東西,讓你覺(jué)得把所有的對(duì)象清除了。但是總的來(lái)講vector并沒(méi)有出現(xiàn)內(nèi)存泄漏。在《effective STL》中的“條款17" 已經(jīng)指出了:
當(dāng)vector、string大量插入數(shù)據(jù)后,即使刪除了大量數(shù)據(jù)(或者全部都刪除,即clear)并沒(méi)有改變?nèi)萜鞯娜萘浚╟apacity),所以仍然會(huì)占用著內(nèi)存。為了避免這種情況,我們應(yīng)該想辦法改變?nèi)萜鞯娜萘渴怪M可能小的符合當(dāng)前數(shù)據(jù)所需(shrink to fit)
《Effective STL》給出的解決方案是:
vector<type> v;
//.... 這里添加許多元素給v
//.... 這里刪除v中的許多元素
vector<type>(v).swap(v);
//此時(shí)v的容量已經(jīng)盡可能的符合其當(dāng)前包含的元素?cái)?shù)量
//對(duì)于string則可能像下面這樣
string(s).swap(s);
即先創(chuàng)建一個(gè)臨時(shí)拷貝與原先的vector一致,值得注意的是,此時(shí)的拷貝 其容量是盡可能小的符合所需數(shù)據(jù)的。緊接著該拷貝與原先的vector v 進(jìn)行交換。好了此時(shí),執(zhí)行交換后,臨時(shí)變量會(huì)被銷毀,內(nèi)存得到釋放。此時(shí)的v即為原先的臨時(shí)拷貝,而交換后的臨時(shí)拷貝則為容量非常大的vector(不過(guò)已經(jīng)被銷毀)
為了證明這一點(diǎn),我寫了一個(gè)程序,如下:
#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;
// 此時(shí)檢查內(nèi)存情況 占用54M
v.clear();
cin >> ch;
// 此時(shí)再次檢查, 仍然占用54M
cout << "Vector 的 容量為" << v.capacity() << endl;
// 此時(shí)容量為 1048576
vector<string>(v).swap(v);
cout << "Vector 的 容量為" << v.capacity() << endl;
// 此時(shí)容量為0
cin >> ch;
// 檢查內(nèi)存,釋放了 10M+ 即為數(shù)據(jù)內(nèi)存
return 0;
}
在創(chuàng)建一個(gè)vector后,vector的實(shí)際容量一般會(huì)比所給數(shù)據(jù)要大,這樣做應(yīng)該是避免過(guò)多的重新分配內(nèi)存的吧。
當(dāng)然,上面這種方法雖然釋放了內(nèi)存,但是同時(shí)也增加了拷貝數(shù)據(jù)的時(shí)間消耗。不過(guò)一般需要重新調(diào)整容量的情況都是 vector 本身元素較少的情況,所以,時(shí)間消耗可以忽略不計(jì)。
再回過(guò)來(lái)看vector內(nèi)存釋放機(jī)制
vector 中的內(nèi)建有內(nèi)存管理,當(dāng)vector離開(kāi)它的生存期的時(shí)候,它的析構(gòu)函數(shù)會(huì)把vector 中的元素銷毀,并釋放它們所占用的空間,所以用 vector 一般不用顯式釋放 ——不過(guò),如果你 vector 中存放的是指針,那么當(dāng) vector 銷毀時(shí),那些指針指向的對(duì)象不會(huì)被銷毀,那些內(nèi)存不會(huì)被釋放。 其實(shí)這也是一種比較常見(jiàn)的內(nèi)存泄漏。
vector的工作原理是系統(tǒng)預(yù)先分配一塊capacity大小的空間,當(dāng)插入的數(shù)據(jù)超過(guò)這個(gè)空間的時(shí)候,這塊空間會(huì)以某種方式擴(kuò)展,但是你刪除數(shù)據(jù)的時(shí)候,它卻不會(huì)縮小。vector為了防止大量分配連續(xù)內(nèi)存的開(kāi)銷,保持一塊默認(rèn)的尺寸,clear只是清數(shù)據(jù)了,為清內(nèi)存,因?yàn)関ector的capacity容量未變化,系統(tǒng)維護(hù)一個(gè)的默認(rèn)值。
有什么方法可以釋放掉vector中占用的全部?jī)?nèi)存呢?
標(biāo)準(zhǔn)的解決方法如下
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èn)為適當(dāng)?shù)臅r(shí)候,執(zhí)行如下代碼:
//這里就是通過(guò)一輪循環(huán),然后將每一個(gè)節(jié)點(diǎn)的指針給銷毀了,再調(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: 若你的程序是多線程的,注意以下線程安全問(wèn)題,必要時(shí)加個(gè)臨界區(qū)控制一下。
總結(jié):
vector與deque不同,其內(nèi)存占用空間只會(huì)增長(zhǎng),不會(huì)減小。比如你首先分配了10,000個(gè)字節(jié),然后erase掉后面9,999個(gè),則雖然有效元素只有一個(gè),但是內(nèi)存占用仍為10,000個(gè)。所有空間在vector析構(gòu)時(shí)回收。empty()是用來(lái)檢測(cè)容器是否為空的,clear()可以清空所有元素。但是即使clear(),所占用的內(nèi)存空間依然如故。如果你需要空間動(dòng)態(tài)縮小,可以考慮使用deque。如果非要用vector,這里有一個(gè)方法:
vector<int>().swap(nums); //nums.swap(vector<int>());
vector<int>().swap(nums);
{
std::vector<int> tmp = nums;
nums.swap(tmp);
}
// 加一對(duì)大括號(hào)是可以讓tmp退出{}的時(shí)候自動(dòng)析構(gòu)。
// swap技法就是通過(guò)交換函數(shù)swap(),使得vector離開(kāi)其自身的作用域,從而強(qiáng)制釋放vector所占的內(nèi)存空間。
assign的設(shè)計(jì)
assign的函數(shù)的好處,應(yīng)該很好理解就是在不能使用賦值符“=”的情況下,可以將一個(gè)容器中的部分元素通過(guò)迭代器傳遞賦值到另一個(gè)容器中,但是在assign的使用過(guò)///程中,有一點(diǎn)需要特別注意,就是調(diào)用assign()函數(shù)的容器必須有足夠的空間來(lái)容納復(fù)制過(guò)來(lái)的元素。
函數(shù)原型:
void assign(const_iterator first, const_iterator last);
void assign(size_type n, const T&x = T());
功能:
將區(qū)間(first,last)的元素賦值到當(dāng)前的vector容器中,或者賦n個(gè)值為x的元素到vector容器中,這個(gè)容器會(huì)清除掉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;
}
說(shuō)到安全問(wèn)題,我還想問(wèn),vector是不是線程安全的?http://book.51cto.com/art/201305/394132.htm
標(biāo)準(zhǔn) C++的世界相當(dāng)狹小和古舊。在這個(gè)純凈的世界中,所有的可執(zhí)行程序都是靜態(tài)鏈接的。不存在內(nèi)存映像文件或共享內(nèi)存。沒(méi)有窗口系統(tǒng),沒(méi)有網(wǎng)絡(luò),沒(méi)有數(shù)據(jù)庫(kù),也沒(méi)有其他進(jìn)程。考慮到這一點(diǎn),當(dāng)你得知 C++標(biāo)準(zhǔn)對(duì)線程只字未提時(shí),你不應(yīng)該感到驚訝。于是,你對(duì)STL的線程安全性的第一個(gè)期望應(yīng)該是,它會(huì)因不同實(shí)現(xiàn)而異。
當(dāng)然,多線程程序是很普遍的,所以多數(shù)STL提供商會(huì)盡量使自己的實(shí)現(xiàn)可在多線程環(huán)境下工作。然而,即使他們?cè)谶@一方面做得不錯(cuò),多數(shù)負(fù)擔(dān)仍然在你的肩膀上。理解為什么會(huì)這樣是很重要的。STL提供商對(duì)解決多線程問(wèn)題只能做很有限的工作。
在STL容器中支持多線程的標(biāo)準(zhǔn)(這是多數(shù)提供商們所希望的)已經(jīng)為SGI所確定,并在它們的STL Web站點(diǎn)上發(fā)布。概括來(lái)說(shuō),它指出,對(duì)一個(gè) STL實(shí)現(xiàn)你最多只能期望:
多個(gè)線程讀是安全的。多個(gè)線程可以同時(shí)讀同一個(gè)容器的內(nèi)容,并且保證是正確的。自然地,在讀的過(guò)程中,不能對(duì)容器有任何寫入操作。
多個(gè)線程對(duì)不同的容器做寫入操作是安全的。多個(gè)線程可以同時(shí)對(duì)不同的容器做寫入操作。
就這些。我必須指明,這是你所能期待的,而不是你所能依賴的。有些實(shí)現(xiàn)提供了這些保證,有些則沒(méi)有。
寫多線程的代碼并不容易,許多程序員希望 STL的實(shí)現(xiàn)能提供完全的線程安全性。如果是這樣的話,程序員可以不必再考慮自己做同步控制。這無(wú)疑是很方便的,但要做到這一點(diǎn)將會(huì)很困難。考慮當(dāng)一個(gè)庫(kù)試圖實(shí)現(xiàn)完全的容器線程安全性時(shí)可能采取的方式:
對(duì)容器成員函數(shù)的每次調(diào)用,都鎖住容器直到調(diào)用結(jié)束。
在容器所返回的每個(gè)迭代器的生存期結(jié)束前,都鎖住容器(比如通過(guò) begin 或 end 調(diào)用)。
對(duì)于作用于容器的每個(gè)算法,都鎖住該容器,直到算法結(jié)束。
(實(shí)際上這樣做沒(méi)有意義。因?yàn)椋惴o(wú)法知道它們所操作的容器。盡管如此,在這里我們?nèi)砸懻撨@一選擇。因?yàn)榧词惯@是可能的,我們也會(huì)發(fā)現(xiàn)這種做法仍不能實(shí)現(xiàn)線程安全性,這對(duì)于我們的討論是有益的)。
現(xiàn)在考慮下面的代碼。它在一個(gè)vector<int>中查找值為5的第一個(gè)元素,如果找到了,就把該元素置為0.
vector<int> v;
...
vector<int>:: iterator first5(find(v.begin(),v.end(),5)); // 第 1 行
if (first5 != v.end()){ //第 2行
*first5 =0; //第 3行
}
在一個(gè)多線程環(huán)境中,可能在第一行剛剛完成后,另一個(gè)不同的線程會(huì)更改 v中的數(shù)據(jù)。如果這種更改真的發(fā)生了,那么第2行對(duì) first5 和 v.end是否相等的檢查將會(huì)變得沒(méi)有
意義,因?yàn)関的值將會(huì)與在第一行結(jié)束時(shí)不同。事實(shí)上,這一檢查會(huì)產(chǎn)生不確定的行為,因?yàn)榱硗庖粋€(gè)線程可能會(huì)夾在第1行和第2行中間,使 first5 變得無(wú)效,這第二個(gè)線程或許會(huì)執(zhí)行一個(gè)插入操作使得vector 重新分配它的內(nèi)存。類似地,第 3行對(duì)*first5的賦值也是不安全的,因?yàn)榱硪粋€(gè)線程可能在第 2行和第 3行之間執(zhí)行,該線程可能會(huì)使 first5無(wú)效,例如可能會(huì)刪除它所指向的元素(或者至少是曾經(jīng)指向過(guò)的元素)。
要做到線程安全, v必須從第 1行到第 3行始終保持在鎖住狀態(tài),很難想象一個(gè) STL實(shí)現(xiàn)能自動(dòng)推斷出這一點(diǎn)。考慮到同步原語(yǔ)(例如信號(hào)量、互斥體等)通常會(huì)有較高的開(kāi)銷,這就更難想象,一個(gè) STL實(shí)現(xiàn)如何既能夠做到這一點(diǎn),同時(shí)又不會(huì)對(duì)那些在第 1行和第 3行之間本來(lái)就不會(huì)有另外線程來(lái)訪問(wèn) v的程序(假設(shè)程序就是這樣設(shè)計(jì)的)造成顯著的效率影響。
這樣的考慮說(shuō)明了為什么你不能指望任何 STL 實(shí)現(xiàn)來(lái)解決你的線程難題。相反,在這種情況下,必須手工做同步控制。在這個(gè)例子中,或許可以這樣做:
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)建一個(gè)Lock類,它在構(gòu)造函數(shù)中獲得一個(gè)互斥體,在析構(gòu)函數(shù)中釋放它,從而盡可能地減少 getMutexFor 調(diào)用沒(méi)有相對(duì)應(yīng)的 releaseMutexFox調(diào)用的可能性。這樣的類(實(shí)際上是一個(gè)類模板)看起來(lái)大概像下面這樣:
template<typename Container> //一個(gè)為容器獲取和釋放互斥體的模板
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)來(lái)管理資源的生存期的思想通常被稱為 "獲得資源時(shí)即初始化",你可以在任何一本全面介紹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é)束,自動(dòng)釋放互斥體
因?yàn)?Lock對(duì)象在其析構(gòu)函數(shù)中釋放容器的互斥體,所以很重要的一點(diǎn)是,當(dāng)互斥體應(yīng)該被釋放時(shí),Lock就要被析構(gòu)。為了做到這一點(diǎn),我們創(chuàng)建了一個(gè)新的代碼塊(block),在其中定義了Lock,當(dāng)不再需要互斥體時(shí)就結(jié)束該代碼塊。看起來(lái)好像是我們把“調(diào)用releaseMutexFor”這一任務(wù)換成了“結(jié)束代碼塊”,事實(shí)上這種說(shuō)法是不確切的。如果我們忘了為L(zhǎng)ock創(chuàng)建新的代碼塊,則互斥體仍然會(huì)被釋放,只不過(guò)會(huì)晚一些——當(dāng)控制到達(dá)包含 Lock的代碼塊末尾時(shí)。而如果我們忘記了調(diào)用releaseMutexFor,那么我們永遠(yuǎn)也不會(huì)釋放互斥體。
而且,基于 Lock的方案在有異常發(fā)生時(shí)也是強(qiáng)壯的。C++保證,如果有異常被拋出,局部對(duì)象會(huì)被析構(gòu),所以,即便在我們使用 Lock對(duì)象的過(guò)程中有異常拋出, Lock仍會(huì)釋放它所擁有的互斥體。
如果我們依賴于手工調(diào)用 getMutexFor和releaseMutexFor,那么,當(dāng)在調(diào)用 getMutexFor之后而在調(diào)用 releaseMutexFor之前有異常被拋出時(shí),我們將永遠(yuǎn)也無(wú)法釋放互斥體。
異常和資源管理雖然很重要,但它們不是本條款的主題。本條款是講述STL中的線程安全性的。當(dāng)涉及STL容器和線程安全性時(shí),你可以指望一個(gè)STL庫(kù)允許多個(gè)線程同時(shí)讀一個(gè)容器,以及多個(gè)線程對(duì)不同的容器做寫入操作。你不能指望 STL庫(kù)會(huì)把你從手工同步控制中解脫出來(lái),而且你不能依賴于任何線程支持。
已經(jīng)證實(shí)存在一個(gè)漏洞。如果該異常根本沒(méi)有被捕獲到,那么程序?qū)⒔K止。在這種情況下,局部對(duì)象(如 lock)可能還沒(méi)有讓它們的析構(gòu)函數(shù)被調(diào)用到。有些編譯器會(huì)調(diào)用它們,有些編譯器不會(huì)。這兩種情況都是有效的。