STL中的六大部件
(1)容器
(2)分配器
(3)算法
(4)迭代器
(5)适配器
(6)仿函数
这六大部件中,我们一般直接使用的是容器、算法和迭代器以及仿函数。其中算法通过迭代器对容器中的元素进行操作。所以实际上各类部件是相互作用的关系。知己知彼才能更好的使用各个部件。推荐去看侯捷老师的STL体系结构与内核分析这一门课,我自己听完之后感觉受益匪浅。
接下来开始总结常见容器:
一、简述
容器可以分类为**sequence container **和 **associative container **(关联容器(key和value))以及 unordered container(不定序容器(集合),元素在容器中无序,哈希实现)。图中红色圈起来的部分是c++11中新添加的部分。
**sequence container **
Array:大小是静态的,不能够修改的。
Vector:自动动态内存扩充,使用者不需要关心vector的大小,只需要把元素放进去就行了,但实际上需要考虑内存的扩充需要把元素进行搬移的消耗。
List:双向链表(事实上是双向环状链表)。
Forward-list:单向链表。
(note:使用链表的时候需要注意list的内存比forward-list的内存大的多,所以当功能要求不大时,使用forwwad-list就够了)
Associative container
Set/Multiset: Key就是value,value就是key。(1)set放的元素不能重复,multiset的元素可以重复
Map/Multimap:每一个结点都有key和value(1)map放的元素不能重复,multimap的(key)可以重复
(note:由于红黑树结构的优势,所以大多编译器的标准库都使用红黑树结构来实现以上两种方式。)
unordered container
Unordered Set/Multiset
Unordered map/Multimap
二、各个容器的底层结构(源码分析)
1、list容器
主要看这里的__list_node的结构和list——iterator的结构,右上角的代码框可以看到链表中的结点的结构为一个pre指针和一个next指针以及一个数据域,这也是我们熟知的双向链表的结构,这里补充一点容器都是前闭后开区间,所以可以看到这里的结构图中,end()指向的结点是灰色的,是一个虚结点,就是为了满足前闭后开的性质。
顺便介绍一下iterator的原则:
其中后面的两种几乎没有使用过。
Iterator_category:分类,移动性质,有的迭代器只能++、–,有的迭代器可以跳着走…这就是迭代器的性质。(list不能像数组那要可以通过下标直接访问,只能一个接着一个进行遍历,所以它的iterator只能进行++或 – ,像array、vector这样的内存空间联系,支持随机访问的容器所对应的iterator就是可以+5、+n这样进行访问的。)
Value_type:容器内部的元素的数据类型
Difference_type:两个迭代器之间的距离需要用什么数据类型来表现,比如用int够不够表示?或者unsigned int够不够来表现?还是说需要long类型才能表示。
其他两个几乎不用。
这几个是iterator向算法提供的回答,就是说算法会去问迭代器这些值,iterator需要回答这几个问题。
2、vector容器
//看左边的内存示意图可以看到capacity和size的关系,可以看到capacity比size要大一些,当使用的空间大过了capacity之后,需要进行扩展,在linux里是呈两倍增长,VS中是1.5倍增长。而且内存进行扩展之后,需要把原来的元素全部搬移到新的空间去,所以可以体会出我们使用vector的时候最好先预测好所需要的内存空间大小,然后预先分配好,尽量避免内存的扩充。
这里推荐一个网站,http://cplusplus.com/查看各个容器简单信息。
vector实际上就是一个数组,但是是动态增长的,而且内置了许多方法可以可以直接调用,通过使用vector之后就会感受到容器存在的意义就是方便我们使用…
3、deque容器
//iterator在每一次的前进过程中,都需要判断是否已经到达了边界,如果到达了边界,就应该到达下一个缓冲区。所以看之前的ppt可以发现,end()指向的实际上是虚元素,当指向边界时,指针就会指向中控中心(图中的map),然后由中控中心指向下一个缓冲区。如图所示。
//之前一直不理解这里的缓冲区,其实是为了减轻内存的负担设置的。缓冲区随着申请次数的增加,每一次所分配的缓冲区大小也会逐渐增长,8,16,64指数增长。从缓冲区来增大存储空间当然会比从内存获取的效率高很多。
可以看到这里的map_pointer map;就是我们图中所说的中控中心。
//上面的注解,主要是说明buffer的大小可以人为指定。具体看代码。
4、set、multiset容器
//这一页ppt就是强调 set/multiset 的value就是key,所以不能通过iterator来修改结点的值。但是本质上这是由于红黑树的性质的决定的,红黑树中结点的值不能随意的改变,不然会破坏树的高度平衡性。
//散列表的特点就是每一个元素的value = key + data,key是唯一的值,通过key可以快速的找到对应的元素。不过对于set和multiset来说其key就是value,所以value不能随便的改。
//set容器底层使用了rb_tree的结构,可以看到右边的三个框框的使用,第一个是我们使用set容器的标准方式,我们只用指明一个key的类型,然后其他的两个参数有默认值,然后从而决定set中的key_type和value_type,从而确定rb_tree中的5个值…
//然后再看ppt中的最后一句代码,这里把iterator定义为了const_类型,就是为了防止使用者通过iterator修改value的值。
//multiset可以允许重复元素。
5、map、multimap
//例如,一个学生的学号可以设置为key,学生的成绩设置为value。Key是不可以修改的,而value是可以修改的。
//注意看这里文字的第一段,这里说map和multimap容器有元素自动排序的特性。排序的依据是key。以后需要用到有序数组时可以考虑使用这个容器。
//select1st()函数,就是选择出容器中的第一个元素。是gunC独有的。其他的编译器可能没有。
//可以看到这里的随后一句迭代器的定义,没有指定const类型,也就是可以进行修改的。
//然后另一个与set不同的是,使用map容器需要指定key和data(value)的类型共两个类型。
//需要自己把key和value包装成pair在放进容器中去。
//不能使用c[key]来直接修改value。与下面讲的map可以进行对比。
//这里展示了map容器直接通过c[key] = value。来修改或插入新的元素。
//可以与multimap需要先把key和value封装为pair再放进multimap中去进行比较。可以发现这种方法更方便使用者使用,不过在时间上可以看到运算符[]重载的函数中看到,需要先进行排序和二分查找再进行插入,会比multimap直接使用insert会慢一点。
6、hashtable
//上图解释了hashtable的问题冲突情况——空间不足。
//为了解决哈希表的冲突问题,
//第一步:每一个篮子设置为链表separate chaining,使产生冲突的元素统一放在该篮子下。但是这遇到了另一个问题是——一直放元素后可能会造成篮子的链表过长,这也会减慢查找效率。
//第二步:当篮子中的链表长度大于当前篮子的个数时(假设一开始篮子的个数为53个,当其中一个篮子的链表有54个结点的时候),就把篮子的个数增大一倍,53-》97。
//第三步:然后进行rehash ing(再散列),也就是原本的元素比如是59%53而找到对应的位置的,现在需要把所有元素key%97,再找到对应的位置放到对应的篮子中去。
//但事实上篮子的个数一定会比总元素的个数多。那为什么不用数组呢?因为hashtable便于查找。
//function object 的大小都是0,也有可能编译器会赋予其1。这里的private里面的前两个就是function obJ。
//size_type一般是unsigned int 4个字节。
//这里的node*指针应该指向篮子链表中的结点,这里图画错了。
//hashtable的数据结构。
7、Unordered container
//篮子永远大于元素的个数。
//这里这个例子想要放100万个元素进去,但是由于使用的是unordered set,不允许有重复的数据(因为key就是value,所以这里说的是数据),所以实际上放进去的元素只有32768(这个数据也是有原因的,因为使用的随机函数的种子范围是0~32768),然后会发现篮子的个数这里是62233。