STL之deque源码剖析

来源:互联网 发布:风险评价矩阵图 编辑:程序博客网 时间:2024/05/18 20:50


暑假那会就开始看 STL 源码了,看了不少,空间配置器,vector,list,map 等等,但是都没有总结,现在感觉忘了不少。最近趁上课没事干,把 STL 源码复习复习,今天就看的所谓的双端队列 deque。


一: Deque 概述


vector 是我们最常用的容器了,vector 是单向开口的连续线性空间,简单说就是数组。deque 则是一种双向开口的连续线性空间,就是可以在头部和尾部添加或删除元素。

deque 和 vector 的差异:
  1. deque 允许常数时间内对头端进行插入或删除操作。
  2. deque 没有所谓的容量概念,因为它是动态的以分段连续空间组合而成,随时可以增加一段新的空间并链接起来。像 vector 那样 “因旧空间不足而配置重新配置一块更大空间,然后复制元素,再释放旧空间” 这样的事情 deque 是不会发生的。因此,deque 也没有 提供 reserve 方法。(注意:deque 迭代器也存在失效的可能,后面会说)。
相同:
        deque 和 vector 一样,也提供随机访问能力,但其迭代器实现过于复杂(内部有三个指针),所以,除非必要,尽量用 vector 。甚至对 deque 的排序,为了最高效率,都要先将 deque 复制到 vector,然后排序完毕再还回去(坑)。


二:Deque 的中控器



deque 内部采用分段式的连续线性空间,整体不是连续的,但每一段确实连续空间。为了给用户造成一种整体就是连续一大片的现象,就需要一个控制层面来管理这些整体不连续的段空间,STL 使用了一个 map,不是 map 容器,map 实际上是一个 T** 类型的指针,当它动态分配好空间之后,就是一个指针数组,数组长度就是我们要分配的缓冲区数目,也就是要分配几段这种连续空间,默认最小为 8 个。数组中的每个指针指向我们使用 alloctor 分配的一片缓冲区,缓冲区用来存放元素内容。如图:


缓冲区用来存放实际元素,每个缓冲区大小默认512字节。

缓冲区大小函数:
inline size_t __deque_buf_size(size_t n, size_t sz)   //__deque_iterator的构造函数模板参数为:template <class T, class Ref, class Ptr, size_t BufSiz>{  return n != 0 ? n : (sz < 512 ? size_t(512 / sz) : size_t(1));}

到此处本来应该分析缓冲区的配置的,但是我们要知道 deque 的 iterator 和普通的容器的 iterator 不一样,剖析 deque 实际上最重要的就是这个迭代器,因为它不像 vector,list 等容器的迭代器,内部仅封装了一个薄薄的指针。deque 的迭代器内部有 4 个指针。下面会分析。

三:Deque 的迭代器


先说下为什么需要 4 个指针,因为我们需要该迭代器做到以下几点,而不是像 vector 的迭代器那样封装 T* ,能够取用元素,前进后退等就足够了:
  1. 它必须能够指出分段连续空间(即缓冲区)在哪里,能进行缓冲区内部元素的增添等操作。
  2. 它必须能够判断自己是否处于缓冲区的边缘,如果是,一旦前进后退就必须跳跃至下一个或上一个缓冲区。
  3. 为了能够跳跃,deque 必须能够随时掌控管控中心。
所以 STL 为 deque 设计了 4 个指针:
  1. cur 指针:cur 指针指向此迭代器所指向缓冲区的 current 元素,用于支持数据操作。
  2. first 指针:first 指针指向所指缓冲区的头,用来控制头部边界
  3. last 指针:last 指针指向所指缓冲区的尾部下一个,实际上已经指向缓冲区之外,符合 [ first, last)。
  4. node 指针:node 指针指向管控中心,当元素到达缓冲区边沿时,元素需要跳到下一缓冲区,这是就要和 node 配合了。
所以我习惯称 deque 的迭代器为一个 ”大迭代器“,不过这个 ”大迭代器“ 的操作核心其实还是 cur,cur 指针真正意义上就相当于我们所用的 vector 的那种迭代器,它表示元素的指针情况,其他三个指针只是为了辅助 cur 指针实现数据结构的功能而存在。如果不是因为底层空间是分段的话,其他几个指针都没有存在的必要了。在分段连续空间中,我们要保证cur 指针的正常前进,就得用其他 3 个指针,前进不了则让 cur 跳跃就行了。

iterator 的图示如下:

iterator 的构造函数:
template <class T, class Ref, class Ptr, size_t BufSiz>struct __deque_iterator {  typedef __deque_iterator<T, T&, T*, BufSiz>             iterator;  typedef __deque_iterator<T, const T&, const T*, BufSiz> const_iterator;  static size_t buffer_size() {return __deque_buf_size(BufSiz, sizeof(T)); }  typedef random_access_iterator_tag iterator_category;  typedef T value_type;  typedef Ptr pointer;  typedef Ref reference;  typedef size_t size_type;  typedef ptrdiff_t difference_type;  typedef T** map_pointer;  typedef __deque_iterator self;  T* cur;  T* first;  T* last;  map_pointer node;   //T**类型  ...}
我们说到元素到缓冲区边界需要跳跃到其他缓冲区,其实是靠这个函数实现的:
 //跳跃缓冲区,可以前进也可以后退  void set_node(map_pointer new_node) {    node = new_node;   //node指针指向要跳跃的node    first = *new_node;   //改变 first    last = first + difference_type(buffer_size());  //改变last  }};
下面将会给出 iterator 支持的常见操作,不过要注意的是,它的任何 operator 重载都是围绕 cur 指针的,例如 operaror++,并不是说迭代器+1就到了下一个结点,其意义实际上是迭代器内部的 cur 指针进行了+1操作,向后移动了 T 类型的举例。 所以可以说cur 指针是迭代器唯一的身份认证,cur 不一样就说明不是同一个迭代器

重载 * 和 -> 函数:
reference operator*() const { return *cur; }pointer operator->() const { return &(operator*()); }
如你所见,这两个函数返回的就是 cur 的内容,所以它们才是迭代器的身份认证,是真正的迭代器,其他指针指示 ”打辅助“ 的。

一些其他函数:
difference_type operator-(const self& x) const {  //迭代器位置之差,就是cur之差,也就是中间缓冲区长度,加上它们分别所在缓冲区实际元素长度    return difference_type(buffer_size()) * (node - x.node - 1) +         (cur - first) + (x.last - x.cur);  }  self& operator++() {  //前置++,进行 cur 的++操作,如果等于 last(last 指向缓冲区外),跳到下一个缓冲区    ++cur;    if (cur == last) {      set_node(node + 1);      cur = first;    }    return *this;   }  self operator++(int)  {  //后置++,调用前置即可    self tmp = *this;    ++*this;    return tmp;  }  self& operator--() {  //同理    if (cur == first) {      set_node(node - 1);      cur = last;    }    --cur;    return *this;  }  self operator--(int) {  //同理    self tmp = *this;    --*this;    return tmp;  }  self& operator+=(difference_type n) {  // +=     difference_type offset = n + (cur - first);   //首先计算总长度    if (offset >= 0 && offset < difference_type(buffer_size()))  //如果加上之后还在本缓冲区内      cur += n;  //直接 += 原始指针    else {  //目标位置不再同一缓冲区内      difference_type node_offset =             offset > 0 ? offset / difference_type(buffer_size())  //不同缓冲区需要跳跃                   : -difference_type((-offset - 1) / buffer_size()) - 1;//>0向后跳,<0向前跳,向前跳注意不能直接除bufsize,否则值不足bufsize,计算结果为0      set_node(node + node_offset);  //切换到正确结点      cur = first + (offset - node_offset * difference_type(buffer_size()));  //切换正确元素(核心还是要处理 cur 吧)    }    return *this;  }  self operator+(difference_type n) const {  //同理    self tmp = *this;    return tmp += n;  }  self& operator-=(difference_type n) { return *this += -n; }   self operator-(difference_type n) const {     self tmp = *this;    return tmp -= n;  }  reference operator[](difference_type n) const { return *(*this + n); }  bool operator==(const self& x) const { return cur == x.cur; }   // 呵呵,注意判断的是 cur 相等不相等,它决定是否是统一迭代器  bool operator!=(const self& x) const { return !(*this == x); }  bool operator<(const self& x) const {    return (node == x.node) ? (cur < x.cur) : (node < x.node);  // 判断 cur  }


四:Deque 的数据结构


直接来看它的类吧:
template <class T, class Alloc = alloc, size_t BufSiz = 0> class deque {public:                         // Iterators  typedef __deque_iterator<T, const T&, const T&, BufSiz>  const_iterator;protected:                      // Internal typedefs  typedef pointer* map_pointer;  typedef simple_alloc<value_type, Alloc> data_allocator;  typedef simple_alloc<pointer, Alloc> map_allocator;  static size_type initial_map_size() { return 8; }protected:                      // Data members  iterator start;  iterator finish;  map_pointer map;  size_type map_size;public:                         // Basic accessors  iterator begin() { return start; }  ...}
其中包含了迭代器、默认的两个空间配置器(用来配置数据和map),以及一个 start 迭代器和 finish 迭代器:
deque 构造好可能是这样的:

其中 first 迭代器代表了 map 的第一个缓冲区的 cur ,同样还是那句老话,first 迭代器不是结点,也不是指向了第一个缓冲区,它代表了第一个缓冲区的 cur 指针所对应的迭代器,也就是 deque 的第一个元素。同理,finish 代表的是最后一个缓冲区的 cur,这个 cur 符合 STL 左闭右开区间原则,所以实际上指向的元素不属于 deque,所以 finish 是一个实际元素以外的迭代器。那么 deque 的 begin() 函数就应该返回迭代器 start,end() 函数就应该返回迭代器 finish了。

来看下 deque 的构造函数:
deque(long n, const value_type& value)    : start(), finish(), map(0), map_size(0)  {    fill_initialize(n, value);  }
实际调用的是:
template <class T, class Alloc, size_t BufSize>void deque<T, Alloc, BufSize>::fill_initialize(size_type n,                                               const value_type& value) {  create_map_and_nodes(n);  map_pointer cur;  __STL_TRY {    for (cur = start.node; cur < finish.node; ++cur)      uninitialized_fill(*cur, *cur + buffer_size(), value);    uninitialized_fill(finish.first, finish.cur, value);  }}
该函数会调用 create_map_and_nodes() 函数进行创建过程,以及用 uninitialized_fil() 函数取填充值,我们只看前者:
template <class T, class Alloc, size_t BufSize>void deque<T, Alloc, BufSize>::create_map_and_nodes(size_type num_elements) {  size_type num_nodes = num_elements / buffer_size() + 1;  //如果结点刚好整除,会多分配一个结点。  map_size = max(initial_map_size(), num_nodes + 2);  //默认最小8个结点,大于8个,则分配num+2个,因为前后预留一个,翻遍扩充  map = map_allocator::allocate(map_size);  //以上培植出一个 "具有map_size个节点的"map  //以下令nstart和nfinish指向map数组全部结点的中央区段  //保持在最中央,可使头尾两端扩充能量一样大,每个节点可对应一个缓冲区  map_pointer nstart = map + (map_size - num_nodes) / 2;    map_pointer nfinish = nstart + num_nodes - 1;      map_pointer cur;  __STL_TRY {    //为mep内的每个 现用 节点配置缓冲区,注意前后预留的不会配置缓冲区,所以缓冲区加起来就是deque的可用空间    //可用空间(最后一个可用空间可能留有一些余裕澹?    for (cur = nstart; cur <= nfinish; ++cur)      *cur = allocate_node();  }  start.set_node(nstart);  finish.set_node(nfinish);    start.cur = start.first;  finish.cur = finish.first + num_elements % buffer_size();  //前面说过,如果刚好整除,会多配一个节点  //此时即令cur指向这多配的一个节点起始处}

嗯,只要明白了迭代器的实质是 cur 指针,知道了 deque 的构造轮廓,其他函数也没什么难点了,本文就到这里 。其他函数可以参考这篇:STL之deque容器的实现框架。


参考:《STL源码剖析》
0 0
原创粉丝点击