前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >基于STL源码分析deque容器整体实现及内存结构

基于STL源码分析deque容器整体实现及内存结构

作者头像
cpp加油站
发布2021-08-05 11:51:15
6000
发布2021-08-05 11:51:15
举报
文章被收录于专栏:cpp加油站

本篇文章基于gcc中stl的源码介绍deque容器的整体实现和它的内存结构。

说明一下,我用的是gcc7.1.0编译器,标准库源代码也是这个版本的。

首先呢,还是看一下思维导图,如下:

1. deque容器整体源码实现介绍

deque容器是stl中顺序容器的一种,之前已经介绍过array和vector了,今天介绍deque容器,deque的本质是一个类模板,它的声明位于头文件bits/stl_deque.h,实现位于bits/deque.tcc,接下来我们就围绕这两个文件来介绍一下deque容器的实现原理。

先看一下deque容器相关的一个整体类图:

下面对这个类图进行一个简单的解读:

  • deque容器保护继承于类模板_Deque_base,也就是_Deque_base是deque的基类,并且内存分配和释放都是通过基类来完成的;
  • 容器首地址和迭代器等保存在结构体成员变量_M_impl中,它继承于别名类型_Tp_alloc_type,最终的内存分配其实就是通过它完成的;
  • deque容器使用了它自己的迭代器_Deque_iterator,没有直接使用stl中的公共迭代器,且迭代器里面保存了当前地址、首地址、尾地址以及当前节点。

这里有几个类型是不好理解的,第一个是_Tp_alloc_type,这是一个别名,关于这个类型的解读,我之前专门写过一篇文章:三张图带你弄懂STL中内存分配器

然后就是_Elt_pointer_Map_pointer这两个类型,他们的原型如下:

代码语言:javascript
复制
//这里_Tp是模板类型
#if __cplusplus < 201103L
      typedef _Deque_iterator<_Tp, _Tp&, _Tp*>         iterator;
      typedef _Deque_iterator<_Tp, const _Tp&, const _Tp*> const_iterator;
      typedef _Tp*                     _Elt_pointer;
      typedef _Tp**                    _Map_pointer;
#else
    private:
      template<typename _Up>
    using __ptr_to = typename pointer_traits<_Ptr>::template rebind<_Up>;
      template<typename _CvTp>
    using __iter = _Deque_iterator<_Tp, _CvTp&, __ptr_to<_CvTp>>;
    public:
      typedef __iter<_Tp>        iterator;
      typedef __iter<const _Tp>        const_iterator;
      typedef __ptr_to<_Tp>        _Elt_pointer;
      typedef __ptr_to<_Elt_pointer>    _Map_pointer;
#endif

在c++11以前,它们之前就直接是指针类型,在c++11以后,使用了类模板pointer_traits的rebind类型属性,有关pointer_traits的详细说明,请看下面这篇文章:

从c++标准库指针萃取器谈一下traits技法

仔细看一下,其实最终pointer_traits的rebind得到的类型同样也是指针类型哈,结果是一致的,只是可能代码这么写更加的清新脱俗一点,哈哈,开个玩笑,估计是因为模板类型这么写更加规范一点。

2. deque容器构造时内存结构是怎样的

在源代码里面,deque容器构造函数重载了很多,我们选取其中一种典型的类型看一下,构造函数原型如下:

代码语言:javascript
复制
//构造一个大小为n的deque容器,容器中所有元素的值为value,__a是默认分配器
deque(size_type __n, const value_type& __value, const allocator_type& __a = allocator_type())
      : _Base(__a, __n)
      { _M_fill_initialize(__value); }

整个构造的调用过程,我们看下面这张图:

通过图片,我们可以看到三个构造函数只是对分配器和其他成员变量等做了一下初始化,而真正申请内存的是模板函数_M_initialize_map,然后给容器填充数据的模板函数_M_fill_initialize,我们主要看下这两个函数的实现。

先看下模板函数_M_initialize_map,源代码如下:

代码语言:javascript
复制
//deque容器初始化元素个数
template<typename _Tp, typename _Alloc>
    void _Deque_base<_Tp, _Alloc>::_M_initialize_map(size_t __num_elements)
    {
     //如果sizeof(_Tp)<512,那么__deque_buf_size返回512/sizeof(_Tp),否则返回1,_Tp是容器的元素类型,所以这里返回的是一个默认块内存的元素数量,也就是一个buffer里面包含的元素个数,后续以buffer代指一块内存
     //__num_nodes = 元素个数/块内存元素数量 + 1,得到的是总共有多少个buffer,为什么要加1,防止有余数,所以需要加1
      const size_t __num_nodes = (__num_elements/ __deque_buf_size(sizeof(_Tp))
                  + 1);
     //_S_initial_map_size默认为8,根据max调用得到最终的块内存个数
      this->_M_impl._M_map_size = std::max((size_t) _S_initial_map_size,
                       size_t(__num_nodes + 2));
     //这里根据buffer类型和buffer个数申请动态内存,说白了这里申请节点的动态内存,并没有申请真正保存元素的块内存的动态空间
      this->_M_impl._M_map = _M_allocate_map(this->_M_impl._M_map_size);
     //根据以下两行可知,_M_map的第一个和最后一个节点其实是保留的,暂时不会使用
      _Map_pointer __nstart = (this->_M_impl._M_map
                   + (this->_M_impl._M_map_size - __num_nodes) / 2);
      _Map_pointer __nfinish = __nstart + __num_nodes;
      __try
    { 
        //该函数根据节点循环对每一块buffer申请动态空间,一个节点指向一个buffer
          _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_set_node对节点所对应的位置和迭代器位置进行初始化,并使用成员变量保存节点开始和结束位置
      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容器首先申请一段连续内存,然后连续内存里面每一个节点指向一个buffer,如下图所示:

上图中node代表节点,这些节点是一段连续内存,每一个节点指向一块独立的buffer,从数据结构的层面讲,deque容器其实就是一个双端队列。

接下来我们看下另外一个模板函数_M_fill_initialize,它的源代码如下:

代码语言:javascript
复制
template <typename _Tp, typename _Alloc>
    void
    deque<_Tp, _Alloc>::
    _M_fill_initialize(const value_type& __value)
    {
      _Map_pointer __cur;
      __try
        {
          //循环填充容器每一个元素的值为__value
          for (__cur = this->_M_impl._M_start._M_node;
           __cur < this->_M_impl._M_finish._M_node;
           ++__cur)
           //__uninitialized_fill_a函数会根据值对每一个元素进行构造,它其实相当于一个placement new的作用
            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;
        }
    }

这个函数比较简单,注释也说的比较清楚了,这里不再多说。

接下来我们给一个使用案例,如下所示:

代码语言:javascript
复制
#include <deque>

int main()
{
    std::deque<int> deq(1024, 100);
    return 0;
}

简单的定义了一个deque,元素个数为1024,每一个元素值为100。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-07-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 cpp加油站 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. deque容器整体源码实现介绍
  • 2. deque容器构造时内存结构是怎样的
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档