STL中的deque及源码实现 std::deque

本博客中涉及到的所有代码均在我的github上存放,地址:mySTL

如果有兴趣的话可以去下载查看,注释比较详尽。

说点什么

相信大家如果对C++有一定的了解的话,都会知道C++中有STL这个超大的模版库,这个模版库提供了非常多的模版容器和模版算法,如常用的vectorliststackqueuemapset等等容器,sortfind_iffindswap等模版函数
这个库由于创建时间过长、版本更替过多,现在也越来越臃肿了,导致我们想要去探究其源码的难度也越来越高(比如很多的编译时的宏和各种千奇百怪的性能提升手段),但是千变万变,这些牺牲代码可读性的措施都是为了提高算法和容器的效率或者说空间利用率
不过其实质的原理还是差不多的,比如各种空间构造和内存存储方式大致上都是差不多的,但具体到很细节的东西,各个版本就有各个版本的不同

比如这次我们要介绍的deque,就是我根据比较易懂的版本自行写出的代码,所以注释还是比较充足的。
首先还是惯例,先介绍一下deque

deque的基本介绍

std::dequedouble-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<T>

> class deque;

以上就是一个典型的deque的头部

T 必须满足可复制赋值 (CopyAssignable) 和可复制构造 (CopyConstructible) 的要求。

Allocator 用于获取/释放内存及构造/析构内存中元素的分配器。类型必须满足分配器 (Allocator) 的要求。若 Allocator::value_typeT 不同则行为未定义。

以下是我自己写的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 T, class Alloc = allocator<T>>
	class deque;
	//deque_iterator detail
	namespace Detail {

		template<class T>
		class deque_iterator :public iterator<random_access_iterator_tag, T> {//继承自包含标准迭代器typedef的iterator
			template<class T, class Alloc>
			friend class deque;
			typedef deque<T>* 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<class T>
			friend typename deque_iterator<T>::difference_type operator -(const deque_iterator<T>& lhs, const deque_iterator<T>& rhs);
			template<class T>
			friend deque_iterator<T> operator +(const deque_iterator<T>& it,  typename deque_iterator<T>::difference_type n);
			template<class T>
			friend deque_iterator<T> operator -(const deque_iterator<T>& it, typename deque_iterator<T>::difference_type n);

		private://容器相关
			T* getNowBuckTail()const;
			T* getNowBuckHead()const;
			size_t getBuckSize()const;

		};

	}//end of Detail

	template<class T, class Alloc = allocator<T>>
	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<T>   iterator;
	private:
		template<class T>
		friend class       ::STL::Detail::deque_iterator;
		typedef Alloc              dataAllocator;
		typedef allocator<T*>       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 <class InputIterator>
		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<class InputIterator>
		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_

由于一些原因,其中的部分函数我并没有实现,一部分原因是我想实现的dequeSTLdeque有一些不同,一部分原因是觉得重复的代码让我觉得很无趣
让我们开始一点点的分析其实现吧

deque头文件分析

1、迭代器的设计

		template<class T>
		class deque_iterator :public iterator<random_access_iterator_tag, T> {//继承自包含标准迭代器typedef的iterator
			template<class T, class Alloc>
			friend class deque;
			typedef deque<T>* containerPtr;
		private:
			containerPtr container_;  
			size_t    mapIndex_;
			T*           cur_;      //此迭代器所指缓存区的当前位置

首先让我们看一看迭代器的设计,deque的迭代器必须知道当前所在区块的头和尾,以便于能够判断自己是否处于需要更换区块。所以其应该具有以下的成员变量:

  • 自己当前所在的区块的下标:size_t mapIndex_;
  • 还有自己在当前区块所处的位置:T* cur_;
  • 为了能够通过mapIndex_访问到区块的头尾,还需要将自己绑定到具体的deque上:containerPtr container_;

还有它所继承的iterator<random_access_iterator_tag, T>,这只是一个简单的空结构体,其内部拥有几个typedef去保证其符合STL标准(我在以后的博客讲到特性萃取的时候会说到这些),iterator实现如下:

	template<class Category, class T, class Distance = ptrdiff_t,
		class Pointer = T* , class Reference = T& >
		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<class T>
		deque_iterator<T>  & deque_iterator<T>::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_进行比较做准备,并无其他作用,所以不存在指针越界的情况。
以上就差不多是迭代器的设计部分

2、deque的设计

成员变量


	template<class T, class Alloc = allocator<T>>
	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<T>   iterator;
	private:
		template<class T>
		friend class       ::STL::Detail::deque_iterator;
		typedef Alloc              dataAllocator;
		typedef allocator<T*>       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 newoperator delete进行为初始化空间的申请和释放
让我们考虑一下deque该如何进行push_back操作?
其实这个函数并不简单:

	template<class T, class Alloc>
	void deque<T, Alloc>::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<class T, class Alloc>
	void deque<T, Alloc>::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<typename T1, typename T2>
	inline void construct(T1* p, const T2& value) {
		new(p)T1(value);
	}

而如果当前区块被填满了呢?
我们就执行__push_back(value)函数去进行新的区块的内存分配以及元素填充操作:

	template<class T, class Alloc>
	void  deque<T, Alloc>::__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();//指向新的区块的开头
	}

可以看到,好像越来越复杂了,我们该如何去理解呢?不要急,一步步地来看吧
这个函数实际上做了如下的事情:

  1. 由于我们已经知道了finish_所在的区块已经被填满了,所以我们首先去判断是否deque的指针数组满了,如果满了我们就执行reallocateMap函数(这个函数就不进行分析了,太过于复杂,感兴趣的话,最后会帖出来)去分配新的指针数组空间并将这些指针移向新的数组中,然后调整start_finish_的指向(所以我们也可以知道为什么迭代器不会失效了吧,因为我们并没有申请新的空间去移动对象到新的空间中,而只是移动了指向对象块的指针的数组
  2. 不管上一步做了什么,我们这一步都需要将当前finish_指向的数组空间元素的下一个元素赋值为新分配的空间的首地址(到了这一步才进行空间分配,所以你可以知道STL将空间的利用达到了何种境界了吧)
  3. 然后在finish_的位置上进行对象构造,最后进行finish_的调整,整个push_back函数结束

push_back函数的内存分配就是如此,我想大家也能管中窥豹,知道STL中的deque如何进行的非常高效以及节约的内存管理
如果大家对这整个源码都有兴趣,可以参考

mySTL

本篇中出现的所有代码均来自我的github

最后再贴上刚才所说的reallocateMap函数的源码:

template<class T, class Alloc>
	void deque<T, Alloc>::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使用中出现的问题你也能很好的明白为何了。

  • 14
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值