SGISTL源码探究-deque容器(下)

前言

在上一小节中我们介绍了deque容器实现的大致思路以及它内部使用的一些机制,比如存储元素的空间其实是由指针数组里面的指针指向的等。
在本小节中,我们主要介绍一些deque容器封装的一些常用的操作,比如push_backpush_frontinserteraseclear等。这些函数无非就是根据迭代器内部的firstlastcur的位置来决定进行操作。
所以有必要再重温一下firstlastcur指针指向的位置。

  • first:指向当前线性空间的第一个元素
  • last:指向当前线性空间的末尾,最后一个元素的下一个位置
  • cur:指向当前线性空间的当前元素。在迭代器start中,默认当然指向第一个元素。在迭代器finish中,默认指向最后一个元素的下一个位置。
    在分析它的源码之前,我们应该考虑到插入元素在deque尾部时会发生的特殊情况,即当前线性空间快满时,需要存储到下一个线性空间的情况,因此我们可能需要使得finish迭代器指向下一个结点,甚至没有可用的下一个结点时,我们还需要扩展map

插入元素到deque的尾部

接下来我们来看看push_back是如何操作的。

void push_back(const value_type& t) {
  /* 当前线性空间还可以容纳2个及以上的元素时
   * 就无需申请下一个线性空间
   * 否则调用push_back_aux
   */
  if (finish.cur != finish.last - 1) {
    construct(finish.cur, t);
    ++finish.cur;
  }
  else
    push_back_aux(t);
}

push_back_aux的代码如下:

// Called only if finish.cur == finish.last - 1.
/* 调用本函数证明finish.cur == finish.last-1
 * 即只有一个元素可以容纳了
 * 内部调用reserve_map_at_back,当其达到某种要求时会重新申请map,具体是什么要求,我们放在后面讲
 * 接着申请一个新的结点,将元素插入到原来结点的最后一个位置上
 * 将finish迭代器指向新的结点并更新其cur指针
 */
template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>::push_back_aux(const value_type& t) {
  value_type t_copy = t;
  //调用该函数,可能会导致map进行重新分配,只是可能
  reserve_map_at_back();
  //申请新的线性空间
  *(finish.node + 1) = allocate_node();
  __STL_TRY {
    //赋值并调整finish迭代器
    construct(finish.cur, t_copy);
    finish.set_node(finish.node + 1);
    finish.cur = finish.first;
  }
  //发生异常了之后,释放空间
  __STL_UNWIND(deallocate_node(*(finish.node + 1)));
}

插入元素到deque的首部

其实push_backpush_front函数很类似,不过还是有几个比较特殊的地方有点不同,代码如下。

push_front
void push_front(const value_type& t) {
  /* 当首部前有一个位置及以上就不申请新的线性空间(注意这里是只有一个位置及以上)
   * 否则调用push_front_aux
   */
  if (start.cur != start.first) {
    construct(start.cur - 1, t);
    --start.cur;
  }
  else
    push_front_aux(t);
}
push_front_aux
// Called only if start.cur == start.first.
//会调用这个函数,证明当前线性空间的first指向的位置之前已经没有空间了
template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>::push_front_aux(const value_type& t) {
  value_type t_copy = t;
  reserve_map_at_front();
  //注意这里是向前申请线性空间
  *(start.node - 1) = allocate_node();
  __STL_TRY {
    //接着这里也应该改变start迭代器
    start.set_node(start.node - 1);
    start.cur = start.last - 1;
    construct(start.cur, t_copy);
  }
#     ifdef __STL_USE_EXCEPTIONS
  catch(...) {
    //异常处理,将start迭代器恢复原样并释放空间
    start.set_node(start.node + 1);
    start.cur = start.first;
    deallocate_node(*(start.node - 1));
    throw;
  }
#     endif /* __STL_USE_EXCEPTIONS */
}
reserve_map_at_xxx()

当调用push_xxx_aux函数时,开头都会调用一个reserve_map_at_front或者reserve_map_at_back函数。它们的作用就时检测是否需要重新申请map。源码如下:

void reserve_map_at_back (size_type nodes_to_add = 1) {
  /* 当map后端的备用节点不足需要分配的节点数时
   * 调用reallocate_map重新分配
   */
  if (nodes_to_add + 1 > map_size - (finish.node - map))
    reallocate_map(nodes_to_add, false);
}

void reserve_map_at_front (size_type nodes_to_add = 1) {
  /* 当map前端的备用节点不足需要分配的新的节点数时
   * 调用reallocate_map重新分配map
   */
  if (nodes_to_add > start.node - map)
    reallocate_map(nodes_to_add, true);
}

reallocate_map的第二参数,为true时代表向首部扩展,为false代表向尾部扩展。

reallocate_map

reallocate_map才是真正为map重新分配空间的函数。源码如下

template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>::reallocate_map(size_type nodes_to_add,
                                              bool add_at_front) {
  //旧节点数
  size_type old_num_nodes = finish.node - start.node + 1;
  //加上需要新增加的总的节点数
  size_type new_num_nodes = old_num_nodes + nodes_to_add;

  map_pointer new_nstart;
  /* 原map_size很充裕时只用调整下start和finish的位置就行了
   * 至于为什么会导致这种的情况的发生
   * 请考虑如下场景
   * 初始化了deque之后,一直朝尾部或首部添加元素
   * 这样导致的结果就是一端已经无结点可以用,另一端还有很多未用
   * 而此时并不用重新申请map,只需要调整一下位置
   * 重新将已用的结点挪动到中间位置即可
   */
  if (map_size > 2 * new_num_nodes) {
    //重新计算start和finish迭代器应处于的位置
    new_nstart = map + (map_size - new_num_nodes) / 2
                     + (add_at_front ? nodes_to_add : 0);
    if (new_nstart < start.node)
      //首部需要往后调整
      copy(start.node, finish.node + 1, new_nstart);
    else
      //尾部需要往前调整
      copy_backward(start.node, finish.node + 1, new_nstart + old_num_nodes);
  }
  /* 如果map的大小确实不够,则只有重新找一块空间了
   * 新申请一片空间,将原来的值拷贝过去
   * 然后释放原空间,并对相关数据进行调整
   */
  else {
    size_type new_map_size = map_size + max(map_size, nodes_to_add) + 2;

    map_pointer new_map = map_allocator::allocate(new_map_size);
    new_nstart = new_map + (new_map_size - new_num_nodes) / 2
                         + (add_at_front ? nodes_to_add : 0);
    copy(start.node, finish.node + 1, new_nstart);
    map_allocator::deallocate(map, map_size);

    map = new_map;
    map_size = new_map_size;
  }
  //最后调整deque容器中的start和finish迭代器
  start.set_node(new_nstart);
  finish.set_node(new_nstart + old_num_nodes - 1);
}

弹出deque末尾的元素

这里也需要考虑特殊情况,即当最后一个线性空间没有元素时,该线性空间会被回收,并且finish迭代器也会前移。

pop_back
void pop_back() {
  /* 最后一段线性空间有一个及以上元素时则只用简单的移动cur以及析构当前元素
   * 否则调用pop_back_aux
   */
  if (finish.cur != finish.first) {
    --finish.cur;
    destroy(finish.cur);
  }
  else
    pop_back_aux();
}
pop_back_aux
// Called only if finish.cur == finish.first.
/* 最后一段线性空间为空时,需要释放该空间
 * 并将finish前移,释放当前线性空间的最后一个结点
 */
template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>:: pop_back_aux() {
  //释放最后一段线性空间
  deallocate_node(finish.first);
  //移动finish迭代器
  finish.set_node(finish.node - 1);
  //使cur指针指向最后一个元素并析构
  finish.cur = finish.last - 1;
  destroy(finish.cur);
}

关于pop_front操作和pop_back很类似,也就是当第一段线性空间没有元素时则释放该空间,调整start迭代器,如果有元素,直接释放了就行了。这里就不再赘述了。

deque任意位置插入元素

insert的版本确实是最多的,不管是vector
还有list或者是deque,这里只选择最通用的几个版本进行分析。

指定位置插入元素x
iterator insert(iterator position, const value_type& x) {
  if (position.cur == start.cur) {
    /* 如果插入位置是deque容器的最前面
     * 则让push_front去做就行了
     */
    push_front(x);
    return start;
  }
  else if (position.cur == finish.cur) {
    /* 如果插入位置是deque容器的末尾
     * 则让push_back去做
    push_back(x);
    iterator tmp = finish;
    --tmp;
    return tmp;
  }
  else {
    //否则调用insert_aux
    return insert_aux(position, x);
  }
}
指定位置插入n个x
template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>::insert(iterator pos,
                                      size_type n, const value_type& x) {
  /* 逻辑指定位置插入x类似
   * 不过要借助uninitialized_fill函数进行插入
   */
  if (pos.cur == start.cur) {
    iterator new_start = reserve_elements_at_front(n);
    uninitialized_fill(new_start, start, x);
    start = new_start;
  }
  else if (pos.cur == finish.cur) {
    iterator new_finish = reserve_elements_at_back(n);
    uninitialized_fill(finish, new_finish, x);
    finish = new_finish;
  }
  else
    insert_aux(pos, n, x);
}

还有一种迭代器的范围插入其实和指定位置插入n个x这一类相似,只是插入元素时调用的函数不一样,但是逻辑都是一样的。

insert_aux

该函数供以上insert调用,对应不同的insert也有不同的版本,这里只分析最重要的一个,其他的都与之类似。

template <class T, class Alloc, size_t BufSize>
typename deque<T, Alloc, BufSize>::iterator
deque<T, Alloc, BufSize>::insert_aux(iterator pos, const value_type& x) {
  //计算出插入点之前的元素个数
  difference_type index = pos - start;
  value_type x_copy = x;
  /* 要在非头/尾位置插入元素,对于线性空间而言,势必涉及到移动元素
   * 为了使得效率达到最优,需要尽量少移动元素
   * 所以接下来我们比较插入点前的元素个数和插入点之后的元素个数
   * 然后来选择移动哪一边的元素
   */
  if (index < size() / 2) {
    //插入点前的元素较少
    //在首部插入一个与第一个元素相等的值
    push_front(front());
    iterator front1 = start;
    ++front1;
    iterator front2 = front1;
    ++front2;
    pos = start + index;
    iterator pos1 = pos;
    ++pos1;
    //移动元素
    copy(front2, pos1, front1);
  }
  else {
    //插入点后的元素较少
    //在尾部插入一个与最后一个元素相等的值
    push_back(back());
    iterator back1 = finish;
    --back1;
    iterator back2 = back1;
    --back2;
    pos = start + index;
    //移动元素
    copy_backward(pos, back2, back1);
  }
  //赋值
  *pos = x_copy;
  return pos;
}

删除元素

erase函数有两个不同的版本,一个是针对具体的位置删除元素,另一个是迭代器范围删除元素。

指定位置删除元素
iterator erase(iterator pos) {
  /* 由于是线性空间
   * 所以删除元素时也涉及到元素的移动
   * 思想和insert_aux相同
   * 选择较少的元素移动
   */
  iterator next = pos;
  ++next;
  difference_type index = pos - start;
  if (index < (size() >> 1)) {
    copy_backward(start, pos, next);
    pop_front();
  }
  else {
    copy(next, finish, pos);
    pop_back();
  }
  return start + index;
}
迭代器范围删除
deque<T, Alloc, BufSize>::erase(iterator first, iterator last) {
  //若是删除整个deque,则调用clear进行删除
  if (first == start && last == finish) {
    clear();
    return finish;
  }
  else {
    /* 这里同样也要判断移动哪一端的元素较少
     * 来选择效率较高的方法
     */
    difference_type n = last - first;
    difference_type elems_before = first - start;
    if (elems_before < (size() - n) / 2) {
      copy_backward(start, first, last);
      iterator new_start = start + n;
      /* 移动完了之后,对空间进行释放 */
      destroy(start, new_start);
      for (map_pointer cur = start.node; cur < new_start.node; ++cur)
        data_allocator::deallocate(*cur, buffer_size());
      start = new_start;
    }
    else {
      copy(last, finish, first);
      iterator new_finish = finish - n;
      destroy(new_finish, finish);
      for (map_pointer cur = new_finish.node + 1; cur <= finish.node; ++cur)
        data_allocator::deallocate(*cur, buffer_size());
      finish = new_finish;
    }
    return start + elems_before;
  }
}

删除整个deque,回到初始状态

负责该功能的是clear函数。

template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>::clear() {
  /* 依次遍历所有结点
   * 析构所有的元素
   * 并释放线性空间
   */
  for (map_pointer node = start.node + 1; node < finish.node; ++node) {
    destroy(*node, *node + buffer_size());
    data_allocator::deallocate(*node, buffer_size());
  }
  /* 若还存在两个缓冲区及以上
   * 则对其元素进行析构
   * 最后将第一个线性空间保留下来
   */
  if (start.node != finish.node) {
    destroy(start.cur, start.last);
    destroy(finish.first, finish.cur);
    data_allocator::deallocate(finish.first, buffer_size());
  }
  else
    //将唯一的线性空间上的元素进行析构
    destroy(start.cur, finish.cur);
  //回归到deque的初始状态,至少有一个线性空间
  finish = start;
}

小结

本小节介绍了大量的跟deque容器紧密相关的一些操作。由于deque容器本身的实现就比较复杂,所以它的操作也稍微复杂一些,其实最主要的就是需不需要跨结点的问题,以及释放空的结点。以及在增加/删除元素时,会选择移动元素次数最少的一端进行移动等等。
在下一小节中,我们将介绍stack配接器。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值