STL學(xué)習(xí)筆記之容器(二)

vector和string

所有的STL容器都很有用,但是相比于其他容器,vector和string更常用。本章從多個(gè)角度覆蓋vector和string,如:為什么提倡使用 vector代替數(shù)組,怎樣改進(jìn)vector和string的性能?怎樣除去過剩的內(nèi)存,vector<string>是個(gè)什么東西

條款13:盡量使用vector和string來代替動(dòng)態(tài)分配的數(shù)組

使用vector和string代替動(dòng)態(tài)分配的數(shù)組是個(gè)很明智的選擇,它們不僅能夠自動(dòng)管理內(nèi)存(主要是自動(dòng)釋放內(nèi),自動(dòng)增加內(nèi)存),還提供了很多可用的函數(shù)和類型:既有像begin、end和size這樣的成員函數(shù),也有內(nèi)嵌的像iterator、 reverse_iterator或value_type的typedef
對(duì)于string實(shí)現(xiàn),可能使用了引用計(jì)數(shù)器,這是一種那個(gè)消除了不必要的內(nèi)存分配和字符拷貝的策略,而且在很多應(yīng)用中可以提高性能。但這種方案在多線程環(huán)境下可能會(huì)嚴(yán)重降低性能,可能的解決方法是:
(1)關(guān)閉引用計(jì)數(shù)(如果可能的話)
(2)尋找或開發(fā)一個(gè)不使用引用計(jì)數(shù)的string實(shí)現(xiàn)(或部分實(shí)現(xiàn))替代品
(3)考慮使用vector<char>來代替string,vector實(shí)現(xiàn)不允許使用引用計(jì)數(shù),所以隱藏的多線程性能問題不會(huì)出現(xiàn)了


條款14:使用reserve避免不必要的重新分配

reserve成員函數(shù)允許你最小化必須進(jìn)行的重新分配的次數(shù),因而可以避免真分配的開銷和迭代器/指針/引用失效

  • size()返回容器中已經(jīng)保存的元素個(gè)數(shù)
  • capacity()返回容器可以保存的最大元素個(gè)數(shù)。如果要知道一個(gè)vector或string中有多少?zèng)]有被占用的內(nèi)存,可以讓capacity() 減去size();如果size和capacity返回同樣的值,容器中就沒有剩余空間了,而下一次插入(通過insert或push_back等)會(huì)引 發(fā)重分配。
  • resize(Container::size_type n)強(qiáng)制把容器改為容納n個(gè)元素。調(diào)用resize之后,size將會(huì)返回n。如果n小于當(dāng)前大小,容器尾部的元素會(huì)被銷毀。如果n大于當(dāng)前大小,新默認(rèn) 構(gòu)造的元素會(huì)添加到容器尾部。如果n大于當(dāng)前容量,在元素加入之前會(huì)發(fā)生重新分配
  • reserve(Container::size_type n)強(qiáng)制容器把它的容量改為至少n,提供的n不小于當(dāng)前大小。這一般強(qiáng)迫進(jìn)行一次重新分配,因?yàn)槿萘啃枰黾印#ㄈ绻鹡小于當(dāng)前容量,vector忽略 它,這個(gè)調(diào)用什么都不做,string可能把它的容量減少為size()和n中大的數(shù),但string的大小沒有改變。)

條款16: 如何將vector和string的數(shù)據(jù)傳給遺留的API

我們可以將vector或者string傳遞給數(shù)組/指針類型的參數(shù),如:

  • 用C風(fēng)格API返回的元素初始化一個(gè)vector,可以利用vector和數(shù)組潛在的內(nèi)存分布兼容性將存儲(chǔ)vecotr的元素的空間傳給API函數(shù):
// C API:此函數(shù)需要一個(gè)指向數(shù)組的指針,數(shù)組最多有arraySize個(gè)double
// 而且會(huì)對(duì)數(shù)組寫入數(shù)據(jù)。它返回寫入的double數(shù),不會(huì)大于arraySize
size_t fillArray(double *pArray, size_t arraySize);
// 建立一個(gè)vector,它的大小是maxNumDoubles 
vector<double> vd(maxNumDoubles); 
// 讓fillArray把數(shù)據(jù)寫入vd,然后調(diào)整vd的大小 為fillArray寫入的元素個(gè)數(shù)
vd.resize(fillArray(&vd[0], vd.size())); 
  • 讓C風(fēng)格API把數(shù)據(jù)放入一個(gè)vector,然后拷到你實(shí)際想要的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()); // 拷貝數(shù)據(jù)到deque
list<double> l(vd.begin(), vd.end()); // 拷貝數(shù)據(jù)到list
set<double> s(vd.begin(), vd.end()); // 拷貝數(shù)據(jù)到set
  • 如何將vector和string以外的STL容器中的數(shù)據(jù)傳給C風(fēng)格API?只要將容器的每個(gè)數(shù)據(jù)拷到vector,然后將它們傳給API:
void doSomething(const int* pints, size_t numInts); // C API (同上)
set<int> intSet; // 保存要傳遞給API數(shù)據(jù)的set
...
vector<int> v(intSet.begin(), intSet.end()); // 拷貝set數(shù)據(jù)到vector
if (!v.empty()) doSomething(&v[0], v.size()); // 傳遞數(shù)據(jù)到API

條款17:使用“交換技巧”來修整過剩容量

實(shí)際項(xiàng)目中可能遇到這樣的情況:剛開始時(shí),將大量數(shù)據(jù)插入到一個(gè)vector中,后來隨著實(shí)際的需要,將大量元素從這個(gè)vector中刪除,這樣的 話,vector中會(huì)占用大量未使用的內(nèi)存(通過函數(shù)capacity()可看到結(jié)果),如何將這些未使用的內(nèi)存釋放,可采用以下幾種方法:

vector<Contestant>(contestants).swap(contestants);

表達(dá)式vector<Contestant>(contestants)建立一個(gè)臨時(shí)vector,它是 contestants的一份拷貝:vector的拷貝構(gòu)造函數(shù)做了這個(gè)工作。但是,vector的拷貝構(gòu)造函數(shù)只分配拷貝的元素需要的內(nèi)存,所以這個(gè)臨 時(shí)vector沒有多余的容量。然后我們讓臨時(shí)vector和contestants交換數(shù)據(jù),這時(shí)contestants只有臨時(shí)變量的修整過的容量, 而這個(gè)臨時(shí)變量則持有了曾經(jīng)在contestants中的發(fā)脹的容量。在這里(這個(gè)語句結(jié)尾),臨時(shí)vector被銷毀,因此釋放了以前 contestants使用的內(nèi)存
同樣的技巧可以應(yīng)用于string:

string s;
... // 使s變大,然后刪除所有它的字符
string(s).swap(s); // 在s上進(jìn)行“收縮到合適”

條款18:避免使用vector<bool>

作為一個(gè)STL容器,vector<bool>有兩個(gè)問題。
它不是一個(gè)STL容器。它并未實(shí)際保存一個(gè)bool, 而是用位域的概念進(jìn)行了封裝。

標(biāo)準(zhǔn)庫提供了兩個(gè)替代品,它們能滿足幾乎所有需要。
第一個(gè)是deque<bool>。deque提供了幾乎所有vector所提供的(唯一值得 注意的是reserve和capacity),而deque<bool>是一個(gè)STL容器,它保存真正的bool值。
當(dāng)然,deque內(nèi)部內(nèi)存不是連續(xù)的。所以不能傳遞deque<bool>中的數(shù)據(jù)給一個(gè)希望得到bool數(shù)組的C API。
第二個(gè)vector<bool>的替代品是bitset。bitset不是一個(gè)STL容器,但它是C++標(biāo)準(zhǔn)庫的一部分。與STL容器不同,它的大小(元素?cái)?shù)量)在編譯期固定,因此它不支持插入和刪除元素。此外,因?yàn)樗皇且粋€(gè)STL容器,它也不支持iterator。
標(biāo)準(zhǔn)非STL容器是指可以認(rèn)為它們是容器,但是他們并不滿足STL容器的所有要求。前文提到的容器適配器stack、queue及priority_queue都是標(biāo)準(zhǔn)非STL容器的一部分。此外,valarray也是標(biāo)準(zhǔn)非STL容器。
bitset:一種高效位集合操作容器。


關(guān)聯(lián)容器

條款19:了解相等和等價(jià)的區(qū)別

set中的find函數(shù)采用的是等價(jià)準(zhǔn)則,而find算法采用的是相等準(zhǔn)則。

find算法在某個(gè)區(qū)間中查找一個(gè)元素時(shí),需要比較兩個(gè)對(duì)象,看一個(gè)對(duì)象的值是否等于零一個(gè)對(duì)象,它對(duì)相同的定義是相等,是以operator==為基礎(chǔ)的。
set的insert需要確定插入的元素的值是否已經(jīng)在set中了,它對(duì)相同的定義是等價(jià),是以operator<為基礎(chǔ)的。
兩個(gè)對(duì)象相等并不意味著它們的所有數(shù)據(jù)成員都有相等的值。
等價(jià)關(guān)系是以“在已排序的區(qū)間中對(duì)象值的相對(duì)順序”為基礎(chǔ)的。對(duì)于兩個(gè)對(duì)象x和y,如果按照關(guān)聯(lián)容器的排列順序,每個(gè)都不在另一個(gè)的前面,那么稱這兩個(gè)對(duì)象按照關(guān)聯(lián)容器的排列順序有等價(jià)的值。用表達(dá)式表示就是:!(x < y) && !(y < x) 為true,x和y對(duì)于operator<有等價(jià)的值。
一般情況下,一個(gè)關(guān)聯(lián)容器的比較函數(shù)不是operator<,甚至也不是less,是用戶定義的判別式,只要把<改為判別式的調(diào)用即可。
如果有一個(gè)不區(qū)分大小寫的set<string>,我們向其中插入Persephone,如果再插入persephone,后面一個(gè)將不會(huì)被插入,因?yàn)閮蓚€(gè)是等價(jià)的。稍后我們使用成員函數(shù)find去查找persephone,查找會(huì)成功,但是如果用find算法去查找,那么查找將會(huì)失敗。


條款20:為指針的關(guān)聯(lián)容器指定比較類型

當(dāng)關(guān)聯(lián)容器中保存的是對(duì)象指針時(shí),需要自己定義比較器(不是一個(gè)函數(shù),而是一個(gè)仿函數(shù)模板),不然關(guān)聯(lián)容器會(huì)按照指針大小進(jìn)行排序,而不是指針指向的內(nèi)容。


條款21: 永遠(yuǎn)讓比較函數(shù)對(duì)“相等的值”返回false

在關(guān)聯(lián)容器中,用戶自定義比較類型時(shí),當(dāng)兩個(gè)元素相等時(shí),應(yīng)該返回false。
舉例:建立一個(gè)set,比較類型用less_equal,然后插入一個(gè)10:

set<int, less_equal<int> > s; // s以“<=”排序
s.insert(10); // 插入10
然后再插入一次10:
s.insert(10);

關(guān)聯(lián)容器對(duì)“相同”的定義是等價(jià),因此set測試10B是否等價(jià)于10A。當(dāng)執(zhí)行這個(gè)測試時(shí),它自然是使用set的比較函數(shù)。在這一例子 里,是operator<=,因?yàn)槲覀冎付╯et的比較函數(shù)為less_equal,而less_equal意思就是operator<=。 于是,set將計(jì)算這個(gè)表達(dá)式是否為真:

!(10A <= 10B) && !(10B <= 10A) // 測試10A和10B是否等價(jià)

顯然,該表達(dá)式返回false,于是兩個(gè)10都會(huì)插入這個(gè)set,結(jié)果是set以擁有了兩個(gè)為10的值的拷貝而告終,也就是說它不再是一個(gè)set了。通過使用less_equal作為我們的比較類型,我們破壞了容器!


條款22:避免原地修改set和multiset的鍵

原地修改map和multimap的鍵值是不允許的,同時(shí),應(yīng)避免原地修改set和multiset的鍵(盡管這是允許的),因?yàn)檫@可能影響容器有序性的元素部分,破壞掉容器


條款23:考慮用有序vector代替關(guān)聯(lián)容器

在你的應(yīng)用中,如果查找的頻繁程度比插入和刪除的高很多,那么推薦你用有序vector代替關(guān)聯(lián)容器,這主要是從內(nèi)存引用失效頻率考慮的
用vector模擬關(guān)聯(lián)數(shù)組的代碼如下:

typedef pair<string, int> Data; // 在這個(gè)例子里 "map"容納的類型
class DataCompare { // 用于比較的類
 public:
     bool operator()(const Data& lhs, // 用于排序的比較函數(shù)
     const Data& rhs) const {
         return keyLess(lhs.first, rhs.first); // keyLess在下面
     }
 
   // 用于查找的比較函數(shù)
    bool operator()(const Data& Ihs, const Data::first_type& k) const{ 
         return keyLess(lhs.first, k);
    }

   bool operator()(const Data::first_type& k,  const Data& rhs) const{
         return keyLess(k, rhs.first);
   }
 
 private:
     bool keyLess(const Data::first_type& k1, const Data::first_type& k2) const{
         return k1 < k2;
     }
};
 
vector<Data> vd; // 代替map<string, int>
... // 建立階段:很多插入,幾乎沒有查找

// 結(jié)束建立階段。(當(dāng)模擬multimap時(shí),你可能更喜歡用stable_sort 來代替)
sort(vd.begin(), vd.end(), DataCompare());  
string s; // 用于查找的值的對(duì)象
... // 開始查找階段
if (binary_search(vd.begin(), vd.end(), s, DataCompare()))... // 通過binary_search查找
vector<Data>::iterator i = lower_bound(vd.begin(), vd.end(), s, 
DataCompare()); // 再次通過lower_bound查找,

對(duì)于這么使用它們的數(shù)據(jù)結(jié)構(gòu)的應(yīng)用來說,一個(gè)vector可能比一個(gè)關(guān)聯(lián)容器能提供更高的
性能(時(shí)間和空間上都是)。但不是任意的vector都會(huì),只有有序vector。因?yàn)橹挥杏行?br> 容器才能正確地使用查找算法——binary_search、lower_bound、equal_range等。但為什么一個(gè)(有序的)vector的二分法查找比一個(gè)二叉樹的二分法查找提供了
更好的性能?其中的一個(gè)是大小問題,另外一個(gè)是引用局部性問題。

考慮第一個(gè)大小問題。假設(shè)我們需要一個(gè)容器來容納Widget對(duì)象,而且,因?yàn)椴檎宜俣葘?duì)
我們很重要,我們考慮一個(gè)Widget的關(guān)聯(lián)容器和一個(gè)有序vector<Widget>。如果我們選擇
一個(gè)關(guān)聯(lián)容器,我們幾乎確定了要使用平衡二叉樹。這樣的樹是由樹節(jié)點(diǎn)組成,每個(gè)都不
僅容納了一個(gè)Widget,而且保存了一個(gè)該節(jié)點(diǎn)到左孩子的指針,一個(gè)到它右孩子的指針,
和(典型的)一個(gè)到它父節(jié)點(diǎn)的指針。這意味著在關(guān)聯(lián)容器中用于存儲(chǔ)一個(gè) Widget的空間
開銷至少會(huì)是三個(gè)指針。

與之相對(duì)的是,當(dāng)在vector中存儲(chǔ)Widget并沒有開銷:我們簡單地存儲(chǔ)一個(gè)Widget。當(dāng)然
,vector本身有開銷,在vector結(jié)尾也可能有空的(保留)空間(參見條款14),但是每
個(gè)vector開銷是可以忽略的(通常是三個(gè)機(jī)器字,比如,三個(gè)指針或兩個(gè)指針和一個(gè)int)
,而且如果必要的話,末尾空的空間可以通過“交換技巧”去掉(看見條款17)。即使這
個(gè)附加的空間沒有去掉,也并不影響下面的分析,因?yàn)楫?dāng)查找時(shí)不會(huì)引用那段內(nèi)存。

假設(shè)我們的數(shù)據(jù)結(jié)構(gòu)足夠大,它們可以分成多個(gè)內(nèi)存頁面,但是vector比關(guān)聯(lián)容器需要的
頁面要少。那是因?yàn)関ector不需要每個(gè)Widget的開銷,而關(guān)聯(lián)容器給每個(gè)Widget上附加了
三個(gè)指針。要知道為什么這很重要,假設(shè)在你使用的系統(tǒng)上一個(gè)Widget的大小是12個(gè)字節(jié)
,指針是4個(gè)字節(jié),一個(gè)內(nèi)存頁面是4096(4K)字節(jié)。忽略每個(gè)容器的開銷,當(dāng)用vector保
存時(shí),你可以在一頁面上放置341個(gè)Widget,但使用關(guān)聯(lián)容器時(shí)你最多只能放170個(gè)。因此
關(guān)聯(lián)容器和vector比起來,你將會(huì)使用大約兩倍的內(nèi)存。如果你使用的環(huán)境可以用虛擬內(nèi)
存,就很可以容易地看出那會(huì)造成大量的頁面錯(cuò)誤,因此一個(gè)系統(tǒng)會(huì)因?yàn)榇髷?shù)據(jù)量而明顯
慢下來。

實(shí)際上我在這里還是對(duì)關(guān)聯(lián)容器很樂觀的,因?yàn)槲覀兗僭O(shè)在二叉樹中的節(jié)點(diǎn)都群集在一個(gè)
相關(guān)的小內(nèi)存頁面集中。大部分STL實(shí)現(xiàn)使用自定義內(nèi)存管理器(實(shí)現(xiàn)在容器的配置器上—
—參見條款10和11)來達(dá)到這樣的群集,但是如果你的STL實(shí)現(xiàn)沒有改進(jìn)樹節(jié)點(diǎn)中的引用局
部性,這些節(jié)點(diǎn)會(huì)分散在所有你的內(nèi)存空間。那會(huì)導(dǎo)致更多的頁面錯(cuò)誤。即使使用了自定
義群集內(nèi)存管理器,關(guān)聯(lián)容器也會(huì)導(dǎo)致很多頁面錯(cuò)誤,因?yàn)椋幌襁B續(xù)內(nèi)存容器,比如ve
ctor,基于節(jié)點(diǎn)的容器更難保證在容器的遍歷順序中一個(gè)挨著一個(gè)的元素在物理內(nèi)存上也
是一個(gè)挨著一個(gè)。但當(dāng)進(jìn)行二分查找時(shí)那種內(nèi)存組織方式(譯注:遍歷順序中一個(gè)挨著一
個(gè)的元素在物理內(nèi)存上也是一個(gè)挨著一個(gè))正好是頁面錯(cuò)誤最少的。

概要:在有序vector中存儲(chǔ)數(shù)據(jù)很有可能比在標(biāo)準(zhǔn)關(guān)聯(lián)容器中保存相同的數(shù)據(jù)消耗更少的
內(nèi)存;當(dāng)頁面錯(cuò)誤值得重視的時(shí)候,在有序vector中通過二分法查找可能比在一個(gè)標(biāo)準(zhǔn)關(guān)
聯(lián)容器中查找更快。

當(dāng)然,有序vector的大缺點(diǎn)是它必須保持有序!當(dāng)一個(gè)新元素插入時(shí),大于這個(gè)新元素的
所有東西都必須向上移一位。它和聽起來一樣昂貴,如果vector必須重新分配它的內(nèi)在內(nèi)
存(參見條款14),則會(huì)更昂貴,因?yàn)関ector中所有的元素都必須拷貝。同樣的,如果一
個(gè)元素從vector中被刪除,所有大于它的元素都要向下移動(dòng)。vector的插入和刪除都很昂
貴,但是關(guān)聯(lián)容器的插入和刪除則很輕量。這就是為什么只有當(dāng)你知道你的數(shù)據(jù)結(jié)構(gòu)使用
的時(shí)候查找?guī)缀醪缓筒迦牒蛣h除混合時(shí),使用有序 vector代替關(guān)聯(lián)容器才有意義


條款24:當(dāng)關(guān)乎效率時(shí)應(yīng)該在map::operator[]和map-insert之間仔細(xì)選擇

STL map的operator[]被設(shè)計(jì)為簡化“添加或更新”功能,但事實(shí)上,當(dāng)“增加”被執(zhí)行時(shí),insert比operator[]更高效。當(dāng)進(jìn)行更新時(shí),情形正好相反,也就是,當(dāng)一個(gè)等價(jià)的鍵已經(jīng)在map里時(shí),operator[]更高效。
理由如下:當(dāng)進(jìn)行“增加”操作時(shí),operator[]會(huì)有三個(gè)函數(shù)調(diào)用:構(gòu)造臨時(shí)對(duì)象,撤銷臨時(shí)對(duì)象和對(duì)對(duì)象復(fù)制,而insert不會(huì)有;而對(duì)于更新操作,insert需要構(gòu)造和析構(gòu)對(duì)象,而operator[] 采用的對(duì)象引用,不會(huì)有這樣的效率損耗。一個(gè)較為高效的“添加或更新”功能實(shí)現(xiàn)如下:

 // map的類型
// KeyArgType和ValueArgtype是類型參數(shù)
template<typename MapType, typename KeyArgType,  typename ValueArgtype>
typename MapType::iterator
efficientAddOrUpdate(MapType& m, const KeyArgType& k, const ValueArgtype& v)
{
  // 找到k在或應(yīng)該在哪里;
  typename MapType::iterator Ib =  m.lower_bound(k); 
  // 如果Ib指向一個(gè)pair而且它的鍵等價(jià)于k...
  if(Ib != m.end() && !(m.key_comp()(k, Ib->first))) { 
       Ib->second = v; // 更新這個(gè)pair的值
       return Ib; // 并返回指向pair的迭代器
 } else{
      typedef typename MapType::value_type MVT;
      // 把pair(k, v)添加到m并返回指向新map元素的迭代器
       return m.insert(Ib, MVT(k, v)); 
 }
}

原創(chuàng)文章,轉(zhuǎn)載請(qǐng)注明: 轉(zhuǎn)載自董的博客
本文鏈接地址: http://dongxicheng.org/cpp/effective-stl-part1/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容