1 前言
上一篇
STL深入探究(一、空间配置器)我详细总结了SGI STL采用的空间配置机制,这一篇来总结一下stl容器的底层实现机制。
2 序列式容器
2.1 Vector
Vector实现方式类似于“数组”,与array的数据安排和操作方式非常类似,两者唯一的差别就在于空间运用的灵活性。
array是静态空间,一旦配置了就不能改变,要想换一个大点的空间,就必须由客户自己实现:(1)配置一块新空间;(2)将元素从旧址一一搬到新址;(3)释放原空间。
而vector是动态空间,随着元素的加入,它的内部机制会自行扩充空间以容纳新元素。
2.1.1 vector的数据结构
vector的数据结构非常简单:线性连续空间,以两个迭代器start、finish分别指向配置得来的连续空间中目前已被使用的范围,并以迭代器end_of_storage指向整块连续空间的尾部。
为了降低空间配置的速度成本,vector的实际配置大小可能会比客户端需求量更大,已备将来可能扩充。
2.1.2 vector 构造与内存管理
vector缺省使用std::alloc作为空间配置器,并另外定义了一个data_allocator为了更方便以元素大小为配置单位。
当以push_back()函数将新元素插入vector尾端,该函数首先检查是否还有备用空间,如果有就直接在备用空间上构造元素,并调整迭代器finish,使得vector变大;如果没有备用空间,就扩充空间(重新配置(需求的两倍)、移动数据、释放原空间)。
对于vector支持的动态增加大小,并不是在原空间之后接续新空间(因为并不能保证原空间之后尚有足够的可供配置的新空间),而是以原大小的两倍另外配置一块较大空间,然后将原空间内容拷贝过来,再开始在原内容之后构造新元素,并释放原空间。
所以说,对vector的任何操作,一旦引起空间重新配置,指向原vector的所有迭代器就都失效。
2.2 list
相比较于vector的连续线性空间,list就比较复杂,每次插入或者删除一个元素都会独立的配置或释放一个元素空间。list对于空间利用非常精准,不会浪费。
list是环状双向链表,我们在数据结构中都曾学习过它的特性与使用方法,不再多说。值的一提的是,list由于其双向性,必须提供前移与后移的能力,所以提供BidirectionalIterators,而vector提供的是Random Access Iterators。
注:
list有一个重要性质,插入(insert)和接合(splice)操作都不会造成原有的list迭代器失效。list的元素删除(erase)操作仅仅“指向被删除元素”的迭代器失效,其它迭代器不受影响。 vector并做不到这一点,因为vector的插入操作可能引起空间重新配置,这导致原有的迭代器全部失效。
另外,SGI list不仅仅是一个双向链表,而且是一个环状双向链表,所以它只需一个指针即可完整表示整个结构。
2.3 deque
vector底层采用的是一个数组来实现,list底层采用的是一个环形的双向链表实现,而deque则采用的是两者相结合,所谓结合,并不是两种数据结构的结合,而是某些性能上的结合。我们知道, vector支持随机访问,而list支持常量时间的删除,deque支持的是随机访问以及首尾元素的插入删除 。
deque与vector的最大差异,一在于deque允许常数时间内对起头端进行元素的插入和删除,二在于deque没有所谓的容量概念,因为它是以分段连续空间组合而成,随时可以增加一段新的空间并连接起来。换句话说,像vector那样因旧空间不足而重新配置更大空间,复制元素,释放原空间的事情,deque是不会出现的。也因此,deque不需要提供所谓的空间保留。
2.3.1 deque的数据结构
如上图所示,deque是由一段一段的定量连续空间构造。一旦有必要在deque的前端或尾端增加新空间,便配置一段连续空间,串接在整个deque的头部或尾部。
deque采用一块所谓的map(不是STL的map容器)作为主控,这里的map也是一块连续空间,其中每个元素为一个节点(也是deque的迭代器),指向另一段较大的连续线性空间,称为缓冲区,缓冲区才是deque的储存空间主体。
2.3.2 deque的迭代器
下面,来看一下deque的迭代器设计。
举例来说,如果deque中存储20个元素,每个缓冲区大小为8,则需要20/8=3个缓冲区,所以map中会运用3个节点。deque的begin()和end()始终会返回map中节点的头和尾,名为start和finish,其中,start的cur指向第一个缓冲区中的首元素,finish的cur指向最后一个缓冲区的最后一个元素的下一位置。
2.3.3 deque的构造和内存管理
deque的缓冲区扩充操作比较繁杂,此处仅仅截取《STL源码剖析》的几张图展示,具体细节见书籍P153-166。
注:上面的连环图解,充分展示了deque容器的空间处理。那么现在就出现一个问题,什么时候map需要重新整治?
如果无穷尽的向deque中存储数据,map所含有的缓冲区肯定会填满的。也就是说,当map的前端节点或后端节点备用空间不足时,map便也需要重新配置,依然是经过三个步骤,配置更大的、拷贝原来的、释放原空间。