1. 程式人生 > 其它 >Effective STL 讀書筆記

Effective STL 讀書筆記

第二章 vector和string

第13條:vector和string優先於動態分配的陣列

使用new來動態分配記憶體,需要承擔以下責任

1.確保有配套的delete呼叫

2.確保delete呼叫形式正確,單個物件使用delete,陣列使用delete[]

3.delete只調用一次

每當需要動態分配一個數組時,都應該考慮使用vector和string來替代,原因是

它們自己管理記憶體

它們可以使用全部stl演算法

它們和舊程式碼可以相互轉化

只有一種情況使用動態的陣列取代string,關於string的引用計數技術

·多執行緒中使用引用計數的string,在避免記憶體分配和字元拷貝所節省下的時間比不過在背後同步控制上的時間

規避方法:

使用不使用引用計數的string
考慮使用vector<char>{=html}而不是string

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

stl容器的自動增長機制

當需要更多空間時,就呼叫realloc類似的操作

1.分配 一塊當前容量的某個倍數的新記憶體。一般是2倍
把容器的所有元素從舊的記憶體拷貝到新的記憶體
析構舊記憶體中的物件
釋放舊記憶體

這些操作十分費時,這些步驟發生時,容器所有的指標、迭代器和引用都無效

4個函式

size(),告訴你容器有多少元素,不會告訴你該容器為自己所包含的元素分配多少記憶體

capacity(),告訴你該容器已經分配的記憶體可以容納多少個元素,是容器可以容納的元素最大總數,不是還能容納的多少元素,還能容納的多少元素,capacity-size

resize(n),強迫容器改變到包含n個元素的狀態,n如果比當前size要小,尾部多餘元素會析構,如果n比當前size要大,通過預設建構函式建立的新元素將會被新增到容器的末尾,如果n比當前的capacity要大,在新增元素之前,將先重新分配記憶體

reserve(n),強迫容器把它容量變為至少是n,前提是n不小於當前大小,這通常會導致重新分配,

避免重新分配的關鍵是,儘早的使用reserve把容器的容器設為足夠大的值

第15條:注意String實現的多樣性

string大小和實現不同而不同

size資訊

capacity容量

value值

可能包括:分配子的拷貝

建立在引用計數之上的string:對值的引用計數

string型別的區別

string的值可能會被引用計數

大小的範圍值時一個char*指標大小的1-7倍

建立新的字串子可能需要零次、一次或者兩次動態分配

string型別可能共享、也可能不共享其大小和容量資訊

string 可能支援,也可能不支援針對單個物件的分配子

不同的實現對字元記憶體的最小分配單位有不同的策略

第16條:瞭解如何把 vector 和 string 資料傳給舊的 API。

vector v表示陣列的指標 &v[0]

string s.ctr() 表示char*

事實上,先讓C API把資料寫到一個vector中,再把資料拷貝到STL容器中,這思想總是可行的

可以先把其他容器轉換為vector再使用

第17條:使用swap技巧除去多餘的容量

shrink to fit思想(壓縮至適當大小)

vector<contestant>{=html}(contestants).swap(contestants);

vector<contestant>{=html}(contestants)建立一個臨時的vector,是contestants的拷貝,只為所拷貝的元素分配了所需要的記憶體,swap後臨時變數的容量是原先contestants的容量,臨時變數執行完後被析構,而contestants擁有合適的記憶體值

vector<contestant>{=html}().swap(v);// 清除v並把它的容量變為最小

swap不僅兩個容器的內容被交換,同時它們的迭代器、指標和引用也將被交換

第18條:避免使用vector<bool>{=html}

它不是一個STL容器

STL容器的一個條件:

T *p = &c[0] 如果operator[取得了Container<t>{=html}中的一個T物件,那麼你可以通過取它的地址得到一個指向該物件的指標。

它不儲存bool

它儲存的是bool的緊湊表示,而非真正的bool,使用了bitfield思想

使用deque<bool>{=html} 或bitset代替

第三章 關聯容器

equivalence(等價)而不是equality(相等)來對待自己的內容

第19條:理解相等(equality)和等價(equivalence)的概念

相等:以operator==為基礎 :find

等價:以operator< insert>{=html}

相等不一定所有資料成員都有相同值

等價關係:"在已排序的區間中物件值的相對順序",針對oprator <

!(w1 < w2) && !(w2 < w1)這兩個值就是等價的

使用者判別式(predicate):比較函式

標註關聯容器通過key_comp成員函式可被外部使用
!c.key_comp()(x,y) && !c.key_comp()(y,x) // 在c的排列順序中,x不在y前,y也不在x之前

例子:自定義set<string>{=html} 不區分大小寫

使用set的find成員函式,查詢含義僅大小寫不同的string的set裡面,可以成功查詢(基於等價),使用非成員的find演算法就不會成功(基於相等)

標註關聯容器使用等價的原因

容器總是保持排列順序的,那麼必須要實現比較相對大小,如果使用相等,需要多定義一個操作符

第20條:為包含指標的關聯容器制定比較型別

set<string*>{=html} ssp;

是下面程式碼的縮寫:set<string*,less<string*>{=html}> spp;

最精確的:set<string*,less<string*>{=html},allocator<string*>{=html}> spp;

定義比較函式子類

set 比較不需要函式,而是需要一個型別,在內部通過它建立一個函式

其他容器包含的物件與指標的行為類似:比如智慧指標和迭代器

第21條:總是讓比較函式在等值情況下返回false

比較函式的返回值表明的按照該函式定義的排列順序,相等的值從來不會有前後順序關係,比較函式應當始終返回false

第22條:切勿直接修改set或multiset中的鍵

為什麼set或者multiset中的元素不能是const的

針對物件,如果用物件中某個引數表示key,其他引數表示value,如果設定const,則無法改變value

更改鍵部分(key part):這部分資訊會影響容器的排序性,可能破壞容器

強制型別轉換是危險的,只要您能避免使用它就應用避免使用

第23條:考慮用排序的vector替代關聯容器

需要可提供快速查詢功能的資料結構時,可以選擇關聯容器

考慮查詢速度,非標準的雜湊容器是值得的

標準關聯容器比vector效率還低的情況並不少見

標準關聯容器通常被實現為平衡二叉查詢樹

平衡二叉樹對插入、刪除和查詢的混合操作做了優化,總的來說就是沒辦法預測針對這棵樹的下個操作是什麼

常見的資料結構過程

設定階段:幾乎所有操作都是插入和刪除

查詢階段:幾乎所有操作都是查詢

重組階段:改變改資料結構,再插入新的的數,和第1階段類似

這樣的情況下,vector可能比關聯容器提供了更好的效能,必須是排序的vector

排序的vector效能強的原因

大小問題:關聯容器中一個widget伴隨的空間至少是3個指標,更非記憶體

但是需要對每個元素都需要排序

第24條:當效率至關重要時,請在map::operator[]與map::insert之間謹慎做出選擇

map的operator[]與眾不同,它的設計目的是提高"新增和更新"

如果鍵k沒有的map中,需要先初始化一個物件給其拷貝賦值,效率低,應該使用insert

第25條:熟悉非標註的雜湊容器

目前應該有std::map等實現

第四章 迭代器

第26條:Iterator優先於const_iterator、reverse_iterator以及const_reverse_iterator

四個迭代器

iterator 相當於T*,

const_iterator相當於const T*

iterator、const_iterator 遞增效果:頭部到尾部

reverse_iterator、const_reverse_iterator相當於T、const T,遞增效果:尾部到頭部

類似的引數基本引數型別為:Iterator

不同迭代器的轉換:base()轉換

const 轉普通的Iterator不能直接得到

第27條:使用distance和advance將容器的const_iterator轉換成iterator

const_iterator無法強制轉換為Iterator

第28條:正確理解由reverse_iterator的base()成員函式所產生的Iterator的用法

例子

vector<int>{=html} v = {1,2,3,4,5};
​vector<int>{=html}::reverse_iterator ri =
find(v.rbegin(),v.rend(),3); vector<int>{=html}::iterator i (ri.base()

圖例

對於插入來說:直接使用ri和ri.base()是等價的

對於刪除來說:如果需要刪除ri所指的元素,必須刪除i前面的元素:v.erase(--ri.base())

對於vector和string的很多實現來說,Iterator和const_iterator是以內建指標的方式來實現的,這樣--ri.base()的表示式就無法通過編譯。必須使用 v.erase((++ri).base())

第29條:對於逐個字元的輸入請考慮使用istreambuf_iterator

istream_iterator<char>{=html}物件使用oprator>>從輸入流中讀取單個字元:每呼叫一次operator>>操作符,都需要執行許多附近的操作

istreambuf_iterator<char>{=html} 直接從流的緩衝區中讀取下一個字元

第五章 演算法

第30條:確保目標區間足夠大

在vector尾部新增物件

失敗案例

成功案例

如果插入的目標容器是vector和string,預先呼叫reserve,可以提高插入操作的效能。

reserve和insert建議同時呼叫

需要牢記的是:如果使用的演算法需要一個目標區間,那麼必須確保目標區間足夠大,或者確保它會隨著演算法的執行而增大。

要在演算法執行過程中增大目標區間,使用插入型迭代器

第31條:瞭解各種與排序有關的選擇

nth_element演算法:

排序一個區間,使得位置n上的元素正好是全排列情況下的第n個元素,當nth_element返回時,所有按全排列(sort的結果)排在位置n之前的元素也都排在位置n之前,而所有按照全排序規則排在位置n之後的元素則都排在位置n之後。

例子:將最好的20個元素放在容器前列,而不關心他們的具體排序

nth_element,沒有對位置1-20中的元素排序

用途:找到一個區間的中間值,或者特定百分比上的值

排序演算法的穩定性:排序後,等價的值,先後次序穩定

sort、stable_sort和partial_sort

partition演算法

sort、stable_sort、partial_sort和nth_element演算法都要求隨機訪問迭代器

list是唯一一個需要排序卻無法使用這些排序演算法的容器,只能sort完全排序

對於標準關聯容器的元素進行排序無意義

partion和stable_partion只要求雙向迭代器就能完成工作

總結

完全排序使用sort

等價性前n個元素排序,使用partial_sort

需要找到前n個元素但又不進行排序,nth_element

標準容器需要按照滿足某個特定的條件區分開,使用partion和stable_partion

list中只能實現list::sort,使用其他演算法需要別的轉化

第32條:如果確實需要刪除元素,則需要在remove這一類演算法之後呼叫erase

用remove刪除容器元素,容器中的元素數目不會因此減少

remove不是真正意義上的刪除,因為它做不到

例子

呼叫remove之前:

呼叫之後

remove只是移動了區間中的元素,把不用刪除的元素移到了區間的前部

真正刪除值

list.remove() 唯一一個命名為remove而確實刪除了容器元素的函式

類似的函式:remove_if和unique,同時unique和list::unique和remove的關係一致

第33條:對包含指標的容器使用remove這一類演算法時要特別小心

例子:

對於vector<widget*>{=html} v

刪除操作有問題,在remove_if中就出現

呼叫之前
remove_if呼叫之後
erase執行完

容器中存放的是指向動態分配物件指標的時候

避免使用remove和類似演算法,使用partition演算法時不錯的選擇

如果是智慧指標,問題就不存在

第34條:瞭解哪些演算法要求使用排序的區間作為引數

有些演算法需要排序的區間:違反這一規則並不會導致編譯器錯誤,而會導致執行時錯誤

有些演算法在排序的區間上,演算法會更加有效

要求排序區間的演算法

lower_bound/upper_bound

equal_range

set_union/set_intersection

set_difference/set_symmetric_difference

merge/inplace_merge

includes

不要求區間排序,但一般和排序區域一起使用的

unique/unique_copy

需要用二分法查詢資料

binary_search、lower_bound/upper_bound、equal_range

如果區域時排序好的,承諾對數時間的查詢效率

只有接受隨機迭代器的時候,才能保證此效率
如果不支援隨機迭代器,只能保證線性時間

提供線性時間效率的集合操作

set_union/set_intersection、set_difference/set_symmetric_difference

不使用排序區間,無法保證線上性時間完成

提供線性時間的合併和排序聯合操作

merge/inplace_merge

讀入兩個排序的區間、然後合併為一個新的排序區間

不使用排序演算法,變得很慢

includes

對於未排序區間有很好行為

unique和unique_copy

unique如果想要刪除區間的重複元素,必須保證所有相等的元素都是連續存放的

這樣需要

一個區間被排序的含義

區間可能有不同的排序比較函式,演算法也有比較函式,需要保證兩者有一致的行為

正確用法

第35條:通過mismatch或lexicograhical_compare實現簡單的忽略大小寫的字串比較

該條例實現簡單的英語字串的比較

忽略大小寫的字串比較功能

實現類似strcmp介面

實現與operator<類似介面>{=html}

strcmp類似介面

第一種實現

比較函式

c1、c2的char強制轉換為unsigned char

介面函式
介面實現
mismatch呼叫前提:必須把短字串作為第一個區間傳入,真實實現是:ciStringCompareImpl
not2(ptr_fun( ciCharCompare)):兩個字元匹配時返回true

第二種實現

比較函式同上
介面函式
lexicographicalcompare()是strcmp的泛化版本,可以接收一個判別式

第36條:理解copy_if演算法的正確實現

名字包含copy的演算法

沒有包含copy_if的演算法

合理的copy_if演算法(不是有效)

正確的實現

第37條:使用accumulate或者for_each進行區間統計

count/count_if

min_element/max_element

accumulate的第一種形式

使用形式

0.0表示儲存值的型別

輸入職要求為輸入迭代器

accumulate的第二種形式

起始的總和值和如何更新值

更新函式

string::size_type 表示容器中用於計數的型別
返回值的型別與函式的一個引數的型別相同

使用形式(使用函式)

使用形式(區間的數值的乘積)

for_each的使用

使用形式

允許有副作用

accumulate直接返回統計結果,for_each返回一個函式物件

第六章 函式子、函式子類、函式及其他

基礎

函式子(functor):類似函式的物件

not1和bind2nd的配接器則可以動態生成函式子

第38條:遵循按值傳遞的原則來設計函式子類

函式指標是按值傳遞的

按值傳遞的行為雖然是std預設卻可以改變的,但是使用者幾乎不會使用

函式物件特點

函式物件需要儘可能小,否則拷貝開銷大

函式物件必須是單態的,避免剝離問題(slicing problem)

設計想法

即允許函式物件可能很大並且/或者保留多型性

又需要與STL所採用的按值傳遞函式子保持一致

實現思路:

將所需的資料和虛擬函式從函式子類中分離出來,放在新類中

在函式子類中,包含該新類的物件

例子:建立一個包含大量資料並使用了多型性的函式子類

原來(成員包含大量資料,且存在虛擬函式)

修改後(包含指標,指向另一個實現類,將所有資料和虛擬函式放在實現類中)

PIMPL技術
注意項
必須以合理的方式支援拷貝動作(必須確保BPFC的拷貝函式正確處理它所指向的BPFCImpl物件)

第39條:確保判別式是"純函式"

判別式(predicate):返回值為bool型別的函式

純函式(pure function):返回值僅依賴於其傳入引數的函式

純函式能訪問的資料應該僅侷限於引數以及常量

判別式類(predicate class):是一個函式子類,它的operator()函式是一個判別式,凡是STL能接受判別式的地方,也可以接受判別式的物件

為什麼

接受函式子的STL演算法可能先建立函式子的拷貝

違反原則的結果

函式拷貝狀態量狀態不保持

避免違反原則的簡單方法

在判別式類中,將operator()函式的宣告為const

上述操作不能完全規避問題,比如mutable資料成員,非const的區域性static物件,依然無法限制

最嚴格的限制是"純函式",純函式沒有狀態

第40條:若一個類是函式子,這應使他可配接

使用函式判別容器成員滿足條件容易實現,判斷不滿足條件可能無法通過正向判別取反來實現

錯誤做法:

正確做法:

ptr_fun作用:完成型別定義工作

4個標準的函式配接器(not1、not2、bind1st和bind2st)要求特殊的型別定義,提供必要的型別定義的函式物件稱為可配接的(adaptable)函式物件,應該儘可能使你的編寫函式物件可以配接

不需要知道配接器型別細節,讓函式子從特定基類繼承,stl提供模板,不能直接繼承,需要繼承類似實參。

例子:

結構定義:引數型別和返回值型別

如果一個函式子的所有物件都是公開的,一般被定義為struct,否則是class

oprator()引數與結構引數

如果是非指標物件,省略const修飾

如果是指標物件,不能省略修飾

第41條:理解ptr_fun、mem_fun和mem_fun_ref的由來

目的:為了掩飾C++語言中語法不一致問題

函式呼叫基本語法

例子:

void test(widget& w)

在存放Widget物件的容器中vector<widget>{=html} vw; 支援for_each(vw.begin(),vw.end(),test)// 支援呼叫
如果test為Widget成員函式:for_each(vw.begin(),vw.end(),&idget::test)// 不能通過編譯
對於list<widget*>{=html} lpw; for_each(lpw.begin(),lpw.end(),&idget::test)// 不能通過編譯

STL慣例:函式或者函式物件在被呼叫的時候,總是使用非成員函式的語法形式

ptr_fun、mem_fun和mem_fun_ref用了調整成員函式,使之能通過語法1被呼叫

ptr_fun將語法1轉換為語法1

mem_fun 將語法3轉換為語法1

mem_fun_ref把語法2轉換為語法1

第42條:確保less<t>{=html}與operator<具有相同的語義>{=html}

針對std::less進行特化是常見的,但是針對Widget而特化未必是個合理選擇

less和operator < 最好保持一致

第七章 在程式中使用STL

第43條:演算法呼叫優先於手寫的迴圈

演算法的內部都是迴圈:演算法基本都需要一個

本該編寫迴圈來完成的任務可以用STL演算法來完成

三個理由:

效率:演算法通常效率更高

正確性:演算法更不容易出錯

可維護性:演算法更加簡潔明瞭

效率更高:底層有優化

正確性:不需要擔心迭代器的正確性問題

例子:給一個數組(C Style)每個元素加41,插入到一個deque的前部。

錯誤1
錯誤2:insert呼叫後使queue的所有迭代器無效
正確1:使用迴圈:花了太多功夫實現簡單的功能
正確2:使用transfrom演算法
關鍵點bind2nd的使用
正確的區間的起點和終點
inserter的使用

可維護性:

演算法名稱比普遍迴圈更有意義

例子:

結論

如果功能和一個演算法很相似,應該使用演算法實現

如果迴圈簡單,使用迴圈實現

如果迴圈做的事很多,又複製最好使用演算法呼叫

第44條:容器成員函式優先於同名演算法

成員函式速度快

成員函式期望行為一致,而演算法不一定

例子:set::find 與find演算法

set::find 只需要對數時間,find演算法線性時間
std的關聯容器的底層使用紅黑樹而非完全平衡樹
演算法使用相等性和容器使用等價性來判別

第45條:正確區分count、find、binary_search、lower_bound、upper_bound和equal_range

容器查詢工作,如何選擇函式

區間是否排序:binary_search、lower_bound、upper_bound和equal_range更快

區間未排序:count和find

count回答的問題:區間是否存在某個特定值,如果存在,有多少拷貝
find回答的問題:區間有這樣的值嗎?如果有,它在哪裡?
find找到第一個匹配就馬上返回,count必須到區間尾部

等價性與相等性

count和find使用相等性
binary_search、lower_bound、upper_bound和equal_range使用等價性

排序區間

這個值在區間中嗎,如果在,那它在哪?:equal_range
這個值的區間中嗎:如果在,它的第一份拷貝在哪,如果不在它該往什麼地方插入:lower_bound
錯誤:lower_bound 使用等價性來搜尋

速查表

第46條:考慮使用函式物件而不是函式作為STL演算法的引數

將函式物件傳遞給演算法,比傳遞實際的函式更加高效

函式內聯

傳遞函式的實質是給函式指標

函式指標引數抑制了內聯機制

有助於避免一些微妙的、語音本身的缺陷

第47條:避免產生"直寫型"(write-only)程式碼

程式碼被閱讀的次數遠遠大於它被編寫的次數

直寫型程式碼:容易編寫、難以閱讀與理解

第48條:總是包含(#include)正確的標頭檔案

大部分STL標註容器宣告在同名的標頭檔案中

<multiset>{=html}、<multimap>{=html}例外,都放在非mulit宣告中

大部分演算法被宣告在<algorithm>{=html}中

<accumulate>{=html}<inner_product>{=html}<adjacent_difference>{=html}<partial_sum>{=html}被宣告在<numeric>{=html}中

迭代器<iterator>{=html}

標準函式子和函式子配接器被宣告在<functional>{=html}中

第49條:學會分析與STL相關的編譯器診斷資訊

string不是一個類是typedef,所有與string類似的型別實際都是basic_string模板的例項

字串被泛化為一組具有任意字元特徵的任意字元型別的序列,它的儲存空間由任意分配子來分配

簡化診斷訊息:用全程替換的方法將basic_string長的型別名替換為文字string,

一個被宣告為const的成員函式內部,該類的所有非靜態資料成員被自動轉換為相應的const型別

vector和string的迭代器通常就是指標

如果診斷資訊中出現了back_insert_iterator、front_insert_iterator或者insert_iterator,幾乎意味著你錯誤呼叫了插入器型別物件

如果你正在使用一個常見的STL元件,但是從錯誤訊息來看,編譯器對此一無所知,那麼可能是你沒有包含正確的標頭檔案

第50條:熟悉與STL相關的Web網站