Boolan C++ STL与泛型编程_3
主要内容:
本节深入剖析了各种常用容器和容器适配器的底层支撑,容器主要分为三大类,顺序容器、关联容器、无序容器。其中主要介绍了顺序容器中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个篮子中元素的数量。