體系結構與內核分析
OOP VS GP
<big>OOP將datas和methods關聯在一起,GP卻將datas和methods分開來。
GP可以讓Containers和Algorithms兩個團隊各自進行工作,只需要通過Iterator聯通,讓另一個團隊使用。
對于list來說,由于其迭代器不能進行加減運算,所以不能采用Algorithem所提供的sort排序算法,所以list是通過OOP自己實現一個sort。
對于algorithms來說,無論哪種算法,幾乎都是通過元素的比較運算,通過迭代器來對容器進行各種操作。</big>
操作符重載與模板
<big>對于一些特殊的操作符如: :: . . ?: *這四個操作符不能進行重載。
模板內容見前幾期筆記內容。</big>
分配器allocator
<big>C++的動態內存分配,最終都是通過調用malloc來實現的。malloc在分配內存空間的時候,不僅僅只是分配所需要的內存大小,還會存在其他的內存消耗,比如說內存對齊,記錄分配空間大小的區塊和debug內存等等。詳見上次作業operator new部分。
對于VC++6.0和BC5.0大部分的容器默認都是采用的allocator這個分配器。分配器通過allocate和deallocate來調用operator new和operator delete進行分配和釋放內存,而operator new和operator delete又是通過嗲用malloc和free進行的操作。allocate和deallocate可以直接通過對象調用,如下:
由于每一次申請內存空間都會調用malloc進行申請,這就造成了在多次空間分配的情況下有很大的空間浪費。對于GCC2.9也具有和上面一樣的分配器,但是一般都不使用。GCC還帶有另一種分配器,并且被默認使用,就是alloc,該分配器對內存分配做了相應的優化,可以減少很多額外的內存消耗,但是之后的版本取消了相應的優化設計,而alloc變成了__pool_alloc。其內存模型如下:</big>
容器之間的實現關系與分類
鏈表list
<big>list為雙向鏈表,每一個節點除了數據所占用的空間,還需要兩個指針對象,指向當前結點的上一個節點和下一個節點,最后一個節點的下一個節點的next指向第一個節點成為一個循環(為了實現前閉后開,所以特意加了一個空節點,尾后迭代器指向該節點)。
對于鏈表的迭代器iterator并不是一個指針,因為list并不是一個連續的空間,所以必須自定義操作符重載讓它具有類似于指針的作用,即為智能指針。
對于++、--操作符重載,由于這兩種操作符都只對應一個操作符,但是操作符可以前置也可以后置,對于這種操作符的重載就必須先,區分到底是前置還是后置,就以在重載的時候對后置運算符的參數列表里面加上一個int標記,后置++不能連續兩次在同一個語句中。
對于GCC4.9與2.9的節點設計來說,2.9采用的是void,這種方式需要對指針類型進行轉換操作,而到了4.9有一個良好的改善,將指針類型轉變為*__list_node_base****;并且在創建迭代器的時候4.9也只需要一個模板參數。</big>
迭代器設計原則和iterator traits的作用與設計
<big>iterator是算法和容器之間的橋梁,算法需要知道迭代器的五個associated types(category; difference_type; value_type; reference_type; pointer_type)中的一個或幾個,才能進行運算。所以算法通過提問的方式,得到迭代器的associated types,即iterator必須提供這五種設計。
對于iterator不是一個class,而是一種單純的指針,就不能采用上面這種問答方式。這種情況下就需要單獨設計一種traits,traits必須能夠分別單純的指針迭代器和class迭代器。</big>
Vector
<big>vector可以認為是一種可以自動擴充的數組,每次擴充都是重新分配一塊原來大小雙倍空間,然后把數據拷貝過去,再刪除原有的內存空間,實現容量增大。vector的迭代器為普通的元素指針外覆一個iterator adapter,而不是設計好的智能指針。
到了后期,每一次空間成長和插入都會造成大量調用元素的拷貝構造和析構,所以在經常性在中間插入的情況下盡量不用vector容器。</big>
array
<big>將單純的數組通過容器來實現,可以是數組能夠與算法進行聯系,使所有數據結構都有一個統一的運行方式。array實例化的時候最少都要有一個元素空間并且之后空間大小不可改變。array類沒有構造與析構函數且其迭代器也是一個普通的指針。</big>
deque&queue和stack
<big>deque在內存空間并不是一整塊連續的空間,是多塊連續內存的集合。所以需要對迭代器的操作符進行重載,當迭代器指向當前緩沖區的邊界的時候,會自動移動到下一緩沖區的開始位置,其迭代器不是單純的指針而是一個帶有四個指針和操作符重載的class。</big>
queue一種先進后出的容器(適配器),默認通過deque來實現,封鎖deque的某些功能就能實現一個queue,所有對于queue的操作都是通過底層的deque的相關操作實現的。stack的實現也是這樣的,通過deque實現。
stack和queue都不提供遍歷過程,也不提供迭代器,其內部實現不一定要使用deque,也都可以采用list作為類模板的第二模板參數。對于stack可以用vector作為底層結構,但是queue不可以。</big>
紅黑樹(RB-tree)
<big>紅黑樹是一種平衡式二分查找樹,其排列規則對查找和插入有很大優勢,并且保持平衡,即最大深度與最小深度只差最多為1。對紅黑樹通過迭代器進行遍歷之后,一定是排序后的狀態。對于紅黑樹的迭代器,不應用于修改紅黑樹的節點數值,更改之后會破壞搜索結構。
紅黑樹具有五個模板參數,key作為查找和排序;value用于key所對應的節點的值的結合;keyofvalue為key所對應的打他值;compare為比較運算規則,為仿函數對象;如果key和value的類型設置為相同,則節點沒有data部分,key就是data。</big>
set/multiset
<big>set/multiset都是以紅黑樹為底層結構的容器,因此具有自動排序的特性,依據key排序,且set的value就是key,key也是value。不能使用迭代器來改變元素的值,其迭代器也是const iterator,禁止用戶對元素賦值。set需要三個模板參數:</big>
map/multimap
<big>map/multimap與set的差別不大,不過是map的value是由key和data通過pair組合而成,pair.first 為key, pair.second 為data,其中key不能改變,通過pair的first的類型前加const實現,而data可以改變,通過key進行排序和查找,對于map來說,key可以作為下標。map需要默認四個模板參數,后兩個有默認值。
map有重載操作符[]進行下標運算,[]接受的下標為KEY值,如果** key** 不存在就會創建一個,以下標為** key 的pair節點。但是multi map不支持[]運算符,因為multi map是可以有重復key**值的。</big>
hash table
<big>給定每一個對象設置一個號碼,那么至少需要sizeof(T)2^32大的空間才能完全容納下所有對象。但是并不是每一個號碼都有相應的對象,那么我么可以給定一個小于剛剛所需的最大空間,但是要大于對象個數的空間來存放所有對象。而之后的對象在內存空間所在的位置通過號碼對空間大小取余之后的位置上。比如說有兩個int型整數11和12如果按照編號放的話就需要12個int型對象的空間,但是由于只有兩個數值,卻占用了更大的空間,所以我們可以只分配兩個空間(buckets),讓他們對2取余的到兩個編號1和2,那么11放在buckets1的位置,12放在buckets2的位置,這樣不僅方便查找還省內存空間。這就是hash table的基礎。但是如果兩個數字是11和13那么對2取余就會造成,兩個數值都放在了編號為1的buckets*,但是一個空間不能放兩個數據,所以用一個鏈表把他們串聯起來,如下圖:
在這種情況下,如果鏈表太長又會影響到查找的速度,那么就可以擴大buckets的范圍,使整個空間變大,那么元素取余之后便會變得更分散,鏈表就會變短,鏈表越短查找越快。如下圖:
buckets的大小一般都采用大的素數來實現一個hash table。hash table容器一般都是作為其它容器的底層,不會單獨拿出來使用。hash table的實現中hashfcn又稱作散列函數,用于算出元素所處bukets位置即hash code,Extractkey 去除對象的編號函數,Equalkey用于對象比較的函數。** hash table**的源代碼如下圖:</big>
對本周內容的理解
<big>在STL中常用容器有: vector、list、deque、set、map等。根據他們的特性不同有不同的適用范圍,比如:
set 和map都是無序的保存元素,只能通過它提供的接口對里面的元素進行訪問。set:集合, 用來判斷某一個元素是不是在一個組里面時比較好用,占用空間少,查找快;map:映射,相當于字典,把一個值映射成另一個值,如果想創建字典的話使用它好了。set和map底層多采用的是樹型結構,多數使用平衡二叉樹實現,查找某一值是常數時間,遍歷起來效果也不錯, 只是每次插入值的時候,會重新構成底層的平衡二叉樹,效率有一定影響,也有的是通過hash_table來實現,占用空間比通過樹來實現要多,但是訪問速度更快,特別是空間很大時訪問速度最快。
vector、list、deque、set、array 是有序容器。
vector就是動態數組.它也是在堆中分配內存,元素連續存放,有保留內存,如果減少大小后內存也不會釋放.如果添加新值后占用空間大于當前大小時才會再分配內存。它擁有一段連續的內存空間,并且起始地址不變,因此它能非常好的支持隨即存取,即[]操作符,但由于它的內存空間是連續的,所以在中間進行插入和刪除會造成內存塊的拷貝,另外,當該數組后的內存空間不夠時,需要重新申請一塊足夠大的內存并進行內存的拷貝。這些都大大影響了vector的效率。對最后元素操作最快(在后面添加刪除最快 ), 此時一般不需要移動內存,只有保留內存不夠時才需要對中間和開始處進行添加刪除元素操作需要移動內存,如果你的元素是結構或是類,那么移動的同時還會進行構造和析構操作,所以性能不高 (最好將結構或類的指針放入vector中,而不是結構或類本身,這樣可以避免移動時的構造與析構)。 訪問方面,對任何元素的訪問都是O(1),也就是是常數的,所以vector常用來保存需要經常進行隨機訪問的內容,并且不需要經常對中間元素進行添加刪除操作。
list就是雙向鏈表,元素也是在堆中存放,每個元素都是放在一塊內存中,它的內存空間可以是不連續的,通過指針來進行數據的訪問,這個特點使得它的隨即存取變的非常沒有效率,因此它沒有提供[]操作符的重載。但由于鏈表的特點,它可以以很好的效率支持任意地方的刪除和插入。list沒有空間預留習慣,所以每分配一個元素都會從內存中分配,每刪除一個元素都會釋放它占用的內存。list在哪里添加刪除元素性能都很高,不需要移動內存,當然也不需要對每個元素都進行構造與析構了,所以常用來做隨機操作容器.。但是訪問list里面的元素時就開始和最后訪問最快,訪問其它元素都是O(n) ,所以如果需要經常隨機訪問的話,還是使用其它的好。如果你喜歡經常添加刪除大對象的話,那么請使用list。要保存的對象不大,構造與析構操作不復雜,那么可以使用vector代替。list<指針>完全是性能最低的做法,這種情況下還是使用vector<指針>好,因為指針沒有構造與析構,也不占用很大內存 。
deque是一個雙端隊列(double-ended queue),也是在堆中保存內容的。每個堆保存好幾個元素,然后堆和堆之間有指針指向,看起來像是list和vector的結合品.它支持[]操作符,也就是支持隨即存取,可以讓你在前面快速地添加刪除元素,或是在后面快速地添加刪除元素,然后還可以有比較高的隨機訪問速度,和vector的效率相差無幾,它支持在兩端的操作:push_back,push_front,pop_back,pop_front等,并且在兩端操作上與list的效率也差不多。在標準庫中vector和deque提供幾乎相同的接口,在結構上它們的區別主要在于這兩種容器在組織內存上不一樣,deque是按頁或塊來分配存儲器的,每頁包含固定數目的元素.相反vector分配一段連續的存,vector只是在序列的尾段插入元素時才有效率,而deque的分頁組織方式即使在容器的前端也可以提供常數時間的insert和erase操作,而且在體積增長方面也比vector更具有效率。