deque源码解读

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/wk_bjut_edu_cn/article/details/83714965

deque概述

 vector是单向开口的连续线性空间,deque则是一种双向开口的连续线性空间。所谓双向开口,意思是可以在头尾两端分别做元素的安插和删除动作。vector当然也可以在头尾两端做动作,但是其头部动作效率奇差,无法被接受。

deque和vector的最大差异,一在于deque允许在常数时间内对头尾端进行元素的安插或移除动作。二在于deque没有所有容量概念,因为它是动态地以分段连续空间组合而成,随时可以增加一段新的空间并链接起来。

虽然deque也提供Ramdon Access Iterator,但它的迭代器并不是原生指标,其复杂度和vector不可以并论,这当然在影响了各个运算层面。因此,除非必要,我们应尽量选择使用vector而非deque。

deque的中控器

deque是连续空间,连续线性空间总令我们联想到array或vector。array无法成长,vector虽可成长,却只能向尾端成长,而且其所谓成长原是个假象,事实上是(1)另寻找更大空间,(2)将原数据复制过去,(3)释放原来空间三部曲。如果不是vector每次配置新空间都留下一些富裕,其成长假象所带来的代价是相当高贵。

deque是由一段一段的定量连续空间构成。一旦有必要在deque的前端或尾端增加新空间,便配置一段定量连续空间,串接在整个deque的头端或尾端。deque的最大任务,便是在这些分段的定量连续空间上,维护其整体连续的假象,并提供随机存取的界面。避开了“重新配置,复制,释放”的轮回,代价则是复杂的迭代器架构。

受到分段连续线性空间的字面影响,我们可能以为deque的操作复杂度和vector相差不多。其实不是这样,虽然是分段连续线性空间,就必须有中央控制,而为了维护整体连续的假象,数据结构的设计及迭代器前进后退等动作颇为繁琐。deque的操作代码量远比vector或list都多得多。这也就是deque最主要的缺点。

deque采用一块所谓的map,(不是STL的map容器)作为主控。这里所谓map是一小段连续空间,其中每个元素(此处称为一个节点,node)都是指标,指向另一段较大的连续线性空间,称为缓冲区。缓冲区才是deque的存储空间主体。SGI STL允许我们制定缓冲区大小,默认值0表示将使用512bytes缓存区。

 

deque的迭代器

 deque是分段连续空间。维护其整体连续假象的任务,这就落在迭代器的operator++和operator--两个运算子上。

deque迭代器应该具备什么结构。首先,他必须能够指出分段连续空间(亦即缓冲区)在哪里,其次他必须能够判断自己是否已经处于其所在缓冲区的边缘,如果是,一旦前进或后退时必须跳跃至下一个或上一个缓存区。为了能够正确跳跃,deque必须随时掌握管控中心(map)。

所以迭代器在前进和后退时就需要判断是否为当前缓冲区的最后一个元素或者是第一个元素。     

// __deque_iterator的数据结构
template <class T, class Ref, class Ptr, size_t BufSiz>
struct __deque_iterator
{
	typedef __deque_iterator<T, T&, T*>             iterator;
	typedef __deque_iterator<T, const T&, const T*> const_iterator;
	static size_t buffer_size() {return __deque_buf_size(0, 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;    // 指向管控中心
        ...
}

 

template <class T, class Alloc = alloc, size_t BufSiz = 0> 
class deque {
.....
protected:                      // Data members
  iterator start;   //指向第一个缓冲区
  iterator finish;  //指向最后一个缓冲区

  map_pointer map;  //指向map,map是块连续空间
  size_type map_size;  //map内有多少个指针
.....

deque的构造与内存管理

deuque的构造函数

  deque(size_type 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);
  }

 关键代码(产生deque的结构)

  一个map管理的节点数为至少为8个,至多是所需要的节点数+2,以供deque前后各预留一个,扩充时就可直接使用

template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>::create_map_and_nodes(size_type num_elements) {
  /*
  需要节点数(也就是缓冲区的个数)=(元素个数/每个缓冲区可容纳的元素个数)+1
  如果如果是刚好整除,就会多分配一个节点
  */
  size_type num_nodes = num_elements / buffer_size() + 1;

  /*
  一个map管理的节点数为至少为8个,至多是所需要的节点数+2,以供deque前后
  各预留一个,扩充时就可直接使用
  static size_type initial_map_size() { return 8; }
  */
  map_size = max(initial_map_size(), num_nodes + 2);
  map = map_allocator::allocate(map_size);

  /*
  令nstart和nfinish指向map所拥有之全部节点的最中央区段,这样就可以使
  开始的时候,左右两边的空余空间大致相等
  */
  map_pointer nstart = map + (map_size - num_nodes) / 2;
  map_pointer nfinish = nstart + num_nodes - 1;
    
  map_pointer cur;
  __STL_TRY {
    //为map内的每个现用的节点配置缓冲区,这些缓冲区的大小就是目前deque的
    //可用空间
    for (cur = nstart; cur <= nfinish; ++cur)
      *cur = allocate_node();
  }
#     ifdef  __STL_USE_EXCEPTIONS 
  catch(...) {
    for (map_pointer n = nstart; n < cur; ++n)
      deallocate_node(*n);
    map_allocator::deallocate(map, map_size);
    throw;
  }
#     endif /* __STL_USE_EXCEPTIONS */
  //为deque内的两个迭代器start和end设定正确内容
  start.set_node(nstart);
  finish.set_node(nfinish);
  start.cur = start.first;
  finish.cur = finish.first + num_elements % buffer_size();
}

关键代码(push_back,即尾端插入)

  void push_back(const value_type& t) {
    //最后一个缓冲区有两个及以上的空间时
    if (finish.cur != finish.last - 1) {
      //直接构造元素
      construct(finish.cur, t);
      ++finish.cur;
    }
    //最后一个缓冲区只剩一个空间,那么就需要配置新的缓冲区了
    else
      push_back_aux(t);
  }

配置一整块新的缓冲区,只有当尾端只剩一个空间时才会调用

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 {
    construct(finish.cur, t_copy);
    finish.set_node(finish.node + 1);
    finish.cur = finish.first;
  }
  __STL_UNWIND(deallocate_node(*(finish.node + 1)));
}

关键代码(pop_back)

  void pop_back() {
    //最后缓冲区有一个或多个元素
    if (finish.cur != finish.first) {
      --finish.cur;//调整指针
      destroy(finish.cur);//将最后元素析构掉
    }
    //最后一个缓冲区没有一个元素
    else
      //进行缓冲区的释放
      pop_back_aux();
  }

 缓冲区的释放

template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>:: pop_back_aux() {
  deallocate_node(finish.first); //释放最后一个缓冲区
  finish.set_node(finish.node - 1); //finish指向上一个节点
  finish.cur = finish.last - 1;  //指向上一个缓冲区的最后一个元素
  destroy(finish.cur); //将该元素析构
}

关键代码(清除整个deque)

clear()函数用来清除整个deque。需要注意的是,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);

  //调整状态,start和finish现在指向同一个缓冲区
  finish = start;
}

关键代码(erase()清除某个元素)

deque毕竟也是属于数组,所以清除的时候就要发生元素的移动,因为是双端数组,就要考虑是从前向后移动还是从后向前移动。这可以通过比较删除点前后的元素得出。同理,插入一个元素的时候也是这样考虑。

//清除pos所指的元素,pos为清除点
iterator erase(iterator pos) {
    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;
  }

猜你喜欢

转载自blog.csdn.net/wk_bjut_edu_cn/article/details/83714965
今日推荐