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对于内存的管理的安全性和高效性,值得学习;
- 面对一个成熟的,庞大的开源项目,应该透过层层封装,对基本功能的实现原理进行解析;