Effective STL:02vector和string
在STL容器中,vector和string的使用頻率會更高一些。設計vector和string的目標就是為了替換大多數應用中要使用的數組。
13:vector和string優先於動態分配的數組
一旦要使用new動態分配數組,將要面臨很多問題:必須確保delete、必須使用正確的delete形式;必須保證只delete一次。每當發現自己要動態分配一個數組時,都應該考慮用vector和string來代替。vector和string 消除了上述的負擔,因為它們自己管理內存。如果你擔心還得繼續支持舊的代碼,而它們是基於數組的,使用vector和string也沒有問題,條款16會介紹把vector和string中的數據傳遞給期望接受數組的API是多麽容易。
目前我只能想到在一種情況下,用動態分配的數組取代vector和string是合理的,而且這種情形只對string適用。許多string實現在背後使用了引用計數技術,這種策略可以消除不必要的內存分配和不必要的字符拷貝,從而可以提高很多應用程序的效率。但是如果你在多線程環境中使用了引用計數的string,你會發現,由避免內存分配和字符拷貝所節省下來的時間還比不上花在背後同步控制上的時間。如果你在多線程環境中使用了引用計數的string,那麽註意一下因支持線程安全而導致的性能問題是很有意義的。
為了確定你是否在使用以引用計數方式實現的string,最簡單的辦法是查閱庫文檔。因為引用計數被視為一種優化,所以供應商通常把它作為一個特征而著意指出;另一條途徑是檢查庫中實現string的源代碼,最容易檢查的或許是該類的拷貝構造函數,看看它是否在某處增加了引用計數。如果增加了,那麽string是用引用計數來實現的。
如果string是以引用計數方式來實現的,而你又運行在多線程環境中,並認為string的引用計數實現會影響效率,那麽,你至少有三種可行的選擇,首先,檢查你的庫實現,看看是否有可能禁止引用計數,通常是通過改變某個預處理變量的值;其次,尋找或開發另一個不使用引用計數的string實現(或者是部分實現);第三,考慮使用vector<char>而不是string。vector的實現不允許使用引用計數,所以不會發生隱藏的多線程性能問題。當然,如果你轉向了vector<char>,那麽你就舍棄了使用string的成員函數的機會,但大多數成員函數的功能可以通過STL算法來實現,所以當你使用一種語法形式而不是另一種時,你不會因此而被迫舍棄功能。
總結起來很簡單,如果你正在動態地分配數組,那麽你可能要做更多的工作。為了減輕自己的負擔,請使用vector或string。
14:使用reserve來避免不必要的重新分配
對於vector和string,當需要更多空間時,它會作出下面的動作:
分配一塊大小為當前容量的某個倍數的新內存,在大多數實現中,vector和string 的容量每次以2的倍數增長;
把容器的所有元素從舊的內存拷貝到新的內存中;
析構掉舊內存中的對象;
釋放舊內存;
涉及到內存的分配、釋放,以及對象的復制和析構,因此這個過程會非常耗時。而且每當這些步驟發生時,vector或string中所有的指針、叠代器和引用都將變得無效。因此,應該盡可能的把重新分配的次數減少到最低限度,從而避免重新分配和指針、叠代器、引用失效帶來的開銷。這時可以使用reverse成員函數。
在解釋reserve怎樣做到這一點之前,還需要介紹4個相互關聯,但有時會被混淆的成員函數。在標準容器中,只有vector和string提供了所有這4個函數:
size()返回該容器中當前有多少個元素;
capacity()返回容器利用已經分配的內存可以容納多少個元素,這是容器當前所能容 納的元素總數,而不是它還能容納多少個元素;
resize(Container::size_type n)強迫容器改變到包含n個元素的狀態。在調用resize之後, size將返回n。如果n比當前的size要小,則容器尾部的元素將會被析構;如果n比當前的size要大,則通過默認構造函數創建的新元素將被添加到容器的末尾;如果n比當前的容量要大,那麽在添加元素之前,將先重新分配內存;
reserve(Container::size_type n)強迫容器把它的容量變為至少是n,前提是n不小於當 前的大小。這通常會導致重新分配,因為容量需要增加;
因此,避免重新分配的關鍵在於,盡早地使用reserve,把容器的容量設為足夠大的值,最好是在容器剛被構造出來之後就使用reserve。
通常有兩種方式來使用reserve以避免不必要的重新分配。第一種方式是,若能確切知道或大致預計容器中最終會有多少元素,則此時可使用reserve;第二種方式是,先預留足夠大的空間,然後,當把所有數據都加入以後,再去除多余的容量,條款17條會介紹如何去除多余的容量。
15:註意string實現的多樣性
string的實現方式可能有多種,在有些string的實現中,string和char*指針的大小相同,而在另一些實現中,string的大小是char*的7倍。幾乎每個string實現都包含如下信息:
字符串的大小,也就是string包含的字符的個數;用於存儲字符串中字符的內存容量大小;字符串的值。除了以上這些,string還可能包括分配器的副本,建立在引用計數基礎上的string實現可能還包含了引用計數。下面介紹4種string的實現。
實現A:string對象包含分配器的副本、字符串的大小、字符串的容量、以及一個指針。指針指向動態分配的內存,其中包含了引用計數以及字符串的值。這種實現中,使用默認分配器的string對象大小是一個指針的4倍。若使用了自定義的分配器,則string對象會更大一些:
實現B:如果使用默認的分配器,則string對象大小與指針相同,因為它只包含了一個指向某種結構的指針。如果使用了自定義的分配器,則string對象的大小相應的加上分配器的大小。在該實現中,由於用到了優化,所以使用默認分配器不需要多余的空間。
B實現中指針所指向的結構包含了字符串的大小、容量和引用計數,以及一個指向一塊動態分配的內存的指針,該內存中包含了字符串的值。該對象可能還包含了一些與多線程環境下的同步控制相關的額外數據:
實現C:string對象的大小總是與指針相同,該指針指向一塊動態分配的內存,其中包含了與該字符串相關的一切數據:它的大小、容量、引用計數和值。沒有對單個對象的分配器支持。該內存中也包含了一些與值的可共享性有關的數據,這裏將它標記為X:
實現D:string對象是指針大小的7倍(仍然假定使用的是默認的分配器)。這一實現不使用引用計數,但是每個string內部包含一塊內存,最大可容納15個字符的字符串。因此,小的字符串可以完整地存放在該string對象中,這一特性通常被稱為“小字符串優化”特性。當一個string的容量超過15時,該內存的起始部分被當作一個指向一塊動態分配的內存的指針,而該string的值就放在這塊內存中。
像string s(“Perse”);這樣的語句,在實現D中將不會導致任何動態分配,在實現A和實現C中將導致一次動態分配,而在實現B中會導致兩次動態分配(一次是為string對象所指向的對象,另一次是為該對象所指向的字符緩沖區);
在以引用計數為基礎的設計方案中,string對象之外的一切都可以被多個string所共享,實現A比實現B或實現C提供了較小的共享能力,實現B和實現C可以共享string的大小和容量,從而減少了每個對象存貯這些數據的平均開銷。有趣的是,實現C不支持單個對象的分配器,這意味著所有的string必須使用同一個分配器;在實現D中,所有的string都不共享任何數據。
很多實現默認情況下會使用引用計數,但它們通常提供了關閉默認選擇的方法,只有當字符串被頻繁復制時,引用計數才有用;string對象大小的範圍可以是一個char*指針的大小的1倍到7倍;創建一個新的字符串值可能需要零次、一次或兩次動態分配內存;string對象可能共享,也可能不共享其大小和容量信息;string可能支持,也可能不支持針對單個對象的分配器;不同的實現對字符內存的最小分配單位有不同的策略。
16:了解如何把vector和string數據傳給舊的API
C++的精英們一直試圖使程序員們從數組中解放出來,轉向使用vector。他們同樣努力地試圖使開發者們從char*指針轉向string對象。但是舊的C API還存在,它們使用數組和char*指針來進行數據交換而不是vector或string對象。這樣的APl還將存在很長一段時間,如果想有效地使用STL,我們就必須與它們和平共處。
幸運的是,這很容易做到。如果你有一個vector v,而你需要得到一個指向v中數據的指針,從而可把v中的數據作為數組來對待,那麽只需使用&v[0]就可以了。對於string s,對應的形式是s.c_str()。
&v[0]是指向vector中第一個元素的指針。C++標準要求vector中的元素存儲在連續的內存中,就像數組一樣。所以,可以把vector傳給一個如下所示的C API:
void doSomething(const int* pInts, size_t numlnts); doSomething(&v[0], v.size());
這裏唯一需要註意的問題是v不能為空,否則&v[0]試圖產生一個指針,而該指針指向的東西並不存在。
有些人可能會用v.begin()來代替&v[0],但實際上這是錯誤的,你不應該依賴於這一點。
如果需要將string傳遞給接收const char*的函數,則需要使用string的c_str函數:
void doSomething(const char *pString); doSomething(s.c_str());
再看一下doSomething的聲明:
void doSomething(const int* pints, size_t numlnts); void doSomething(const char *pString);
要傳入的指針都是指向const指針。也就是vector和string的數據被傳遞給一個要讀取而非改寫這些數據的API,對於string,這是唯一所能做的,因為c_str所產生的指針並不一定指向字符串數據的內部表示。對於vector,則允許在C API中改變v元素的值,但被調用的函數不能改變vector中元素的個數。比如,不能試圖在vector的未使用的容量中“創建”新元素。不然,v的內部將會變得不一致,因為它從此無法知道自己的正確大小,v.size()將產生不正確的結果。
如果想用C API中元素初始化一個vector,可以利用vector和數組的內存布局兼容性,向API傳入vector中元素的存儲區域:
size_t fillArray(double *pArray, size_t arraySize); vector<double> vd(maxNumDoubles); // create a vector whose size is maxNumDoubles vd.resize(fillArray(&vd[0], vd.size()));
這一技術只對vector有效,因為只有vector才保證和數組有同樣的內存布局。不過,如果你想用來自C API中的數據初始化一個string,也很容易就能做到。只要讓API把數據放到一個vector<char>中,然後把數據從該vector拷貝到相應字符串中即可:
size_t fillString(char ‘pArray, size_t arraySize); vector<char> vc(maxNumChars); // create a vector whose size is maxNumChars size_t charsWritten = fillString(&vc[0], vc.size()); string s(vc.begin(), vc.begin()+charsWritten);
實際上,先讓C API把數據寫入到一個vector中,然後把數據拷貝到期望最終寫入的STL容器中,這一思想總是可行的:
size_t fillArray(double *pArray, size_t arraySize); vector<double> vd(maxNumDoubles); vd.resize(fillArray(&vd[0], vd.size());+ deque<double> d(vd.begin(), vd.end()); list<double> l(vd.begin(), vd.end()); set<double> s(vd.begin(), vd.end());
這意味著,除了vector和string以外,其他STL容器也能把它們的數據傳遞給C API。只需把每個容器的元素拷貝到一個vector中,然後傳給該API即可:
void doSomething(const int* pints, size_t numlnts); set<int> intSet; … vector<int> v(intSet.begin(), intSet.end()); if (!v.empty()) doSomething(&v[0], v.size());
17:使用swap技巧去除多余的容量
假設一個包含Contestant對象的vector,該vector曾經擁有數以萬計的Contestant對象,之後又經過erase將大部分Contestant從中刪除,該操作縮減了vector的size,但是並沒有減少它的capacity。為了避免vector仍占用不再需要的內存,需要有一種方法可以把它的容量從以前的最大值縮減需要的數量。
使用swap可以實現這一點:
class Contestant {...}; vector<Contestant> contestants; … vector<Contestant>(contestants).swap(contestants);
表達式vector<Contestant>(contestants)創建一個臨時的vector,它是contestants的拷貝,這是由vector的拷貝構造函數來完成的。然而vector的拷貝構造函數只為所拷貝的元素分配所需要的內存,所以這個臨時vector沒有多余的容量。然後我們把臨時vector中的數據和contestants中的數據做swap操作。在這之後,contestants具有了被去除之後的容量,即原先臨時變量的容量,而臨時變量的容量則變成了原先contestants臃腫的容量。到這時,臨時vector被析構,從而釋放了先前為contestants所占據的內存。
同樣的技巧對string也適用:
string s; … //make s large, then erase most of its characters string(s).swap(s);
這一技術並不保證一定能去除所有多余的容量。STL的實現者如果願意的話,他們可以自由地為vector和string保留多余的容量,而有時他們確實希望這樣做。例如,他們可能需要一個最小的容量,或者他們把一個vector或string的容量限制為2的乘冪數。所以,這種技巧實際上並不意味著“使容量盡量小”,它意味著“在容器當前的大小確定的情況下,使容量在該實現下變為最小”。
另外,swap技巧的一種變化形式可以用來清除一個容器,並使其容量變為該實現下的最小值。只要與一個用默認構造函數創建的vector或string做swap就可以了:
vector<Contestant> v; string s; … // use v and s vector<Contestant>().swap(v); //clear v and minimize its capacity string().swap(s); // clear s and minimize its capacity
最後要註意的是:在做swap的時候,不僅兩個容器的內容被交換,同時它們的叠代器、指針和引用也將被交換(string除外)。在swap發生後,原先指向某容器中元素的叠代器、指針和引用依然有效,並指向同樣的元素—但是,這些元素已經在另一個容器中了。
18:避免使用vector<bool>
vector<bool>並不是一個嚴格意義上的STL容器,它並不存儲bool。一個對象要成為STL容器,就必須滿足C++標準的第23.1節列出的所有條件。其中的一個條件是,如果c是包含對象T的容器,而且c支持operator[],那麽下面的代碼必須能夠被編譯:T *p = &c[0];
所以,如果vector<bool>是一個STL容器,下面這段代碼必須可以被編譯:
vector<bool> v; bool *pb = &v[0];
但是它不能編譯。原因是,vector<bool>並不真的儲存bool。相反,為了節省空間,它儲存的是bool的緊湊表示。在一個典型的實現中,儲存在vector中的每個“bool”僅占一個二進制位,一個8位的字節可容納8個“bool"。在內部,vector<bool>使用了與位域一樣的思想,來表示它所存儲的那些bool實際上它只是假裝存儲了這些bool。
指向單個位的引用是被禁止的,這使得在設計vector<bool>的接口時產生了一個問題,因為vector<T>::operator[]的返回值應該是T&。但由於vector<bool>中存儲的並不是bool,所以vector<bool>::operator[]需要返回一個指向單個位的引用,而這樣的引用卻不存在。 為了克服這一困難,vector<bool>::operator[]返回一個對象,這個對象表現得像是一個指向單個位的引用,即所謂的代理對象,vector<bool>看起來像是這樣:
template <typename Allocator> vector<bool, Allocator> { public: class reference {...}; reference operator[](size_type n); … }
這就是bool *pb = &v[0];編譯報錯的原因。
既然vector<booi>應當被避免,那麽當需要vector<bool>時,應該使用什麽呢?標準庫提供了兩種選擇,可以滿足絕大多數情況下的需求。第一種是deque<bool>,deque幾乎提供了vector所提供的一切,但deque中元素的內存不是連續的,所以你不能把deque<bool>中的數據傳遞給一個期望bool數組的C API;第二種可以替代vector<bool>的選擇是bitset。
Effective STL:02vector和string