容器
條款1:仔細選擇你的容器
C++提供了很多可供程序員使用的容器:
(1) 標準STL序列容器:vector,string,deque和list
(2) 標準STL關聯容器:set,multiset,map和multimap
(5) vector<char>可以作為string的替代品
(6) vector作為標準關聯容器的替代品
(7) 幾種標準非STL容器,包括數組、bitset、valarray、stack、queue和priority_queue
又新增了一些容器,包括:array,unordered容器,還有tuple容器。
不同容器有不同的優缺點,用戶需要根據實際應用的特點綜合決定使用哪種容器,如:vector是一種可以默認使用的序列類型,當很頻繁地對序列中部進行插入和刪除時應該用list,當大部分插入和刪除發生在序列的頭或尾時可以選擇deque這種數據結構。
條款2:小心對“容器無關代碼”的幻想
本條款要告誡程序員:編寫與容器無關的代碼是沒有必要的。
有人想編寫這樣的程序,剛開始時使用vector存儲,之后由于需求的變化,將vector改為deque或者list,其他代碼不變。實際上,這基本上是做不到的。這是因為:不同的序列容器所對應了不同的迭代器、指針和引用的失效規則,此外,不同的容器支持的操作也不相同,如:vector支持reserve()和capacity(),而deque和list不支持;即使是相同的操作,復雜度也不一樣(如:insert),這會讓你的系統產生意想不到的瓶頸。
此外,鼓勵程序員在聲明容器和迭代器的時候使用typedef進行重命名,這能夠對你的程序進行封轉,從而使用起來更簡單,如有下面一個map容器:
map<string,vectorWidget>::iterator,CIStringCompare>;
如要用const_iterator遍歷這個map,你需不止一次地寫下:
map<string, vectorWidget>::iterator, CIStringCompare>::const_iterator
如果使用typedef,會快速方便很多。
條款3:使容器里對象的拷貝操作輕量而正確
容器容納了對象,但不是你給它們的那個對象。當你向容器中插入一個對象時,你插入的是該對象的拷貝而不是它本身;當你從容器中獲取一個對象時,你獲取的是容器中對象的拷貝。
拷貝是STL的基本工作方式。當你刪除或者插入某個對象時,現有容器中的元素會移動(拷s貝);當你使用了排序算法,remove、uniquer或者他們的同類,rotate或者reverse,對象會移動(拷貝)。
一個使拷貝更高效、正確的方式是建立指針的容器而不是對象的容器,即保存對象的指針而不是對象,然而,指針的容器有它們自己STL相關的頭疼問題,改進的方法是采用智能指針。
條款4:用empty來代替檢查size()是否為0
對于任意容器c,寫下
if (c.size() == 0)
本質上等價于寫下
if (c.empty())
但是為什么第一種方式比第二種優呢?理由很簡單:對于所有的標準容器,empty是一個常數時間的操作,但對于一些list實現,size花費線性時間。
這什么造成list這么麻煩?為什么不能也提供一個常數時間的size?如果size是一個常數時間操作,當進行增加/刪除操作時每個list成員函數必須更新list的大小,也包括了splice,這會造成splice的效率降低(現在的splice是常量級的),反之,如果splice不必修改list大小,那么它就是常量級地,而size則變為線性復雜度,因此,設計者需要權衡這兩個操作的算法:一個或者另一個可以是常數時間操作。
條款5:盡量使用區間成員函數代替單元素操作
給定兩個vector,v1和v2,怎樣使v1的內容和v2的后半部分一樣?
可行的解決方案有:
(1)使用區間函數assign:
v1.assign(v2.begin() + v2.size() / 2, v2.end());
(2)使用單元素操作:
vector<Widget>::const_iterator ci = v2.begin() + v2.size() / 2;
ci != v2.end();
++ci)
v1.push_back(*ci);
(3)使用copy區間函數
v1.clear();
copy(v2.begin() + v2.size() / 2, v2.end(), back_inserter(v1));
(4)使用insert區間函數
v1.insert(v1.end(), v2.begin() + v2.size() / 2, v2.end());
最優的方案是assign方案,理由如下:
首先,使用區間函數的好處是:
● 一般來說使用區間成員函數可以輸入更少的代碼。
● 區間成員函數會導致代碼更清晰更直接了當。
使用copy區間函數存在的問題是:
【1】 需要編寫更多的代碼,比如:v1.clear(),這個與insert區間函數類似
【2】 copy沒有表現出循環,但是在copy中的確存在一個循環,這會降低性能
使用insert單元素版本的代碼對你征收了三種不同的性能稅,分別為:
【1】 沒有必要的函數調用;
【2】 無效率地把v中的現有元素移動到它們最終插入后的位置的開銷;
【3】 重復使用單元素插入而不是一個區間插入必須處理內存分配。
下面進行總結:
說明:參數類型iterator表示容器的迭代器類型,也就是container::iterator,參數類型InputIterator表示可以接受任何輸入迭代器。
【1】 區間構造
所有標準容器都提供這種形式的構造函數:
container::container(InputIterator begin,
// 區間的起點
InputIterator end);
// 區間的終點
【2】區間插入
所有標準序列容器都提供這種形式的insert:
void container::insert(iterator position,
// 區間插入的位置
InputIterator begin,
// 插入區間的起點
InputIterator end);
// 插入區間的終點
關聯容器使用它們的比較函數來決定元素要放在哪里,所以它們了省略position參數。
void container::insert(lnputIterator begin, InputIterator end);
【3】區間刪除
每個標準容器都提供了一個區間形式的erase,但是序列和關聯容器的返回類型不同。序列容器提供了這個:
iterator container::erase(iterator begin, iterator end);
而關聯容器提供這個:
void
container::erase(iterator begin, iterator end);
為什么不同?解釋是如果erase的關聯容器版本返回一個迭代器(被刪除的那個元素的下一個)會招致一個無法接受的性能下降.
注意這個特點在新版本的STL中已經解決了
【4】 區間賦值
所有標準列容器都提供了區間形式的assign:
void
container::assign(InputIterator begin, InputIterator end);
條款7:當使用new得指針的容器時,記得在銷毀容器前delete那些指針
條款8:永不建立auto_ptr的容器**//注意,在新版本中auto_ptr已經不再被使用
條款9:在刪除選項中仔細選擇
(1)假定你有一個標準STL容器,c,容納int,
Container<int> c;
而你想把c中所有值為1963的對象都去,則不同的容器類型采用的方法不同:沒有一種是通用的.
- 如果采用連續內存容器(vector、queue和string),最好的方法是erase-remove慣用法:
c.erase(remove(c.begin(), c.end(), 1963),c.end());
// 當c是vector、string
// 或deque時,erase-remove慣用法是去除特定值的元素的最佳方法
- 對于list,最有效的方法是直接使用remove函數:
c.remove(1963);
- 對于關聯容器,解決問題的適當方法是調用erase:
c.erase(1963);
// 當c是標準關聯容器時,erase成員函數是去除特定值的元素的最佳方法
(2)讓我們換一下問題:不是從c中除去每個有特定值的元素,而是消除下面判斷式返回真的每個對象:
bool badValue( int x);
// 返回x是否是“bad”
- 對于序列容器(vector、list、deque和string),只需要將remove換成remove_if即可:
// 當c是vector、string
// 或deque時這是去掉badValue返回真的對象的最佳方法
c.erase(remove_if(c.begin(), c.end(), badValue),c.end());
// 當c是list時這是去掉badValue返回真的對象的最佳方法
c.remove_if(badValue);
- 對于關聯容器,有兩種方法處理該問題,一個更容易編碼,另一個更高效。“更容
易但效率較低”的解決方案用remove_copy_if把我們需要的值拷貝到一個新容器中,然后把原容器的內容和新的交換:
“更高效”的解決方案是直接從原容器刪除元素。不過,因為關聯容器沒有提供類似remove_if的成員函數,所以我們必須寫一個循環來迭代c中的元素,和原來一樣刪除元素:
AssocContainer<int> c;
...
for(AssocContainer<int>::iterator i = c.begin();i != c.end();){
if(badValue(*i))
c.erase(i++);
else
++i;
}
- 對于vector、string、list和deque,必須利用erase的返回值。那個返回值正是我們需要的:一旦刪除完成,它就是指向緊接在被刪元素之后的元素的有效迭代器。換句話說,我們這么寫:
for(SeqContainer<int>::iterator i = c.begin(); i != c.end();){
if(badValue(*i)){
i = c.erase(i);
// 通過把erase的返回值賦給i來保持i有效
}
else
++i;
}
條款12:對STL容器線程安全性的期待現實一些
在工程中多線程操作STL的場景應該還是比較常見的,一個典型的例子就是用其來做生產者——消費者模型的隊列或者其他共享隊列,這樣為了應對線程安全問題我們必須自己對容器操作進行封裝。這是我自己實現的的封裝類:
template< typename Container>
// 獲取和釋放容器的互斥量的類的模板核心;
class Lock {
public :
// 忽略了很多細節
Lock( const Containers container) : c(container){
// 在構造函數獲取互斥量
getMutexFor(c);
}
~Lock(){
// 在析構函數里釋放它
releaseMutexFor(c);
}
private:
const Container& c;
};
原創文章,轉載請注明: 轉載自董的博客
本文鏈接地址: http://dongxicheng.org/cpp/effective-stl-part1/