1、基本概念
deque是双向开口的序列空间,可在常数时间内在头尾两段插入和删除元素,相比vector要高效很多。同时,deque没有容量的概念,其内部是以分段的连续空间组合而成的。就我看来,deque是vector和list的折中状态,兼和了vector的快速访问和list的空间动态增长,能够适应更加特殊的需求。
2、设计思路
deque在内部使用一小块连续空间的map作为控制器,其中的每个元素指向另外一块连续空间的缓冲区,实际的数据是存储到缓冲区中的。
控制器map初始大小为8,用于存储缓冲区,当空间不够存储新的缓冲区时,就需要扩充map的空间,扩充方式与vector的扩充方式一致,以下是实现源码:
size_type _Newsize = 0 < this->_Mapsize ? this->_Mapsize : 1;
while (_Newsize - this->_Mapsize < _Count || _Newsize < _DEQUEMAPSIZ)
{ // scale _Newsize to 2^N >= _Mapsize + _Count
if (max_size() / _DEQUESIZ - _Newsize < _Newsize)
_Xlen(); // result too long
_Newsize *= 2;
}
这里_DEQUEMAPSIZ就是map的初始大小8,map大小以2倍速率进行增长。
缓冲区大小随STL版本的不同也是不同的,目前SGI STL是512个字节,而vs使用的STL设置的是16个字节。下面是SGI版本的源码:
inline size_t __deque_buf_size(size_t __size) {
return __size < 512 ? size_t(512 / __size) : size_t(1);
}
通过代码我们可以知道缓冲区的大小是固定不变的,vs版本也是一样不可更改。《STL源码剖析》一书中该函数是多一个参数的,我不知道是否是版本变更导致的。按照目前的源码,我们是完全无法更改缓冲区的大小的,这点留待后续深入了解下。
3、迭代器
deque提供的迭代器能够更vector的迭代器一样进行一次移动n(n>=1)步,因而也是随机迭代器。但由于deque是分段连续空间,就需要迭代器处理好缓冲区跳转的问题。迭代器使用cur、first、last、node四个指针,其中cur指向缓冲器的当前位置,first指向缓冲区的第一个节点,last指向缓冲区的最后一个节点的下一个位置,遵循STL前开后闭的标准。
4、数据结构
deque除了维护控制器map及map的大小外,还有start和finish两个迭代器,其中start指向第一个缓冲区,finish指向最后一个缓冲区。初始化时,deque会根据map的大小和缓冲区的数量调整start和finish的位置,使缓冲区设置到map的中间位置,方便后续向两头增长。下面是设置源码:
_Tp** __nstart = _M_map + (_M_map_size - __num_nodes) / 2;
_Tp** __nfinish = __nstart + __num_nodes;
基于前开后闭的原则,start和finish操作是不一样的。分为以下四种情况:
1. push_front:start指向的缓冲区的空间是向左增长,start.cur==start.first时缓冲区满,此时再调用push_front时创建新的缓冲区;
2. pop_front:start.cur==start.last时缓冲区空,但是当start.cur==start.last-1,即缓冲区只剩一个元素时,此时调用pop_front会先销毁元素然后回收当前的缓冲区;
3. push_back: finish指向的缓冲区的空间向右增长 finish.cur==finish.last时缓冲区满,当finish.cur==finish.last-1,即缓冲区剩余一个位置时,此时再调用push_back会创建新的缓冲区,同时插入元素;
4. pop_back:finish.cur==finish.first是缓冲区空,此时调用pop_back时先回收当前缓冲区然后销毁元素。
这里需要注意的是,对于插入,后插是在还有一个空余位置就会马上创建新的缓冲区;对于删除,前删是在只剩一个元素时就会马上回收缓冲区。