(Boolan)STL與泛型編程學(xué)習(xí)筆記(第二周)

STL整體結(jié)構(gòu)

STL主要由六部分組成,分別為容器(containers)、迭代器(iterators)、空間配置器(allocator)、配接器(adapters)、算法(algorithms)、仿函數(shù)(functors)。

它們之間的關(guān)系如下:

容器通過內(nèi)存分配器分配空間

容器和算法分離

算法通過迭代器訪問容器

仿函數(shù)協(xié)助算法完成不同的策略變化

適配器套接仿函數(shù)

仿函數(shù),仿函數(shù)適配器

仿函數(shù)又稱為函數(shù)對象(Function Object),其作用相當(dāng)于一個函數(shù)指針。 在STL中,將std::remove_if(v.begin().v.end(),ContainsString(L"C++"));中類似于ConstainString這種形為函數(shù)指針的對象定義為仿函數(shù),其實它是一個重載了括號運算符的Class。因此,自定義的仿函數(shù)必須重載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;

}

仿函數(shù)存在的意義:

1. 普通函數(shù)指針不能滿足STL的抽象要求(參數(shù)和返回型別的定義問題)

2. 函數(shù)指針無法和STL其他組件交互

3. 仿函數(shù)可以作為模板實參來定義對象的某種默認行為:

//定義set的默認排序行為

template,...>

class set{

...

}

仿函數(shù)適配器。STL提供三種適配器:改變?nèi)萜鹘涌诘娜萜鬟m配器、改變迭代器接口的迭代器適配器以及改變仿函數(shù)接口的仿函數(shù)適配器。前兩者都較為簡單,而最后一種則是靈活性最大的,有了它我們可以構(gòu)造非常復(fù)雜的表達式策略。

在一些情況下仿函數(shù)可能無法匹配成合適的型別,這個時候我們就需要使用仿函數(shù)適配器:binder1st/binder2nd, mem_fun/men_fun_ref。例如,在一個給定的vector中尋找不為零的元素。通常我們會想到使用std::not_equal_to這個仿函數(shù),但是該函數(shù)接受兩個參數(shù)。為了能在std::find_if中使用這個函數(shù),我們這時候就需要綁定其中一個變量為0,以實現(xiàn)判斷一個元素是否不為零的功能:

std::vector::iterator it = std::find_if(v.begin(),v.end(),

std::bind1st(std::not_equal_to(),0));

//bind1st 封裝了binder1st的調(diào)用復(fù)雜性:

template

inline binder1st<_Fn2> bind1st(const _Fn2& _Func,const _Ty& _Left)

{

typename _Fn2::first_argument_type_Val(_Left);

return (binder1st<_Fn2>(_Func,_Val));

}

對于類的成員函數(shù)的適配,我們可以使用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是否帶引用計數(shù)。在多線程環(huán)境下,避免分配和拷貝所節(jié)省下的開銷轉(zhuǎn)嫁到了并發(fā)控制上。一般考慮使用vector/vector,因為vector的實現(xiàn)是不帶引用計數(shù)的。

(2) 當(dāng)用new創(chuàng)建的對象直接放入容器時,要在銷毀容器前delete那些對象:

v.push_back(new Person("TOM",1));

...

for(vector::iterator it = v.begin(); it!=v.end();it++)

{

delete (*it);

}

v.clear();

(3)盡量使用算法調(diào)用代替手寫循環(huán),如上面的刪除,我們可以定義一個仿函數(shù)在for_each中實現(xiàn):

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)由于繼承的存在,拷貝會發(fā)生slicing,導(dǎo)致丟失數(shù)據(jù)。

泛型算法

簡單列出STL為我們提供的算法:

非變易性算法

for_each 提供對于容器內(nèi)每個元素進行循環(huán)操作

find 線性查找

find_fist_of 對于給定值的集合,在容器內(nèi)線性查找

adjacent_find 線性查找鄰近且相等的元素對

count 計算給定值的出現(xiàn)次數(shù)

mismatch 比較兩個序列,找出第一個不相同元素的位置

equal 兩個序列的判等操作,逐一比較元素是否相等

search 在一個序列中查找與另一個序列匹配的子序列

search_n 在序列中查找一系列符合給定值的元素

find_end 在一個序列中查找最后一個與另一個序列匹配的子序列

變易性算法

copy 復(fù)制元素到另外一個序列

swap 兩個容器元素交換

transform 序列中的元素都用這個元素變換后的值代替

replace 替換給定值的元素

fill 填充給定值的元素

generate 用某函數(shù)的返回值來代替序列中的所有元素

remove 刪除序列中等于某一給定之的所有元素

unique 刪除所有連續(xù)相等的元素

reverse 將元素之間的位置關(guān)系取逆

rotate 循環(huán)移動序列中的元素

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 依據(jù)字典序生成排列

通用數(shù)字算法

accumulate 累加

inner_product 內(nèi)積

partial_sum 累加部分元素

adjacent_difference 計算相鄰元素的差,保存在另一個序列中

泛型算法的結(jié)構(gòu)

就像所有的容器都建立在一致的設(shè)計模式上一樣,算法也具有共同的設(shè)計基礎(chǔ)。

算法最基本的性質(zhì)是需要使用的迭代器種類。 另一種算法分類方法是前面介紹的按實現(xiàn)的功能分類:只讀算法,不改變元素的值和順序;給指定元素賦新值的算法;將一個元素的值移給另一個元素的算法。 另外,算法還有兩種結(jié)構(gòu)上的算法模式:一種模式是由算法所帶的形參定義;另一種模式則通過兩種函數(shù)命名和重載的規(guī)范定義。

算法的形參模式

大多數(shù)算法采用下面四種形式之一:

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都是迭代器。

對于帶有單個目標(biāo)迭代器的算法:dest形參是一個迭代器,用于指定存儲輸出數(shù)據(jù)的目標(biāo)對象。算法假定無論需要寫入多少個元素都是安全的。注意:調(diào)用這類算法時,算法是將輸出內(nèi)容寫到容器中已存在的元素上,所以必須確保輸出容器中有足夠大的容量存儲輸出數(shù)據(jù),這也正是通過使用插入迭代器或者ostream_iterator來調(diào)用這些算法的原因。

對于帶第二個輸入序列的算法:beg2和end2標(biāo)記了完整的輸出范圍。而只有beg2的算法將beg2視為第二個輸入范圍的首元素,算法假定以beg2開始的范圍至少與beg和end指定的范圍一樣大。

算法的命名規(guī)范

包括兩種重要模式:第一種模式包括測試輸入范圍內(nèi)元素的算法,第二種模式則應(yīng)用于輸入范圍內(nèi)元素的重新排序的算法。

1)區(qū)別帶有一個值或一個謂詞函數(shù)參數(shù)的算法版本

很多算法通過檢查其輸入范圍內(nèi)的元素實現(xiàn)其功能。這些算法通常要用到標(biāo)準(zhǔn)關(guān)系操作符:==或<。其中的大部分算法都提供了第二個版本的算法,允許程序員提供比較或測試函數(shù)取代默認的操作符的使用。

例如, 排序算法默認使用 < 操作符,其重載版本帶有一個額外的形參,表示取代默認的 < 操作符。

sort (beg, end);? ? ? ? // use < operator to sort the elements

sort (beg, end, comp);? // use function named comp to sort the elements

又如,查找算法默認使用 == 操作符。標(biāo)準(zhǔn)庫為這類算法提供另外命名的(而非重載的)版本,帶有謂詞函數(shù)形參。對于帶有謂詞函數(shù)形參的算法,其名字帶有后綴 _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

標(biāo)準(zhǔn)庫為這類算法提供另外命名的版本,而非重載版本,原因在于這兩種版本的算法帶有相同的參數(shù)個數(shù),容易導(dǎo)致二義性。

2)區(qū)別是否實現(xiàn)復(fù)制的算法版本

默認情況下,算法將重新排列的寫回其范圍。標(biāo)準(zhǔn)庫也為這類算法提供了另外命名的版本,將元素寫到指定的輸出目標(biāo)。此版本的算法在名字中添加 _copy后綴,例如:

reverse (beg, end);

reverse_copy (beg, end, dest);

第一個版本將輸入序列中的元素反向重新排列;而第二個版本將復(fù)制輸入序列中的元素,并將它們以逆序存儲到dest開始的序列中。

容器特有的算法

list容器上的迭代器是雙向的,而不是隨機訪問類型。由于list容器不支持隨機訪問,因此,在此容器上不能使用需要隨機訪問迭代器的算法。如sort類算法。其它有些算法,如merge, remove, reverse, unique等,雖然可以用在list上,但性能太差。list容器結(jié)合自己的結(jié)構(gòu)專門實現(xiàn)了更為高效的算法。因此,對于list對象,應(yīng)該優(yōu)先使用list容器特有的成員版本,而不是泛型算法。

list容器特有的算法與其泛型算法版本之間有兩個重要的差別:1)remove和unique的list版本修改了其關(guān)聯(lián)的基礎(chǔ)容器:真正刪除了指定的元素;2)list容器提供的merge和splice操作會破壞它們的實參。使用泛型算法的merge版本,合并的序列將寫入目標(biāo)迭代器指向的對象,而它的兩個輸入序列保持不變。

STL的內(nèi)存分配器

隱藏在STL的容器后的內(nèi)存管理工作是通過STL提供的一個默認的allocator實現(xiàn)的。當(dāng)然,用戶也可以定制自己的allocator,只要實現(xiàn)allocator模板所定義的接口方法即可,然后通過將自定義的allocator作為模板參數(shù)傳遞給STL容器,創(chuàng)建一個使用自定義allocator的STL容器對象,如:

stl::vector array;

大多數(shù)情況下,STL默認的allocator就已經(jīng)足夠了。這個allocator是一個由兩級分配器構(gòu)成的內(nèi)存管理器,當(dāng)申請的內(nèi)存大小大于128byte時,就啟動第一級分配器通過malloc直接向系統(tǒng)的堆空間分配,如果申請的內(nèi)存大小小于128byte時,就啟動第二級分配器,從一個預(yù)先分配好的內(nèi)存池中取一塊內(nèi)存交付給用戶,這個內(nèi)存池由16個不同大小(8的倍數(shù),8~128byte)的空閑列表組成,allocator會根據(jù)申請內(nèi)存的大小(將這個大小round up成8的倍數(shù))從對應(yīng)的空閑塊列表取表頭塊給用戶。

這種做法有兩個優(yōu)點:

1)小對象的快速分配。小對象是從內(nèi)存池分配的,這個內(nèi)存池是系統(tǒng)調(diào)用一次malloc分配一塊足夠大的區(qū)域給程序備用,當(dāng)內(nèi)存池耗盡時再向系統(tǒng)申請一塊新的區(qū)域,整個過程類似于批發(fā)和零售,起先是由allocator向總經(jīng)商批發(fā)一定量的貨物,然后零售給用戶,與每次都總經(jīng)商要一個貨物再零售給用戶的過程相比,顯然是快捷了。當(dāng)然,這里的一個問題時,內(nèi)存池會帶來一些內(nèi)存的浪費,比如當(dāng)只需分配一個小對象時,為了這個小對象可能要申請一大塊的內(nèi)存池,但這個浪費還是值得的,況且這種情況在實際應(yīng)用中也并不多見。

2)避免了內(nèi)存碎片的生成。程序中的小對象的分配極易造成內(nèi)存碎片,給操作系統(tǒng)的內(nèi)存管理帶來了很大壓力,系統(tǒng)中碎片的增多不但會影響內(nèi)存分配的速度,而且會極大地降低內(nèi)存的利用率。以內(nèi)存池組織小對象的內(nèi)存,從系統(tǒng)的角度看,只是一大塊內(nèi)存池,看不到小對象內(nèi)存的分配和釋放。

實現(xiàn)時,allocator需要維護一個存儲16個空閑塊列表表頭的數(shù)組freelist,數(shù)組元素i是一個指向塊大小為8*(i+1)字節(jié)的空閑塊列表的表頭,一個指向內(nèi)存池起始地址的指針startfree和一個指向結(jié)束地址的指針end_free。空閑塊列表節(jié)點的結(jié)構(gòu)如下:

union obj {

union obj *free_list_link;

char client_data[1];

};

這個結(jié)構(gòu)可以看做是從一個內(nèi)存塊中摳出4個字節(jié)大小來,當(dāng)這個內(nèi)存塊空閑時,它存儲了下個空閑塊,當(dāng)這個內(nèi)存塊交付給用戶時,它存儲的時用戶的數(shù)據(jù)。因此,allocator中的空閑塊鏈表可以表示成

obj* free_list[16];

分配算法

allocator分配內(nèi)存的算法如下:

算法:allocate

輸入:申請內(nèi)存的大小size

輸出:若分配成功,則返回一個內(nèi)存的地址,否則返回NULL

{

if(size大于128){ 啟動第一級分配器直接調(diào)用malloc分配所需的內(nèi)存并返回內(nèi)存地址;}

else {

將size向上round up成8的倍數(shù)并根據(jù)大小從free_list中取對應(yīng)的表頭free_list_head;

if(free_list_head不為空){

從該列表中取下第一個空閑塊并調(diào)整free_list;

返回free_list_head;

} else {

調(diào)用refill算法建立空閑塊列表并返回所需的內(nèi)存地址;

}

}

}

算法: refill

輸入:內(nèi)存塊的大小size

輸出:建立空閑塊鏈表并返回第一個可用的內(nèi)存塊地址

{

調(diào)用chunk_alloc算法分配若干個大小為size的連續(xù)內(nèi)存區(qū)域并返回起始地址chunk和成功分配的塊數(shù)nobj;

if(塊數(shù)為1)直接返回chunk;

否則

{

開始在chunk地址塊中建立free_list;

根據(jù)size取free_list中對應(yīng)的表頭元素free_list_head;

將free_list_head指向chunk中偏移起始地址為size的地址處, 即free_list_head=(obj*)(chunk+size);

再將整個chunk中剩下的nobj-1個內(nèi)存塊串聯(lián)起來構(gòu)成一個空閑列表;

返回chunk,即chunk中第一塊空閑的內(nèi)存塊;

}

}

算法:chunk_alloc

輸入:內(nèi)存塊的大小size,預(yù)分配的內(nèi)存塊塊數(shù)nobj(以引用傳遞)

輸出:一塊連續(xù)的內(nèi)存區(qū)域的地址和該區(qū)域內(nèi)可以容納的內(nèi)存塊的塊數(shù)

{

計算總共所需的內(nèi)存大小total_bytes;

if(內(nèi)存池中足以分配,即end_free - start_free >= total_bytes) {

則更新start_free;

返回舊的start_free;

} else if(內(nèi)存池中不夠分配nobj個內(nèi)存塊,但至少可以分配一個){

計算可以分配的內(nèi)存塊數(shù)并修改nobj;

更新start_free并返回原來的start_free;

} else { //內(nèi)存池連一塊內(nèi)存塊都分配不了

先將內(nèi)存池的內(nèi)存塊鏈入到對應(yīng)的free_list中后;

調(diào)用malloc操作重新分配內(nèi)存池,大小為2倍的total_bytes加附加量,start_free指向返回的內(nèi)存地址;

if(分配不成功) {

if(16個空閑列表中尚有空閑塊)

嘗試將16個空閑列表中空閑塊回收到內(nèi)存池中再調(diào)用chunk_alloc(size, nobj);

else {

調(diào)用第一級分配器嘗試out of memory機制是否還有用;

}

}

更新end_free為start_free+total_bytes,heap_size為2倍的total_bytes;

調(diào)用chunk_alloc(size,nobj);

}

}

算法:deallocate

輸入:需要釋放的內(nèi)存塊地址p和大小size

{

if(size大于128字節(jié))直接調(diào)用free(p)釋放;

else{

將size向上取8的倍數(shù),并據(jù)此獲取對應(yīng)的空閑列表表頭指針free_list_head;

調(diào)整free_list_head將p鏈入空閑列表塊中;

}

}

內(nèi)存分配器小結(jié)

STL中的內(nèi)存分配器實際上是基于空閑列表(free list)的分配策略,最主要的特點是通過組織16個空閑列表,對小對象的分配做了優(yōu)化。

1)小對象的快速分配和釋放。當(dāng)一次性預(yù)先分配好一塊固定大小的內(nèi)存池后,對小于128字節(jié)的小塊內(nèi)存分配和釋放的操作只是一些基本的指針操作,相比于直接調(diào)用malloc/free,開銷小。

2)避免內(nèi)存碎片的產(chǎn)生。零亂的內(nèi)存碎片不僅會浪費內(nèi)存空間,而且會給OS的內(nèi)存管理造成壓力。

3)盡可能最大化內(nèi)存的利用率。當(dāng)內(nèi)存池尚有的空閑區(qū)域不足以分配所需的大小時,分配算法會將其鏈入到對應(yīng)的空閑列表中,然后會嘗試從空閑列表中尋找是否有合適大小的區(qū)域,

但是,這種內(nèi)存分配器局限于STL容器中使用,并不適合一個通用的內(nèi)存分配。因為它要求在釋放一個內(nèi)存塊時,必須提供這個內(nèi)存塊的大小,以便確定回收到哪個free list中,而STL容器是知道它所需分配的對象大小的,比如上述:

stl::vector array;

array是知道它需要分配的對象大小為sizeof(int)。一個通用的內(nèi)存分配器是不需要知道待釋放內(nèi)存的大小的,類似于free(p)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容