本博客中涉及到的所有代码均在我的github上存放,地址:mySTL
如果有兴趣的话可以去下载查看,注释比较详尽。
相信大家如果对C++有一定的了解的话,都会知道C++
中有STL
这个超大的模版库,这个模版库提供了非常多的模版容器和模版算法,如常用的vector
、list
、stack
、queue
、map
、set
等等容器,sort
、find_if
、find
、swap
等模版函数
这个库由于创建时间过长、版本更替过多,现在也越来越臃肿了,导致我们想要去探究其源码的难度也越来越高(比如很多的编译时的宏和各种千奇百怪的性能提升手段),但是千变万变,这些牺牲代码可读性的措施都是为了提高算法和容器的效率或者说空间利用率
不过其实质的原理还是差不多的,比如各种空间构造和内存存储方式大致上都是差不多的,但具体到很细节的东西,各个版本就有各个版本的不同
比如这次我们要介绍的deque
,就是我根据比较易懂的版本自行写出的代码,所以注释还是比较充足的。
首先还是惯例,先介绍一下deque
吧
deque
的基本介绍std::deque
( double-ended queue ,双端队列)是可以进行下标访问的顺序容器,它允许在其首尾两端快速插入及删除元素。
另外,在 deque
任一端插入或删除元素不会让当前正在使用的迭代器失效,至于原因,这个就和deque
的实现方式有很大的关系了。
与 std::vector
相反, deque
的元素不一定是相邻存储的:典型实现是采用单独分配的固定大小的相邻元素数组,外加一个额外的数组去存储这些空间的首地址,这表示下标访问必须进行二次指针解引用,与之相比 vector
的下标访问只需要进行一次。
deque
的存储按需自动扩展及收缩。扩张 deque
比扩展 std::vector
的消耗要少,因为它不涉及到复制当前的元素到新内存位置。但是同时, deque
也因此拥有较大的最小内存开销。就算只保有一个元素的 deque
也必须为其分配至少一个内存数组(例如 64 位 libstdc++
上为对象大小的 8 倍空间; 64 位 libc++
上为对象大小的16 倍或 4096 字节的较大者)。
deque
上常见操作的复杂度(效率)如下:
- 随机访问——常数 O(1)
- 在结尾或起始插入或移除元素——常数 O(1)
- 插入或移除元素——线性 O(n)
deque
的内存布局通过上面的空间构造,我们可以知道,我们在使用deque
的的元素操作的时候(比如[ ]
操作,或者迭代器的++
操作的时候,并不是简单的将其指针+1或者+n ),所以我们需要进行更加细致的设计和安排,为deque
设计一个特化的、合适的迭代器,以及设计和其对应的空间配置方案
deque
需要维护一个指向各个连续内存空间的指针数组,所以它的迭代器势必也要能够判断各个连续空间的边界以便于知道自己该如何进行++
和- -
,下面将会对这个迭代器进行一定的代码分析。
deque
最重要的就是其空间不足进行重新配置的过程,了解了这个,我们差不多就完全理解了deque
是个怎样的东西
我们边看代码边说吧,毕竟网上关于deque
的内存布局的东西已经说的太多了
以下代码仅供参考,不具备实际操作的意义。
deque
的类定义template<
class T,
class Allocator = std::allocator
> class deque;
以上就是一个典型的deque
的头部
T
必须满足可复制赋值 (CopyAssignable) 和可复制构造 (CopyConstructible) 的要求。
Allocator
用于获取/释放内存及构造/析构内存中元素的分配器。类型必须满足分配器 (Allocator
) 的要求。若Allocator::value_type
与T
不同则行为未定义。
以下是我自己写的deque
的头文件代码(说是头文件,实质上模版类只含有头文件,其定义我写在了另外一个头文件中#include"Deque_detail.h"
),其完整头文件代码如下:
#ifndef _DEQUE_H_
#define _DEQUE_H_
#include"Allocator.h"
#include"Iterator.h"
#include"Algorithm.h"
#include"UninitializedFunc.h"
namespace STL {
template>
class deque;
//deque_iterator detail
namespace Detail {
template
class deque_iterator :public iterator {//继承自包含标准迭代器typedef的iterator
template
friend class deque;
typedef deque* containerPtr;
private:
containerPtr container_;
size_t mapIndex_;
T* cur_; //此迭代器所指缓存区的当前位置
public:
//构造相关
deque_iterator()
:mapIndex_(-1), cur_(nullptr), container_(nullptr) {}
deque_iterator(size_t index, T *ptr, containerPtr container)
:mapIndex_(index), cur_(ptr), container_(container) {}
deque_iterator(const deque_iterator& it)
:mapIndex_(it.mapIndex_), cur_(it.cur_), container_(it.container_) {}
deque_iterator& operator = (const deque_iterator& it);
//符号重载
deque_iterator& operator ++ ();
deque_iterator operator ++ (int);
deque_iterator& operator -- ();
deque_iterator operator -- (int);
reference operator *() { return *cur_; }
const reference operator *()const { return *cur_; }
pointer operator ->() { return &(operator*()); }
const pointer operator ->()const { return &(operator*()); }
bool operator ==(const deque_iterator& rhs)const;
bool operator !=(const deque_iterator& rhs)const;
public:
template
friend typename deque_iterator::difference_type operator -(const deque_iterator& lhs, const deque_iterator& rhs);
template
friend deque_iterator operator +(const deque_iterator& it, typename deque_iterator::difference_type n);
template
friend deque_iterator operator -(const deque_iterator& it, typename deque_iterator::difference_type n);
private://容器相关
T* getNowBuckTail()const;
T* getNowBuckHead()const;
size_t getBuckSize()const;
};
}//end of Detail
template>
class deque {
public:
typedef T value_type;
typedef value_type* pointer;
typedef T& reference;
typedef const reference const_reference;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
typedef Alloc allocator_type;
typedef Detail::deque_iterator iterator;
private:
template
friend class ::STL::Detail::deque_iterator;
typedef Alloc dataAllocator;
typedef allocator mapAllocator;
typedef pointer* map_Pointer;
private:
iterator start_; //指向map的头部
iterator finish_; //指向map的尾部
map_Pointer map_; //指向map(map为一段连续空间),其内部元素为指针,每个指针指向一个缓存区
size_type map_size_; //map内部指针的个数
enum class BuckSize { BUCK_SIZE = 16};
public:
//元素访问
iterator begin() { return start_; }
iterator end() { return finish_; }
iterator begin()const { return start_; }
iterator end() const { return finish_; }
reference operator[] (size_type n);
reference front();
reference back();
const_reference operator[] (size_type n) const;
const_reference front() const;
const_reference back() const;
//空间
size_type size() const { return end() - begin(); }
bool empty() const { return begin() == end(); }
public:
deque();
explicit deque(size_type n, const value_type& val = value_type());
template
deque(InputIterator first, InputIterator last);
deque(const deque& rhs);
deque(const deque&& rhs);
~deque();
private:
//空间配置器
T * getANewBuck();
T ** getNewMapAndGetNewBucks(const size_t& size);
T** GetNewMap(const size_t& size);
void __deque(size_t n, const value_type& value, std::true_type);
template
void __deque(InputIterator first, InputIterator last, std::false_type);
void __push_back(const value_type& value);
void __push_front(const value_type& value);
void reallocateMap(size_t nodes_to_add,bool add_at_front);
void init();
void __pop_front();
void __pop_back();
void deallocateABuck(size_t index);
void creat_map_and_nodes(size_t n);
public:
//元素操作
void push_back(const value_type& val);
void push_front(const value_type& val);
void pop_back();
void pop_front();
void clear();
private:
//获取操作(内部或迭代器使用)
size_type getBuckSize()const { return (size_type)BuckSize::BUCK_SIZE; }
size_t getNewMapSize(const size_t size);
bool isBackFull()const;
bool isFrontFull()const;
};
}
#include"Deque_detail.h"
#endif // !_DEQUE_H_
由于一些原因,其中的部分函数我并没有实现,一部分原因是我想实现的deque
和STL
的deque
有一些不同,一部分原因是觉得重复的代码让我觉得很无趣
让我们开始一点点的分析其实现吧
deque
头文件分析 template
class deque_iterator :public iterator {//继承自包含标准迭代器typedef的iterator
template
friend class deque;
typedef deque* containerPtr;
private:
containerPtr container_;
size_t mapIndex_;
T* cur_; //此迭代器所指缓存区的当前位置
首先让我们看一看迭代器的设计,deque
的迭代器必须知道当前所在区块的头和尾,以便于能够判断自己是否处于需要更换区块。所以其应该具有以下的成员变量:
size_t mapIndex_;
T* cur_;
mapIndex_
访问到区块的头尾,还需要将自己绑定到具体的deque
上:containerPtr container_;
还有它所继承的iterator
,这只是一个简单的空结构体,其内部拥有几个typedef
去保证其符合STL
标准(我在以后的博客讲到特性萃取的时候会说到这些),iterator
实现如下:
template
struct iterator
{
typedef Category iterator_category;
typedef T value_type;
typedef Distance difference_type;
typedef Pointer pointer;
typedef Reference reference;
};
由于其成员变量比较多,所以它的构造函数也比较丰富:
deque_iterator()
:mapIndex_(-1)
, cur_(nullptr)
, container_(nullptr) {}
deque_iterator(size_t index, T *ptr, containerPtr container)
:mapIndex_(index)
, cur_(ptr)
, container_(container) {}
deque_iterator(const deque_iterator& it)
:mapIndex_(it.mapIndex_)
, cur_(it.cur_)
, container_(it.container_) {}
deque_iterator& operator = (const deque_iterator& it);
这些,都是为了能够更好的进行绑定和判断边界,以及进行++
、--
的操作
还有如下的符号重载函数,都是为了让我们以为这个迭代器就是一个简简单单的指针,我们要使用的时候只需要进行->
就可以完成操作。
但实际上在这个简单的操作背后有大量的的符号重载和大量的判断(我写的已经是很简略的版本):
deque_iterator& operator ++ ();//前置++
deque_iterator operator ++ (int);//后置++
deque_iterator& operator -- ();
deque_iterator operator -- (int);
reference operator *() { return *cur_; }//*运算符
const reference operator *()const { return *cur_; }//const限定
pointer operator ->() { return &(operator*()); }//->运算符
const pointer operator ->()const { return &(operator*()); }
bool operator ==(const deque_iterator& rhs)const;
bool operator !=(const deque_iterator& rhs)const;
当然上面的函数我们当然不可能全部都看一遍实现代码(需要看的可以去我的github
上下载,地址在篇头),实际上,我们只需要看一个符号重载,就能大致上了解其工作性质了:
如下,是++
的符号重载代码(由于注释比较充分,就不再多说):
template
deque_iterator & deque_iterator::operator++() {
if (cur_ != getNowBuckTail()) {//+1之后依然在桶内
++cur_;
}
else if (mapIndex_ + 1 < container_->map_size_) {//已经在桶的结尾,但是之后还有新的map指针
++mapIndex_;
cur_ = getNowBuckHead();//指向下一个桶的开头
}
else {//mapIndex_ +1之后没有了map
mapIndex_ = container_->map_size_;
cur_ = container_->map_[mapIndex_];//指向最后一个桶的最后一个元素的后方区域
}
return *this;
}
cur_ = container_->map_[mapIndex_];//指向最后一个桶的最后一个元素的后方区域
这句代码我要着重说一下,让其指向这个未知位置只是为了在后面进行空间拓展的时候让其和finish_
进行比较做准备,并无其他作用,所以不存在指针越界的情况。
以上就差不多是迭代器的设计部分
deque
的设计
template>
class deque {
public:
typedef T value_type;
typedef value_type* pointer;
typedef T& reference;
typedef const reference const_reference;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
typedef Alloc allocator_type;
typedef Detail::deque_iterator iterator;
private:
template
friend class ::STL::Detail::deque_iterator;
typedef Alloc dataAllocator;
typedef allocator mapAllocator;
typedef pointer* map_Pointer;
allocator
的实现我会在下一个博客进行介绍,这里我们先假设大家已经知道了allocator
是一个空间配置器,支持分配以字节为单位的内存并返回其地址,和在指定的内存空间上实行构造函数的功能
下面的一堆typedef
我也不再过多介绍,STL
的标准就是如此,不支持此标准的不能溶于STL
中。
iterator start_; //指向map的头部
iterator finish_; //指向map的尾部
map_Pointer map_; //指向map(map为一段连续空间),其内部元素为指针,每个指针指向一个缓存区
size_type map_size_; //map内部指针的个数
enum class BuckSize { BUCK_SIZE = 16};
上面的几个成员变量都是如此,enum class BuckSize { BUCK_SIZE = 16};
是一个枚举类,用来存放一个每一个区块的大小,由于其为补不可修改的值,所以并不占用类的储存空间
首先要假设我们已经完成了deque
的迭代器的设计,并且其边界判定完好,然后我们才能进行下一步。
然后观察下面的几个成员变量
iterator start_; //指向map的头部
iterator finish_; //指向map的尾部
这两个自然不必多说,类型都为iterator
,分别表示指向当前存放的所有元素的头和尾部(并不一定是内存空间的头尾)的迭代器。
map_Pointer map_;
,这个成员的类型实际上为T **
类型,指向我们分配的存放区块空间指针数组的头部。
deque
如何进行内存管理——push_back
deque
如何进行空间管理?
这是一个很简单也比较复杂的问题,具体的可以看看这篇博客【C++】 深入探究 new 和 delete,去先了解一下C++中空间配置的不同方式,我写的allocator
采用的方式就是placement new
的方式去进行对象的构造,用operator new
和 operator delete
进行为初始化空间的申请和释放
让我们考虑一下deque
该如何进行push_back
操作?
其实这个函数并不简单:
template
void deque::push_back(const value_type& value) {
if (empty()) {
init();
}
if (finish_.cur_ != finish_.getNowBuckTail()) {//如果当前区块没有被填满
STL::construct(finish_.cur_, value);
++finish_;
}
else {//当前区块被填满了
__push_back(value);
}
}
如果为空,我们做什么?
(实际上STL
的源码里面并没有为空的操作,直接声明一个deque
就会分配一定大小的内存空间。但是我想了一下,为了不浪费额外的内存空间,写了一个当调用默认构造函数的时候不分配内存空间的deque
)
如果为空,我们执行init
函数去初始化整个内存空间:
template
void deque::init() {
map_size_ = 2;
map_ = getNewMapAndGetNewBucks(map_size_);//获得大小为map_size_的指针空间并且为其分配未构造的内存空间
//将起始点放置在中间
start_.container_ = finish_.container_ = this;
start_.mapIndex_ = finish_.mapIndex_ = map_size_ - 1;
start_.cur_ = finish_.cur_ = map_[map_size_ - 1];
}
我们在第一次进行push_back
的时候才进行容器的初始化,产生两块分别可以容纳16个对象的内存空间。
然后如果当前区块的空间没被填满,则在finish_
的位置直接用placement_new
的方式构造一个值为value
的对象。
调用我自己写的**STL::construct
函数进行指定地址的对象构造**
STL::construct(finish_.cur_, value);
++finish_;
其源码如下:
template
inline void construct(T1* p, const T2& value) {
new(p)T1(value);
}
而如果当前区块被填满了呢?
我们就执行__push_back(value)
函数去进行新的区块的内存分配以及元素填充操作:
template
void deque::__push_back(const value_type& value) {
if (isBackFull()) {//map后端填满
reallocateMap(1, false);//将指向区块的指针数组移去新的更大的数组空间
}
map_[finish_.mapIndex_ + 1] = getANewBuck();//在当前区块地址元素的后一个元素赋值为新分配的内存空间的首地址
STL::construct(finish_.cur_, value);//构造
++finish_.mapIndex_;//调整finish_的区块位置
finish_.cur_ = finish_.getNowBuckHead();//指向新的区块的开头
}
可以看到,好像越来越复杂了,我们该如何去理解呢?不要急,一步步地来看吧
这个函数实际上做了如下的事情:
finish_
所在的区块已经被填满了,所以我们首先去判断是否deque
的指针数组满了,如果满了我们就执行reallocateMap
函数(这个函数就不进行分析了,太过于复杂,感兴趣的话,最后会帖出来)去分配新的指针数组空间并将这些指针移向新的数组中,然后调整start_
和finish_
的指向(所以我们也可以知道为什么迭代器不会失效了吧,因为我们并没有申请新的空间去移动对象到新的空间中,而只是移动了指向对象块的指针的数组)finish_
指向的数组空间元素的下一个元素赋值为新分配的空间的首地址(到了这一步才进行空间分配,所以你可以知道STL
将空间的利用达到了何种境界了吧)finish_
的位置上进行对象构造,最后进行finish_
的调整,整个push_back
函数结束push_back
函数的内存分配就是如此,我想大家也能管中窥豹,知道STL
中的deque
如何进行的非常高效以及节约的内存管理
如果大家对这整个源码都有兴趣,可以参考
本篇中出现的所有代码均来自我的github
最后再贴上刚才所说的reallocateMap
函数的源码:
template
void deque::reallocateMap(size_t nodes_to_add, bool add_at_front) {
size_t old_num_mapNodes = finish_.mapIndex_ - start_.mapIndex_ + 1;
size_t new_num_mapNodes = old_num_mapNodes + nodes_to_add;
map_Pointer newStart;
size_t newStartIndex;
if (map_size_ > 2 * new_num_mapNodes) {//剩余map空间还有很多
newStartIndex = (map_size_ - new_num_mapNodes) / 2
+ (add_at_front ? nodes_to_add : 0);
newStart = map_ + newStartIndex;
if (newStartIndex < start_.mapIndex_) {
STL::copy(map_ + start_.mapIndex_, map_ + finish_.mapIndex_, newStart);
}
else {
STL::copy_backward(map_ + start_.mapIndex_, map_ + finish_.mapIndex_, newStart + old_num_mapNodes);
}
}
else {
size_t new_map_size = map_size_ + max(map_size_, nodes_to_add) + 2;
//配置新map空间
map_Pointer new_map = GetNewMap(new_map_size);
newStartIndex = (new_map_size - new_num_mapNodes) / 2
+ (add_at_front ? nodes_to_add : 0);
newStart = new_map + newStartIndex;
//复制原map的内容
STL::copy(map_ + start_.mapIndex_, map_ + finish_.mapIndex_, newStart);
//释放原map
mapAllocator::deallocate(map_);
//设定新的map的地址和大小
map_ = new_map;
map_size_ = new_map_size;
}
start_ = iterator(newStartIndex, start_.cur_, this);
finish_ = iterator(newStartIndex + old_num_mapNodes - 1, finish_.cur_, this);
}
也许还有很多没有说到的部分,但是我想,如果你能很细致的看完以及看懂这些代码的话,那么你就能很好的了解deque
的内存布局以及其空间分配的方式
你在使用deque
使用中出现的问题你也能很好的明白为何了。