主要內容:
本節深入剖析了各種常用容器和容器適配器的底層支撐,容器主要分為三大類,順序容器、關聯容器、無序容器。其中主要介紹了順序容器中deque的內部實現,以及默認deque作為底層支撐的兩個容器適配器stack和queue。并且對紅黑樹進行了深入探索,以及由它實現的set、multiset、map、multimap。還有對hash table進行了詳細分析,由它實現了unordered容器。
1. deque
deque是分段(buffer)連續的,deque內部利用vector存放所有段的首地址。向deque的頭尾插入數據時,都會先查看最后一個段是否還有空閑空間,如果有,則直接插入數據;如果沒有,那么會新分配一個段,并把段的地址存放在vector中,再向其插入數據。
deque的iterator是一個class,內部存放4個指針(first,last,cur,node),其中node是指向vector中第一個數據段的指針,利用這個指針可以使cur指針自增自減到另一個段。first指向一個段的頭,last指向一個段的尾(段中最后一個元素的下一個位置)。first和last表示段的邊界,每次自增自減都要判斷是否到達邊界,如果是,那么要借助node指針,將iterator指向下一個段。cur表示段中當前指向的位置。當在一個段內操作時,只有cur會變動,當到達邊界切換到下一個段時,這4個指針都要改變。
deque中iterator的大小是16字節(4個指針)。deque中有4個數據成員:兩個iterator分別指向deque的begin和finish,一個指向vector的指針(map)和vector的大小(map_size),所以共40個字節(32位cpu)。
deque的模板參數有3個,包括元素類型、分配器、BufSiz(默認是0). __deque_buf_size用于計算buffer size. 如果沒有指定BufSiz,那么會根據元素的大小(sz)設置buffer size,如果一個元素的大小小于512,那么要用512除以元素的大小來計算buffer size. 如果大于等于512,buffer size會設置為1.
deque<T>::insert(iterator pos, const value_type &x) 允許指定位置放入元素。內部會先判斷pos是否在頭或尾(pos.cur是否是start.cur或是finish.cur),如果是的話,那么會調用push_front(x)或push_back(x)實現。如果不是,那么就要在中間插入元素調用insert_aux(pos, x)。insert_aux插入數據時會先判斷插入點前后哪一邊的元素少,取少的那一邊全部元素向前移動一個位置,之后空出來的位置放入x。這樣做可以盡可能高效。
deque如何模擬連續空間:
*it是取得iterator的cur的值.
difference_type operartor-(const self& x) const 得到兩個迭代器之間的距離(元素個數),中間完整緩沖區個數(node-x.node-1)*bufsiz+起始buffer的元素數量(x.last-x.cur)+結尾buffer的元素數量(cur-first)。
用后++調用前++,用后--調用前--. 前++會先++cur,檢查cur是否到達邊界,如果是,則set_node(node+1),node,first和last要重設。前--會先判斷cur==first,如果是,那么set_node(node-1)切換到前一個緩沖區的最后一個元素的位置。
deque也重載了+=,-=,+,-運算符,迭代器可以一次移動n個位置。先判斷是否會跨越緩沖區,如果是,那么再計算要跨越幾個緩沖區,再移動node指針來跨越緩沖區,到達最后一個緩沖區,再計算要偏移幾個元素,最后到達指定的位置。operator-=相當于+= -n,所以-=是利用operator+=來實現的。operator[]是利用operator+實現的,operator+是利用operator+=實現的。operater-是利用operator-=實現的,所以歸根結底[],-=,+,-這三個運算符重載底層都是由self & operator+=(difference_type n)實現的。G4.9不允許用戶調整buffer size的大小,但是也是利用__deque_buf_size函數隊友沒有指定bufSiz的情況的策略計算buffer size大小。
deque中維護buffer地址的vector每次按照2倍大小進行擴充,擴充后,會將之前的元素拷貝到中段,前后都能留有空閑空間,便于前后擴展。
2. queue和stack
- 它們內部擁有一個deque. 他們的各個函數都是調用的deque的函數。queue和stack也可以選擇list作為底部支撐,默認是選擇deque. 使用方式eg.stack<string, list<string>> c; queue<string, list<string>> q; 選擇deque作為底部支撐效率更高。
- queue和stack都不允許遍歷,也不提供iterator,因為不允許中間插入/刪除元素。
- queue不能用vector作為底部支撐,但stack可以,因為pop操作vector所提供的函數中沒有pop_front函數,編譯器會在調用pop的時候報錯,但不調用pop不會報錯,也就是說當在定義一個用vector作為底部支撐的queue時,編譯器不會檢查deque提供的各個函數是否可用,只有在調用各個函數的時候才會檢查。
- queue和stack也不允許選擇set和map作為底部支撐。因為set不會提供pop_front,push_back,back等函數接口,所以在調用時會保錯。在定義deque的時候就不能使用map作為底層的支持,因為map存放的是鍵值對,但queue和stack中需要的是一個類型的元素。所以在定義時就會報錯。
3. RB-tree深度探索
- 紅黑樹是平衡二元搜索樹。有利于查找和插入。遍歷時,++iter得到的元素是按照key排序的。map允許改變data,不允許改變key. rb_tree提供兩種insert操作:insert_unique()和insert_equal().
- rb_tree的模板參數:key, value(key和data合成value),KeyofValue(從value中取key的方法),Compare(兩個節點key比較大小的方法),Alloc=alloc.
- rb_tree的數據成員:node_count(節點數量),header(指向節點的指針),key_compare(比較key的準則,是個泛函數,大小是1) G2.9 大小是12(4+4+1=9,對齊后是12).G4.9中是24包含3個指針和一個枚舉類型_Rb_tree_color(1個字節)。
- G4.9的stl結構設計采用handler and body 分離(base和impl分離)的方式.
- set和map都是由紅黑樹實現的。
4. set和multiset
- 對于set來講key就是value,不能改變元素值。set插入數據是調用的rb_tree的insert_unique(), multiset插入數據是調用的rb_tree的insert_equal().
template<class Key, class Compare = less<Key>, class Alloc = alloc> class set{...};
eg. set<int> iset;<==> template<int, int, identity<int>, less<int>, alloc>class rb_tree;
- set的iterator是rb_tree中的const_interator,防止修改iterator指向的元素。
- set的所有操作都是調用底層rb_tree的操作。所以set也未嘗不是一個container adapter.
- vc6不提供identity(),是創建一個泛函數實現的。
5. map和multimap
- 這里key是const類型,防止用戶更改key.
eg. map<int, string> imap; <==>
template<int, pair<const int, string>, select1st<pair<const int, string>>, less<int>, alloc> class rb_tree;
- map的iterator就是rb_tree的iterator.
- vc6不提供select1st函數。
- operator[]如果map中沒有這個key,就會自動創建并插入這個pair, 且value是默認值。operator[]的內部實現,iterator _i = lower_bound(_k);如果map中沒有_k, 那么會返回適合放置_k的位置,之后會調用insert插入到map中。
6. hashtable
當元素個數比籃子的個數多的時候,會將hashtable打散,將籃子增加到大約是原有的2倍。gcc中將籃子的數量已經保存在static const unsigned long __stl_prime_list[]數組中,擴展籃子的數量時會查看這個數組。G2.9中的做法:初始籃子的個數是53,之后擴展時,53*2=106,會在106附近找到一個素數(97),作為籃子新的數量。之后所有的元素要重新計算放置位置。
class hashtable要指定6個模板參數:value,key, class HashFcn(hash function計算元素對應的編號hash code), class ExtractKey(從元素中取出key), class EqualKey(計算key相等的方法), class Alloc=alloc.
hashtable的大小:HashFcn,EqualKey,ExtractKey的對象各占1個字節,vector<node*,Alloc> buckects占12字節(內部有3個指針)。size_type num_elements占4個字節。總共19個字節,對齊后是20個字節。__hastable_node中有一個指向下一個節點的指針和value。
hashtable中iterator的設計,考慮到遍歷完一個鏈表后,要能夠回到hashtable, 再遍歷下一個鏈表,所以iterator中除了有一個指向當前節點的指針外node* cur, 還有一個指向hashtable本身的指針hashtable* ht目的就是能回到hashtable再遍歷下一個鏈表。
hash-function,hash-code: 標準庫中有hash類的泛化模板,也有對數值類型和char*類型元素計算hash-code特化,元素是數值的話,會默認就將這個數值作為hash-code, 也可以用戶自己寫這樣的特化hash類,設計得到hash-code。注意:標準庫中沒提供hash<std::string>需要自己寫.
計算元素落在哪個籃子的方法:hash(key)%n.
eg. G2.9 hashtable<const char, const char, hash<const char *>, identity<const char >, eqstr, alloc>ht(50, hash<const char>(), eqstr()); 第一個參數50是設置籃子的數量,實際會查看__stl_prime_list數組(G4.9版和G2.9有區別,這是G2.9版的情況),找到第一個比50大的籃子數量53進行分配。
G4.9的hashtable名稱變了,并且參數由6個變成了10個。
7. unordered容器
c++11將hash_set,hash_multiset,hash_map,hash_multimap這4個名字改為unordered_set, unordered_multiset, unordered_map,unordered_multimap.
c.bucket_size(i)打印第i個籃子中元素的數量。