目錄ios
====================================================程序員
第一章 容器算法
第二章 Vector和string數組
第三章 關聯容器安全
第四章 迭代器數據結構
第五章 算法多線程
第六章 函數less
第七章 在程序中使用STL函數
====================================================佈局
第1章 容器
第1條:慎重選擇容器類型。
標準STL序列容器:vector、string、deque和list。
標準STL關聯容器:set、multiset、map和multimap。
非標準序列容器slist和rope。slist是一個單向鏈表,rope本質上是一「重型」string。
非標準的關聯容器hash_set、hase_multiset、hash_map和hash_multimap。
vector<char> 做爲string的替代。(見第13條)
vector做爲標準關聯容器的替代。(見第23條)
幾種標準的非STL容器,包括數組、bitset、valarray、stack、queue和priority_queue。
你是否關心容器中的元素是如何排序的?若是不關心,選擇哈希容器.
容器中數據的佈局是否須要和C兼容?若是須要兼容,就只能選擇vector。(第16條)
元素的查找速度是不是關鍵的考慮因素?若是是,就要考慮哈希容器、排序的vector和標準關聯容器-或許這就是優先順序。
對插入和刪除操做,你須要事務語義嗎?若是是,只能選擇list。由於在標準容器中,只有list對多個元素的插入操做提供了事務語義。
deque是惟一的、迭代器可能會變爲無效(插入操做僅在容器末尾發生時,deque的迭代器可能會變爲無效)而指向數據的指針和引用依然有效的標準STL容器。
第2條:不要試圖編寫獨立於容器類型的代碼。
若是你想編寫對大多數的容器都適用的代碼,你只能使用它們的功能的交集。不一樣的容器是不一樣的,它們有很是明顯的優缺點。它們並非被設計用來交換使用的。
第3條:確保容器中的對象拷貝正確而高效。
copy in,copy out,是STL的工做方式,它總的設計思想是爲了不沒必要要的拷貝。
第4條:調用empty而不是檢查size()是否爲0。
理由很簡單:empty對全部的標準容器都是常數時間操做,而對一些list的實現,size耗費線性時間。
第5條:區間成員函數優先於與之對應的單元素成員函數。
區間成員函數寫起來更容易,更能清楚地表達你的意圖,並且它們表現出了更高的效率。
第6條:小心C++編譯器最煩人的分析機制。
把形參加括號是合法的,把整個形參(包括數據類型和形參名)用括號括起來是非法的。
第7條:若是容器中包含經過new操做建立的指針,切記在容器對象析構前將指針delete。
STL很智能,但沒有智能到知道是否該刪除本身所包含的指針所指向的對象的程度。爲了不資源泄漏,你必須在容器被析構前手工刪除其中的每一個指針,或使用引用計數形式的智能指針(好比Boost的sharedprt)代替指針。
第8條:切勿建立包含auto_ptr的容器對象。
拷貝一個auto_ptr意味着改變它的值。例如對一個包含auto_ptr的vector調用sort排序,結果是vector的幾個元素被置爲NULL而相應的元素被刪除了。
第9條:慎重選擇刪除元素的方法。
要刪除容器中指定值的全部對象:
若是容器是vector、string或deque,則使用erase-remove習慣用法。
SeqContainer<int> c;
c.erase(remove(c.begin(),c.end(),1963),c.end());
若是容器是list,則使用list::remove。
若是容器是一個標準關聯容器,則使用它的erase成員函數。
要刪除容器中知足特定條件的全部對象:
若是容器是vector、string或deque,則使用erase-remove_if習慣用法。
若是容器是list,則使用list::remove_if。
若是容器是一個標準關聯容器,則使用remove_copy_if和swap,或者寫一個循環遍歷容器的元素,記住當把迭代器傳給erase時,要對它進行後綴遞增。
AssocCOntainer<int> c;
...
AssocContainer<int> goodValues;
remove_copy_if(c.begin(), c.end(),
inserter(goodValues, goodValues.end()), badValue);
c.swap(goodValues);
或
for(AssocContainer<int>::iterator i = c.begin();i !=c.end();/* do nothing */){
if(badValue(*i)) c.erase(i++);
else ++i;
}
要在循環內部作某些(除了刪除對象以外的)操做:
若是容器是一個標準序列容器,則寫一個循環來遍歷容器中的元素,記住每次掉用erase時,要用它的返回值更新迭代器。
若是容器是一個標準關聯容器,則寫一個循環來遍歷容器中的元素,記住每次把迭代器傳給erase時,要對迭代器作後綴遞增。
第12條:切勿對STL容器的線程安全性有不切實際的依賴。
對一個STL實現你最多隻能指望:多個線程讀是安全的;多個線程對不一樣的容器寫入操做是安全的。
你不能指望STL庫會把你從手工同步控制中解脫出來,且你不能依賴於任何線程支持。
第2章 vector和string
第13條:vector和string優先於動態分配的數組。
若是用new,意味着你要確保後面進行了delete。
若是你所使用的string是以引用計數來實現的,而你又運行在多線程環境中,並認爲string的引用計數實現會影響效率,那麼你至少有三種可行的選擇,並且,沒有一種選擇是捨棄STL。首先,檢查你的庫實現,看看是否能夠禁用引用計數,一般是經過改變某個預處理變量的值。其次,尋找或開發一個不使用引用計數的string實現。第三,考慮使用vector<char>而不是string。vector的實現不容許使用引用計數,因此不會發生隱藏的多線程性能問題。
第14條:使用reserve來避免沒必要要的從新分配。
一般有兩種方式來使用reserve以免沒必要要的從新分配。第一種方式是,若能確切知道或大體預計容器中最終會有多少個元素,則此時可以使用reserve。第二種方式是,先預留足夠大的空間,而後,當把全部的數據都加入後,再去除多餘的容量。
第15條:注意string實現的多樣性。
若是你想有效的使用STL,那麼你須要知道string實現的多樣性,尤爲是當你編寫的代碼必需要在不一樣的STL平臺上運行而你又面臨着嚴格的性能要求的時候。
第16條:瞭解如何把vector和string數據傳給舊的API。
若是你有個vector v,而你須要獲得一個只想v中的數據的指針,從而可把數據做爲數組來對才,那麼只須要使用&v[0]就能夠了,也能夠用&*v.begin(),可是很差理解。對於string s,隨應的形式是s.c_str()。
若是想用來自C API的數據來初始化一個vector,那麼你能夠利用vector和數組的內存佈局兼容性,先把數據寫入到vector中,而後把數據拷貝到指望最終寫入的STL容器中。
第17條:使用「swap技巧」除去多餘的容量。
vector<Contestant>(contestants).swap(contestants);
表達式vector<Contestant>(contestants)建立一個臨時的矢量,它是contestants的拷貝:這是由 vector的拷貝構造函數來完成的。然而,vector的拷貝構造函數只爲所拷貝的元素分配所須要的的內存,因此這個臨時矢量沒有多餘的容量。而後咱們把臨時矢量中的數據和contestants中的數據做swap操做,在這以後,contestants具備了被去除以後的容量,即原先臨時變量的容量,而臨時變量的容量則變成了原先contestants臃腫的容量。到這時,臨時矢量被析構,從而釋放了先前爲contestants所佔據的內存。
一樣的技巧對string也實用:
string s;
...
string(s).swap(s);
第3章 關聯容器
第19條:理解相等(equality)和等價(equivalence)的區別。
標準關聯容器老是保持排列順序的,因此每一個容器必須有一個比較函數(默認爲less)。等價的定義正是經過該比較函數而肯定的。相等必定等價,等價不必定相等。
第20條:爲包含指針的關聯容器指定比較類型。
每當你建立包含指針的關聯容器時,容器將會按照指針的值(就是內存地址)進行排序,絕大多數狀況下,這不是你所但願的。
第21條:老是讓比較函數在等值狀況下返回false。
如今我給你演示一個很酷的現象。建立一個set,用less_equal做爲它的比較類型,而後把10插入到該集合中:
set<int, less_equal<int> > s; //s 用"<=" 來排序
s.insert(10);
s.insert(10);
對於第二個insert,集合會檢查下面的表達式是否爲真:
!(10a <= 10b) && !(10b <= 10a); //檢查10a和10b是否等價,結果爲false
結果集合中有兩個10!
從技術上講,用於對關聯容器排序的比較函數必須爲他們所比較的對象定義個「嚴格的弱序化」(strict weak ordering)。
第22條:切勿直接修改set或multiset中的鍵。
若是你不關心可移植性,而你想改變set或multiset中元素的值,而且你的STL實現(有的STL實現中,好比set<T>:: iterator 的operator*老是返回const T&,就不能修改了)容許你這麼作,則請繼續作下去。只是注意不要改變元素中的鍵部分,即元素中可以影響容器有序性的部分。
若是你重視可移植性,就要確保set和multiset中的元素不能被修改。至少不能未通過強制類型轉換(轉換到一個引用類型const_cast<T&>)就修改。
若是你想以一種老是可行並且安全的方式來許該set、multiset、map和multimap中的元素,則能夠分5個簡單步驟來進行:
1. 找到你想修改的容器的元素。若是你不能確定最好的作法,第45條介紹瞭如何執行一次恰當的搜索來找到特定的元素。
2. 爲將要被修改的元素作一份拷貝,。在map和multimap的狀況下,請記住,不要把該拷貝的第一個部分聲明爲const。畢竟,你想要改變它。
3. 修改該拷貝,使它具備你指望的值。
4. 把該元素從容器中刪除,一般是經過erase來進行的(見第9條)。
5. 把拷貝插到容器中去。若是按照容器的排列順序,新元素的位置可能與被刪除元素的位置相同或緊鄰,則使用「提示」(hint)形式的insert,以便把插入的效率從對數時間提升到常數時間。把你從第1步得來的迭代器做爲提示信息。
第23條:考慮用排序的vector替代關聯容器。
標準關聯容器一般被實現爲平衡的二叉查找樹。也就是說,它所適合的那些應用程序首先作一些插入操做,而後作查找,而後可能又插入一些元素,或許接着刪掉一些,隨後又作查找,等等。這一系列時間的主要特徵是插入、刪除和超找混在一塊兒。總的來講,沒辦法預測出針對這顆樹的下一個操做是什麼。
不少應用程序使用其數據結構的方式並不這麼混亂。他們使用其數據結構的過程能夠明顯地分爲三個階段,總結以下:
1. 設置階段。建立一個新的數據結構,並插入大量元素。在這個階段,幾乎全部的操做都是插入和刪除操做。不多或幾乎沒有查找操做。
2. 查找操做。查詢該數據結構以找到特定的信息。在這個階段,幾乎全部的操做都是查找操做,不多或幾乎沒有插入和刪除操做。
3. 重組階段。改變該數據結構的內容,或許是刪除全部的當前數據,再插入新的數據。在行爲上,這個階段與第1階段相似。當這個階段結束之後,應用程序又回到第2階段。
第24條:當效率相當重要時,請在map::operator[]與map::insert之間謹慎做出選擇。
若是要更新一個已有的映射表元素,選擇operator[];若是要添加一個新的元素,選擇insert。
第25條:熟悉非標準的哈希容器。
標準C++庫沒有任何哈希容器,每一個人認爲這是一個遺憾,可是C++標準委員會認爲,把它們加入到標準中所需的工做會拖延標準完成的時間。已經有決定要在標準的下一個版本中包含哈希容器。
第4章 迭代器
第26條:iterator優先於const_iterator、reverse_iterator以及const_reverse_iterator。
減小混用不一樣類型的迭代器的機會,儘可能用iterator代替const_iterator。從const正確性的角度來看,僅僅爲了不一些可能存在的 STL實現缺陷而放棄使用const_iteraor顯得有欠公允。但考慮到在容器類的某些成員函數中指定使用iterator的現狀,得出 iterator較之const_iterator更爲實用的結論也就不足爲奇了。更況且,從實踐的角度來看,並不老是值得捲入 const_iterator的麻煩中。
第27條:使用distance和advance將容器的const_iterator轉換成iterator。
下面的代碼試圖把一個const_iterator強制轉換爲iterator:
typedef deque<int> IntDeque; //類型定義,簡化代碼
typedef IntDeque::iterator Iter;
typedeef IntDeque:;const_iterator ConstIter;
ConstIter ci; //ci 是一個const_iterator
...
Iter i(ci); //編譯錯誤!從const_iterator 到 iterator沒有隱式轉換途徑
Iter i(const_cast<Iter>(ci)); //仍然是編譯錯誤!不能將const_iterator強制轉換爲iterator
包含顯式類型轉換的代碼不能經過編譯的緣由在於,對於這些容器類型,iterator和const_iterator是徹底不一樣的類,他們之間的關係甚至比string和complex<double>之間的關係還要遠。
下面是這種方案的本質。
typedef deque<int> IntDeque; //類型定義,簡化代碼
typedef IntDeque::iterator Iter;
typedeef IntDeque:;const_iterator ConstIter;
IntDeque d;
ConstIter ci; //ci 是一個const_iterator
... //使ci指向d
Iter i(d.begin());//使i指向d的起始位置
advance(i,distance<ConstIter>(i,ci));//移動i,使它指向ci所指的位置
這中方法看上去很是簡單和直接,也很使人吃驚。爲了獲得一個與const_iterator指向同一位置的iterator,首先建立一個新的 iterator,將它指向容器的起始位置,而後取得const_iterator距離容器起始位置的偏移量,並將iterator向前移動相同的偏移量便可。這項技術的效率取決於你所使用的迭代起,對於隨機迭代器,它是常數時間的操做;對於雙向迭代器,以及某些哈希容器,它是線性時間的操做。
第28條:正確理解由reverse_iterator的base()成員函數所產生的iterator的用法。
若是要在一個reverse_iterator ri指定的位置上插入元素,則只需在ri.base()位置處插入元素便可。對於插入操做而言,ri和ri.base()是等價的,ri.base()是真正與ri對應的iterator。
若是要在一個reverse_iterator ri指定的位置上刪除一個元素,則須要在ri.base()前一個位置上執行刪除操做。對於刪除操做而言,ri和ri.base()是不等價的。
咱們仍是有必要來看一看執行這樣一個刪除操做的實際代碼,其中暗藏着驚奇之處:
vector<int> v;
... //同上,插入1到5
vector<int>::reverse_iterator ri = find(v.rbegin(),v.rend(),3);//使ri指向3
v.erase(--ri.base()); //試圖刪除ri.base()前面的元素,對於vector,每每編譯通不過
對於vector和string,這段代碼也許能工做,但對於vector和string的許多實現,它沒法經過編譯。這是由於在這樣的實現中, iterator(和vconst_iterator)是之內置指針的方式實現的,因此ri.base()的結果是一個指針。C和C++都規定了從函數返回的指針不該該被修改,因此因此編譯不能經過。
既然不能對base()的結果作遞減操做,那麼只要先遞增reverse_iterator,而後再調用base()函數便可!
...
v.erase((++ri).base()); //刪除ri所指的元素,這下編譯沒問題了!
第29條:對於逐個字符的輸入請考慮使用istreambuf_iterator。
假如你想把一個文本文件的內容拷貝到一個string對象中,如下的代碼看上去是一種合理的解決方案:
ifstream inputFile("interestingData.txt");
inputFIle.unsetf(ios::skipws);//istream_iterator使用operator>>函數來完成實際的讀操做,而默認狀況下operator>>函數會跳過空白字符
string fileData((istream_iterator<char> (inputFIle)),istream_iterator<char>());
然而,你可能會發現整個拷貝過程遠不及你但願的那般快。istream_iterator內部使用的operator>>實際上執行了格式化的輸入,但若是你只是想從輸入流中讀出下一個字符的話,它就顯得有點多餘了。
有一種更爲有效的途徑,那就是使用STL中最爲神祕的法寶之一:istreambuf_iterator。 istreambuf_iterator<char>對象使用方法與istream_iterator<char>大體相同,可是istreambuf_iterator<char>直接從流的緩衝區讀取下一個字符。(更爲特殊的是, istreambuf_iterator<char>對象從一個輸入流istream s中讀取下一個字符的操做是經過s.rdbuf()->sgetc()來完成的。)
ifstream inputFile("interestingData.txt");
string fileData((istreambuf_iterator<char>(inputFile)),istreambuf_iterator<char>());
此次咱們用不着清楚輸入流的skipws標誌,由於istreambuf_iterator不會跳過任何字符。
一樣的,對於非格式化的逐個字符輸出過程,你也應該考慮使用ostreambuf_iterator。
第5章 算法
第30條:確保目標區間足夠大。
當程序員但願向容器中添加新的對象,這裏有一個例子:
int transmogrify(int x); //該函數根據x生成一個新的值
vector<int> values;
vector<int> results;
transform(values.begin(),values.end(),back_inserter(results),transmogrify);
back_inserter返回的迭代起將使得push_back被調用,因此back_inserter可適用於全部提供了push_back方法的容器。同理,front_inserter僅適用於那些提供了push_front成員函數的容器(如deque和list)。
當是使用reserver提升一個序列插入操做的效率的時候,切記reserve只是增長了容器的容量,而容器的大小並未改變。當一個算法須要向 vector或者string中加入新的元素,即便已經調用了reserve,你也必須使用插入型的迭代器。以下代碼給出了一種錯誤的方式:
vector<int> values;
vector<int> results;
...
results.reserve(results.size() + values.size());
transform(values.begin(), values.end(), results.end(), transmogrify);//變換的結果會寫入到還沒有初始化的內存,結果將是不肯定的
在以上代碼中transform欣然接受了在results尾部未初始化的內存中進行復制操做的任務。因爲賦值操做重視在兩個對象之間而不是在一個對象與一個未初始化的內存塊之間進行,因此通常狀況下,這段代碼在運行時會失敗。
假設但願transform覆蓋results容器中已有的元素,那麼就須要確保results中已有的元素至少和values中的元素同樣多。不然,就必須使用resize來保證這一點。
vector<int> values;
vector<int> results;
...
if(results.size() < values.size()){
results.resize(values.size());
}
transform(values.begin(),values.end(),results.begin(),transmogrify);
或者,也能夠先清空results,而後按一般的方式使用一個插入型迭代起:
...
results.clear();
results.reserve(values.size());
transform(values.begin(),values.end(),back_inserter(results),transmogrify);
第31條:瞭解各類與排序有關的選擇。
sort(stable_sort)、partial_sort和nth_element算法都要求隨即訪問迭代器,因此這些算法只能被應用於 vector、string、deque和數組。partion(stable_partion)只要求雙向迭代器就能完成工做。
對於標準關聯容器中的元素進行排序並無實際意義,由於它們老是使用比較函數來維護內部元素的有效性。
list是惟一須要排序卻沒法使用這些排序算法的容器,爲此,list特別提供了sort成員函數(有趣的是,list::sort執行的是穩定排序)。若是但願但願一個list進行徹底排序,能夠用sort成員函數;可是,若是須要對list使用partial_sort或者nth_element算法的話,你就只能經過間接途徑來完成了。一種間接作法是,將list中的元素拷貝到一個提供隨即訪問迭代器的容器中,而後對該容器執行你所指望的算法;另外一種簡介作法是,先建立一個list::iterator的容器,再對該容器執行相應的算法,而後經過其中的迭代器訪問list的元素;第三中方法是利用一個包含迭代器的有序容器的信息,經過反覆地調用splice成員函數,將 list中的元素調整到指望的目標位置。能夠看到,你會有不少中選擇。
第32條:若是確實須要刪除元素,則須要在remove這一類算法以後調用erase。
1 2 3 99 5 99 7 8 9 99
調用remove(v.begin(),v.end(),99);後變成
1 2 3 5 7 8 9 8 9 99
remove沒法從迭代器推知對應的容器類型,因此就沒法調用容器的成員函數erase,所以就沒法真正刪除元素。其餘兩個算法remove_if和 unique也相似。不過list::remove和list::unique會真正刪除元素(比用erase-remove和erase-unique 更爲高效),這是STL中一個不一致的地方。
第33條:對包含指針的容器使用remove這一類算法時要特別當心。
不管你如何處理那些存放動態分配的指針的容器,你老是能夠這樣來進行:或者調用remove類算法以前先手工刪除指針並將它們置爲空,或者經過引用計數的智能指針( 如boost::shared_ptr),或者你本身發明的其餘某項技術。
下面的代碼利用第一種方式:
void delAndNullifyUncertified(Widget*& pWidget)
{
if(!pWidget->isCertified())
{
delete pWidget;
pWidget = 0;
}
}
for_each(v.begin(),v.end(),delAndNullifyUndertified);
v.erase(vemove(v.begin(),v.end(),static_cast<Widget*>(0)),v.end());
下面的的代碼使用第二中方式:
template<typename T> //RSCP = "Reference Counting Smart Pointer"
class RCSP{...};
tpedef RCSP<Widget> RCSPW;
vector<RCSPW> v;
...
v.push_back(RCSPW(new Widget));
...
v.erase(remove_if(v.begin(),v.end(),not1(mem_fun(&Widget::isCertified))),v.end());
第34條:瞭解哪些算法要求使用排序的區間做爲參數。
下面的代碼要求排序的區間:
binary_search lower_bound
upper_bound equal_range
set_union set_intersection
set_difference set_symmetric_difference
merge inplace_merge
includes
下面的算法並不必定須要排序的區間:
unique unique_copy
第35條:經過mismatch或lexicographical_compare實現簡單的忽略大小寫的字符串比較。
用mistatch實現:
//此函數判斷兩個字母是否相同,而忽略它們的大小寫
int ciCharCompare(char c1, char c2)
{
int lc1 = tolower(static_cast<unsigned_char>(c1));
int lc2 = tolower(static_cast<unsigned_char>(c2));
if(lc1 < lc2) return -1;
if(lc1 > lc2) return 1;
return 0;
}
/* 此函數保證傳遞給ciStringCompareImpl的s1比s2短,若是s1和s2相同,返回0;若是s1比s2短,返回-1;若是s1比s2長,返回1。*/
int ciStringCompare(const string& s1, const string& s2)
{
if(s1.size() <= s2.size()) return ciStringCompareImpl(s1, s2);
else return – ciStringCompareImpl(s2, s1);
}
//若是s1和s2相同,返回0;若是s1比s2短,返回-1;若是s1和s2都是在非結尾處發生不匹配,有開始不匹配的那個字符決定。
int ciStringCompareImpl(const string &s1, const string &c2)
{
typedef pair<string::const_iterator,string::const_iterator> PSCI;
PSCI p = mismatch(s1.begin(),s1.end(),s2.begin(),not2(ptr_fun(ciCharCompare)));
if(p.first == s1.end()){
if(p.second == s2.end()) return 0;
else return -1;
}
return ciCharCompair(*p.first, *p.second);
}
用lexicographical_compare實現:
bool ciCharLess(char c1, char c2)
{
return tolower(static_cast<unsigned char>(c1)) < tolower(static_cast<unsigned char>(c2));
}
bool ciStringCompare(const string &s1,const string &s2)
{
return lexicographical_compare(s1.begin(), s1.end(), s2.begin(), s2.end(), ciCharLess);
}
第36條:理解copy_if算法的正確實現。
STL中沒有copy_if的算法,下面是一個實現,可是不夠完美:
template<typename INputIterator,typename OUtputIterator,tpename Predicate>
OutputIterator copy_if(INputIterator begin,INputIterator end,OutputIterator destBegin,Predicate p)
{
return remove_copy_if(begin,end,destBegin, not1(0));
}
copy_if(widgets.begin(), widgets.end(), ostream_iterator<Widget>(cerr, "\n"),isDefective);//編譯錯誤
由於not1不能被直接應用到一個函數指針上(見41條),函數指針必須先用ptr_fun進行轉換。爲了調用copy_if的這個實現,你傳入的不只是一個函數對象,並且還應該是一個可配接(adaptable)的函數對象。雖然這很容易作到,可是要想成爲STL算法,它不能給客戶這樣的負擔。
下面是copy_if的正確實現:
template<typename INputIterator,typename OUtputIterator,typename Predicate>
OutputIterator copy_if(INputIterator begin,INputIterator end,OutputIterator destBegin,Predicate p)
{
while(begin != end){
if(p(*begin)) *destBegin++ = *begin;
++begin;
}
return destBegin;
}
第37條:使用accumulate或者for_each進行區間統計。
確保accumulate的返回類西和初始值類型相同。for_each返回的是一個函數對象。accumulate不容許反作用而for_each容許。(這是一個深層次的問題,也是一個涉及STL核心的問題,待解)
第6章 函數子、函數子類、函數及其餘
第38條:遵循按值傳遞的原則來設計函數子類。
在STL中,函數對象在函數之間來回傳遞的時候也是像函數指針那樣按值傳遞的。所以,你的函數對象必須儘量的小,不然拷貝的開銷會很大;其次,函數對象必須是單態的,也就是說,它們不得使用虛函數。這是由於,若是參數的類型是基類類型,而實參是派生類對象,那麼在傳遞過程當中會產生剝離問題(slicing problem):在對象拷貝過程當中,派生部分可能會被去掉,而僅保留了基類部分(見第3條)。
試圖禁止多態的函數子一樣也是不實際的。因此必須找到一種一箭雙鵰的辦法,既容許函數對象能夠很大而且/或保留多態性,又能夠與STL所採用的按值傳遞函數子的習慣保持一致。這個辦法就是:將所須要的數據和虛函數從函數子中分離出來,放到一個新的類中,而後在函數子中設一個指針,指向這個新類。
第39條:確保判別式是「純函數」。
一個判別式(predicate)是一個返回值爲bool類型的函數。一個純函數(pure function)是指返回值僅僅依賴於其參數的函數。
由於接受函數子的STL算法可能會先建立函數子對象的拷貝,而後使用這個拷貝,所以這一特性的直接反映就是判別式函數必須是純函數。
template<typename FwdIterator,typename Predicata>
FwdIterator remove_if(FwdIterator begin, FwdIterator end, Predicate p)
{
begin = find_if(begin, end, p);//多是p的拷貝
if(begin == end return begin;
else{
FwdIterator next = begin;
return remove_copy_if(++next, end, begin, p);//多是p的另外一個拷貝
}
}
第40條:若一個類是函數子,則應使它可配接。
4個標準的函數配接器(not一、not二、bind1st和bind2nd)都要求一些特殊的類型定義。提供了這些必要的類型定義(argument_type、first_argument_type、second_argument_type以及result_type)的函數對象被稱爲可配接的(adaptable)函數對象,反之,若是函數對象缺乏這些類型定義,則稱爲不可配接的。可配接的函數對象可以與其餘STL組件更爲默契地協同工做。不過不一樣種類的函數子類所須要提供的類型定義也不盡相同,除非你要編寫自定義的配接器,不然你並不須要知道有關這些類型定義的細節。這是由於,提供這些類型定義最簡便的辦法是讓函數子從特定的基類繼承,或者更準確的說,若是函數子類的operator()只有一個形參,那麼它應該從 std::unary_function模板的一個實例繼承;若是函數子類的operator()有兩個形參,那麼它應該從std:: binary_function繼承。
對於unary_function,你必須指定函數子類operator()所帶的參數的類型,以及返回類型;對於binary_function,你必須指定三個類型:operator()的第一個和第二個參數的類型,以及operator()的返回類型。如下是兩個例子:
template<typename T>
class MeetsThreshold: public std::unary_function<Widget, bool> {
private:
const T threshold;
public:
MeetsThreshold(const T& threshold);
bool operator()(const Widget&) const;
...
};
struct WidgetNameCompare:
public std::binary_function<Widget, Widget, bool> {
bool operator() (const Widget& lhs, const Widget& rhs) const;
};
你可能已經注意到MeetsThreshold是一個類,而WidgetNameCompare是一個結構。這是由於MeetsThreshold包含了狀態信息(數據成員threshold),而類是封裝狀態信息的一種邏輯方式;與此相反,WidgetNameCompare並不包含狀態信息,於是不須要任何私有成員。若是一個函數子的全部成員都是公有的,那麼一般會將其聲明爲結構而不是類。到底是選擇結構仍是類來定義函數子純屬我的編碼風格,可是若是你正在改進本身的編碼風格,並但願本身的風格更加專業一點的話,你就應該注意到,STL中全部無狀態的函數子類(如less<T>、 plus<T>等)通常都定義成結構。
咱們在看一下WidgetNameCompare:
struct WidgetNameCompare:
public std::binary_function<Widget, Widget, bool> {
bool operator() (const Widget& lhs, const Widget& rhs) const;
};
雖然operator()的參數類型都是const Widget&,但咱們傳遞給binary_function的類型倒是Widget。通常狀況下,傳遞給unary_function或 binary_function的非指針類型須要去掉const和引用(&)部分(不要問其中的緣由,若是你有興趣,能夠訪問 boost.org,卡可能看他們在調用特性(traits)和函數對象配接器方面的工做)。
若是operator()帶有指針參數,規則又有不一樣了。下面是WidgetNameCOmpare函數子的另外一個版本,所不一樣的是,此次以Widget*指針做爲參數:
struct PtrWidgetNameCompare:
public std::binary_function<const Widget*, const Widget*, bool> {
bool operator() (const Widget* lhs, const Widget* rhs) const;
};
第41條:理解ptr_fun、mem_fun和mem_fun_ref的來由。
若是有一個函數f和一個對象x,如今但願在x上調用f,而咱們在x的成員函數以外,那麼爲了執行這個調用,C++提供了三種不一樣的語法:
f(x); //語法#1:f是一個非成員函數
x.f(); //語法#2:f是一個成員函數,而且x是一個對象或一個對象引用
p->f(); //語法#3:f是成員函數,而且p是一個指向對象x的指針
如今假設有個可用於測試Widget對象的函數:
void test(Widget& w);
另有一個存放Widget對象的容器:
vector<Widget> vw;
爲了測試vw中的每個Widget對象,天然能夠用以下的方式來調用for_each:
for_each(vw.begin(), vw.end(), test); //調用#1 (能夠經過編譯)
可是,加入test是Widget的成員函數,即Widget支持自測:
class Widget{
public:
...
void test();
....
};
那麼在理想狀況下,應該也能夠用for_each在vw中的每一個對象上調用Widget::test成員函數:
for_each(vw.begin(), vw.end(), &Widget::test);//調用#2(不能經過編譯)
實際上,若是真的很理想的話,那麼對於一個存放Widget* 指針的容器,應該也能夠經過for_each來調用Widget::test:
list<Widget*> lpw;
for_each(lpw.begin(), lpw.end(), &Widget::test);//調用#3(也不能經過編譯)
這是由於STL中一種和廣泛的慣例:函數或函數對象在被調用的時候,老是使用非成員函數的語法形式(即#1)。
如今mem_fun和mem_fun_ref之因此必須存在已經很清楚了--它們被用來調整(通常是#2和#3)成員函數,使之可以經過語法#1被調用。 mem_fun、mem_fun_ref的作法其實很簡單,只要看一看其中任意一個函數的聲明就清楚了。它們是真正的函數模板,針對它們所配接的成員函數的圓形的不一樣,有幾種變化形式。咱們來看其中一個聲明,以便了解它是如何工做的:
template<typename R, typename C> //該mem_fun聲明針對不帶參數的非const成員函數,C是類,R是所指向的成員函數的返回類型
mem_fun_t<R,C>
mem_fun(R(C::*pmf) ());
mem_fun帶一個指向某個成員函數的指針參數pmf,而且返回一個mem_fun_t類型的對象。mem_fun_t是一個函數子類,它擁有該成員函數的指針,並提供了operator()函數,在operator()中調用了經過參數傳遞進來的對象上的該成員函數。例如,請看下面一段代碼:
list<Widget*> lpw;
...
for_each(lpw.begin(),lpw.end(),mem_fun(&Widget::test));//如今能夠經過編譯了
for_each接受到一個類型爲mem_fun_t的對象,該對象中保存了一個指向Widget::test的指針。對於lpw中的每個 Widget*指針,for_each將會使用語法#1來調用mem_fun_t對象,而後,該對象當即用語法#3調用Widget*指針的 Widget::test()。
(ptr_fun是多餘的嗎?)mem_fun是針對成員函數的配接器,mem_fun_ref是針對對象容器的配接器。
第42條:確保less<T>與operator<具備相同的含義。
operator<不只僅是less的默認實現方式,它也是程序員指望less所作的事情。讓less不調用operator<而去坐別的事情,這會無故地違背程序員的意願,這與「少」帶給人驚奇的原則(the principle of least astonishment)徹底背道而馳。這是很很差的,你應該儘可能避免這樣作。
若是你但願以一種特殊的方式來排序對象,那麼最好建立一個特殊的函數子類,它的名字不能是less。
第7章 在程序中使用STL
第43條:算法調用優於手寫的循環。
有三個理由:
效率:算法一般比程序員本身寫的循環效率更高。STL實現者能夠針對具體的容器對算法進行優化;幾乎全部的STL算法都使用了複雜的計算機科學算法,有些科學算法很是複雜,並不是通常的C++程序員所可以到達。
正確性:本身寫的循環比使用算法容易出錯。好比迭代器可能會在插入元素後失效。
可維護性:使用算法的代碼一般比手寫循環的代碼更加簡介明瞭。算法的名稱代表了它的功能,而for、while和do卻不能,每一位專業的C++程序員都應該知道每個算法所作的事情,看到一個算法就能夠知道這段代碼的功能,而對於循環只能繼續往下看具體的代碼才能懂代碼的意圖。
第44條:容器的成員函數優先於同名的算法。
第一:成員函數每每速度快;第二,成員函數一般與容器(特別是關聯容器)結合得更緊密(相等和等價的差異,好比對於關聯容器,count只能使用相等測試)。
第45條:正確區分count、find、binary_search、lower_bound、upper_bound和equal_range。
想知道什麼 |
||||
|
|
|
||
使用算法使用成員函數未排序排序set或mapmultiset或multimap特定的值存在嗎findbinary_searchcountfind特定值存在嗎?若是有,第一個在哪findequal_rangefindfind或lower_bound第一個不超過特定值的對象在哪裏find_iflower_boundlower_boundlower_bound第一個超過某個特定值的對象在哪裏find_ifupper_boundupper_boundupper_bound具備特定值的對象有多少個countequal_range (而後distance)countcount具備特定值的對象都在哪裏find(反覆調用)equal_rangeequal_rangeequal_range
第46條:考慮使用函數對象而不是函數指針做爲STL算法的參數。
函數指針抑制了內聯機制,而函數對象能夠被編譯器優化爲內聯。
另外一個理由是,這樣作有助於避免一些微妙的、語言自己的缺陷。在偶然的狀況下,有些看似合理的代碼會被編譯器以一些合法但又含糊不清的理由而拒絕。例如,當一個函數模板的實例化名稱並不徹底等同於一個函數的名稱時,就可能會出現這樣的問題。下面是一個例子:
template<typename FPType>
FPType average(FPType val1, FPType val2)//返回兩個浮點的平均值
{
return (val1 + val2) / 2;
}
template<typename InputIter1,typename InputIter2>
void writeAverages(InputIter1 begin1, INputIter1 end1, InputIter2 begin2,ostream& s) //將兩個序列的值按順序對應取平均,而後寫到一個流中
{
transform(begin1, end1, begin2,
ostream_iterator<typename iterator_trais<InputIterl>::value_type(s,"\n")>,
average<typename iterator_traits<InputIterl>::value_type> //錯誤?
);
}
許多編譯器接受這段代碼,可是C++標準卻不認同這樣的代碼。緣由在於,理論上存在另外一個名爲average的函數模板,它也只帶一個類型參數。若是這樣的話,表達式average<typename iterator_traits<InputIterl>::value_type>就會有二義性,由於編譯器沒法分辨到底應該實例化哪個模板。換成函數對象就能夠了。
第47條:避免產生「直寫型」(write-only)的代碼。
代碼被閱讀的次數遠遠大於它被編寫的次數。
第48條:老是包含(#include)正確的頭文件。
幾乎全部的標準STL容器都被聲明在與之同名的頭文件中。
除了4個STL算法外,其餘全部的算法都被聲明在<algorithm>中,這4個算法是accumulate、 inner_product、adjacent_difference和partial_sum,它們都被聲明在頭文件<numeric> 中。
特殊類型的迭代器,包括istream_iterator和istreambuf_iterator(見第29條),被聲明在<iterator>中。
標準的函數子(好比less<T>)和函數子配接器(好比not一、bind2nd)被聲明在頭文件<functional>中。
第49條:學會分析與STL相關的編譯器診斷信息。
用文本替換(例如用string替換掉basic_string<char,struct std::char_traits<char>,class std::allocator<char> >)。
第50條:熟悉STL相關的Web站點。
SGI STL
站點:http://www.sig.com/tech/stl/
STLport
站點:http://stlport.org
BOost
站點:http://boost.org