STL源码之deque容器深入学习

vector和deque都是连续线性空间,这往往给我们造成一种假设,认为vector和deque的区别仅仅在于一个单向一个双向。但是剖开它们的源码分析,会发现deque和vector的内部实现机制迥然不同,并且deque要复杂的多。

  1. deque和vector的区别
    (1)vector是单向开头的连续线性空间,deque则是一种双向开口的连续线性空间。
    (2)deque允许在常数时间内对头部进行元素的插入或删除操作。
    (3)deque没有所谓容量(capacity)观念,因为它是动态地以分段连续空间组合而成随时可以增加一段新的空间并链接起来。
    (4)也因此,deque没有必要提供所谓的空间保留(reserve)功能。
    (5)虽然deque也提供Ramdon Access Iterator,但它的迭代器并不是普通的指针。
    (6)对deque进行排序,为了最高效率,可将deque先完整复制到一个vector内,对vector排序后,在复制回deque。现在我们知道了,deque是动态地以分段连续空间组合而成的。它不像vector那样是严格的连续线性空间,但却能实现vector连续线性空间的特性操作。那么问题来了,它究竟是怎么实现的呢?
  2. deque的中控器
    (1)在前面有关vector部分内容中我们已经知道,vector虽然可以增长,却只能向尾端增长,并且其所谓增长实际上是做了三个方面的工作:
    寻找更大空间—>将原数据复制过去—>释放原空间。
    (2)而deque的增长机制是:在头部或尾部增加新空间,deque的最大任务就是在这些分段的定量连续空间上,维护其整体连续的假象,并提供随机存取的接口。
    (3)deque采用一块所谓的map(注意,不是STL的map容器)作为主控。
    (4)这里指的map是一小块连续空间,其中每个元素(称为节点node)都是一个指针,指向另一端(较大的)连续线性空间,称为缓冲区。缓冲区才是deque的存储空间主体。
    (5)SGI STL允许我们指定缓冲区大小,默认值0表示将使用512bytes缓冲区。

    如下图,map和node-buffer(节点-缓冲区)的关系:stl源码图4-10
  3. deque的迭代器
    (1)deque是分段连续空间,维持其“整体连续”的假象的任务,落在了迭代器的operator++和operator–两个运算符上。
    (2)deque迭代器应该具备什么结构?
    (a)它必须能够指出分段连续空间(即缓冲区)在哪里。
    (b)它必须能够判断自己是否已经处于其所在缓冲区的边缘。
    (c)如果是,一旦前进或后退时就必须跳跃至下一个缓冲区。
    (d)为了能够正确跳跃,deque必须随时掌握管控中心(map)。
    基于上述四条,我们实现迭代器如下:

    (3)其中用来决定缓冲区大小的函数buffer_size(),调用__deque_buf_size(), 后者是一个全局函数,定义如下:

    如下图,deque的中控器、缓冲区、迭代器的关系:stl源码图4-11

    举个栗子:现在我们产生一个元素类型为int,缓冲区大小为8(个元素)的deque(语法形式为deque<int, alloc, 8>)。经过某些操作之后,deque拥有20个元素,那么气begin()和end()所传回的两个迭代器应该如下图所示。这两个迭代器事实上一直保持在deque内,名为start和finish。stl源码图4-1220个元素需要20/8=3个缓冲区,所以map之内运用了三个节点。迭代器start内的cur指向缓冲区的第一个元素,迭代器finish内的cur指向缓冲区的最后元素(的下一位置)。

    (4)下面是deque迭代器的几个关键行为,其中最关键的就是:一旦行进时遇到缓冲区边缘,要特别当心,视前进或后退而定,可能需要调用set_node()跳一个缓冲区:

     

  4. deque的数据结构
    deque除了维护指向map的指针外,也维护start, finish两个迭代器,分别指向第一缓冲区的第一个元素和最后缓冲区的最后一个元素(的下一个位置)。
    deque数据结构源码:

  5. deque的构造与内存管理:ctor, push_back, push_front
    deque的缓冲区扩充操作相当复杂。下面一步一步说明:
    (1)deque自行定义了两个专属的空间配置器:

    (2)以下是push_back()函数内容:

    (3)接着是push_front()函数源码:

    (4)还有一个问题:什么时候map需要重新整治呢?这个问题的判断由reserve_map_at_back()reserve_map_at_front()进行,实际操作则由reallocate_map()执行:

     (5)实例
    (a)假设我们对ideq进行了ideq.push_back()等一系列操作之后得到下图的情形:stl源码图4-13

    (b)现在如果在新增加一个元素于尾端:ideq.push_back(3);由于尾端只剩一个元素备用空间,于是push_back()调用push_back_aux(),先配置一整块新的缓冲区,在设妥新元素内容,然后更改迭代器finish的状态。如下图:stl源码图4-14(c)随后,我们再在ideq的前端插入一个新元素:iqed.push_front(99);由于目前状态下,第一缓冲区并无备用空间,所以调用push_front_aux(),如下图:stl源码图4-15
    参考:STL源码剖析

发表评论

电子邮件地址不会被公开。 必填项已用*标注