C++进阶——STL源码之顺序容器deque

14 篇文章 4 订阅

顺序容器deque

deque 是一种双向开口的连续线性空间,所谓双向开口,就是可以在头尾两端分别做元素的插入和删除;与vector 相比,vector可以在头尾两端进行操作,但是其头部的操作效率奇差,无法接受。

deque是分段连续线性空间,随时可以增加一段新的空间;与vector相比,vector当内存不够时,需重新分配、复制数据、释放原始空间。

deque的迭代器需要处理段之间的过渡,相比于vector的普通指针迭代器要复杂的多。

deque的类结构图

deque的中控器

deque采用一块所谓的map作为主控,这里所谓map是一小块连接空间,其中每一个元素都是指针,指向另一段(较大的)连续线性空间,称为缓冲区,缓冲区才是deque的储存空间主体。

从上述的类结构图可以看出中控器的实现在_Deque_impl中,源码如下:

 struct _Deque_impl
      : public _Tp_alloc_type
      {
	_Tp** _M_map;
	size_t _M_map_size;//指向的缓冲区数目
	iterator _M_start;//用于管理中控器map
	iterator _M_finish;//用于管理中控器map

	_Deque_impl()
	: _Tp_alloc_type(), _M_map(0), _M_map_size(0),
	  _M_start(), _M_finish()
	{ }

	_Deque_impl(const _Tp_alloc_type& __a) _GLIBCXX_NOEXCEPT
	: _Tp_alloc_type(__a), _M_map(0), _M_map_size(0),
	  _M_start(), _M_finish()
	{ }

#if __cplusplus >= 201103L
	_Deque_impl(_Tp_alloc_type&& __a) _GLIBCXX_NOEXCEPT
	: _Tp_alloc_type(std::move(__a)), _M_map(0), _M_map_size(0),
	  _M_start(), _M_finish()
	{ }
#endif
      };

deque的结构设计中,map和buffer 之间的关系:

deque的迭代器

deque是分段连续空间,维持其“整体连续”假象的任务,落在了迭代器的operator++和 operator--两个运算子身上,它必须能够指出分段连续空间在哪里,其次必须能够判断自己是否已经处于其所在缓冲区的边缘,一旦前进或者后退就必须跳跃至下一个或上一个缓冲区;看下__deque_iterator源码:

 template<typename _Tp, typename _Ref, typename _Ptr>
    struct _Deque_iterator
    {
      typedef _Deque_iterator<_Tp, _Tp&, _Tp*>             iterator;
      typedef _Deque_iterator<_Tp, const _Tp&, const _Tp*> const_iterator;

      static size_t _S_buffer_size() _GLIBCXX_NOEXCEPT
      { return __deque_buf_size(sizeof(_Tp)); }//获取缓冲区的大小

      typedef std::random_access_iterator_tag iterator_category;
      typedef _Tp                             value_type;
      typedef _Ptr                            pointer;
      typedef _Ref                            reference;
      typedef size_t                          size_type;
      typedef ptrdiff_t                       difference_type;
      typedef _Tp**                           _Map_pointer;
      typedef _Deque_iterator                 _Self;

      _Tp* _M_cur;//此迭代器所指之缓冲区中现行的元素
      _Tp* _M_first;//迭代器所指缓冲区的头
      _Tp* _M_last;//迭代器所指之缓冲区的尾(含备用空间)
      _Map_pointer _M_node;//指向管控中心

       //。。。

   }

deque的中控器、缓冲区、迭代器的相互关系如下:

再来看中控器的两个迭代器成员:

iterator _M_start;//用于管理中控器map
iterator _M_finish;//用于管理中控器map

中控器中的两个迭代器的表示形式:

结合deque的源码理解上述的start 和 finish两个迭代器:

      iterator
      begin() _GLIBCXX_NOEXCEPT
      { return this->_M_impl._M_start; }//这里的_M_start,就是中控器中的成员,对应图中的start

      iterator
      end() _GLIBCXX_NOEXCEPT
      { return this->_M_impl._M_finish; }//这里的_M_finish,就是中控器中的成员,对应图中的finish

迭代器 前++ 的实现

deque是分段连续线性空间,迭代器就要来维持其“整体连续”假象的任务,operator的具体实现如下:

      _Self&
      operator++() _GLIBCXX_NOEXCEPT
      {
    ++_M_cur;//迭代器中的_M_cur为真正指向当前缓冲区的元素,加1 后移到下一个元素
    if (_M_cur == _M_last)//判断当前的位置是否超出缓冲区范围,后开区间
      {
        _M_set_node(_M_node + 1);//操作指向的缓冲区,使其加1 指向下一个缓冲区
        _M_cur = _M_first;//更新当前指向的实际元素
      }
    return *this;//返回迭代器
      }

_M_set_node为操作中控器,来看下使如何操作缓冲区的:

      void
      _M_set_node(_Map_pointer __new_node) _GLIBCXX_NOEXCEPT
      {
    _M_node = __new_node;//使中控器指向新的缓冲区
    _M_first = *__new_node;//_M_first 表示当前缓冲区的第一个元素位置
    _M_last = _M_first + difference_type(_S_buffer_size());//_M_last 表示当前缓冲区的最后一个元素的下一个位置
      }

_S_buffer_size实际调用下面的__deque_buf_size函数,用来获取每个缓冲的大小:

#define _GLIBCXX_DEQUE_BUF_SIZE 512

static size_t _S_buffer_size() _GLIBCXX_NOEXCEPT
{ return __deque_buf_size(sizeof(_Tp)); }

inline size_t
  __deque_buf_size(size_t __size)
  { return (__size < _GLIBCXX_DEQUE_BUF_SIZE
        ? size_t(_GLIBCXX_DEQUE_BUF_SIZE / __size) : size_t(1)); }

下面再来看下operator--的实现,和++相反,这里回去判断当前元素的指向是否超出当前缓冲区的第一个元素的位置:

      _Self&
      operator--() _GLIBCXX_NOEXCEPT
      {
    if (_M_cur == _M_first)
      {
        _M_set_node(_M_node - 1);
        _M_cur = _M_last;
      }
    --_M_cur;
    return *this;
      }

迭代器 += 的实现

迭代器的 += 实现随机存取,迭代器可以直接跳跃n个距离,实现如下:

_Self&
operator+=(difference_type __n) _GLIBCXX_NOEXCEPT
{
    const difference_type __offset = __n + (_M_cur - _M_first);//计算_M_cur加n之后的偏移值
    if (__offset >= 0 && __offset < difference_type(_S_buffer_size()))//判断_M_cur是否超出缓冲区的范围
        _M_cur += __n;//如果没有超出缓冲区的返回,直接对_M_cur 加 n
    else
    {//如果超出该缓冲区的范围执行如下操作
        const difference_type __node_offset =
            __offset > 0 ? __offset / difference_type(_S_buffer_size())
            : -difference_type((-__offset - 1)
                / _S_buffer_size()) - 1;//计算中控器中节点(也就是缓冲区)的偏移,从这个可以看出n可以是负值
        _M_set_node(_M_node + __node_offset);//设置缓冲区的指针,指向+n 之后落到的缓冲区
        _M_cur = _M_first + (__offset - __node_offset
            * difference_type(_S_buffer_size()));//设置迭代器指向+n指向实际指向元素
    }
    return *this;
}

deque的内存管理

在了解deque内存管理之前来看下deque size的获取:

size_type
size() const _GLIBCXX_NOEXCEPT
{
    return this->_M_impl._M_finish - this->_M_impl._M_start;//简单的两个迭代器相减,结合上图理解
}

  template<typename _Tp, typename _Ref, typename _Ptr>
    inline typename _Deque_iterator<_Tp, _Ref, _Ptr>::difference_type
    operator-(const _Deque_iterator<_Tp, _Ref, _Ptr>& __x,
          const _Deque_iterator<_Tp, _Ref, _Ptr>& __y) _GLIBCXX_NOEXCEPT
    {
       // 首先将_S_buffer_size()获取到的缓冲区元素数目转为difference_type类型
       // 然后将计算两个迭代器之间的缓冲区的数目
       // 最后加上两个迭代器指向的缓冲区实际的元素像素
      return typename _Deque_iterator<_Tp, _Ref, _Ptr>::difference_type
    (_Deque_iterator<_Tp, _Ref, _Ptr>::_S_buffer_size())   
    * (__x._M_node - __y._M_node - 1) + (__x._M_cur - __x._M_first)
    + (__y._M_last - __y._M_cur);
    }

构造函数

      explicit
      deque(size_type __n, const value_type& __value = value_type(),
        const allocator_type& __a = allocator_type())
      : _Base(__a, __n)
      { _M_fill_initialize(__value); }

调用_M_fill_initialize负责设置初值:

  template <typename _Tp, typename _Alloc>
    void
    deque<_Tp, _Alloc>::
    _M_fill_initialize(const value_type& __value)
    {
      _Map_pointer __cur;
      __try
        {
          for (__cur = this->_M_impl._M_start._M_node;
           __cur < this->_M_impl._M_finish._M_node;
           ++__cur)
            std::__uninitialized_fill_a(*__cur, *__cur + _S_buffer_size(),
                    __value, _M_get_Tp_allocator());//为每个缓冲区设置初值
          std::__uninitialized_fill_a(this->_M_impl._M_finish._M_first,
                      this->_M_impl._M_finish._M_cur,
                      __value, _M_get_Tp_allocator());//最后一个节点稍有不同,因为可能有备用空间,不设初值
        }
      __catch(...)
        {
          std::_Destroy(this->_M_impl._M_start, iterator(*__cur, __cur),
            _M_get_Tp_allocator());
          __throw_exception_again;
        }
    }

父类_Base(__a, __n)负责deque的结构设置:

      _Deque_base(const allocator_type& __a, size_t __num_elements)
      : _M_impl(__a)
      { _M_initialize_map(__num_elements); }

_M_initialize_map的实现如下:

  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管理的节点最少8个,如果大于8个要预留前后个一个节点用于扩充
      this->_M_impl._M_map_size = std::max((size_t) _S_initial_map_size,
                       size_t(__num_nodes + 2));
      this->_M_impl._M_map = _M_allocate_map(this->_M_impl._M_map_size);//分配节点指针_Tp**

      // 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.

     //设置_nstart 为起始节点,注意对扩充节点的处理
     //另外使start 和 finish 处于map的中间位置
     _Tp** __nstart = (this->_M_impl._M_map
            + (this->_M_impl._M_map_size - __num_nodes) / 2);
      _Tp** __nfinish = __nstart + __num_nodes;//设置__nfinish为最后节点

      __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 = 0;
      this->_M_impl._M_map_size = 0;
      __throw_exception_again;
    }

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

_M_create_nodes的实现:

template<typename _Tp, typename _Alloc>
void
_Deque_base<_Tp, _Alloc>::
_M_create_nodes(_Tp** __nstart, _Tp** __nfinish)
{
    _Tp** __cur;//cur为指针,指向缓冲区,缓冲区有 n 个 _Tp
    __try
    {
        for (__cur = __nstart; __cur < __nfinish; ++__cur)
            *__cur = this->_M_allocate_node();//遍历所有的节点,分配缓冲区
    }
    __catch(...)
    {
        _M_destroy_nodes(__nstart, __cur);
        __throw_exception_again;
    }
}

_M_allocate_node的实现源码如下:

_Tp*
_M_allocate_node()
{//调用配置器分配缓冲区内存
    return _M_impl._Tp_alloc_type::allocate(__deque_buf_size(sizeof(_Tp)));
}

最后更新迭代器为正确的信息:

      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;
      //_M_finish._M_cur指向最后一个元素的下一个位置
      this->_M_impl._M_finish._M_cur = (this->_M_impl._M_finish._M_first
                    + __num_elements
                    % __deque_buf_size(sizeof(_Tp)));

push_back的实现

push_back的源码:

void
push_back(const value_type& __x)
{
   // 首先判断是否有空间,有的话直接插入
    if (this->_M_impl._M_finish._M_cur
        != this->_M_impl._M_finish._M_last - 1)
    {
        this->_M_impl.construct(this->_M_impl._M_finish._M_cur, __x);
        ++this->_M_impl._M_finish._M_cur;
    }
    else//没有空间调用此处
        _M_push_back_aux(__x);
}

_M_push_back_aux的实现如下:

执行过程如下,首先判断当前map的节点是否满足使用,在之前的构造函数中知道map会做预留,如果map的节点不能满足当前使用,扩展节点——>接着分配节点的内存——>构造元素——>调整迭代器

template<typename _Tp, typename _Alloc>
void
deque<_Tp, _Alloc>::
_M_push_back_aux(const value_type& __t)
{
    _M_reserve_map_at_back();
    *(this->_M_impl._M_finish._M_node + 1) = this->_M_allocate_node();
    __try
    {
        this->_M_impl.construct(this->_M_impl._M_finish._M_cur, __t);
        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;
    }
}

对于边界的处理如下图所示(如果当前缓冲区使用完,finish迭代器会指向下一个节点,即缓冲区):

push_front的实现

push_front为在前端插入元素,其实现如下:

void
push_front(const value_type& __x)
{
    if (this->_M_impl._M_start._M_cur != this->_M_impl._M_start._M_first)
    {//如果当前缓冲区未使用完,直接使用
        this->_M_impl.construct(this->_M_impl._M_start._M_cur - 1, __x);
        --this->_M_impl._M_start._M_cur;
    }
    else
        _M_push_front_aux(__x);//调用_M_push_front_aux申请新的节点
}

_M_push_front_aux的实现:

template<typename _Tp, typename _Alloc>
void
deque<_Tp, _Alloc>::
_M_push_front_aux(const value_type& __t)
{
    _M_reserve_map_at_front();
    *(this->_M_impl._M_start._M_node - 1) = this->_M_allocate_node();
    __try
    {
        this->_M_impl._M_start._M_set_node(this->_M_impl._M_start._M_node
            - 1);//更新_M_start的节点信息
        this->_M_impl._M_start._M_cur = this->_M_impl._M_start._M_last - 1;//指向新节点的最后一个元素位置

        this->_M_impl.construct(this->_M_impl._M_start._M_cur, __t);//构造元素

    }
    __catch(...)
    {
        ++this->_M_impl._M_start;
        _M_deallocate_node(*(this->_M_impl._M_start._M_node - 1));
        __throw_exception_again;
    }
}

_M_reserve_map_at_front的实现:

      void
      _M_reserve_map_at_front(size_type __nodes_to_add = 1)
      {//首先判断是否有未使用的节点,没有申请分配
    if (__nodes_to_add > size_type(this->_M_impl._M_start._M_node
                       - this->_M_impl._M_map))
      _M_reallocate_map(__nodes_to_add, true);
      }

边界处理示意图:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
对于C++ STL码分析,这是一个广泛而复杂的话题。C++ STL是C++标准库中的一部分,包含了许多不同的容器、算法和迭代器等组件,用于提供通用的数据结构和算法支持。 在码分析之前,你需要具备一定的C++编程知识和理解C++模板的工作原理。然后,你可以通过查看STL码实现来深入了解其内部机制。 在C++ 11中,STL引入了一些新的特性和容器。例如,引用中提到的range-based for循环语句,可以更方便地遍历容器中的元素。此外,C++ 11还对容器行了分类,包括序列容器、关联容器和无序容器等。 引用提到了STL的六个主要部分,包括容器、算法、迭代器、函数对象、适配器和分配器。这些部分提供了不同的功能和特性,可以满足各种编程需求。 在C++ 11中,一些容器名称发生了变化,如slist被重命名为forward_list,hash_set和hash_map被重命名为unordered_set和unordered_map。这些变化是为了更好地反映容器的功能和语义。 要深入了解STL码,你可以参考一些重要的资网站,如cplusplus.com、cppreference.com和gcc.gnu.org。这些网站提供了详细的文档和例子,以帮助你理解STL的实现细节。 总之,要行C++ STL码分析,你需要具备一定的编程和模板知识,并参考相关的文档和资。通过深入研究STL码实现,你将能够更好地理解其内部机制和使用方法。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值