【C++】Effective STL:50条有效使用STL的经验
第一條:慎重選擇容器類型
1、C++容器:先混個眼熟
序列容器:array、vector、string、deque、list、forward_list
有序關聯容器:set、map、multiset、multimap
無序關聯容器:unordered_set、unordered_map、unordered_multiset、unordered_multimap
容器適配器:stack、queue、priority_queue
2、C++容器:簡介
2.1 序列容器
序列容器實現了可以順序訪問的數據結構。
array:靜態連續容器;
vector:動態連續容器,超出容器大小會自動分配內存;
deque:雙端隊列;
list:雙鏈表;
forward_list:單鏈表;
2.2 有序關聯容器
關聯容器實現可以快速搜索的排序數據結構(O(log n)復雜度)。
set:只有鍵,沒有值的集合,按鍵排序,并且鍵是唯一的;
map:鍵值對的集合,按鍵排序,并且鍵是唯一的;
multiset:只有鍵,沒有值的集合,按鍵排序,鍵可以不唯一;
multimap:鍵值對的集合,按鍵排序,鍵可以不唯一;
2.3 無序關聯容器
無序關聯容器實現了可以快速搜索的未排序(哈希)數據結構(O(1)最好,O(n)最壞情況的復雜性)。
unordered_set:只有鍵,沒有值的集合,按鍵哈希散列,并且鍵是唯一的;
unordered_map:鍵值對的集合,按鍵哈希散列,并且鍵是唯一的;
unordered_multiset:只有鍵,沒有值的集合,按鍵哈希散列,鍵可以不唯一;
unordered_multimap:鍵值對的集合,按鍵哈希散列,鍵可以不唯一;
2.4容器適配器
容器適配器為順序容器提供了不同的接口。
stack:先進后出;
queue:先進先出;
priority_queue:優先級隊列
3、選擇容器
默認序列容器:vector,先考慮vector是否符合要求,如果不符合再選擇其它的;
容器大小從始至終不會變,選擇array:例如記錄一年中每個月的收入;
需要在序列中間做插入和刪除操作,選擇list;
需要在序列頭部、尾部做插入和刪除操作時,選擇deque;
第二條:不要試圖編寫獨立于容器類型的代碼
1、禁止對容器進一步泛化
每一種容器是針對一類場景的泛化,不要試圖進一步泛化容器。比如:針對當下場景使用vector容器,寫代碼時就圍繞vector接口來寫,不要試圖寫出能夠兼容list、deque的代碼。雖然這么做出發點是好的,但是最終總是誤入歧途。
2、建議封裝容器到類中
要想減少再替換容器類型時所需要修改的代碼,可以把容器隱藏到一個類中,并盡量減少與容器相關的外部可見的接口。
第三條:確保容器中的對象拷貝正確而高效
1、使用智能指針
使拷貝動作高效、正確,并防止剝離問題發生的一個簡單辦法是使容器包含指針而不是對象,最佳選擇是使用智能指針。
第四條:調用empty而不是檢查size()是否為0
理由很的簡單:empty對所有的標準容器都是常數時間操作,而對一些list實現,size耗時線性增長。
第五條:區間成員函數優于與之對應的單元素成員函數
1、區間成員函數
區間成員函數像STL算法一樣,使用兩個迭代器參數來確定該成員操作所執行的區間。
2、區間成員函數的優點
代碼量少、表達清晰直接,易寫易懂。
3、何時使用區間成員函數
1)通過利用插入迭代器的方式來限定目標區間的copy調用,幾乎都應該被替換為對區間成員函數的調用;
2)所有標準容器都提供了使用區間創建的構造函數;
3)所有標準容器都提供了使用區間插入的insert函數;
4)所有標準容器都提供了使用區間刪除的erase函數;
5)所有標準容器都提供了使用區間賦值的assign函數
第六條:當心C++編譯器最煩人的分析機制
請使用命名的迭代器對象,這消除二義性,維護代碼的人更容易理解。
這里舉例一個容易犯錯的代碼:將變量定義寫成了函數聲明。
第七條:如果容器中包含了通過new操作創建的指針,切記在容器對象析構前將指針delete掉
STL容器很智能,但沒有智能到知道是否該刪除自己所包含的指針所指向的空間。為了避免內存泄漏,建議使用引用計數形式的智能指針對象。
第八條:切勿創建包含auto_ptr的容器對象
不多說了,很多年以前就不再使用auto_ptr了。
第九條:慎重選擇刪除元素的方法
1、刪除特定值
對于連續容器,使用erase-remove:v.erase(remove(v.begin(), v.end(), 1987), c.end());
對于非連續容器list,使用remove函數;
對于關聯容器,使用erase;
2、刪除滿足特定條件的對象
對于連續容器,使用erase-remove_if;
對于非連續容器,使用list::remove_if
對于關聯容器,使用remove_copy_if和swap或者遍歷使用erase,注意參數使用后綴遞增;
第十條:了解分配子(allocator)的約定和限制
一個類型為T的對象,它的默認分配子allocator的兩個類型定義分別是allocator::pointer和allocator::reference。
其他待完善,沒有理解分配子的作用。
第十一條:理解自定義分配子的合理用法
第十二條:切勿對STL容器的線程安全性有不切實際的依賴
對容器成員函數的每次調用,都鎖住容器直到調用結束;
在容器返回的每個迭代器的生存期結束前,都鎖住容器;
第十三條:vector和string優先于動態分配的數組
不解釋了,直接照做就是了。
第十四條:使用reserver來避免不必要的重新分配
reserver成員函數能使你把重新分配的次數減少到最低限度。
size:容器中有多少元素;注意:不等于容器容納多少元素;
capacity:容器已經分配的內存可以容納多少元素;
resize:強迫容器改變到包含n個元素的狀態。
reserve:強迫容器把它的容器變為至少是n,如果n不小于當前的大小,會重新分配;如果小于,什么也不做。
第十五條:注意string實現的多樣性
不同的string實現以不同的方式來組織下列信息:字符串的大小、容量capacity、字符串的值、對值的引用計數。
第十六條:了解如果把vector和string數據傳給舊的API
vector返回C API:
vector<int> v;
if(!v.empty()){doSomething(&v[0], v.size());
}
注意:不要用v.begin()代替&v[0]
string返回C API:s.c_str();
第十七條:使用“swap技巧”除去多余的容量
class Contestant{... ...};
vector<Contestant> contestants;
... ...
vector<Contestant>(contestants).swap(contestants);
string s;
... ...
string(s).swap(s);
第十八條:避免使用vector
首先,它不是一個STL容器;
其次,它并不存儲bool
代替方法:
deque<bool>
或者使用bitset
第十九條:理解相等(equality)和等價(equivalence)的區別
相等:operator==
等價:!(x<y) && !(y<x),對于兩個對象x和y,如果按照關聯容器c的排列順序,每個都不在另一個的前面,那么稱這兩個對象按照c的排列順序是等價的。
第二十條:為包含指針的關聯容器指定比較類型
如果什么也不做,默認是對指針的地址做排序。必須自己編寫比較函數子類。
比較函數模板如下:
struct DereferenceLess {tumplate<typename PtrType>bool operator()(PtrType pT1, PtyType pT2) const{return *pT1 < *pT2;}
}
第二十一條:總是讓比較函數在等值情況下返回false
如果比較函數在等值情況下返回true,兩個相等的值,可能不等價。
比如使用less_equal(operator<=)作為比較函數,x=y=10時,!(x<=y) && !(y<=x)的結果為false,即最終會得出:x!=y
第二十二條:切勿直接修改set或multiset的值
所有的標準關聯容器是按照一定順序來存放的,如果修改了會打破容器的有序性。
第二十三條:考慮用排序的vector替代關聯容器
如果元素少并且幾乎不會有插入刪除操作,可以考慮使用排序的vector替代關聯容器,無論是空間還是時間都是最優的。
第二十四條:當效率至關重要時,請在map::operator[]和map::insert之間謹慎做出選擇
map::operator[]的設計目的是為了提供“添加和更新”的功能;
在作為“添加”操作時,insert比operator[]效率高;
當作為“更新”操作時,優先使用operator[]
第二十五條:熟悉非標準的散列表
非標準的散列表:hast_set、hast_multiset、hash_map、hash_multimap
注意這里說的是舊版本的STL
第二十六條:iterator優先于const_iterator、reverse_iterator及const_reverse_iterator。
原因:
1、容器中的instert和crase函數的形參只接受iterator類型,不接受const_iterator、reverse_iterator及const_reverse_iterator。
2、從iterator到const_iterator,或者從reverse_iterator到const_reverse_iterator之間都存在隱式轉換,但是反過來技術上可以實現,但不推薦,效率不能保證。
3、在同一個表達式中混用iterator和const_iterator,比如比較操作,會發生隱式轉換,有時會出現問題。
第二十七條:使用distance和advance將容器的const_iterator轉換成iterator
distance:用于取得兩個迭代器之間的距離;
advance:用于將一個迭代器移動指定的距離。
typedef deque<int> IntDeque;
typedef IntDeque::iterator Iter;
typedef IntDeque::const_iterator ConstIter;IntDeque d;
ConstIter ci;
...
Iter i(d.begin()); /迭代器 i 指向容器 d 起始位置
advance(i, distance<ConstIter>(i, ci)); /注意這里指明distance所使用的類型為ConstIter
效率問題:對于隨機訪問的迭代器(如vector、string、deque產生的迭代器)而言,執行時間是常數時間;對于其他標準容器以及散列表的迭代器而言,執行時間是一個線性時間,因此推薦二十六條,盡量用iterator代替const_iterator
第二十八條:正確理解由reverse_iterator的base()成員函數所產生的iterator的用法
1、插入操作:如果要在一個迭代器reverse_iterator ri指定的位置上插入一個新元素,則只需在ri.base()位置處插入元素即可。
2、刪除操作:如果要在一個迭代器reverse_iterator ri指定的位置上刪除一個元素,要先遞增ri,然后在調用base()函數,例如:v.erase((++ri).base());
第二十九條:對于逐個字符的輸入請考慮使用istreambuf_iterator
對比istream_iterator,它在默認請看下會跳過空白字符,并且效率低。
讀取一個文本文件的內容到一個string對象中,推薦的方案如下:
ifstream inputFile("test.txt");
string fileData((istreambuf_iterator<char>(inputFile)), istreambuf_iterator<char>());
第三十條:確保目標區間足夠大
對于vector、string、deque、list容器在尾部插入對象時,需要使用back_inserter函數;
其中deque和list容器還可以使用front_inserter在頭部插入對象。
為了提供插入操作的性能,對于vector和string容器可以使用reserve來提前分配好內存。
第三十一條:了解各種與排序有關的選擇
按照性能由高到底排序:
1、partition:把滿足特定條件的元素放到前面;
2、stable_partition:把滿足特定條件的元素放到前面,并且是穩定的排序(在遇到相等的對象時,還會按照出場順序排序);
3、nth_element:找出部分最優的對象,可以不按照順序,比如列出前十個,而且這十個可以不用排序;
4、partial_sort:部分排序;
5、sort:全部排序
6、stable_sort:穩定版本的全部排序,不僅全部排好序,而且在遇到相等的對象時,還會按照出場順序排序。
注意:list::sort是穩定排序。
第三十二條:如果確實需要刪除元素,則需要在remove這一類算法之后調用erase
注意:remove不能刪除容器中元素。
因為從容器中刪除元素的唯一方法是調用該容器的成員函數,而remove并不知道它操作的元素所在的容器,所以remove不可能從容器中輸出刪除元素。
remove實際功能:把不用被刪除的元素放到容器前部,把需要被刪除的元素放到容器尾部。
如果需要真正刪除元素,需要使用erase和remove配合
vector<int> v;
...
v.erase(remove(v.begin(), v.end(), 99), v.end());
同理unique也需要和erase配合使用。
注意:list::remove會真正刪除元素,并且比使用erase-remove配合使用更高效,list::unique也會真正刪除元素。
第三十三條:對包含指針的容器使用remove這一類算法時要特別小心
對指針容器使用erase-remove組合來刪除時,很可能造成內存泄漏。
其實在執行remove后,還沒有執行erase之前已經發生內存泄漏。因為remove在將不需要刪除的指針元素移動到容器頭部時,會覆蓋掉需要刪除的指針元素,造成內存泄漏;
再執行erase時,也會造成內存泄漏,因為還沒釋放內存,就刪除了指針。
同理:remove_if和unique也會造成內存泄漏。
解決方法有兩種:
1、使用帶有引用計數的智能指針
2、使用partition代替remove
第三十四條:了解哪些算法要求使用排序的區間作為參數
需要排序后才能使用的算法:
// 使用二分法查找的算法,需要先排序
binary_search
lower_bound
upper_bound
equal_range// 集合操作,為了提高效率(為了保證線性時間效率),需要先排序
set_union
set_intersection
set_defference
set_symmetric_defference// 合并操作,為了提高效率
merge
inplace_merge// 判斷一個區間中的所有的對象是否都在另一個區間中,為了提高效率
includes
第三十五條:通過mismatch或lexicographical_compace實現簡單的忽略大小寫的字符串比較
略
第三十六條:理解copy_if算法的正確實現
STL中包含copy的算法
copy
copy_backward
replace_copy
replace_copy_if
reverse_copy
unique_copy
remove_copy
remove_copy_if
rotate_copy
partial_sort_copy
uninitialized_copy
但是就是沒有copy_if算法,需要自己實現
template<typename InputIterator,typename OutputIteratortypename Predicate>
OutputIterator copy_if(InputIterator begin,InputIterator end,OutputIterator destBegin,Predicate p)
{while (begin != end) {if (p(*begin))*destBegin++ = *begin;++begin;}return destBegin;
}
第三十七條:使用accumulate或者for_each進行區間統計
count:統計一個區間中有多少個元素;
count_if:統計滿足某個條件的元素個數;
min_element:獲取區間中最小值;
max_element:獲取區間中最大值;
accumulate:對區間進行自定義的操作,比如計算一個容器中字符串長度的總和。
/計算和
list<double> ld;
...
double sum = accumulate(ld.begin(), ld.end(), 0.0);/ 計算容器中字符串長度的總和
string::size_type
stringLengthSum(string::size_type sumSoFar ,const string& s)
{return sumSoFar + s.size();
}set<string> ss;
...//插入一些字符串
string::size_type lengthSum = accumulate(ss.begin(), ss.end(), static_cast<string::size_type>(0),stringLengthSum);
};
第三十八:遵循按值傳遞的原則來設計函數子類
1、在C和C++的標準庫函數中,如果需要函數作為參數,需要使用函數指針,并且函數指針是按值傳遞的(被復制);
2、在STL中,函數對象在函數之間來回傳遞也是按值傳遞(被復制);
3、因為函數對象是按值傳遞,即需要來回復制,因此函數對象要盡可能的小;
4、函數對象必須是單態的(不能是多態),也就是說,它們不能有虛函數,因此在傳遞過程中,會出現剝離問題(slicing problem):在對象復制過程中,派生部分可能會被去掉,而僅保留了基類部分。
第三十九:確保判別式是“純函數”。
1、判別式函數(predicate function):是一個返回值為bool類型(或者可以隱式地轉換為bool類型)的函數。
2、判別式類(predicate class):是一個函數類,它的operator()函數是一個判別式。
3、純函數(pure function):是指返回值僅僅依賴于其參數的函數。
4、在STL中容器使用的比較函數都是判別式,比如:find_if以及各種與排序相關的算法,這些算法要求每執行一次都要保證結果是唯一,不會受其他參數的干擾,比如全局變量、靜態變量等。
第四十條:若一個類是函數類,則應使它可配接
1、STL函數配接器not1、not2、bind1st、bind2nd等都要求一些特殊的類型定義,提供這些必要的類型定義的函數對象被稱為可配接的(adaptable)函數對象。
2、特殊的類型定義:argument_type、first_argument_type、second_argument_type、result_type
3、繼承 std::unary_function、std::binary_function來完成上述的特殊的類型定義
第四十一條:理解ptr_fun、mem_fun和mem_fun_ref的來由
STL算法只支持非成員函數,不支持成員函數(無法通過編譯),如果要使用成員函數需要使用ptr_fun、mem_fun或者mem_fun_ref來將成員函數封裝到一個函數類中
第四十二條:確保less與operator<具有相同的語義
1、不要特化一個位于std名字空間中的模板
2、operator<是std::less默認實現方式,不要修改std::less的行為,因為這樣做很可能會誤導其它的程序員。
第四十三條:算法調用優先于手寫的循環
1、先看一個手寫循環和使用算法的例子
一個支持重畫的類Widget
class Widget{
public:...void redraw() const;...
}
使用手寫循環來重畫容器list中的所有的Widget:
list<Widget> lw;
...
for (list<Widget>::iterator i=iw.begin(); i!=iw.end(); ++i){i->redraw();
}
使用for_each循環算法來完成
for_each(lw.begin(), lw.end()),mem_fun_ref(&Widget::redray));
mem_fun_ref:封裝成員函數,使它可以用在算法中,因為算法只能使用非成員函數
2、使用算法的優點
效率高、可維護性好。
理由:略,一定要用起來
第四十四條:容器的成員函數優先于同名的算法
1、列舉出這些函數
關聯容器:count、find、lower_bound、upper_bound、equal_range
list容器:remove、remove_if、unique、sort、merge、reverse
2、理由
速度更快、成員函數通常與容器結合的更加緊密
第四十五條:正確區分count、find、binary_search、lower_bound、upper_bound和equal_range
1、如果容器的區間是排序好的使用:binary_search、lower_bound、upper_bound和equal_range,
這些算法是對數時間的效率。
2、如果容器的區間不是排序好的使用:count、count_if、find、find_if,
這些算法是線性時間的效率。
count:區間中是否存在某個特定的值?如果存在的話,有多少個拷貝?
find:區間中有這樣的值嗎?如果有,它在哪里?
兩者還有一個區別:find找到后就返回,效率高;count要遍歷一遍容器的區間
binary_search:測試一個排序的容器區間中是否存在某一個特定的值,返回true或者false;
equal_range:查找一個值在容器區間中的位置;
lower_bound:查找一個值在容器區間中第一次出現的位置,如果沒有,則返回適合插入該值的位置;
第四十六條:考慮使用函數對象而不是函數作為STL算法的參數
使用函數對象比直接使用函數作為STL算法的參數,更高效。
原因是:編譯器可以對函數對象做內斂優化,而函數作為參數,其實是指針,編譯器不會對指針做優化。
第四十七條:避免產生“直寫型”(wirte-only)的代碼
1、不要寫過于復雜的嵌套函數調用
2、注意添加代碼的注釋
第四十八條:總是包含(#include)正確的頭文件
1、有些標準頭文件可以省略,也可以通過編譯,但是考慮移植性,強烈建議不要省略;
2、標準的STL容器都被聲明在與之同名的頭文件中:vector、list等,特殊的:multiset和set都在<set>中,map和multimap都在<map>中
3、算法被聲明在<algorithm>中,除了下面四個
4、accumulate、inner_product、adjacent_difference、partial_sum被聲明在<numeric>中
5、特殊類型的迭代器被聲明在<iterator>中
6、標準的函數類和函數配接器被聲明在<functional>中:如not1、bind2nd
第四十九條:學會分析與STL相關的編譯器診斷信息
1、std::basic_string<… …>很長的一個信息,翻譯成string就好;
2、vector和string的迭代器通常就是指針,所以錯誤的使用iterator錯誤信息中會有:double *
3、如果錯誤消息源于某一個STL算法的內部實現,那么也許在調用算法的時候,使用了錯誤的類型。
第五十條:熟悉與STL相關的web站點
SGI站點:www.sgi.com/tech/stl/
STLport站點:www.stlport.org
Boost:www.boost.org
總結
以上是生活随笔為你收集整理的【C++】Effective STL:50条有效使用STL的经验的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【C++】智能指针(一)入门
- 下一篇: 【SVN】linux下svn命令参数详解