C++標準庫(體系結構與內核分析)(侯捷第二講)(OOP GP、分配器、容器之間關係、list、萃取機、vector、array、deque、stack、queue、RB-tree、set、multi

1、OOP和GP的區別(video7)node

OOP:面向對象編程(Object-Oriented programming)算法

GP:泛化編程(Generic programming)編程

對於OOP來講,咱們要實現容器,應該是這樣的:數組

將數據和方法關聯在一塊兒,例如排序,用成員方法的方式將其放在容器類中。cookie

對於GP來講,咱們將數據和方法分開:less

如圖中所示,左邊是容器的定義,而右邊是sort算法的定義。咱們用::sort(c.begin(),c.end())就能夠調用全局的sort算法,參數表明數據的存放範圍(首尾的迭代器來指明範圍)。dom

採用GP有什麼好處:ide

  • Containers和Algorithms團隊可各自閉門造車,他們之間以Iterator溝通便可。
  • Algorithms經過Iterators肯定操做範圍,並經過Iterators取用Container元素。

例如:函數

簡單的算法,比大小函數min和max:post

min和max接受兩個對象的引用,而後使用「<」和「>」來比較大小,這是算法團隊實現的。可是至於如何比對,就須要由數據團隊本身來實現,例如比較兩個石頭(stone)的大小,是比較直徑呢仍是比較重量,這不是算法團隊關心的。

 

對於某些容器,可能並不適合使用全局算法,例如list容器:

list容器不能採用全局的sort來進行排序,這是爲何?由於全局的sort方法,要正確使用,對迭代器有必定的要求,必需要求迭代器是隨機訪問迭代器(Random Access Iterator),可是list的node在內存中是離散分佈的(用指針連接起來),因此他的迭代器不知足隨機訪問這個需求,因此list本身提供了成員方法sort(),那麼咱們要對list排序,就要使用它本身的sort()。

可以使用全局::sort()的容器有array、deque、vector等內存是連續分配的容器。

 

標準庫提供的不一樣版本的算法:

max函數,標準庫提供了兩種版本:

第一種版本是利用類的操做符重載來完成大小的比對,但若是比對的是語言自己帶的類型,可能只提供了一個默認的比較方式,例如string默認是按字符一個一個比對的。可能不能知足咱們的個性化需求。

第二種版本的參數除了提供要比對的數據,還能夠提供一個方法,這個方法咱們能夠自定義如何比對數據,例如咱們能夠比對string的長度。

2、分配器Allocator(video11)

瞭解operator new、operator delete、malloc、free:

在VC6.0裏,operator new的實現如圖中所示,底層是調用的malloc。而malloc分配的內存如圖中右邊所示,詳細解釋可參照 C++程序設計1(侯捷video 7-13)中的第五節。

 

在VC6.0中,標準庫提供的容器所使用的默認Allocator都是出於頭文件<xmemory>中的allocator。如圖:

VC6.0中Allocator是怎麼工做的:

allocator類中最重要的就是allocate()方法和deallocato()方法。

在allocate()方法中,使用的是operator new來進行內存分配,而咱們知道operator new的底層是使用的malloc,因此內存的分配流程就比較清晰了。

圖中,右邊部分有一個書寫代碼的技巧:

使用allocator<int>()能夠產生一個臨時的allocator對象,而後直接調用allocate來分配512個字節的空間。第二個參數是什麼呢,注意,在allocate成員方法的定義中,第二個參數只有類型,而沒有形參名,這就說明這個參數不會被使用,他是利用這個參數來獲取須要分配的數據類型(不深刻探討)。

 

除了VC6.0,在BC++5中一樣是這種結構:

從這個圖能夠看出deallocate的實現方式,就是調用operator delete。而後operator delete再調用free來釋放內存。

可是在deallocate的實現中,參數須要指明歸還內存的大小,這就是爲何不建議直接使用分配器來申請內存的緣由,allocator和deallocator是提供給容器實現用的,而不是爲了提供給用戶使用的。

 

再看GNU C2.9的allocator實現:

在GNU C的實現中,一樣採用了同樣的方法來實現,可是右下角的說明表示:

GNU C雖然實現了標準庫規定的默認allocator,可是並未在任何的容器中將其運行爲默認分配器

 

爲何不用這種allocator呢?

由於咱們在實際應用中,所須要分配的空間通常都是比較小的,那麼就必定對應大量的allocate調用(也就對應malloc調用次數),每次malloc分配的空間都有額外開銷(參照 C++程序設計1(侯捷video 7-13)中的第五節 ),當須要的空間小時,額外空間佔的比例就大,因此咱們須要儘可能減小分配的次數。

額外開銷:

Debug模式下,額外開銷就是除藍色部分之外的全部(包含紅色、灰色和綠色部分)。

Release模式下,額外開銷就是紅色部分(cookie)。cookie是用來供malloc記錄分配內存的代銷的,使用free釋放時就不要指定大小了。

 

在GNU C中有另一種更好的默認Allocator,供容器做爲默認選項:

這種叫作alloc的分配器儘可能減小malloc的調用,避免cookie佔太多額外空間。計算一下,當分配100W個空間時,能夠節省100W*8=800Wbytes的空間,也就是8MB的空間。那麼若是是10億呢,那就大約是8GB的內存空間。(固然,這裏計算的節省值只是一個直觀的表現,實際並不會節省這麼多,由於alloc畢竟仍是須要使用malloc來拿內存,只是次數相對少一些)

 

 alloc的實現流程簡介:

如圖所示,alloc中維護了一個長度爲16的數組,該數組中放了16個指針,每一個指針都指向一塊內存區域(這個內存區域也是使用malloc拿到的,固然也帶cookie)。在這個數組中,每一個指針所指向內存的單位大小是不一樣的,例如#0指向的內存塊大小都是8bytes,#1指向的都是16bytes,以此類推,#15指針指向的是128bytes。當一個容器須要申請內存時,alloc會根據容器所需存放的數據類型來決定從哪個指針指向的內存塊中來分配,固然,數據類型的大小須要調整爲8的倍數。例如容器須要的類型大小爲50,alloc會選擇#7號指針指向的內存中來分配。

從上面的工做過程當中能夠看出alloc有必定的優勢,但確定也有必定的缺點。好比何時使用free來歸還指針指向的一大塊內存。好比容器釋放內存後,這塊內存是否當即free,仍是保留在alloc中供下次分配。好比,當容器須要的內存大於alloc如今擁有的內存塊,alloc是從新申請更大的塊,仍是採用何種機制擴充。(這些咱們內存管理中來剖析)

 

上面所述的這種比較好的alloc實在GNU C2.9版本中做爲默認allocator的,但到了4.9版本,他的默認allocator又用回了標準庫規定的allocator(也就是和VC6.0、BC++5中同樣的實現)。具體緣由不得而知,在GNU C4.9版本中allocator的標準實如今new_allocator中。如圖所示:

那麼在2.9版本中做爲默認使用的alloc還在不在呢?答案是還在,只是將其改成_gnu_cxx::__pool_alloc(觀察其工做過程,有內存池的感受)。

咱們能夠在從此的使用中,本身制定使用這個__pool_alloc,具體使用方法,在前面的章節中已經示範過,能夠自行參考。

3、容器之間的關係(video12)

在STL提供的如此豐富的容器之間是否存在必定的聯繫,以下圖所示:

解釋:

1.上圖所述內容是基於GNU C2.91版本。

2.縮進表示複合關係,而不是繼承關係,例如set和map中包含有rb-tree,stack和queue中包含一個deque。

3.非標準的意思是在C++1.0的時候未歸入標準庫。例如slist、hash_**。

4.圖的下方描述了C++11中對標準庫容器的改變,一些非標容器轉正,並新增array。

5.左右兩邊的藍色框中的數字表示在GNU C2.9和4.9版本下,容器自己所佔大小的比對。容器自己所佔大小的意思是不包含容器中存放數據的空間,而僅指本身用於控制或其餘功能所消耗的空間。

4、深度探索list容器(video13)

在GNU C2.9版本中,list容器以下圖所示:

 

解釋:

1.標準庫中的list容器是一個雙向環狀鏈表。

2.list的定義中只有一個成員變量,就是node,類型是link_type,實際就是__list_node*。

3.__list_node的結構如圖右上角所示,除了包含一個數據意外,還有指向前一個node的指針,以及指向後一個node的指針。但在2.9版本中,設計得不夠好,他使用了void指針,按理說應該使用__list_node*會更好。

4.list的定義中還有begin()、end()等能夠獲取Iterator的方法,list搭配使用的Iterator是定義在全局中的__list_iterator,這個迭代器應該重載「++」、「--」等指針經常使用的操做符,但因爲list的內存分佈是離散的,因此這裏的「++」操做符必須實現得更聰明,調用iterator++的時候,iterator應該指向下一個node的指針(其實就是檢查本節點的next指針)。

5.在__list_interator的結構體實現中,模板傳入了三個參數T、T&、T*,實際上只須要傳入一個T就能夠了,引用和指針均可以在使用的時候再定義,不必經過參數傳入。後面咱們能夠看到,在4.9版本有所改善。

6.因爲標準庫提供的list是一個環狀雙向鏈表,爲了實現前閉後開區間的規定,因此最後一個node是一個空白節點,如圖中左邊文字描述,咱們使用begin()獲取鏈表首個節點地址,使用end()獲取最後一個非空節點的下一個節點地址(空白節點地址)。

__list_iterator的實現:

上圖中對「++」的重載有兩個方法,分別爲前++(prefix)和後++(postfix):

解釋:

1.爲了區分前++和後++兩種重載,使用一個int參數才區分。不帶int參數的是前++,帶int參數的是後++。

2.前++是比較好理解的,先取到本節點的next指針,並賦值給node。在返回本身的引用,能夠前++是能夠連續使用的,例如++++i。

3.後++不是很好理解,後++的意思是先保留目前的數據,而後再實現+1。因此第一步就是將當前狀態的本身記住,使用self tmp = *this,這裏的*this並不會調用重載後的「*」(獲取節點中data的指針),而是由於「=」已經被重載過,如今是拷貝構造的做用,因此*this會被解釋爲迭代器構造函數的參數,因此取得是當前的本身,而後拷貝構造了一份tmp。

4.而後調用++*this,使用已經重載的前++,讓node指向下一個節點(next)。

5.最後返回tmp對象。爲何這裏返回的不是引用(前++返回的是引用),是由於爲了迎合C++中後++不能連續使用的

規定或習慣。也就是不能使用i++++這種方式,如圖中左下角,咱們已整數爲標準,整數不支持的操做符功能,咱們也遵循其規格。

__list_iterator迭代器對「*」和「->」的重載:

「*」和「->」的重載比較簡單,分別返回裏面存放數據data的引用和指針。

 

__list_iterator在GNU C2.9和4.9的不一樣:

解釋:

1.在2.9版本中出現的同時傳入T、T&和T*的問題獲得了改善,4.9中只傳入一個_Tp,而後在內部進行typedef。

2.在2.9版本的__list_node定義中出現的void指針也獲得了改善,替換成了_list_node的父類指針。固然,__list_node也變爲了有有繼承關係的_List_node和_List_node_base。

5、Iterator Traits萃取機(video15)

Traits的概念比較抽象,咱們一步一步來引導出這個概念。

前面咱們看到標準庫在__list_iterator的定義中,使用了多個typedef,如圖所示:

這些typedef到底有什麼用呢?

這裏的typedef所定義的類型,例如iterator_category、value_type、pointer、reference和difference_type,他表示了一個Iterator的屬性。好比有些容器須要雙向可移動的迭代器(實現了++和--),有些容器只須要單向移動的迭代器,還有些容器的迭代器支持多步跳着走,那麼這些迭代器就能夠經過typedef定義iterator_category來分類。當算法要經過迭代器做用於這個容器數據時,就能夠經過這個iterator_category詢問迭代器他所屬的類型。

例如上圖中,這個迭代器是bidirectional_iterator_tag,表示雙向迭代器。

上述的五種類型,就是C++標準庫所規定和設計出的5種算法提問

咱們看下面這張圖:

解釋:

1.圖中的iterator_category就是迭代器的類型。

2.value_type顧名思義就是迭代器指向數據的類型。

3.pointer和reference就是value_type的指針和引用。

4.difference_type就是一個用於描述兩個迭代器之間距離大小的類型,例如兩個迭代器相差100W個數據,那麼difference_type這個類型必需要可以支持100W這麼大的數據,好比int。更大的話可能就須要long。在標準庫中,選擇的是ptrdiff_t這個類型,具體是什麼須要查看源代碼。

5.圖右邊的算法能夠直接經過I::iterator_category來得到他想問的結果。

6.可是,咱們使用標準庫提供的算法,不是必定會傳給他迭代器,也有可能會傳給他一個原始的指針,可是原始的指針內部沒有定義這些類型(問題的答案)。因此咱們要引入Traits

什麼是Traits?以下圖所示:

Traits要可以區分class iterators和non-class iterators。

Traits是如何實現的:

解釋:

1.當算法想要詢問value_type時,會經過iterator_traits中轉。

2.若是算法要問的東西是一個迭代器,那麼會經過iterator_traits的泛化定義(圖中的黑一)。

3.若是算法要問的東西是一個指針,那麼會經過iterator_traits的範圍特化定義(圖中的黑二)。

4.若是算法要問的東西是一個常量指針,那麼會經過iterator_traits的範圍特化定義(圖中的黑三),注意這裏返回的是T而不是const T,見圖中右邊黃色框的解釋。

5.這裏只展現了value_type,其餘4個問題,也是同樣的。

完整的iterator_traits:

Traits的簡單總結:

Traits是一個算法與迭代器(或指針)之間的中間層,當算法須要詢問問題的時候,若是詢問對象是指針,那麼由Traits來代替其回答。若是詢問對象是迭代器,那麼就由Traits幫算法詢問,並返回答案。

其餘的一些Traits:

6、深度探索Vector容器(video16)

Vector是一個能夠動態擴展容量的數組(始終是連續空間)。可是,沒有什麼東西能夠在原地擴充內存,因此在vector中,當空間不足時,會從內存中找到另一塊2倍大的空間,而後把原來的數據都搬過去,從而實現空間的擴充。如圖:

如圖中左邊所示,空間僅剩兩個單位,但要存放一個佔用三個單位的數據。vector找到另一個2倍大的空間,而後將本來的數據搬進去,再存放新的數據。

代碼解釋:

1.vector經過三根指針來控制空間和數據,start、finish和end_of_storage。分別對應左圖中的位置。

2.begin()返回start,end()返回finish,知足前閉後開的規範。

3.size()返回目前數據所佔空間的大小,capacity()返回vector目前總共可用空間大小。

4.empty()返回vector是否爲空,用begin()==end()來判斷是否有數據。

5.提供operator [],[]中能夠傳入index。

6.front()和back()分別返回第一個元素和最後一個元素的迭代器。

vector的基本實現:

解釋:

1.調用push_back()的時候(調用insert()的時候也同樣),檢查空間是否足夠,若是足夠就放入x,而後調整finish的位置。

2.若是空間不夠,則調用insert_aux(),在insert_aux()中再次檢查是否還有空間,這裏的再次檢測主要是提供給其餘函數調用的,例如insert()函數,因此咱們能夠看到,在檢測到有空間的時候,將position這個位置(insert插入點)的後面全部數據所有朝後移動了一格。最後在position的地方放入x。

3.若是在insert_aux()中檢測到沒有空間了,那麼就須要擴充空間。見第二個圖。

4.先記住舊的size,而後判斷size是否爲0,若是不爲0,則擴充爲2倍,新size爲len。

5.新申請一個長度爲len的空間,將其頭指針賦值給new_start。

6.而後開始拷貝舊的數據到新的空間中,這裏也是爲了兼容insert,因此先拷貝的position以前的,而後插入x,再拷貝position以後的數據。

7.數據所有拷貝完後,使用destroy()釋放舊空間。

8.最後調整start、finish以及end_of_storage的指針位置。

vector iterator中的traits:

工做過程和list差很少,能夠參考list的traits原理。

7、深度探索Array容器(video17)

爲了讓普通的數組也能享受標準庫帶來的優秀的算法等功能,在C++2.0版本中,把數組作了封裝的,產生了容器array。

在array的TR1版本(technology reportor 1,是一個C++1.0和C++2.0之間的過分版本)中,array設計的結構是比較簡單的。

解釋:

1._M_instance就是內部的數組,類型就是_Tp,也就是定義array時元素類型模板參數。

2._Nm是定義array時給定的大小,當_Nm爲0時,內部的數組的size定義爲1,由於不存在size爲0的數組。當_Nm不爲0的時候,則size爲_Nm。

3.一樣實現一堆操做函數,都是模擬普通數組的。

4.array的迭代器,直接使用指針來代替,因此算法詢問問題得時候,Traits代替指針進行回答(參考list traits解釋)。

在GNU C4.9版本中,array的設計結構複雜了不少,沒有太多參考價值,由於不懂實現團隊的遠期目標,不知道爲何要搞那麼複雜。

8、深度探索deque容器(video18)

 deque是雙向隊列,即先後均可以擴展空間,他的實現方式是比較複雜的一種。如圖所示:

解釋:

1.deque又一個控制中心和多個內存塊組成,控制中心是一個vector,裏面按順序存放每一個內存塊的地址。又這些地址串聯起來造成整個deque的空間。而且面向用戶展示的是相似連續空間的表徵。

2.deque的iterator結構如圖所示,由4個指針組成,cur表示當前數據的位置,也就是其餘iterator中數據指針的位置。first表示iterator所在的當前內存塊的首地址,last表示當前內存塊的尾地址。最重要的是node,node指向控制中心存放當前內存塊地址的那一格的指針,也就是經過node,iterator知道本身所處在哪一個內存塊。

3.向後擴展:當iterator的node指向的是控制中心中最後一個內存塊的指針,當cur==last時,就須要向後擴展內存了。此時,會新申請一塊內存塊,並將內存塊的地址存放到控制中心的最後面,而後該iterator的node向後移一格,而後first爲當前新內存塊的首地址,last爲新內存的最後,而後cur調整爲等於first,再寫入數據。

4.向前擴展:當iterator的node指向的是控制中心中第一個內存塊的指針格,此時,當cur=first時,就須要向前擴展內存了,新申請一塊內存,將內存塊的地址插入到控制中心的最前面,而後該iterator的node指向新內存的指針格,將first調整爲新內存的首地址,last爲新內存的尾地址,cur調整爲等於last,而後再last-1放入數據。

deque具體實現代碼:

解釋:

1.deque定義中有4個成員屬性,start表示處於第一塊內存塊的iterator,其中的cur指向第一個數據。

2.finish表示處於最後一個內存塊的iterator,其中的cur指向最後一個數據。

3.map指向控制中心的vector,他的類型是T**,T是模板參數,表示deque中數據的類型,好比int。map中存放的是指向內存塊的指針,在這裏就是int*類型。而後map又是指向vector的指針,vector中存放的是int*,因此map就是int**,也就是T**。map佔用大小爲4bytes。

4.map_size表示控制中心vector的大小。map_size佔用大小爲4bytes。

5.deque帶3個模板參數,第一個是數據類型,第二個是分配器,第三個是每一個內存塊的大小,默認爲0。具體爲多少,參照圖中灰色框裏的說明。(新版本的deque實現貌似不容許用戶指定內存塊大小)

deque iterator實現:

解釋:

1.iterator中也有4個屬性,cur表示當前指向的數據位置。所佔大小爲4bytes。

2.first指向的是該iterator當前所處內存塊的首地址。所佔大小爲4bytes。

3.last指向的是該iterator當前所處內存塊的尾地址。所佔大小爲4bytes。

4.node指向的是控制中心map中它目前所在內存塊對應的指針塊,類型是T**。node所佔大小爲4bytes。

5.因此,一個deque iterator所佔的空間爲16bytes。

6.咱們能夠推出,定義一個空的deque,他自身所佔空間大小爲兩個iterator的大小加上控制器指針的大小,再加上控制器vector的map_size大小,一共是40bytes。

7.iterator要提供給算法使用,必須知足traits要求,也即定義5種類型。在deque的iterator中,iterator_category是random_access_iterator_tag,由於deque對外宣稱他的內存是連續的(其實是分散的),iterator也提供了「++」「--」等random access的操做。

deque的insert操做:

解釋:

1.首先兩個判斷,是否在deque的最前段或是最後段,若是是,那就直接插入就行了。

2.若是不是,交給insert_aux()函數處理。

3.計算插入的位置離deque的頭近仍是尾近。離哪邊近,數據就往哪邊推。

4.肯定往哪邊推後,就開始搬移數據,以離頭近爲例。先在最前面加一個元素,值等於如今的第一個數據,而後依次將後面的數據向前挪一個位置,將須要insert的位置騰出來。而後再插入值x。日後推同理。

deque是如何實現空間連續的假象:

主要是經過operator的重載來實現的,當咱們使用「++」或「--」這些指針操做時,內部實現應該是可以自動的在不一樣的內存塊之間切換,造成內存地址連續的假象。

具體實現參照video19。

9、深度探索stack和queue(video19)

stack和queue都默認使用deque做爲底層結構。

例如queue:

queue使用Sequence模板參數,默認選擇就是deque做爲底層結構,queue中全部的方法實現,都是經過deque來實現的。stack也是同理。

stack和queue也能夠選擇其餘的容器做爲底層結構,例如list等。只要選擇的容器支持上圖中被調用的方法,就能夠做爲底層結構。可是默認採用的deque應該效率最高的。

注意:

queue不能選擇vector做爲底層結構,而stack能夠採用vector做爲底層結構。

queue和stack都不能夠選用set和map做爲底層結構。

stack和queue都不提供遍歷功能,也不提供iterator:

10、RB-TREE紅黑樹(video20)

 紅黑樹的一些特性:

1.是平衡二叉搜索樹,不會出現一條臂很深的狀況,能夠保證查詢效率

2.能夠支持遍歷操做以及iterators,按正常狀況遍歷出的結果是有序的

3.儘可能不要修改元素的值,由於值被修改了,樹的機構會改變,而改變的效率是比較低的(在編程層面並不拒絕這個操做,只是不推薦),在set中最好不要修改值,而在map中,能夠修改value的值,但最好不要修改key的值。

4.紅黑樹提供兩種插入,一種是key不重複的insert_unique(),一種是能夠重複的insert_equal()。

紅黑樹代碼簡要介紹:

解釋:

1.紅黑樹的實現要傳入5個模板參數,第一個是node中key的類型,例如int。第二個Value,並非咱們想象中key:value中的value,這裏的value是指key:data的組合。第三個參數KeyOfValue是提供一個函數,告訴他如何從Value組合中得到key。第四個參數Compare是告訴他如何比較key的大小。第五個參數是分配器。

2.rb_tree中的數據部分:node_count用來記錄樹種一共有多少個節點。header是一個空的節點,從這裏開始訪問樹,有這個header,也方便代碼的設計。key_compare是比大小的一個函數。

3.在GNU C++2.9版本中,一個rb-tree的大小爲9字節,其中node_count佔4bytes,header是一個指針,也佔4bytes,key_compare是一個函數,大小應該是0,但編譯器都會將其變爲1byte。加起來一共是9bytes。

如何直接使用標準庫中的rb-tree:

解釋:

1.傳入key的類型爲int,value的類型爲int,表示key和value是同一個,也就是set。KeyOfValue,傳入的是identity<int>類,其實是一個仿函數。compare傳入的是less<int>,這是標準庫提供的,也是做爲一個仿函數。

2.在identity類中,實現了operator(),就至關於把該類變爲了一個仿函數,而這個函數就是將參數返回出去,也就是說明從value裏獲取key的方法,value就是key。

3.less類中,也實現了operator(),這是使用小於符號比較大小,能夠用來比較int。

4.identity和less類都繼承了父類,而父類裏只有類型定義,這在標準庫中叫作可適配的,不用深究。

如下是在GC++2.9中使用rb-tree的代碼:

注意:該代碼只能在GNU C++2.9中使用,若是要在其餘編譯器使用,須要本身實現identity類,能夠參照上一個圖中的代碼。在VC6.0中,這個函數叫_Kfn()。

11、set、multiset深度探索(video21)

1.set和multiset以rb-tree爲底層結構,所以元素時自動有序排列的。排序是根據key來排的,而set和multiset元素的value就是key,key就是value。

2.set和multiset提供iterator遍歷操做,能夠直接得到排序的數據。

3.沒法經過iterator來修改元素的值,由於在set和multiset中,key就是value。在rb-tree底層實現中使用的是const iterator。

4.set的元素必須是獨一無二的,他的insert()使用的是rb-tree的insert_unique()。multiset的元素能夠是重複的,他的insert()使用的是rb-tree的insert_equal()。

解釋:

1.圖中綠色方框中描述的是,咱們建立一個數據爲int類型的set,實際上還有兩個默認的模板參數。

2.對應底層rb-tree,會自動將須要的5個模板參數給填充好。

3.若是咱們建立的set中存放的元素不是int類型,而是咱們自動定義的類,例如Stone,那麼咱們就要考慮到less函數中是使用小於號比對大小的,那麼咱們就須要在Stone中重寫operator <(或者本身實現對應的仿函數),從而知足其要求。

4.set類中所提供的iterator的類型,其實是rb-tree所提供的const_iterator,因此在rb-tree底層是一個不可改變其指向值的迭代器。

5.因爲set的全部操做都轉到rb-tree來作,因此咱們能夠稱set和multiset是一個容器適配器。

使用multiset示例:

上述代碼是往multiset中放入100W個元素,這些元素都是0-3W+的隨機數,理論上應該都有重複。放入multiset後,數量仍是100W(若是放入set的話,估計就只有3W多)。調用全局的find函數(::find())比調用本身提供的find函數(c.find())要慢不少,因此若是本身提供了算法,優先使用本身的(效率必定更高)。

12、map、multimap深度探索(video22)

1.map和multimap以rb-tree爲底層結構,所以元素時自動有序排列的。排序是根據key來排的。

2.map和multimap提供iterator遍歷操做,能夠直接得到排序的數據。

3.沒法經過iterator來修改key的值,但卻能夠修改key對應元素中data的值。

4.map中元素的key必須是獨一無二的,他的insert()使用的是rb-tree的insert_unique()。multimap中元素的key能夠是重複的,他的insert()使用的是rb-tree的insert_equal()。

解釋:

1.圖中綠色方框中描述的是,咱們建立一個key爲int類型、data爲string類型的map。

2.對應底層rb-tree,會自動將須要的5個模板參數給填充好。

3.注意看rb-tree的第二個參數,他將key和data組合成了一個pair。並且key的類型是const int,就說明key不能修改。

4.rb-tree的第三個參數,使用select1st函數來獲取pair中的第一個元素,就能夠獲取到key。

5.因爲set的全部操做都轉到rb-tree來作,因此咱們能夠稱map和multimap是一個容器適配器。

使用multimap示例:

map獨特的操做符operator []:

方括號中傳入的參數是key,用於查找或插入data。

使用方法:

當map中存在對應key的元素時,返回該元素,獲取其中的data。

當map中不存在對應key的元素時,若是咱們有賦值操做,則建立一個等於指定key的元素,而且存入data。

使用代碼以下:

相關文章
相關標籤/搜索