stl三大序列容器

vector

简介

序列容器之一,vector是比较常见的,我们可以把它理解为一个动态数组,使用它的方法和c风格的数组无异,只不过它隐藏了处理动态内存的细节(这里提到动态分配,相比都应该能知道它使用的是堆上的内存)。并且它的迭代器其实就是容器元素类型的指针,因此迭代器类型是随机迭代器,因此才能实现和c风格数组指针操作类型一样,內部主要有三个迭代器,分别指向自己动态内存的开始,已经使用的内存位置的尾部,还有就是所有空闲空间的尾部,分别对应下面的iterator start 、iterator finish 、iterator end_of_storage

iterator start;
iterator finish;
iterator end_of_storage;

这里可能对空闲空间的尾部有点懵嗷,它是干啥的呢,我们c风格的数组没有这个end_of_storage啊,想要了解这个就需要知道vector隐藏的动态分配细节了,其实vector对内存的管理不是一成不变的,它会在使用过程中动态的扩容,也就是只要我们使用了vector,我们就不需要了解它的大小,可以一直向里面存储数据,直到系统的内存不足,那么要实现这样的机制,固定的大小肯定是不可以的,固定大小永远有使用完那一刻。因此vector就有动态扩容机制,这个机制非常的重要,因为好多数据结构都会采用这个机制来实现,比如redis的sds数据结构,既然很多数据结构都会采用,那么它肯定是有很多优点,我们来详细介绍一下这个动态扩容。

动态扩容

这里会触发动态扩容机制的主要有下面几个方法

vector<T>::push_back();
vector<T>::insert();

向vector里面加元素肯定会有用完剩余空间的那一刻,也就是上述三个迭代器finish == end_of_storage时,此时表示剩余空间不足了,就需要动态分配,如果我们每次分配的内存都正好能够用所有加入的元素的空间,那么finish == end_of_storage就一直成立,也就导致只要我们向vector里面加元素,就会触发动态分配机制,这是很耗时间的,因为分配堆上的内存需要系统调用,这就涉及两次上下文切换。因此我们每次分配的时候不会分配正好够用的内存,需要留有剩余空间,因此我们选择每次动态分配,二倍扩容,剩下的空间当作剩余空间减少内存分配次数,redis的sds也是2倍扩容。

动态扩容后

vector动态扩容后,紧接着就会把数据倒腾到新分配的内存位置,并释放原来的内存空间,并且更新上述三个迭代器,这里面会涉及大量的复制操作(c++11新特性好像可以使用移动语义来实现,但是要确保异常安全性的条件需要满足),并且涉及元素倒腾,那么动态扩容完后,所有的迭代器就失效了,这是个知识点。

list

list倒是没有什么特殊的,底层的实现是一个双向环形链表,因为是环形的链表,因此list只需要保存一个节点就可以遍历所有节点,并且这个节点指向一个尾部的空节点,那么就可以满足stl的左闭右开的规则。内存也是动态分配的,这个不能扩容两倍,用一个节点就分配一个节点,并且因为是链表,迭代器不是随机迭代器,只能向前或者向后遍历,并且由于是链表形式,对其元素进行删除和添加都不会导致其他迭代器的失效。这个没有特别亮眼的地方,当然介绍也少,感觉最厉害的是deque的实现,看下面

deque

简介

看完上面的vector后我们可以发现,向vector的头部插入元素时间复杂度挺高的,元素越多越慢,插入也一样。这个deque简直了,相比vector它不用保留剩余空间,并且可以常数级别插入头部,并且还可以有随机迭代器,是不是感觉很强,是不是想舍弃vector一直使用deque。等了解完具体机制在做决定吧,这个随机迭代器的随机访问和vector还是有点区别的。

随机访问的实现

这里deque有一个中控器的概念,这是个啥呢,其实就是一个指向指针的指针,中控器表示一小段连续的空间,然后里面的连续空间存着指向更大的连续空间的指针,然后这个更大的连续空间才会存储具体的数据,其实说白了,deque就是有多个连续的内存块,并在迭代器的层面实现随机访问。怎么实现的呢,这里我们把中控器叫map,中控器中存的指针指向的是叫node,这里迭代器里面有四个重要的参数

T *cur;
T *first;
T *last;
map_pointer node;

可以看出迭代器里面存的就是原始指针,cur表示当前迭代器指向的元素(node里面存的元素,node是固定大小的),first指向的是当前迭代器指向元素所在node的第一个元素,last表示最后一个元素(即使没有分配元素)。node比较重要嗷,是实现随机访问的重中之重,node表示的就是当前迭代器所指向的元素所在node的地址,也就是中控器map所存的地址,提醒一下map存的是连续地址,因此可以随机访问。其实有了这些知识,随机访问应该可以推出来了吧,我来简单说一下,比方说我们有迭代器i,我想访问i+n,那么我们就可以在迭代器查看i+n是否超过当前node的last了,超过了,就要涉及node的操作了,调整node值,在下一个node里面继续随机访问。ok了吧,是不是随机访问了,虽然需要一些判断、多次取址什么的。

实现push_front

ok解决下一个问题,怎么实现常数头插的呢,deque内部保存两个迭代器start、finish,一个指向前面的第一个节点,一个指向后面的最后一个节点,deque向里面添加元素,并不是从map的起始地址开始插入的,我们是向map连续地址的中间区域添加元素的,因此push_front和push_back都可以轻易实现

实现insert

insert方法,会先判断插入位置里最前面有多少个元素,和里最后面有多少个元素,向少的一端进行移动(copy)。因此insert会有倒腾操作,迭代器会失效,指向的位置不对了。这里会先push_front(front())或者push_back(end())在进行移动,因此扩容的操作在push_back或者push_front()中解决

动态扩容策略

在push_front时,当迭代器first里的成员cur等于成员first的时候,就要新分配node内存了,新分配的内存被迭代器first的成员node的前一个指向,并更新迭代器first,当map的向前增长到头之后,会进入一个函数判断map是否需要重新分配,重新分配的条件是map要分别的节点数的2倍是否大于当前的map_size,大于才会分配动态内存,把原来的map倒腾到新分配的内存上,并更新map_size,否则就是向前插入和向后插入不均,重新调整map内的元素,不需要重新分配,经常保持map中所使用的元素调整至中间区域。push_back也是一样的操作,因此map的重新分配是二倍扩容,但是node的分配是一个一个节点分配。

你可能感兴趣的:(STL,c++,开发语言)