STL中deque详解

主流的STL容器的数据结构都比较常规,类似List就是实现了链表的数据结构,数据以一个node接连串接一个node的形式存储;vector则是一个连续空间存储的变长数组,当空间用完后则申请一倍的空间并老数据拷贝到新分配空间中;map和set则是红黑树,一种特殊的二叉平衡搜索树,保证根到叶节点最大的层数差距在两倍以内,以此保证搜索的速度和维护树结构的性能平衡;unordered_map和unordered_set则使用哈希表,有不同的哈希函数,也有不同的桶实现方法,在此不予细究;唯独deque的实现略“非主流”,这里详细结合STL中的实现解析一下。

基本原理概述

侯捷的《STL源码剖析》中有详细的理论解析,只是示例代码有点老,在此截图一用:
在这里插入图片描述
上图可以明显的看出,deque对于数据的中的存储并不是严格连续的,而是分成了很多个缓冲区,用一个map(并非STL中map,而只是一个简单的连续数组)存储所有缓冲区的地址指针,每个缓冲区都可以存储多个数据,当一个缓冲区填满以后,会开辟新的缓冲区存储数据,并将其地址指针存入map,若map也已经存满以后,会创建新的更大的map,并将旧的map数据拷贝进去并释放旧的空间。

代码分析

下面结合代码来详细看看细节(代码来自gcc5.4自带的STL,stl_deque.h):

  template<typename _Tp, typename _Alloc = std::allocator<_Tp> >
    class deque : protected _Deque_base<_Tp, _Alloc>

deque继承自_Deque_base类,空间分配器默认为std标准分配器allocator(该版本底层实现为new和delete的封装,而并非一级二级配置器的模式),_Deque_base类主要完成了deque的内存管理相关的任务,拿一些主要的部分来看:

  template<typename _Tp, typename _Alloc>
    class _Deque_base
    {
    protected:
      typedef typename __gnu_cxx::__alloc_traits<_Alloc>::template
	rebind<_Tp>::other _Tp_alloc_type;  //确定_Tp空间分配器类型
      typedef __gnu_cxx::__alloc_traits<_Tp_alloc_type>	 _Alloc_traits;         //alloc_traits会实际调_Tp_alloc_type来分配内存和完成构造和析构操作,这里不详细分析
      
      typedef typename _Alloc_traits::pointer		_Ptr;
      typedef typename _Alloc_traits::const_pointer	_Ptr_const;

      typedef typename _Alloc_traits::template rebind<_Ptr>::other
	_Map_alloc_type;  //确定_Map空间分配器类型
      typedef __gnu_cxx::__alloc_traits<_Map_alloc_type> _Map_alloc_traits;   //同上
	  ...

以上代码主要用来确定空间分配器类型,之后的接口中会利用以上确定的空间分配器分配空间以及构造对象,再看看主要成员和接口:

	struct _Deque_impl
      : public _Tp_alloc_type
    {
	_Map_pointer _M_map;		//上文提到的map地址
	size_t _M_map_size;			//map大小
	iterator _M_start;			//首元素所在map节点迭代器
	iterator _M_finish;			//尾后元素所在map节点迭代器
	...
	}
	_Deque_impl _M_impl;

实现了一个内置类型,继承自_Tp空间分配器类,定义一个其对象并作为空间分配器,之后所有空间分配任务和构造析构任务都由这个对象完成,同时包含一些重要成员。

	  _Ptr
      _M_allocate_node()
      { 
	typedef __gnu_cxx::__alloc_traits<_Tp_alloc_type> _Traits;
	return _Traits::allocate(_M_impl, __deque_buf_size(sizeof(_Tp)));
      }

      void
      _M_deallocate_node(_Ptr __p) _GLIBCXX_NOEXCEPT
      {
	typedef __gnu_cxx::__alloc_traits<_Tp_alloc_type> _Traits;
	_Traits::deallocate(_M_impl, __p, __deque_buf_size(sizeof(_Tp)));
      }

_Deque_base仅仅只包涵了分配空间和回收的接口,这里只拿了node(上图中的缓冲器)相关的接口,还有map的接口,都大同小异,都是直接调用空间分配起来分配空间。最后提一下_Deque_base的构造中会用到的_M_initialize_map(size_t __num_elements)函数:

template<typename _Tp, typename _Alloc>
    void
    _Deque_base<_Tp, _Alloc>::
    _M_initialize_map(size_t __num_elements)
    {
      const size_t __num_nodes = (__num_elements/ __deque_buf_size(sizeof(_Tp))
				  + 1);           //根据初始化元素的个数确定需要的map大小

      this->_M_impl._M_map_size = std::max((size_t) _S_initial_map_size,
					   size_t(__num_nodes + 2));      //根据上面确定map的大小决定(最小为_S_initial_map_size=8)
      this->_M_impl._M_map = _M_allocate_map(this->_M_impl._M_map_size);				//分配map的空间

      // For "small" maps (needing less than _M_map_size nodes), allocation
      // starts in the middle elements and grows outwards.  So nstart may be
      // the beginning of _M_map, but for small maps it may be as far in as
      // _M_map+3.

      _Map_pointer __nstart = (this->_M_impl._M_map
			       + (this->_M_impl._M_map_size - __num_nodes) / 2);    //因为是双端队列map都是从中间开始分配空间,首元素向左生长,尾元素向右,这点比较重要
      _Map_pointer __nfinish = __nstart + __num_nodes;    
	
	//以下给缓冲器分配空间
      __try
	{ _M_create_nodes(__nstart, __nfinish); }
      __catch(...)
	{
	  _M_deallocate_map(this->_M_impl._M_map, this->_M_impl._M_map_size);
	  this->_M_impl._M_map = _Map_pointer();
	  this->_M_impl._M_map_size = 0;
	  __throw_exception_again;
	}
	  //以下就是空间分配完成后初始化_M_impl中各个关键元素,上文已经提过相应意义
      this->_M_impl._M_start._M_set_node(__nstart);
      this->_M_impl._M_finish._M_set_node(__nfinish - 1);
      this->_M_impl._M_start._M_cur = _M_impl._M_start._M_first;
      this->_M_impl._M_finish._M_cur = (this->_M_impl._M_finish._M_first
					+ __num_elements
					% __deque_buf_size(sizeof(_Tp)));
    }

到这里_Deque_base的任务基本就完成了,它所有的任务就是提供创建、回收map和node的接口,方便后面的使用。
然后来看看deque的主要实现,主要就是完成元素的插入删除,以插入为例子:

	  void
      push_back(const value_type& __x)
      {
	if (this->_M_impl._M_finish._M_cur
	    != this->_M_impl._M_finish._M_last - 1) 
	  {
	    _Alloc_traits::construct(this->_M_impl,
	                             this->_M_impl._M_finish._M_cur, __x);
	    ++this->_M_impl._M_finish._M_cur;
	  }
	else
	  _M_push_back_aux(__x);
      }

逻辑还是比较简单的,if判断该缓冲区还有没有空间,有就直接创建对象返回,没有就调用_M_push_back_aux(__x),看看它的实现:

      void
      deque<_Tp, _Alloc>::
      _M_push_back_aux(_Args&&... __args)
      {
	_M_reserve_map_at_back();   // 1
	*(this->_M_impl._M_finish._M_node + 1) = this->_M_allocate_node();  // 2
	__try
	  {
	    _Alloc_traits::construct(this->_M_impl,     // 3
	                             this->_M_impl._M_finish._M_cur,
			             std::forward<_Args>(__args)...);
	    this->_M_impl._M_finish._M_set_node(this->_M_impl._M_finish._M_node
						+ 1);
	    this->_M_impl._M_finish._M_cur = this->_M_impl._M_finish._M_first;
	  }
	__catch(...)
	  {
	    _M_deallocate_node(*(this->_M_impl._M_finish._M_node + 1));
	    __throw_exception_again;
	  }
      }

_M_push_back_aux(__x)也并不复杂,因为目前尾元素所在缓冲区空间不够了,因此需要申请新的空间,1、_M_reserve_map_at_back()实现的主要功能就是检查map空间是否足够,不够就申请新的空间,并将数据移动过去,并更新_M_impl中存储关键元素的信息;2、然后根据尾元素所在节点申请新的缓冲区空间;3、最后构造元素。
push_front()基本完全一致,只不过其生长方向与back完全相反,可以简单理解为front和back从初始的中间位置分别向两端生长,一旦所在缓冲区空间不够就申请新的空间,并在新的空间继续生长,最后看下pop_back函数,front类似:

	  void
      pop_back() _GLIBCXX_NOEXCEPT
      {
	if (this->_M_impl._M_finish._M_cur
	    != this->_M_impl._M_finish._M_first)
	  {
	    --this->_M_impl._M_finish._M_cur;
	    _Alloc_traits::destroy(this->_M_impl,
	                           this->_M_impl._M_finish._M_cur);
	  }
	else
	  _M_pop_back_aux();
      }

可以看到,基本就是push_front反过来,if检查当前元素是否到达当前缓冲区最前端,不在则直接pop,否则调用_M_pop_back_aux(),_M_pop_back_aux()与_M_push_back_aux()也是几乎完全相反,需要注意的是它会回收缓冲区内存,其他的这里就不再赘述了。

总结

总体来看,deque由于结构的原因,它整体的实现比vector要复杂很多,缓冲区有点类似于一个每个“节点”都是一个大block的List,只不过它的操作主要在头尾两端,头尾节点一旦空间不够了就新建“节点”,pop空了就删除“节点”,因此如果不想要频繁在头部操作时,选择vector要远远优于deque;但如果有大量头部操作时(例如FIFO)还是老老实实用deque吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章