C++ STL顺序容器 —— deque底层实现原理剖析

版权声明:本文为博主整理文章,未经博主允许不得转载。 https://blog.csdn.net/ZYZMZM_/article/details/89716913


概述

vector是单向开口的连续线性空间,deque则是一种双向开口的连续线性空间。所谓双向开口,意思是可以在头尾两端分别做元素的插入和删除操作。vector当然也可以在头尾端进行操作(从技术观点),但是其从头部操作效率奇差,无法被接受。
在这里插入图片描述
deque和vector的最大差异,一在于deque允许常数时间内对其头端进行元素的插入或移除操作,二在于deque没有所谓的容量概念,因为它是动态地以分段连续空间组合而成,随时可以增加一段新的空间并链接起来

deque的迭代器并不是普通指针,其底层实现非常复杂。因此,除非必要,应尽可能选用vector而非deque。对deque进行的排序操作,为了最高效率,可将deque先完整复制到一个vector上,然后vector排序后,再复制到deque。


deque的中控器

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

deque采用一块所谓的map(不是STL的map容器)作为主控。

map是一小块连续空间,其中每个元素(此处称为一个节点,node)都是指针,指向另一段(较大的)连续线性空间,称为缓冲区

缓冲区才是deque的储存空间主体

SGI STL允许我们指定缓冲区大小,默认值0表示将使用512bytes缓冲区

template<class T, class Alloc = alloc, size_t BufSiz = 0>  
class deque{  
public :  
    typedef T value_type ;  
    typedef value_type* pointer ;  
    ...  
protected :  
    //元素的指针的指针(pointer of pointer of T)  
    // 其实就是T**,一个二级指针,维护一个二维数组
    typedef pointer* map_pointer ; 
  
protected :  
    map_pointer map ; //指向map,map是块连续空间,其内的每个元素  
                      //都是一个指针(称为节点),指向一块缓冲区  
    size_type map_size ;//map内可容纳多少指针  
    ...  
};  

我们可发现,map其实是一个T**,也就是说它是一个指针,所指之物也是一个指针,指向型别为T的一块空间,如下图所示:
在这里插入图片描述


deque的迭代器

deque是分段连续空间维持其"整体连续"假象的任务,落在了迭代器的operator++ 和 operator-- 两个运算符重载身上

deque的中控器、缓冲区、迭代器的相互关系如下:
在这里插入图片描述

template <class T, class Ref, class Ptr>
struct __deque_iterator { 	// 未继承 std::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)); }
	#endif
	
	// 未继承 std::iterator,所以必须自行撰写五个必要的迭代器相应型别
	typedef random_access_iterator_tag iterator_category; // (1)
	typedef T value_type; 				// (2)
	typedef Ptr pointer; 				// (3)
	typedef Ref reference; 				// (4)
	typedef size_t size_type;
	typedef ptrdiff_t difference_type; 	// (5)
	typedef T** map_pointer;
	
	typedef __deque_iterator self;
	
	// 保持与容器的联结
	T* cur;	// 此迭代器所指之缓冲区中的现行(current)元素
	T* first;	// 此迭代器所指之缓冲区的头
	T* last;	// 此迭代器所指之缓冲区的尾(含备用空间)
	map_pointer node; //  指向管控中心
	
	__deque_iterator(T* x, map_pointer y) 
	  : cur(x), first(*y), last(*y + buffer_size()), node(y) {}
	__deque_iterator() : cur(0), first(0), last(0), node(0) {}
	__deque_iterator(const iterator& x)
	  : cur(x.cur), first(x.first), last(x.last), node(x.node) {}
	
	
	// 以下各个重载的运算符是 __deque_iterator<> 成功运作的关键。
	
	reference operator*() const { return *cur; }
	#ifndef __SGI_STL_NO_ARROW_OPERATOR
	pointer operator->() const { return &(operator*()); }
	#endif /* __SGI_STL_NO_ARROW_OPERATOR */
	
	difference_type operator-(const self& x) const {
	  return difference_type(buffer_size()) * (node - x.node - 1) +
	    (cur - first) + (x.last - x.cur);
	}
	
	// 参考 More Effective C++, item6: Distinguish between prefix and
	// postfix forms of increment and decrement operators.
	self& operator++() {
	  ++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;
	    // 切换至正确的节点(亦即缓冲区)
	    set_node(node + node_offset);
	    // 切换至正确的元素
	    cur = first + (offset - node_offset * difference_type(buffer_size()));
	  }
	  return *this;
	}
	
	// 参考 More Effective C++, item22: Consider using op= instead of  
	// stand-alone op.
	self operator+(difference_type n) const {
	  self tmp = *this;
	  return tmp += n; // 调用operator+=
	}
	
	self& operator-=(difference_type n) { return *this += -n; }
	// 以上利用operator+= 来完成 operator-=
	
	// 参考 More Effective C++, item22: Consider using op= instead of  
	// stand-alone op.
	self operator-(difference_type n) const {
	  self tmp = *this;
	  return tmp -= n; // 调用operator-=
	}
	
	reference operator[](difference_type n) const { return *(*this + n); }
	// 以上调用operator*, operator+
	
	bool operator==(const self& x) const { return cur == x.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);
	}
	
	void set_node(map_pointer new_node) {
	  node = new_node;
	  first = *new_node;
	  last = first + difference_type(buffer_size());
	}
};

deque::begin()传回迭代器start,deque::end()传回迭代器finish。这两个迭代器都是deque的数据成员,如下图所示。
在这里插入图片描述


deque的数据结构

deque除了维护一个指向map的指针外,也维护start、finish两个迭代器,分别指向第一缓冲区的第一个元素最后缓冲区的最后一个元素(的下一位置)。此外,也必须记住目前的map大小,因为一旦map的所提供的空间不足,它将需要重新配置一个更大的空间。

// 预设使用 alloc 为配置器
template <class T, class Alloc = alloc, size_t BufSiz = 0> 
class deque {
public:                         // Basic types
  typedef T value_type;
  typedef value_type* pointer;
  typedef size_t size_type;

public:  // Iterators
  typedef __deque_iterator<T, T&, T*, BufSiz>    iterator;
protected:     // Internal typedefs
  // 元素的指标的指标(pointer of pointer of T)
  typedef pointer* map_pointer;	
protected:                      // Data members
  iterator start;		// 表现第一个节点。
  iterator finish;	// 表现最后一个节点。
  
  map_pointer map;	// 指向map,map是块连续空间,
               // 其内的每个元素都是一个节点,指向一块缓冲区。
  size_type map_size;	// map内可容纳多少节点(指针)。

有了上述结构,以下数个机能便可轻易完成。

public:      // Basic accessors
  iterator begin() { return start; }
  iterator end() { return finish; }

  reference operator[](size_type n) {
   // 调用 __deque_iterator<>::operator[]
    return start[difference_type(n)]; 
  }
   
   // 调用 __deque_iterator<>::operator*
  reference front() { return *start; } 
  
  reference back() {
    iterator tmp = finish;	
    --tmp;	// 调用 __deque_iterator<>::operator--
    return *tmp; 	// 调用 __deque_iterator<>::operator*
    // 以上三行何不改为:return *(finish-1);
    // 因为 __deque_iterator<> 没有为 (finish-1) 重载
  }

  size_type size() const { return finish - start;; } 
  // 以上调用iterator::operator-
  size_type max_size() const { return size_type(-1); }
  bool empty() const { return finish == start; }

deque的构造与内存管理

deque自行定义了两个专属的空间配置器

// 专属之空间配置器,每次配置一个元素大小
typedef simple_alloc<value_type, Alloc> data_allocator;
// 专属之空间配置器,每次配置一个指标大小
typedef simple_alloc<pointer, Alloc> map_allocator;

并且提供了一个construct如下:

deque(int n, const value_type& value)
   : start(), finish(), map(0), map_size(0)
 {
   fill_initialize(n, value);
 }

其内部所调用的fill_initialize()负责产生并安排好deque的结构,并将元素的初值设定妥当。

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);	 // 把deque的结构都产生并安排好
  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()负责产生并安排好deque的结构

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”
  // (前后各预留一个,扩充时可用)。
  map_size = max(initial_map_size(), num_nodes + 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 {
    // 为map内的每个现用节点配置缓冲区。所有缓冲区加起来就是deque的空间
   // (最后一个缓冲区可能留有一些余裕)。
    for (cur = nstart; cur <= nfinish; ++cur)
      *cur = allocate_node();
  }
  catch(...) {···}

  // 为deque内的两个迭代器start和end 设定正确的内容。
  start.set_node(nstart);
  finish.set_node(nfinish);
  start.cur = start.first;		// first, cur都是public
  finish.cur = finish.first + num_elements % buffer_size();
  // 前面说过,如果刚好整除,会多配一个节点。
  // 此时即令cur指向这多配的一个节点(所对映之缓冲区)的起头处。
}

以下是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);
}

如果尾端只剩一个元素备用空间,那么push_back()将调用push_back_aux(),先配置一整块新的缓冲区,在设置新元素内容,然后更改迭代器finish的状态:

// 只有当 finish.cur == finish.last – 1 时才会被调用。
// 也就是说只有当最后一个缓冲区只剩一个备用元素空间时才会被调用。
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;
 reserve_map_at_back();		//  若符合某种条件则必须重换一个map
 *(finish.node + 1) = allocate_node();	// 配置一个新节点(缓冲区)
 __STL_TRY {
   construct(finish.cur, t_copy);		// 针对标的元素设值
   finish.set_node(finish.node + 1);	// 改变finish,令其指向新节点
   finish.cur = finish.first;			// 设定 finish 的状态
 }
 __STL_UNWIND(deallocate_node(*(finish.node + 1)));
}

同上,push_front()也是类似的操作,只不过操作方向相反

接下来我们看一个问题:什么时候map需要重新整治,这个问题的判断由reserve_map_at_back()和reserve_map_at_front()进行,实际操作则由reallocate_map()执行:

void reserve_map_at_back (size_type nodes_to_add = 1) {
  if (nodes_to_add + 1 > map_size - (finish.node - map))
    // 如果 map 尾端的节点备用空间不足
    // 符合以上条件则必须重换一个map(配置更大的,拷贝原来的,释放原来的)
    reallocate_map(nodes_to_add, false); 
}

void reserve_map_at_front (size_type nodes_to_add = 1) {
  if (nodes_to_add > start.node - map)
    // 如果 map 前端的节点备用空间不足
    // 符合以上条件则必须重换一个map(配置更大的,拷贝原来的,释放原来的)
    reallocate_map(nodes_to_add, true);	
}
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;
 if (map_size > 2 * new_num_nodes) {
   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);
 }
 else {
   size_type new_map_size = map_size + max(map_size, nodes_to_add) + 2;
   // 配置一块空间,准备给新map使用。
   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);
   // 把原map 内容拷贝过来。
   copy(start.node, finish.node + 1, new_nstart);
   // 释放原map
   map_allocator::deallocate(map, map_size);
   // 设定新map的起始位址与大小
   map = new_map;
   map_size = new_map_size;
 }

 // 重新设定迭代器 start 和 finish
 start.set_node(new_nstart);
 finish.set_node(new_nstart + old_num_nodes - 1);
}

deque的元素操作

deque的元素操作很多,这里只列举一些常用操作:

  • pop_back()、pop_front()
  • clear()
  • erase()
  • insert()

之前介绍过push_back()和push_front()的实现原理,现在我们了解一下pop_back()、pop_front()的具体实现

void pop_back() {
  if (finish.cur != finish.first) {
    // 最后缓冲区有一个(或更多)元素

    --finish.cur;		// 调整指针,相当于排除了最后元素
    destroy(finish.cur);	// 将最后元素析构
  }
  else
    // 最后缓冲区没有任何元素
    pop_back_aux();		// 这里将进行缓冲区的释放工作
}

// 只有当finish.cur == finish.first时才会被调用。
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);				// 将该元素析构。
}
void pop_front() {
   if (start.cur != start.last - 1) {
     // 第一缓冲区有一个(或更多)元素
     destroy(start.cur);	// 将第一元素析构
     ++start.cur;			// 调整指针,相当于排除了第一元素
   }
   else 
     // 第一缓冲区仅有一个元素
     pop_front_aux();		// 这里将进行缓冲区的释放工作
 }
  
// 只有当start.cur == start.last - 1时才会被调用。
template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>::pop_front_aux() {
  destroy(start.cur);				// 将第一缓冲区的第一个元素析构。
  deallocate_node(start.first);		// 释放第一缓冲区。
  start.set_node(start.node + 1);	// 调整 start 的状态,使指向
  start.cur = start.first;			//  下一个缓冲区的第一个元素。
}      

下面是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() 第二版本
    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);	// 将此唯一缓冲区内的所有元素析构
    // 注意,并不释放缓冲区空间。这唯一的缓冲区将保留。

  finish = start;	// 调整状态
}

下面是erase()的实现版本一,它用来清除某个元素:
它是根据清除位置之前的元素个数与之后元素个数的多少,来确定到底移动哪部分的元素,移动完毕后,将冗余的一个元素再剔除。

// 清除 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;
}

下面是erase()的实现版本二,它用来清除区间[first,last)的所有元素:

template <class T, class Alloc, size_t BufSize>
deque<T, Alloc, BufSize>::iterator 
deque<T, Alloc, BufSize>::erase(iterator first, iterator last) {
  if (first == start && last == finish) { // 如果清除区间就是整个 deque
    clear();							// 直接调用 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; // 标记 deque 的新起点
      destroy(start, new_start);	// 移动完毕,将冗余的元素析构
      // 以下将冗余的缓冲区释放
      for (map_pointer cur = start.node; cur < new_start.node; ++cur)
        data_allocator::deallocate(*cur, buffer_size());
      start = new_start;	// 设定 deque 的新起点
    }
    else {	// 如果清除区间后方的元素比较少
      copy(last, finish, first);	// 向前移动后方元素(覆盖清除区间)
      iterator new_finish = finish - n;	// 标记 deque 的新尾点
      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;	// 设定 deque 的新尾点
    }
    return start + elems_before;
  }
}

下面是insert()的实现,deque为insert()方法提供了很多版本,以下是最基础最重要的一个版本,允许在某个点(之前)插入一个元素,并设定其值:

  // 在position 处插入一个元素,其值为 x
  iterator insert(iterator position, const value_type& x) {
    if (position.cur == start.cur) {// 如果安插点是deque 最前端
      push_front(x);				// 交给push_front 去做
      return start;
    }
    else if (position.cur == finish.cur) { // 如果安插点是deque 最尾端
      push_back(x);					  // 交给push_back 去做
      iterator tmp = finish;
      --tmp;
      return tmp;
    }
    else {
      return insert_aux(position, x);	// 交给 insert_aux 去做
    }
  }

insert_aux() 的实现如下:

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;
}

猜你喜欢

转载自blog.csdn.net/ZYZMZM_/article/details/89716913