STL源码阅读笔记【deque】

STL源码阅读笔记【deque】

1.阅前了解

块状链表

简介

数组与链表的结合体,兼具了两者的特性;

原理

与链表等结构维护的方式不同,块状链表会有:①用来存放队列元素的node,每个node里面可以存放相同个数的元素,可以实现一定程度的随机访问,②还有一片连续存储空间[map](用来存储node),当存放node的map空间不足的时候,一般会使用realloc实现重新分配空间大小,可以避免小空间频繁申请导致的内存碎片过多的问题。

优点

1、块状链表支持动态扩容;

2、支持一定程度的随机访问;

双端队列

简介

一种头尾部均支持入队出队操作的队列;

链表实现的缺点

1、维护结构的存储空间开销大;

2、频繁的申请和释放空间(小空间)容易造成内存碎片太多;

2.源码阅读

阅读目标

了解STL中底层实现双端队列以及相应操作的数据结构方案,理解deque的设计原理。

阅读思路

1.定位deque源码文件
*/SGI-STL-master/SGI-STL V3.3/stl_deque.h
2.迭代器类型的操作
template <class _Tp, _Alloc=__STL_DEFAULT_ALLOCATOR(_Tp)>
class deque::protected _Deque_base<_Tp, _Alloc> {

了解到,STL容器中支持迭代器操作的一般会有begin和end两个迭代器对象会被创建,我们可以根据双端队列的迭代器怎么实现的,推演出双端队列在STL中的底层数据结构实现;

public :
	iterator begin() {return _M_start; }//_M_start表示指向第一个元素的迭代器对象
	iterator end() {return _M_finish; }//_M_finish表示指向最后一个元素的后一个位置的迭代器对象

根据上面的代码,我们可以找到两个迭代器定义位置;

using _Base::_M_start;
using _Base::_M_finish;

可以看到两个迭代器对象是由_Base类来声明定义的,因此继续找到 _Base类的位置;

_STL_CLASS_REQUIRES(_Tp, _Assignable);
typedef _Deque_base<_Tp, _Alloc> _Base;//_Base类

确定看到_Base类是 _Deque_base类的别名,继续找到 _Deque_base,最终我们通过寻找看到原来在 _ Deque _base类中迭代器类iterator,它是 _Deque _ iterator;

template <class _Tp, class _Alloc>
class _Deque_base: public _Deque_alloc_base<_Tp,_Alloc, _Alloc_traits<_Tp, _Alloc>::_S_instanceless>
{
    ***
  typedef _Deque_iterator<_Tp,_Tp&,_Tp*>  iterator;//双端队列的迭代器类
	***
protected:
  iterator _M_start;//
  iterator _M_finish;
};

通过最终定位找到了双端队列迭代器类的实现源码,我们主要看相关类型的运算操作,以此来分析设计的原理和底层数据结构实现的方式;

_M_cur:指的是当前迭代器对象所指元素(在某个node内),

_M_last:是指(某个node内)最后一个元素的后一个位置,

_M_set_node(): 修改当前指向的node,

_M_node:指当前所指向的node;

修改node操作
void _M_set_node(_Map_pointer __new_node) {
    _M_node = __new_node;//设置当前node为__new_node
    _M_first = *__new_node;//将_M_first设置为新的node首地址
    _M_last = _M_first + difference_type(_S_buffer_size());//获取元素大小,并重新设置当前node指向的末尾元素的后一个位置
  }
};
运算符操作

涉及到迭代器对象的运算操作,主要就是利用块状链表的结构特性,使得当 _M_cur指向当前node的元素区间最后一个元素的后一位时,就需要进行node + 1的操作,而当 _M_cur指向当前node元素区间第一个元素的前一个位置是,说明需要将 _M_cur指向node - 1的最后一个元素。

******
 //① 前++
_Self& operator++() {
    ++_M_cur;
    if (_M_cur == _M_last) { //当_M_cur走到当前node最后一个元素了,就需要将node + 1;
        _M_set_node(_M_node + 1);
        _M_cur = _M_first;//并将_M_cur指向修改后node的首个元素
    }
    return *this;//返回操作结果
}
// ② 后++操作
_Self operator++(int) {
    _Self __tmp = *this;//保存操作前的元素地址
    ++*this;
    return __tmp;//返回操作前的元素
}

// ③ += n操作
 _Self& operator+=(difference_type __n) 
  {
    difference_type __offset = __n + (_M_cur - _M_first);//计算偏移量
    if (__offset >= 0 && __offset < difference_type(_S_buffer_size()))//如果偏移量大于0并且小于node单位大小可以直接进行+= 操作
      _M_cur += __n;
    else { //否则就需要重新计算偏移量大小,有可能会出现超过当前node的大小或者小于0,因此需要进行分别讨论
      difference_type __node_offset =
        __offset > 0 ? __offset / difference_type(_S_buffer_size())
                   : -difference_type((-__offset - 1) / _S_buffer_size()) - 1;
      _M_set_node(_M_node + __node_offset);
      _M_cur = _M_first + 
        (__offset - __node_offset * difference_type(_S_buffer_size()));
    }
    return *this;
  }

******
3. deque的基本操作
push操作

在双端队列实现向队首、队尾进行入队操作的时候,首先都会判断是否会超出当前node的元素区间,如果超出了,就会调用 _M_push_back_aux_M_push_front_aux进行空间不足就动态扩容,否则就重新设置 _M_cur的位置;

// Called only if _M_finish._M_cur == _M_finish._M_last - 1.
template <class _Tp, class _Alloc>
void deque<_Tp,_Alloc>::_M_push_back_aux()
{
  _M_reserve_map_at_back();//尝试去添加,当空间不足时,就会对map空间进行扩容,每次增加1个node
  *(_M_finish._M_node + 1) = _M_allocate_node();//为deque的末尾添加一个新的节点,,并将其指针存储在map。
  __STL_TRY {
    construct(_M_finish._M_cur);//在新的节点的后面中构造一个新的node。
    _M_finish._M_set_node(_M_finish._M_node + 1);//更新_M_finish迭代器,使其指向新节点node。
    _M_finish._M_cur = _M_finish._M_first;//将_M_finish迭代器的当前指针指向新节点node的起始位置。
  }
  __STL_UNWIND(_M_deallocate_node(*(_M_finish._M_node + 1)));//在尝试执行构造操作期间,如果出现异常,则会使用__STL_UNWIND部分中的代码进行处理,释放先前分配的节点内存。
}

public:                         // push_* and pop_*
  
 void push_back() {
    if (_M_finish._M_cur != _M_finish._M_last - 1) {//未超出当前node的区间
      construct(_M_finish._M_cur);
      ++_M_finish._M_cur;
    }
    else
      _M_push_back_aux();//超出当前node的空间,进行扩容或者node跳转
  }

  void push_front() {
    if (_M_start._M_cur != _M_start._M_first) {//未超出当前node的区间
      construct(_M_start._M_cur - 1);
      --_M_start._M_cur;
    }
    else
      _M_push_front_aux();//超出当前node的空间,进行扩容或者node跳转
  }
 void pop_back() {
    if (_M_finish._M_cur != _M_finish._M_first) { //检查finish迭代器是否指向当前node的第一个元素之前的位置。如果不是,则表示node中还有其他元素。
      --_M_finish._M_cur;//将finish迭代器向前移动一个元素的位置,指向最后一个元素的位置。
      destroy(_M_finish._M_cur);//销毁结束迭代器当前位置指向的元素;
    }
    else
      _M_pop_back_aux();//执行删除操作
  }

  void pop_front() {
    if (_M_start._M_cur != _M_start._M_last - 1) {//检查start迭代器是否指向当前node的最后一个元素。如果不是,则表示node中还有其他元素可存储。
      destroy(_M_start._M_cur);//销毁起始迭代器当前位置指向的元素。
      ++_M_start._M_cur;//将起始迭代器向前移动一个位置,指向下一个元素的位置。
    }
    else 
      _M_pop_front_aux();//如果node中没有其他元素,则调用_M_pop_front_aux()函数来执行删除操作。
  }

pop操作
 void pop_back() {
    if (_M_finish._M_cur != _M_finish._M_first) { 
      --_M_finish._M_cur;
      destroy(_M_finish._M_cur);
    }
    else
      _M_pop_back_aux();
  }

  void pop_front() {
    if (_M_start._M_cur != _M_start._M_last - 1) {//检查起始迭代器是否指向当前node的最后一个元素。如果不是,则表示node中还有其他元素可存储。
      destroy(_M_start._M_cur);//销毁起始迭代器当前位置指向的元素。
      ++_M_start._M_cur;//将起始迭代器向前移动一个位置,指向下一个元素的位置。
    }
    else 
      _M_pop_front_aux();//如果node中没有其他元素,则调用_M_pop_front_aux()函数来执行删除操作。
  }

阅读总结

  • 基于块状链表实现的双端队列,支持动态扩容,在一定程度上可实现随机访问;
  • 在设计实现代码的时候,应该综合考虑底层数据结构的优点和问题;
  • 对于支持迭代器的容器,我们可以从迭代器的运算操作上来入手,这样更加便于我们去了解容器的底层数据结构的实现方案;
  • 从双端队列源码中可以看到STL对于内存的管理的安全性和高效性,值得学习;
  • 面对一个成熟的,庞大的开源项目,应该透过层层封装,对基本功能的实现原理进行解析;
  • 20
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值