《STL源碼剖析》——第一、二、三章


 第一章:概論:

換句話說,STL所實現的,是依據泛型思維架設起來的一個概念結構。這個以抽象概念(abstract concepts)為主體而非以實際類(classes)為主體的結構,形成了一個嚴謹的接口標准。在此接口之下,任何組件都有最大的獨立性,並以所謂迭代器(iterator)膠合起來,或以所謂配接器(adapter)互相配接,或以所謂仿函數(functor)動態選擇某種策略(policy或strategy)。

 

 STL提供六大組件

1.容器(containers):各種數據結構,如 vector,list,deque,set,map,用來存放數據,詳見本書4,5兩章。從實現的角度來看,STL容器是一種class template。就體積而言,這一部分很像冰山在海面下的比率。

 

2.算法(algorithms):各種常用算法如sort,search,copy,erase…詳見第6章。從實現的角度來看,STL算法是一種function template。

 

3.迭代器(iterators):扮演容器與算法之間的膠合劑,是所謂的“泛型指針”,詳見第3章。共有五種類型,以及其它衍生變化。從實現的角度來看,迭代器是一種將 operator*,operator->,operatort+,operator--等指針相關操作予以重載的class template。所有STL容器都附帶有自己專屬的迭代器——是的,只有容器設計者才知道如何遍歷自己的元素。原生指針(native pointer)也是一種迭代器。

 

4.仿函數(functors):行為類似函數,可作為算法的某種策略(policy),詳見第7章。從實現的角度來看,仿函數是一種重載了operator()的class或class template.一般函數指針可視為狹義的仿函數。

 

5.配接器(adapters):一種用來修飾容器containers)或仿函數(functors

或迭代器(iterators)接口的東西,詳見第8章。例如,STL提供的queue和stack,雖然看似容器,其實只能算是一種容器配接器,因為它們的底部完全借助 deque,所有操作都由底層的deque供應。改變functor接口者,稱為function adapter;改變container 接口者,稱為container adapter;改變iterator接口者,稱為iterator adapter。配接器的實現技術很難一言以蔽之,必須逐一分析,詳見第8章。

 

6.配置器(allocators):負責空間配置與管理,詳見第2章。從實現的角度來看,配置器是一個實現了動態空間配置、空間管理、空間釋放的class template。

 

 

class alloc{

};

 

 C++11 STL中的容器

==================================================

一、順序容器:

vector:可變大小數組;

deque:雙端隊列;

list:雙向鏈表;

forward_list:單向鏈表;

array:固定大小數組;

string:與vector相似的容器,但專門用於保存字符。

==================================================

二、關聯容器:

按關鍵字有序保存元素:(底層實現為紅黑樹)

map:關聯數組;保存關鍵字-值對;

set:關鍵字即值,即只保存關鍵字的容器;

multimap:關鍵字可重復的map;

multiset:關鍵字可重復的set;

--------------------------------------------------------------------------------

無序集合

【不會按字典規則進行排序】

unordered_map:用哈希函數組織的map;

unordered_set:用哈希函數組織的set;

unordered_multimap:哈希組織的map;關鍵字可以重復出現;

unordered_multiset:哈希組織的set;關鍵字可以重復出現。

==================================================

三、其他項:

1、stack、queue、valarray、bitset

隨機訪問中,[ ]與 .at( )功能相同,但是[ ]越界了會直接導致程序崩潰,而 .at( )會拋出異常,故其更安全!

 

2、size、capacity與shrink_to_fit

size表示目前容器的實際大小、

capacity表示容器的空間,一般 >=size,因為容器初始化或者賦值時,系統會根據情況給予容器一個適當的空間,避免每次增加數據時又得重新新分配空間,索性一次給多點,但也不會很大,

為了節省空間,你可以使用shrink_to_fit將容器空間capacity縮小為size

 

四、迭代器刪除失效:

1. vector,erase(pos),直接把pos+1到finish的數據拷貝到以pos為起點的區間上,也就是vector的長度會逐漸變短,同時iter會逐漸往后移動,直到iter == cont.end(),由於容器中end()返回的迭代器是最后一個元素的下一個(這個地方沒有任何值),現在考慮這個狀態前一個狀態,此時要刪除的點是iter, tempIt = iter, ++iter會指向此時的end(),但是執行erase(tempIt)之后,end()向前移動了!!!問題來了,此時iter空了!!!不崩潰才怪。

 

2. list,erase(pos),干的事情很簡單,刪除自己,前后的節點連接起來就完了,所以iter自增的過程不會指空,不會崩潰嘍。

 

3. map,erase(pos),干的事情太復雜,但是我們需要知道的信息其實很少。該容器底層實現是RBTree,刪除操作分了很多種情形來討論的,目的是為了維持紅黑樹性質。但是我們需要知道的就是每個節點類似於list節點,都是單獨分配的空間,所以刪除一個節點並不會對其他迭代器產生影響,對應到題目中,不會崩潰嘍。

 

4. deque,erase(pos),與vector的erase(pos)有些類似,基於結構的不同導致中間有些步驟不太一致。先說說deque的結構(這個結構本身比較復雜,揀重要說吧,具體看STL源碼),它是一個雙向開口的連續線性空間,實質是分段連續的,由中控器map維持其整體連續的假象。其實題中只要知道它是雙向開口的就夠了(可以在頭部或尾部增加、刪除)。在題中有erase(pos),deque是這樣處理的:如果pos之前的元素個數比較少,那么把start到pos-1的數據移到起始地址為start+1的區間內;否則把pos后面的數據移到起始地址為pos的區間內。在題中iter一直往后移動,總會出現后面數據比前面少的時候,這時候問題就和1一樣了,必須崩潰!

 

關聯容器(如map, set, multimap,multiset),【不會失效】刪除當前的iterator,只會使當前的iterator失效,只要在erase時,遞增當前iterator即可。

 

對於序列式容器(如vector,deque),【會失效】刪除當前的iterator會使后面所有元素的iterator都失效。這是因為vetor,deque使用了連續分配的內存,刪除一個元素導致后面所有的元素會向前移動一個位置。不過erase方法可以返回下一個有效的iterator,cont.erase(iter++)可以修改為cont.erase(iter)

list使用了不連續分配的內存,並且它的erase方法也會返回下一個有效的iterator。

 

五、為不同的容器選擇不同刪除方式

刪除連續容器(vector,deque,string)的元素

 1 // 當c是vector、string,刪除value
 2 c.erase(remove(c.begin(), c.end(), value), c.end());
 3 
 4 // 判斷value是否滿足某個條件,刪除
 5 bool assertFun(valuetype);
 6 c.erase(remove_if(c.begin(), c.end(), assertFun), c.end());
 7 
 8 // 有時候我們不得不遍歷去完成,並刪除
 9 for(auto it = c.begin(); it != c.end(); ){
10     if(assertFun(*it)){
11         ···
12         it = c.erase(it);
13     }
14     else
15         ++it;
16 }
17 
18 刪除list中某個元素
19 
20 c.remove(value);
21 
22 // 判斷value是否滿足某個條件,刪除    
23 c.remove(assertFun);
24 
25 刪除關聯容器(set,map)中某個元素
26 
27 c.erase(value)
28     
29 for(auto it = c.begin(); it != c.end(); ){
30     if(assertFun(*it)){
31         ···
32         c.erase(it++);
33     }
34     else
35         ++it;
36 }    
37 

 

 各大容器性能對比:

1vector

變長一維數組,連續存放的內存塊,有保留內存,堆中分配內存;

支持[]操作,高效率的隨機訪問;

在最后增加元素時,一般不需要分配內存空間,速度快;在中間或開始操作元素時要進行內存拷貝效率低;

vector高效的原因在於配置了比其所容納的元素更多的內存,內存重新配置會花很多時間;

vector的內存分配:

一般是按你當時存儲數據的兩倍開辟空間,當插入數據空間不夠時,又會重新增加空間至當時數據空間的2倍,此動作降低了vector的工作效率!!!

 

注:需要高效的隨即存取,而不在乎插入和刪除使用vector。

 

 

2list

雙向鏈表,內存空間上可能是不連續的,無保留內存,堆中分配內存;

不支持隨機存取,開始和結尾元素的訪問時間快,其它元素都O(n);

在任何位置安插和刪除元素速度都比較快,安插和刪除操作不會使其他元素的各個pointer,reference,iterator失效;

 

注:大量的插入和刪除,而不關系隨即存取使用list

 

3deque

雙端隊列,在堆上分配內存,一個堆保存幾個元素,而堆之間使用指針連接;

 

支持[]操作,在首端和末端插入和刪除元素比較快,在中部插入和刪除則比較慢,像是list和vector的結合;

 

注:關心插入和刪除並關心隨即存取折中使用deque

 

4set&multiset

有序集合,使用平衡二叉樹存儲,按照給定的排序規則(默認按less排序)對set中的數據進行排序;

set中不允許有重復元素,multiset中運行有重復元素;

兩者不支持直接存取元素的操作;

因為是自動排序,查找元素速度比較快

不能直接改變元素值,否則會打亂原本正確的順序,必須先下刪除舊元素,再插入新的元素。

 

5map&multimap

字典庫,一個值映射成另一個值,使用平衡二叉樹存儲,按照給定的排序規則對map中的key值進行排序;

map中的key值不允許重復,multimap中的key允許重復;

根據已知的key值查找元素比較快;

插入和刪除操作比較慢。

 

6hash_map

    hash_map使用hash表來排列配對,hash表是使用關鍵字來計算表位置。當這個表的大小合適,並且計算算法合適的情況下,hash表的算法復雜度為O(1)的,但是這是理想的情況下的,如果hash表的關鍵字計算與表位置存在沖突,那么最壞的復雜度為O(n)。

     選用map還是hash_map,關鍵是看關鍵字查詢操作次數,以及你所需要保證的是查詢總體時間還是單個查詢的時間。如果是要很多次操作,要求其整體效率,那么使用hash_map,平均處理時間短。如果是少數次的操作,使用 hash_map可能造成不確定的O(N),那么使用平均處理時間相對較慢、單次處理時間恆定的map,便更好些。

 

 

 STL容器對比: 

 

vector

deque

list

set

multiset

map

multimap

名稱

向量容器

雙向隊列容器

列表容器

集合

多重集合

映射

多重映射

內部數

據結構

連續存儲的數組形式(一端開口的組)

連續或分段連續存儲數組(兩端

開口的數組)

雙向環狀鏈表

紅黑樹(平衡檢索二叉樹)

紅黑樹

紅黑樹

紅黑樹

 特  點

獲取元素效率很高,插入和刪除的

效率很低

獲取元素效率較高,插入和刪除的效率較高

獲取元素效率很低,插入和刪除的效率很高

1.鍵(關鍵字)和值(數據)相等(就是模版只有一個參數,鍵和值合起來)

2.鍵唯一

3.元素默認按升序排列

1.鍵和值相等

2.鍵可以不唯一

3.元素默認按升序排列

1.鍵和值分開(模版有兩個參數,前面是鍵后面是值)

2.鍵唯一

3.元素默認按鍵的升序排列

1.鍵和值分開

2.鍵可以不唯一

3.元素默認按鍵的升序排列

頭文件

#include <vector>

#include <deque>

#include <list>

#include <set>

#include <set>

#include <map>

#include <map>

操作元素的方式

下標運算符:[0](可以用迭代器,但插入刪除操作時會失效)

下標運算符或迭代器

只能用迭代器(不斷用變量值來遞推新值,相當於指針),不支持使用下標運算符

迭代器

迭代器

迭代器

迭代器

插入刪除操作迭代器是否失效

插入和刪除元素都會使迭代器失效

插入任何元素都會使迭代器失效。刪除頭和尾元素,指向被刪除節點迭代器失效,而刪除中間元素會使所有迭代器失效

插入,迭代器不會失效。刪除,指向被刪除節點迭代器失效

插入,迭代器不會失效。刪除,指向被刪除節點迭代器失效

插入,迭代器不會失效。刪除,指向被刪除節點迭代器失效

插入,迭代器不會失效。刪除,指向被刪除節點迭代器失效

插入,迭代器不會失效。刪除,指向被刪除節點迭代器失效

 

 總結:

 

vector

 deque

 list

 set

 multiset

 map

 multimap

典型內存結構

單端數組

雙端數組

雙向鏈表

二叉樹

二叉樹

二叉樹

二叉樹

可隨機存取

對 key 而言:不是

 

 

vector

 deque

 list

 set

 multiset

 map

 multimap

元素搜尋速度

非常慢

對 key 而言:快

對 key 而言:快

元素安插移除

尾端

頭尾兩端

任何位置

-

-

-

-

 

 

 容器自定義比較方式:

1 struct fruit 
2 {
3         string name; 
4         int price; 
5         friend bool operator <(fruit fl, fruit f2)
6         {
7                 return fl. price>f2. price;
8         }
9 };

 

 STL容器新穎使用

將數組中的值直接付給容器

int arry[size] = {0,1,2,3,4,5,6};

xxx<int>contain(arry, arry+size);//即將數組中的值直接初始化賦予了容器中

  

 

 

 簡介SGI

vector:

 

 

 1 class alloc{
 2 
 3 };
 4 
 5 template <class T,class Alloc=alloc>
 6 
 7 class vector{
 8 
 9 public10 
11     void swap(vector<T,Alloc>&){
12 
13         cout<<"swap()"<< endl;
14 
15     }
16 
17 };
18 
19 vector<int>x,y;
20 
21 template <class T,class Alloc=alloc>
22 class vector{
23 public:
24     typedef T value_type; 
25     typedef value_type* iterator; 
26     template <class I>
27     void insert(iterator position,I first,I last){
28         cout <<"insert()"<< endl;
29     }
30 };
31 vector<int>x;
32 vector<int>::iterator ite;

  

stack:

 

 1 template <class T,clase A1loc=alloc,size_t BufSiz=0>
 2 
 3 class deque{
 4 
 5 public:
 6 
 7     deque(){
 8 
 9         cout <<"deque"<< endl;
10 
11     }
12 
13 };
14 
15 //根據前一個參數值T,設定下一個參數Sequence的默認值為deque<T>
16 
17 template <class T,claas Sequence= deque<T>>
18 
19 class stack{
20 
21 public:
22 
23     stack(){
24 
25         cout <<"stack"<< end1;
26 
27     }
28 
29  
30 
31 private:
32 
33     Sequence c;
34 
35 };
36 
37 stack<int>x;
38 
39 
40 template <class T,class Alloc=alloc,size_t BufSiz=0>
41 class deque{
42 public: 
43     deque(){
44         cout <<"deque"<<'';
45     }
46 };
47 
48 template <class T,class Sequence>
49 class stack;
50 
51 template <class T,class Sequence>
52 bool operator==(const stack<T,Sequence>&x,const stack<T,Sequence>&y);
53 
54 template <class T,class Sequence>
55 bool operator<(const stack<T,Sequence>&x,const stack<T,Sequence>&y);
56 
57 
58 template <class T,class Sequence=deque<T>>
59 class stack{
60     //寫成這樣是可以的
61     friend bool operator==<T>(const stack<T>&,const stack<T>&);
62     friend bool operator< <T>(const stack<T>&,const stack<T>&);
63     //寫成這樣也是可以的
64     friend bool operator==<T> (const stack&,const stack&);
65     friend bool operator< <T> (const stack&,const stack&);
66     //寫成這樣也是可以的
67     friend bool operator==<> (const stack&,const stack&);
68     friend bool operator<<>(const stack&,const stack&);
69     //寫成這樣就不可以
70     //friend bool operator==(const stack&,const stack&);
71     //friend bool operator<(const stack&,const stack&);
72 public:
73     stack(){cout <<"stack"<<endl;}
74 
75 private:
76     Sequence c;
77 };
78 
79 template <class T, class Sequence>
80 bool operator==(const stack<T, Sequence>& x, const stack<T, Sequence>&y){
81     return cout <<"operator=="<<'\t';
82 }
83 template <class T, class Sequence>
84 bool operator<(const stack<T, Sequence>& x, const stack<T, Sequence>&y){
85     return cout <<"operator<"<<'\t';
86 } 
87 int main(){
88     stack<int>x;//deque stack
89     stack<int>y;//deque stack 
90     cout <<(x==y)<<endl;//operator==1
91     cout <<(x<y)<<end1;//operator<1
92     
93     stack<char>y1;//deque stack
94     //cout c<(x== y1)<< endl; //error: no match for...
95     //cout <<(x< y1)<< end1;//error: no match for...
96 }

 

deque:

 1 inline sizet _deque_buf_size(size_t n,size_t sz){
 2 
 3     return n!=0?n:(sz<512?size_t(512/sz):size_t(1));
 4 
 5 }
 6 
 7  
 8 
 9 template <class T,class Ref,class Ptr,size_t BufSiz>
10 
11 struct _deque_iterator{
12 
13     typedef _deque_iterator<T,T&,T*,BufSiz> iterator;
14 
15     typedef __deque_iterator<T,const T&,const T*,BufSiz>const_iterator;
16 
17  
18 
19     static size_t buffer_size(){
20 
21         return deque_ buf_size(Bufsiz,sizeof(T));
22 
23     }
24 
25 };
26 
27  
28 
29 template <class T,class Alloc=alloc,size_t Buf8iz=0>
30 
31 class deque{
32 
33     typedef deque_iterator<T,T&,T*,BufSiz> iterator;
34 
35 };
36 
37  
38 
39 cout << deque<int>::iterator::buffer_size()<<end1;//128
40 
41 cout << deque<int,alloc,64>::iterator::buffer_size()<<endl;//64

 

 第二章、空間配置: 

(1)考慮到小型區塊所可能造成的內存破碎問題,SGI設計了雙層配置器,第一級配置器直接使用malloc和free,第二級配置器則視情況采用不用的策略(需求區塊是否大於128bytes)。

 

 

 

 

第一級適配器以malloc()free()reaclloc()C函數執行實際的內存配置、釋放、重配置操作。第二級配置器多了一些機制,避免太多小額區塊造成內存的碎片。

 

 

 

 

SGI第二級配置器的做法是,如果區塊夠大,超過128字節時,就移交第一級配置器處理。當區塊小於128字節時,則以內存池(memory pool)管理,此法又稱為次層配置(sub-allocation):每次配置一大塊內存,並維護對應的自由鏈表(free-list)。下次若再有相同大小的內存需求,就直接從free-lists中拔出。如果沒有,則向系統要一大塊內存,然后做切割,此時切割出來的小內存塊,不帶cookie。如果客戶端釋放小額區塊,就由配置器回收。為方便管理,任何小額區塊的內存需求量上調至8的倍數,並維護16free-lists,各自管理大小分別為8,16,24...128字節。

 

 

 

 

G4.9中編譯器使用的是不作任何優化的空間配置器,如果需要制定,則需要指明第二參數:

vector<string, __gnu_cxx::__pool_alloc<string>> vec;

如果free-list中沒有可用的區塊,將區塊大小上調至8的倍數邊界,然后調用refill(),准備為free-list重新填充空間。refill()之后介紹。

空間的釋放,大於128字節就調用第一級配置器,小於128字節就找出對應的free list,將區塊回收。

 

 

 

 

refill()重新填充空間,新的空間將取自內存池(經由chunk_alloc()完成)。內存池實際操練結果如下圖:

 

 

 

  

 

 

 

(2)第一級配置器直接調用C函數執行實際的內存操作,並實現類似C++ new handler的機制。

 

(3)當區塊超過128bytes時,視之為足夠大,便調用第一級配置器。當配置器小於128bytes時,視之為過小,便采用內存池的方式。每次配置一大塊內存,並維護對應之自由鏈表,下次若再有相同大小的內存需求,就直接從free-list中拔出。如果客端釋還小額區塊,就由配置器回收到free-list中。為了方便管理,SGI第二季配置器會主動將任何小額區塊的內存需求量上調至8的倍數(例如客端要求30bytes,就自動調整為32bytes),並維護16個free-list,大小分別是8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128bytes。

 

(4)內存池

 

如果水量充足,就直接調出20個區塊返回給free-list,如果不夠20則返回不足實際個數。如果一個都拿不出,則調用malloc配置40個區塊的空間,其中20個給free-list,剩下的20個留在內存池。如果malloc分配失敗,則調用第一級配置器。因為第一級配置器有new-handler機制,獲取能夠整理出多余的空間。、

 

(5)內存基本處理工具

uninitialized_copy,uninitialized_fill,uninitialized_fill_n,能夠將內存配置與對象構造行為分離開來。並且會對POD(即標量型別或傳統C struct)對象采用最有效率的初值填寫手法,而對non-POD型別采取最保險安全的做法。

 

為了細化分工,STL allocator將兩階段操作區分開來。內存配置操作由alloc::allocate()負責,內存釋放操作由alloc::deallocate()負責;對象構造操作由::construct負責,對象析構操作由::destroy()負責。

 

  • 內存池:

當申請空間不充足時,系統首先將剩余的空間盡量為你申請出來

一般新申請的空間大小為你所需求空間大小的2倍加上一個隨機配置次數增加愈加增大的附加量

 

 

 

 

 

 

 

 第三章、迭代器概念

STL的中心思想在於:將數據容器(containers)和算法(algorithms)分開,彼此獨立設計,最后再以一帖膠着劑將它們撮合在一起。

 

迭代器相應型別:

迭代器相應型別之一:value type:

所謂 value type,是指迭代器所指對象的型別。任何一個打算與STL算法有完美搭配的class,都應該定義自己的vlue type內嵌型別,做法就像上節所述。

 

迭代器相應型別之二:difference type:

difference type用來表示兩個迭代器之間的距離,因此它也可以用來表示一個容器的最大容量,因為對於連續空間的容器而言,頭尾之間的距離就是其最大容量。

如果一個泛型算法提供計數功能,例如STL的count(),其傳回值就必須使用迭代器的diference type:

 

迭代器相應型別之三:reference type

“迭代器所指之物的內容是否允許改變”的角度觀之,迭代器分為兩種:

不允許改變“所指對象之內容”者,稱為constant iterators,例如 const int *pic;【視為不可改的右值】

 

允許改變“所指對象之內容”者,稱為mutable iterators,例如int *pi。當我們對一個mutable iterators進行提領操作時,獲得的不應該是一個右值(rvalue),應該是一個左值(lvalue),因為右值不允許賦值操作(assignment),左值才允許:

 

int *pi=new int(5);

const int *pci=new int(9);

*pi=7;//對mutable iterator進行提領操作時,獲得的應該是個左值,允許賦值

*pci=1;//這個操作不允許,因為pci是個constant iterator,

   //提領pci所得結果,是個右值,不允許被賦值

 

迭代器相應型別之四:pointer type

pointers和references在C++中有非常密切的關聯。如果“傳回一個左值,令它代表p所指之物”是可能的,那么“傳回一個左值,令它代表p所指之物的地址”也一定可以。也就是說,我們能夠傳回一個pointer,指向迭代器所指之物。

 

迭代器相應型別之五:iterator_category

最后一個(第五個)迭代器的相應型別會引發較大規模的寫代碼工程。在那之前,我必須先討論迭代器的分類。

 

根據移動特性與施行操作,迭代器被分為五類:

·Input lterator:這種迭代器所指的對象,不允許外界改變。只讀(read only)。

·Output terator:唯寫(write only)。

·Forward lterator:允許“寫入型”算法(例如replace())在此種迭代器所形成的區間上進行讀寫操作。

·Bidirectiona lterator:可雙向移動。某些算法需要逆向走訪某個迭代器區間(例如逆向拷貝某范圍內的元素),可以使用Biairectional lterators。

·Random Access lterator:前四種迭代器都只供應一部分指針算術能力(前三種支持 operator++,第四種再加上operator--),第五種則涵蓋所有指針算術能力,包括p+n,p-n,p[n],pl-p2,p1<p2。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM