STL整體結構
STL主要由六部分組成,分別為容器(containers)、迭代器(iterators)、空間配置器(allocator)、配接器(adapters)、算法(algorithms)、仿函數(functors)。
它們之間的關系如下:
容器通過內存分配器分配空間
容器和算法分離
算法通過迭代器訪問容器
仿函數協助算法完成不同的策略變化
適配器套接仿函數
仿函數,仿函數適配器
仿函數又稱為函數對象(Function Object),其作用相當于一個函數指針。 在STL中,將std::remove_if(v.begin().v.end(),ContainsString(L"C++"));中類似于ConstainString這種形為函數指針的對象定義為仿函數,其實它是一個重載了括號運算符的Class。因此,自定義的仿函數必須重載operator():
struct ContainString:
public std::unary_function
{
ConstainString(const std::wstring& wszMatch):
m_wszMatch(wszMatch){}
bool operator()(const std::wstring& wszStringToMatch) const
{
return (wszStringToMatch.find(m_wszMatch)!=-1);
}
std::wstring m_wszMatch;
}
仿函數存在的意義:
1. 普通函數指針不能滿足STL的抽象要求(參數和返回型別的定義問題)
2. 函數指針無法和STL其他組件交互
3. 仿函數可以作為模板實參來定義對象的某種默認行為:
//定義set的默認排序行為
template,...>
class set{
...
}
仿函數適配器。STL提供三種適配器:改變容器接口的容器適配器、改變迭代器接口的迭代器適配器以及改變仿函數接口的仿函數適配器。前兩者都較為簡單,而最后一種則是靈活性最大的,有了它我們可以構造非常復雜的表達式策略。
在一些情況下仿函數可能無法匹配成合適的型別,這個時候我們就需要使用仿函數適配器:binder1st/binder2nd, mem_fun/men_fun_ref。例如,在一個給定的vector中尋找不為零的元素。通常我們會想到使用std::not_equal_to這個仿函數,但是該函數接受兩個參數。為了能在std::find_if中使用這個函數,我們這時候就需要綁定其中一個變量為0,以實現判斷一個元素是否不為零的功能:
std::vector::iterator it = std::find_if(v.begin(),v.end(),
std::bind1st(std::not_equal_to(),0));
//bind1st 封裝了binder1st的調用復雜性:
template
inline binder1st<_Fn2> bind1st(const _Fn2& _Func,const _Ty& _Left)
{
typename _Fn2::first_argument_type_Val(_Left);
return (binder1st<_Fn2>(_Func,_Val));
}
對于類的成員函數的適配,我們可以使用mem_fun/mem_fun_ref:
template <_Result, class _Ty> inline
mem_fun_t<_Result,_Ty> mem_fun(_Result (_Ty::*_Pm)() )
{
return (mem_fun_t<_Result, _Ty>(_Pm));
}
template
class mem_fun_t : public unar_function<_Ty *, _Result>
{
public:
explicit mem_fun_t(_Result (_Ty::*_Pm)())
: _Pmemfun(_Pm)? { }
_Result operator()(_Ty *_Pleft) const
{
return ((_Pleft->*_Pmemfun)());
}
private:
_Result (_Ty::*_Pmemfun());
}
其他需要注意的問題
(1) 單線程情況下涉及對字符串的操作,首選std::string/wstring。 多線程情況下要注意string是否帶引用計數。在多線程環境下,避免分配和拷貝所節省下的開銷轉嫁到了并發控制上。一般考慮使用vector/vector,因為vector的實現是不帶引用計數的。
(2) 當用new創建的對象直接放入容器時,要在銷毀容器前delete那些對象:
v.push_back(new Person("TOM",1));
...
for(vector::iterator it = v.begin(); it!=v.end();it++)
{
delete (*it);
}
v.clear();
(3)盡量使用算法調用代替手寫循環,如上面的刪除,我們可以定義一個仿函數在for_each中實現:
struct DeleteElement{
template
void operator() (const TElement* p) const
{
delete p;
}
}
std::for_each(v.begin(),v.end(),DeleteElement());
(4) 可以通過swap為容器“縮水”:
std::vector(v).swap(v);//使capacity = size
std::vector().swap(v) //清除v并最小化其容量:capacity = size = 0
(5) 在有對象繼承的情況下,建立指針的容器,而不是對象的容器。因為:a)容器裝入的對象是原始對象的拷貝,如果對象很大,則有較大性能開銷;b)由于繼承的存在,拷貝會發生slicing,導致丟失數據。
泛型算法
簡單列出STL為我們提供的算法:
非變易性算法
for_each 提供對于容器內每個元素進行循環操作
find 線性查找
find_fist_of 對于給定值的集合,在容器內線性查找
adjacent_find 線性查找鄰近且相等的元素對
count 計算給定值的出現次數
mismatch 比較兩個序列,找出第一個不相同元素的位置
equal 兩個序列的判等操作,逐一比較元素是否相等
search 在一個序列中查找與另一個序列匹配的子序列
search_n 在序列中查找一系列符合給定值的元素
find_end 在一個序列中查找最后一個與另一個序列匹配的子序列
變易性算法
copy 復制元素到另外一個序列
swap 兩個容器元素交換
transform 序列中的元素都用這個元素變換后的值代替
replace 替換給定值的元素
fill 填充給定值的元素
generate 用某函數的返回值來代替序列中的所有元素
remove 刪除序列中等于某一給定之的所有元素
unique 刪除所有連續相等的元素
reverse 將元素之間的位置關系取逆
rotate 循環移動序列中的元素
random_shuffle 隨機排列元素
partition 按某一順序重新排列元素
有序隊列算法
sort,stable_sort,partial_sort 對元素排序
nth_element 查找第n個大的元素
binary_search lower_bound upper_bound equal_range 用二分查找搜索有序隊列
merge 歸并兩個有序隊列
includes set_union set_intersection set_difference set_sysmetric_difference 集合運算
push_heap pop_heap make_heap sort_heap 堆操作
min max min_element max_element 求最大,最小元素
lexicographical_compare 字典序比較
next_permutation prev_permutation 依據字典序生成排列
通用數字算法
accumulate 累加
inner_product 內積
partial_sum 累加部分元素
adjacent_difference 計算相鄰元素的差,保存在另一個序列中
泛型算法的結構
就像所有的容器都建立在一致的設計模式上一樣,算法也具有共同的設計基礎。
算法最基本的性質是需要使用的迭代器種類。 另一種算法分類方法是前面介紹的按實現的功能分類:只讀算法,不改變元素的值和順序;給指定元素賦新值的算法;將一個元素的值移給另一個元素的算法。 另外,算法還有兩種結構上的算法模式:一種模式是由算法所帶的形參定義;另一種模式則通過兩種函數命名和重載的規范定義。
算法的形參模式
大多數算法采用下面四種形式之一:
alg (beg, end, other parms);
alg (beg, end, dest, other parms);
alg (beg, end, beg2, other parms);
alg (beg, end, beg2, end2, other parms);
其中,alg是算法名,[beg, end)是輸入范圍,beg, end, dest, beg2, end2都是迭代器。
對于帶有單個目標迭代器的算法:dest形參是一個迭代器,用于指定存儲輸出數據的目標對象。算法假定無論需要寫入多少個元素都是安全的。注意:調用這類算法時,算法是將輸出內容寫到容器中已存在的元素上,所以必須確保輸出容器中有足夠大的容量存儲輸出數據,這也正是通過使用插入迭代器或者ostream_iterator來調用這些算法的原因。
對于帶第二個輸入序列的算法:beg2和end2標記了完整的輸出范圍。而只有beg2的算法將beg2視為第二個輸入范圍的首元素,算法假定以beg2開始的范圍至少與beg和end指定的范圍一樣大。
算法的命名規范
包括兩種重要模式:第一種模式包括測試輸入范圍內元素的算法,第二種模式則應用于輸入范圍內元素的重新排序的算法。
1)區別帶有一個值或一個謂詞函數參數的算法版本
很多算法通過檢查其輸入范圍內的元素實現其功能。這些算法通常要用到標準關系操作符:==或<。其中的大部分算法都提供了第二個版本的算法,允許程序員提供比較或測試函數取代默認的操作符的使用。
例如, 排序算法默認使用 < 操作符,其重載版本帶有一個額外的形參,表示取代默認的 < 操作符。
sort (beg, end);? ? ? ? // use < operator to sort the elements
sort (beg, end, comp);? // use function named comp to sort the elements
又如,查找算法默認使用 == 操作符。標準庫為這類算法提供另外命名的(而非重載的)版本,帶有謂詞函數形參。對于帶有謂詞函數形參的算法,其名字帶有后綴 _if:
find (beg, end, val);? ? ? // find first instance of val in the input range
find_if (beg, end, pred);? // find first instance for which pred is true
標準庫為這類算法提供另外命名的版本,而非重載版本,原因在于這兩種版本的算法帶有相同的參數個數,容易導致二義性。
2)區別是否實現復制的算法版本
默認情況下,算法將重新排列的寫回其范圍。標準庫也為這類算法提供了另外命名的版本,將元素寫到指定的輸出目標。此版本的算法在名字中添加 _copy后綴,例如:
reverse (beg, end);
reverse_copy (beg, end, dest);
第一個版本將輸入序列中的元素反向重新排列;而第二個版本將復制輸入序列中的元素,并將它們以逆序存儲到dest開始的序列中。
容器特有的算法
list容器上的迭代器是雙向的,而不是隨機訪問類型。由于list容器不支持隨機訪問,因此,在此容器上不能使用需要隨機訪問迭代器的算法。如sort類算法。其它有些算法,如merge, remove, reverse, unique等,雖然可以用在list上,但性能太差。list容器結合自己的結構專門實現了更為高效的算法。因此,對于list對象,應該優先使用list容器特有的成員版本,而不是泛型算法。
list容器特有的算法與其泛型算法版本之間有兩個重要的差別:1)remove和unique的list版本修改了其關聯的基礎容器:真正刪除了指定的元素;2)list容器提供的merge和splice操作會破壞它們的實參。使用泛型算法的merge版本,合并的序列將寫入目標迭代器指向的對象,而它的兩個輸入序列保持不變。
STL的內存分配器
隱藏在STL的容器后的內存管理工作是通過STL提供的一個默認的allocator實現的。當然,用戶也可以定制自己的allocator,只要實現allocator模板所定義的接口方法即可,然后通過將自定義的allocator作為模板參數傳遞給STL容器,創建一個使用自定義allocator的STL容器對象,如:
stl::vector array;
大多數情況下,STL默認的allocator就已經足夠了。這個allocator是一個由兩級分配器構成的內存管理器,當申請的內存大小大于128byte時,就啟動第一級分配器通過malloc直接向系統的堆空間分配,如果申請的內存大小小于128byte時,就啟動第二級分配器,從一個預先分配好的內存池中取一塊內存交付給用戶,這個內存池由16個不同大?。?的倍數,8~128byte)的空閑列表組成,allocator會根據申請內存的大?。▽⑦@個大小round up成8的倍數)從對應的空閑塊列表取表頭塊給用戶。
這種做法有兩個優點:
1)小對象的快速分配。小對象是從內存池分配的,這個內存池是系統調用一次malloc分配一塊足夠大的區域給程序備用,當內存池耗盡時再向系統申請一塊新的區域,整個過程類似于批發和零售,起先是由allocator向總經商批發一定量的貨物,然后零售給用戶,與每次都總經商要一個貨物再零售給用戶的過程相比,顯然是快捷了。當然,這里的一個問題時,內存池會帶來一些內存的浪費,比如當只需分配一個小對象時,為了這個小對象可能要申請一大塊的內存池,但這個浪費還是值得的,況且這種情況在實際應用中也并不多見。
2)避免了內存碎片的生成。程序中的小對象的分配極易造成內存碎片,給操作系統的內存管理帶來了很大壓力,系統中碎片的增多不但會影響內存分配的速度,而且會極大地降低內存的利用率。以內存池組織小對象的內存,從系統的角度看,只是一大塊內存池,看不到小對象內存的分配和釋放。
實現時,allocator需要維護一個存儲16個空閑塊列表表頭的數組freelist,數組元素i是一個指向塊大小為8*(i+1)字節的空閑塊列表的表頭,一個指向內存池起始地址的指針startfree和一個指向結束地址的指針end_free。空閑塊列表節點的結構如下:
union obj {
union obj *free_list_link;
char client_data[1];
};
這個結構可以看做是從一個內存塊中摳出4個字節大小來,當這個內存塊空閑時,它存儲了下個空閑塊,當這個內存塊交付給用戶時,它存儲的時用戶的數據。因此,allocator中的空閑塊鏈表可以表示成
obj* free_list[16];
分配算法
allocator分配內存的算法如下:
算法:allocate
輸入:申請內存的大小size
輸出:若分配成功,則返回一個內存的地址,否則返回NULL
{
if(size大于128){ 啟動第一級分配器直接調用malloc分配所需的內存并返回內存地址;}
else {
將size向上round up成8的倍數并根據大小從free_list中取對應的表頭free_list_head;
if(free_list_head不為空){
從該列表中取下第一個空閑塊并調整free_list;
返回free_list_head;
} else {
調用refill算法建立空閑塊列表并返回所需的內存地址;
}
}
}
算法: refill
輸入:內存塊的大小size
輸出:建立空閑塊鏈表并返回第一個可用的內存塊地址
{
調用chunk_alloc算法分配若干個大小為size的連續內存區域并返回起始地址chunk和成功分配的塊數nobj;
if(塊數為1)直接返回chunk;
否則
{
開始在chunk地址塊中建立free_list;
根據size取free_list中對應的表頭元素free_list_head;
將free_list_head指向chunk中偏移起始地址為size的地址處, 即free_list_head=(obj*)(chunk+size);
再將整個chunk中剩下的nobj-1個內存塊串聯起來構成一個空閑列表;
返回chunk,即chunk中第一塊空閑的內存塊;
}
}
算法:chunk_alloc
輸入:內存塊的大小size,預分配的內存塊塊數nobj(以引用傳遞)
輸出:一塊連續的內存區域的地址和該區域內可以容納的內存塊的塊數
{
計算總共所需的內存大小total_bytes;
if(內存池中足以分配,即end_free - start_free >= total_bytes) {
則更新start_free;
返回舊的start_free;
} else if(內存池中不夠分配nobj個內存塊,但至少可以分配一個){
計算可以分配的內存塊數并修改nobj;
更新start_free并返回原來的start_free;
} else { //內存池連一塊內存塊都分配不了
先將內存池的內存塊鏈入到對應的free_list中后;
調用malloc操作重新分配內存池,大小為2倍的total_bytes加附加量,start_free指向返回的內存地址;
if(分配不成功) {
if(16個空閑列表中尚有空閑塊)
嘗試將16個空閑列表中空閑塊回收到內存池中再調用chunk_alloc(size, nobj);
else {
調用第一級分配器嘗試out of memory機制是否還有用;
}
}
更新end_free為start_free+total_bytes,heap_size為2倍的total_bytes;
調用chunk_alloc(size,nobj);
}
}
算法:deallocate
輸入:需要釋放的內存塊地址p和大小size
{
if(size大于128字節)直接調用free(p)釋放;
else{
將size向上取8的倍數,并據此獲取對應的空閑列表表頭指針free_list_head;
調整free_list_head將p鏈入空閑列表塊中;
}
}
內存分配器小結
STL中的內存分配器實際上是基于空閑列表(free list)的分配策略,最主要的特點是通過組織16個空閑列表,對小對象的分配做了優化。
1)小對象的快速分配和釋放。當一次性預先分配好一塊固定大小的內存池后,對小于128字節的小塊內存分配和釋放的操作只是一些基本的指針操作,相比于直接調用malloc/free,開銷小。
2)避免內存碎片的產生。零亂的內存碎片不僅會浪費內存空間,而且會給OS的內存管理造成壓力。
3)盡可能最大化內存的利用率。當內存池尚有的空閑區域不足以分配所需的大小時,分配算法會將其鏈入到對應的空閑列表中,然后會嘗試從空閑列表中尋找是否有合適大小的區域,
但是,這種內存分配器局限于STL容器中使用,并不適合一個通用的內存分配。因為它要求在釋放一個內存塊時,必須提供這個內存塊的大小,以便確定回收到哪個free list中,而STL容器是知道它所需分配的對象大小的,比如上述:
stl::vector array;
array是知道它需要分配的對象大小為sizeof(int)。一個通用的內存分配器是不需要知道待釋放內存的大小的,類似于free(p)。