【deque容器系列一】基于STL源码分析deque容器整体实现及内存结构

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

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

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

deque容器整体结构

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

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

先看一下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这两个类型,他们的原型如下:

1//这里_Tp是模板类型 2#if __cplusplus < 201103L 3 typedef _Deque_iterator<_Tp, _Tp&, _Tp*> iterator; 4 typedef _Deque_iterator<_Tp, const _Tp&, const _Tp*> const_iterator; 5 typedef _Tp* _Elt_pointer; 6 typedef _Tp** _Map_pointer; 7#else 8 private: 9 template<typename _Up> 10 using __ptr_to = typename pointer_traits<_Ptr>::template rebind<_Up>; 11 template<typename _CvTp> 12 using __iter = _Deque_iterator<_Tp, _CvTp&, __ptr_to<_CvTp>>; 13 public: 14 typedef __iter<_Tp> iterator; 15 typedef __iter<const _Tp> const_iterator; 16 typedef __ptr_to<_Tp> _Elt_pointer; 17 typedef __ptr_to<_Elt_pointer> _Map_pointer; 18#endif

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

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

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

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

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

1//构造一个大小为n的deque容器,容器中所有元素的值为value,__a是默认分配器 2deque(size_type __n, const value_type& __value, const allocator_type& __a = allocator_type()) 3 : _Base(__a, __n) 4 { _M_fill_initialize(__value); }

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

deque构造过程

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

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

1//deque容器初始化元素个数 2template<typename _Tp, typename _Alloc> 3 void _Deque_base<_Tp, _Alloc>::_M_initialize_map(size_t __num_elements) 4 { 5 //如果sizeof(_Tp)<512,那么__deque_buf_size返回512/sizeof(_Tp),否则返回1,_Tp是容器的元素类型,所以这里返回的是一个默认块内存的元素数量,也就是一个buffer里面包含的元素个数,后续以buffer代指一块内存 6 //__num_nodes = 元素个数/块内存元素数量 + 1,得到的是总共有多少个buffer,为什么要加1,防止有余数,所以需要加1 7 const size_t __num_nodes = (__num_elements/ __deque_buf_size(sizeof(_Tp)) 8 + 1); 9 //_S_initial_map_size默认为8,根据max调用得到最终的块内存个数 10 this->_M_impl._M_map_size = std::max((size_t) _S_initial_map_size, 11 size_t(__num_nodes + 2)); 12 //这里根据buffer类型和buffer个数申请动态内存,说白了这里申请节点的动态内存,并没有申请真正保存元素的块内存的动态空间 13 this->_M_impl._M_map = _M_allocate_map(this->_M_impl._M_map_size); 14 //根据以下两行可知,_M_map的第一个和最后一个节点其实是保留的,暂时不会使用 15 _Map_pointer __nstart = (this->_M_impl._M_map 16 + (this->_M_impl._M_map_size - __num_nodes) / 2); 17 _Map_pointer __nfinish = __nstart + __num_nodes; 18 __try 19 { 20 //该函数根据节点循环对每一块buffer申请动态空间,一个节点指向一个buffer 21 _M_create_nodes(__nstart, __nfinish); } 22 __catch(...) 23 { 24 _M_deallocate_map(this->_M_impl._M_map, this->_M_impl._M_map_size); 25 this->_M_impl._M_map = _Map_pointer(); 26 this->_M_impl._M_map_size = 0; 27 __throw_exception_again; 28 } 29 //_M_set_node对节点所对应的位置和迭代器位置进行初始化,并使用成员变量保存节点开始和结束位置 30 this->_M_impl._M_start._M_set_node(__nstart); 31 this->_M_impl._M_finish._M_set_node(__nfinish - 1); 32 this->_M_impl._M_start._M_cur = _M_impl._M_start._M_first; 33 this->_M_impl._M_finish._M_cur = (this->_M_impl._M_finish._M_first 34 + __num_elements 35 % __deque_buf_size(sizeof(_Tp))); 36 }

我们根据以上的代码和注释可以得出一个结论,deque容器首先申请一段连续内存,然后连续内存里面每一个节点指向一个buffer,如下图所示:

deque容器内存结构图

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

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

1template <typename _Tp, typename _Alloc> 2 void 3 deque<_Tp, _Alloc>:: 4 _M_fill_initialize(const value_type& __value) 5 { 6 _Map_pointer __cur; 7 __try 8 { 9 //循环填充容器每一个元素的值为__value 10 for (__cur = this->_M_impl._M_start._M_node; 11 __cur < this->_M_impl._M_finish._M_node; 12 ++__cur) 13 //__uninitialized_fill_a函数会根据值对每一个元素进行构造,它其实相当于一个placement new的作用 14 std::__uninitialized_fill_a(*__cur, *__cur + _S_buffer_size(), 15 __value, _M_get_Tp_allocator()); 16 std::__uninitialized_fill_a(this->_M_impl._M_finish._M_first, 17 this->_M_impl._M_finish._M_cur, 18 __value, _M_get_Tp_allocator()); 19 } 20 __catch(...) 21 { 22 std::_Destroy(this->_M_impl._M_start, iterator(*__cur, __cur), 23 _M_get_Tp_allocator()); 24 __throw_exception_again; 25 } 26 }

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

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

1#include <deque> 2 3int main() 4{ 5 std::deque<int> deq(1024, 100); 6 return 0; 7}

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

好了,本篇文章就为大家介绍到这里,觉得内容对你有用的话,记得顺手点个赞哦~

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

c++11增加的变参数模板,今天总算整明白了

本篇文章介绍一下c11中增加的变参数模板template<typename...Args到底是咋回事,以及它的具体用法。说明一下,我用的是gcc7.1.0编译器,标准库源代码也是这个版本的。按照惯例,还是先看一下本文大纲,如下:在之前写vector和deque容器源码剖析的过程中,经常发现这样的代码,如下:cpptemplate<typename..

【deque容器系列二】基于STL源码分析deque容器插入和删除时内存都是怎么变动的

上篇文章我们介绍了deque容器整体结构和构造实现,链接如下:本篇文章接上篇,继续基于gcc中stl的源码剖析deque容器插入、删除、取值的实现原理,以提问者的角度去深入分析这些操作过程中发生了什么,并对deque容器适合使用的场景和使用时的注意事项进行说明。说明一下,我用的是gcc7.1.0编译器,标准库源代码也是这个版本的。按照惯例,还是先看一下本文

【STL源码拆解】基于源码分析forward_lsit容器实现(详细!)

本篇文章介绍一下c11中新增的顺序容器forwardlist,基于stl的源码分析一下该容器的整体实现及数据结构。说明一下,我用的是gcc7.1.0编译器,标准库源代码也是这个版本的。按照惯例,还是先看一下本文大纲,如下:1.forwardlist是什么forwardlist是c11为STL新增加的一种顺序容器,使用的时候包含头文件forwar