一、顺序容器Deque
在使用vector时,发现它如果在头部进行操作的话,有点复杂,array也是如此。其实在实现一个标准的C数组时,使用的人都会发现,如果在尾部增加或者删除一个元素,其实都是很简单的。可是如果想在头部增加一个,那是真麻烦。STL库是啥?不就是为解决劳苦大众的福祉而诞生的么。没有,就创建一个嘛。deque就横空出世了,它可以解决头尾都需要操作的场景。
那它为啥能头尾都进行操作呢?做一个连续的空间?其实看deque源码就会发现,它其实并不是一个真正的像数组一样的整体的连续空间。而是分组或者说分段式的连续的空间。那么分段连续的空间怎么能让上层应用看起来像一个没分组的呢?那就需要设计一个map来管理他,通过重载迭代器的指针来实现连续的操作。看下图:
其实看到这儿,应该对STL中一定要拆成分配器、算法等的原因了吧。高度重用,快速扩展。
二、源码
老样子看一下它的代码:
template <class _Ty, class _Alloc = allocator<_Ty>>
class deque {
private:
friend _Tidy_guard<deque>;
static_assert(!_ENFORCE_MATCHING_ALLOCATORS || is_same_v<_Ty, typename _Alloc::value_type>,
_MISMATCHED_ALLOCATOR_MESSAGE("deque<T, Allocator>", "T"));
using _Alty = _Rebind_alloc_t<_Alloc, _Ty>;
using _Alty_traits = allocator_traits<_Alty>;
using _Alpty = _Rebind_alloc_t<_Alloc, typename _Alty_traits::pointer>;
using _Alpty_traits = allocator_traits<_Alpty>;
using _Mapptr = typename _Alpty_traits::pointer;//=============map指针扩展
using _Alproxy_ty = _Rebind_alloc_t<_Alty, _Container_proxy>;
using _Alproxy_traits = allocator_traits<_Alproxy_ty>;
using _Scary_val = _Deque_val<conditional_t<_Is_simple_alloc_v<_Alty>, _Deque_simple_types<_Ty>,
_Deque_iter_types<_Ty, typename _Alty_traits::size_type, typename _Alty_traits::difference_type,
typename _Alty_traits::pointer, typename _Alty_traits::const_pointer, _Ty&, const _Ty&, _Mapptr>>>;
......
//C++11增加
void shrink_to_fit() {
size_type _Oldcapacity = _DEQUESIZ * _Mapsize();
size_type _Newcapacity = _Oldcapacity / 2;
if (_Newcapacity < _DEQUESIZ * _DEQUEMAPSIZ)
_Newcapacity = _DEQUESIZ * _DEQUEMAPSIZ;
if ((empty() && 0 < _Mapsize())
|| (!empty() && size() <= _Newcapacity && _Newcapacity < _Oldcapacity)) { // worth shrinking, do it
deque _Tmp(_STD make_move_iterator(begin()), _STD make_move_iterator(end()));
swap(_Tmp);
}
}
......
using _Mybase::operator-;
_NODISCARD _Deque_iterator operator-(const difference_type _Off) const {
_Deque_iterator _Tmp = *this;
return _Tmp -= _Off;
}
_NODISCARD reference operator[](const difference_type _Off) const {
return const_cast<reference>(_Mybase::operator[](_Off));
}
......
}
其实如果明白了讲过的组合的味道,基本就知道了这些源码的味道,不要看这么多的代码,其实有好多是显示的标志一些符号而做的,对于学习本身,用处不是很大。重点是掌握了整个源码的流程,这才是最重要的。
通过分析源码可以看出,deque的访问速度理论上讲要比vector要慢,毕竟有一个中间层的存在,获得便利的同时一定会有牺牲性能。也就是说,得到一些东西,就得舍弃一些东西,编程和实际生活一样,不可能事事完美,学会取舍,也就学会了设计的思想的入门。
三、例程
还是要看例子,毕竟重点是学习使用,而不是学习源码:
#include <deque>
#include <iostream>
void PrintDeque(const std::deque<int> & dq)
{
std::cout << "cur deque print:" << std::endl;
for (auto& d : dq)
{
std::cout << "item value:" << d << std::endl;
}
std::cout << "print end!" << std::endl;
}
void TestDeque()
{
std::deque<int> d1;
for (int num = 0; num < 10; num++)
{
d1.emplace_back(num);
d1.emplace_front(10 - num);
}
PrintDeque(d1);
std::deque<int>::iterator it;
d1.erase(++d1.begin());
int tmp = d1[9];
std::cout << "tmp value is:" << tmp << std::endl;
d1[9] = 1000;
std::cout << "nine value is:" << d1[9] << std::endl;
int f = 8;
auto v = std::find(d1.begin(),d1.end(),f);
std::cout << "find value:" << *v << std::endl;
PrintDeque(d1);
}
int main()
{
TestDeque();
return 0;
}
运行结果是:
cur deque print:
item value:1
item value:2
item value:3
......
item value:8
item value:9
print end!
tmp value is:0
nine value is:1000
find value:8
cur deque print:
item value:1
item value:3
...
item value:10
item value:1000
...
item value:9
print end!
例程非常简单,这也是STL的目的,如果写得天花乱坠,估计更没人用了。
四、总结
std::deque单独使用还是比较少的,一般是使用它的适配器层的std::queue,这个会在适配器中进行学习。其实在于习一门技术或者具体到编程语言时,你会发现,语言的进步往往是实际应用推动的,设计时,可能觉得可有可无的,或者说干脆没有实现的,发现到实际应用中需求很大,不管出于什么原因,一般最后都会添加这个特性。这一点上,Java的模板就是一个很典型的例子。当然,包括Go语言,现在都是这样。c++/c语言自产生后,随着应用的不断的发展,对它们也提出了很多需求,以前的大佬们觉得无所谓,但是随着新语言的不断喷涌而出,大佬儿们发现如果不再进步,极有可能就被消灭,所以c++11,以及以后不断推出的版本更新,都是在拥抱变化。
你不变化,变化就淘汰你!