deque
此章节会详细解析deque这一容器,使用VS STL的deque为例子。VS STL在核心部分和SGI STL是一致的,但是在内存分配和一些地方有着很大的不同,我会着重把他讲的清楚明白。
首先我们还是要讲讲deque与vector的不同,我们称deque为双端队列,理由是他可以同时在容器的结尾和开头进行O(1)的插入删除操作。
正是这一差别导致deque在内部于vector有着很大的不同。vector在扩增的时候时常不停的申请内存复制移动,但是deque却不存在这样的操作。这一行为让deque在某方面又和list有着惊人的相似,所以deque没有扩容的概念,因为他们内部实现也是向list一样,各个元素是连接起来的。
但是,deque却又允许O(1)的访问这又deque和list之间有了差别,也就是说deque内部的迭代器是随机访问迭代器。
为了让deque又如上的特性,STL内部不得不让deque的实现变得很复杂而且运行起来又相当耗费时间,所以deque的效率有时候会不如vector。什么时候deque是程序员们值得考虑的一件事情。
内部结构
依旧,我们打开deque的容器看看他包含着什么,于是我们再次发现了我们的老朋友
_Compressed_pair<_Alty, _Scary_val> _Mypair;
这东西我们见过好多次了,都知道他里面存着对控制容器至关重要的数据。而这些数据,存储在_Deque_val
中,也是_Scary_val
别名过去的东西。
在_Deque_val
内部:
private:
static constexpr size_t _Bytes = sizeof(value_type);
public:
// elements per block (a power of 2)
static constexpr int _Block_size = _Bytes <= 1 ? 16
: _Bytes <= 2 ? 8
: _Bytes <= 4 ? 4
: _Bytes <= 8 ? 2
: 1;
_Mapptr _Map; // pointer to array of pointers to blocks
size_type _Mapsize; // size of map array, zero or 2^N
size_type _Myoff; // offset of initial element
size_type _Mysize; // current length of sequence
deque使用一个指针数组用来掌控他的所有元素,这个数组,就是_Map
。注意,这个_Map
是一个二级指针,因为它是指向指针数组的指针,在_Deque_simple_types
中,我们能之间看到它的定义:
template <class _Ty>
struct _Deque_simple_types : _Simple_types<_Ty> {
using _Mapptr = _Ty**;
};
这个数组的用途是以下图示所指出的。
map中的每个指针指向一个缓冲区,那里存着deque中真正的数据。想必你已经明白为什么deque的可以如此方便在开头和结尾的插入,同时他又像list一样不需要容量的概念。
下面是_Deque_val
中各元素的解释:
- _Map:指向map指针数组。
- _Mapsize:指出了_Map数组的大小,它固定是2^n的数量。
- _Myoff:指出了deque中第一个元素的偏移量。
- _Mysize:deque容器目前的大小,即元素的数量。
之后我会详细解释map数组的扩容。
有了大致的实现我们就可以想想怎么去访问了。deque要求随机访问,就像数组的行为一样。这个责任就落在deque的iterator上。
iterator重载了一系列运算符,我挑一些重点的来说。
_NODISCARD reference operator*() const noexcept {
_Size_type _Block = _Mycont->_Getblock(_Myoff);
_Size_type _Off = _Myoff % _Block_size;
return _Mycont->_Map[_Block][_Off];
}
这个是iterator重载的*运算符,可以看到它最终是通过_Map[_Block][_Off]
来获取我们需要的元素的,这个_Block通过_Myoff算出,_Off通过求余得到。
注意:每一个iterator里面都有一个_Myoff,指出了该迭代器的偏移量。
下面是_Getblock
的内部实现:
return (_Off / _Block_size) & (_Mapsize - 1);
是的,它只有一行代码,但是却有着相当大的作用,之后在deque扩增的时候我们会再次提起它。
重载了operator*()
之后,只需要一个_Myoff
就能够返回我们需要的元素,这样,其他迭代器增加减少的操作只需要控制迭代器内部的_Myoff
即可。比如operator++(int)
_Deque_unchecked_const_iterator operator++(int) noexcept {
_Deque_unchecked_const_iterator _Tmp = *