前言
在上一小节中我们介绍了deque
容器实现的大致思路以及它内部使用的一些机制,比如存储元素的空间其实是由指针数组里面的指针指向的等。
在本小节中,我们主要介绍一些deque
容器封装的一些常用的操作,比如push_back
、push_front
、insert
、erase
、clear
等。这些函数无非就是根据迭代器内部的first
、last
、cur
的位置来决定进行操作。
所以有必要再重温一下first
、last
、cur
指针指向的位置。
first
:指向当前线性空间的第一个元素last
:指向当前线性空间的末尾,最后一个元素的下一个位置cur
:指向当前线性空间的当前元素。在迭代器start
中,默认当然指向第一个元素。在迭代器finish
中,默认指向最后一个元素的下一个位置。
在分析它的源码之前,我们应该考虑到插入元素在deque
尾部时会发生的特殊情况,即当前线性空间快满时,需要存储到下一个线性空间的情况,因此我们可能需要使得finish
迭代器指向下一个结点,甚至没有可用的下一个结点时,我们还需要扩展map
。
插入元素到deque
的尾部
接下来我们来看看push_back
是如何操作的。
void push_back(const value_type& t) {
/* 当前线性空间还可以容纳2个及以上的元素时
* 就无需申请下一个线性空间
* 否则调用push_back_aux
*/
if (finish.cur != finish.last - 1) {
construct(finish.cur, t);
++finish.cur;
}
else
push_back_aux(t);
}
push_back_aux
的代码如下:
// Called only if finish.cur == finish.last - 1.
/* 调用本函数证明finish.cur == finish.last-1
* 即只有一个元素可以容纳了
* 内部调用reserve_map_at_back,当其达到某种要求时会重新申请map,具体是什么要求,我们放在后面讲
* 接着申请一个新的结点,将元素插入到原来结点的最后一个位置上
* 将finish迭代器指向新的结点并更新其cur指针
*/
template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>::push_back_aux(const value_type& t) {
value_type t_copy = t;
//调用该函数,可能会导致map进行重新分配,只是可能
reserve_map_at_back();
//申请新的线性空间
*(finish.node + 1) = allocate_node();
__STL_TRY {
//赋值并调整finish迭代器
construct(finish.cur, t_copy);
finish.set_node(finish.node + 1);
finish.cur = finish.first;
}
//发生异常了之后,释放空间
__STL_UNWIND(deallocate_node(*(finish.node + 1)));
}
插入元素到deque
的首部
其实push_back
、push_front
函数很类似,不过还是有几个比较特殊的地方有点不同,代码如下。
push_front
void push_front(const value_type& t) {
/* 当首部前有一个位置及以上就不申请新的线性空间(注意这里是只有一个位置及以上)
* 否则调用push_front_aux
*/
if (start.cur != start.first) {
construct(start.cur - 1, t);
--start.cur;
}
else
push_front_aux(t);
}
push_front_aux
// Called only if start.cur == start.first.
//会调用这个函数,证明当前线性空间的first指向的位置之前已经没有空间了
template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>::push_front_aux(const value_type& t) {
value_type t_copy = t;
reserve_map_at_front();
//注意这里是向前申请线性空间
*(start.node - 1) = allocate_node();
__STL_TRY {
//接着这里也应该改变start迭代器
start.set_node(start.node - 1);
start.cur = start.last - 1;
construct(start.cur, t_copy);
}
# ifdef __STL_USE_EXCEPTIONS
catch(...) {
//异常处理,将start迭代器恢复原样并释放空间
start.set_node(start.node + 1);
start.cur = start.first;
deallocate_node(*(start.node - 1));
throw;
}
# endif /* __STL_USE_EXCEPTIONS */
}
reserve_map_at_xxx()
当调用push_xxx_aux
函数时,开头都会调用一个reserve_map_at_front
或者reserve_map_at_back
函数。它们的作用就时检测是否需要重新申请map。源码如下:
void reserve_map_at_back (size_type nodes_to_add = 1) {
/* 当map后端的备用节点不足需要分配的节点数时
* 调用reallocate_map重新分配
*/
if (nodes_to_add + 1 > map_size - (finish.node - map))
reallocate_map(nodes_to_add, false);
}
void reserve_map_at_front (size_type nodes_to_add = 1) {
/* 当map前端的备用节点不足需要分配的新的节点数时
* 调用reallocate_map重新分配map
*/
if (nodes_to_add > start.node - map)
reallocate_map(nodes_to_add, true);
}
reallocate_map
的第二参数,为true
时代表向首部扩展,为false
代表向尾部扩展。
reallocate_map
reallocate_map
才是真正为map
重新分配空间的函数。源码如下
template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>::reallocate_map(size_type nodes_to_add,
bool add_at_front) {
//旧节点数
size_type old_num_nodes = finish.node - start.node + 1;
//加上需要新增加的总的节点数
size_type new_num_nodes = old_num_nodes + nodes_to_add;
map_pointer new_nstart;
/* 原map_size很充裕时只用调整下start和finish的位置就行了
* 至于为什么会导致这种的情况的发生
* 请考虑如下场景
* 初始化了deque之后,一直朝尾部或首部添加元素
* 这样导致的结果就是一端已经无结点可以用,另一端还有很多未用
* 而此时并不用重新申请map,只需要调整一下位置
* 重新将已用的结点挪动到中间位置即可
*/
if (map_size > 2 * new_num_nodes) {
//重新计算start和finish迭代器应处于的位置
new_nstart = map + (map_size - new_num_nodes) / 2
+ (add_at_front ? nodes_to_add : 0);
if (new_nstart < start.node)
//首部需要往后调整
copy(start.node, finish.node + 1, new_nstart);
else
//尾部需要往前调整
copy_backward(start.node, finish.node + 1, new_nstart + old_num_nodes);
}
/* 如果map的大小确实不够,则只有重新找一块空间了
* 新申请一片空间,将原来的值拷贝过去
* 然后释放原空间,并对相关数据进行调整
*/
else {
size_type new_map_size = map_size + max(map_size, nodes_to_add) + 2;
map_pointer new_map = map_allocator::allocate(new_map_size);
new_nstart = new_map + (new_map_size - new_num_nodes) / 2
+ (add_at_front ? nodes_to_add : 0);
copy(start.node, finish.node + 1, new_nstart);
map_allocator::deallocate(map, map_size);
map = new_map;
map_size = new_map_size;
}
//最后调整deque容器中的start和finish迭代器
start.set_node(new_nstart);
finish.set_node(new_nstart + old_num_nodes - 1);
}
弹出deque末尾的元素
这里也需要考虑特殊情况,即当最后一个线性空间没有元素时,该线性空间会被回收,并且finish
迭代器也会前移。
pop_back
void pop_back() {
/* 最后一段线性空间有一个及以上元素时则只用简单的移动cur以及析构当前元素
* 否则调用pop_back_aux
*/
if (finish.cur != finish.first) {
--finish.cur;
destroy(finish.cur);
}
else
pop_back_aux();
}
pop_back_aux
// Called only if finish.cur == finish.first.
/* 最后一段线性空间为空时,需要释放该空间
* 并将finish前移,释放当前线性空间的最后一个结点
*/
template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>:: pop_back_aux() {
//释放最后一段线性空间
deallocate_node(finish.first);
//移动finish迭代器
finish.set_node(finish.node - 1);
//使cur指针指向最后一个元素并析构
finish.cur = finish.last - 1;
destroy(finish.cur);
}
关于pop_front
操作和pop_back
很类似,也就是当第一段线性空间没有元素时则释放该空间,调整start
迭代器,如果有元素,直接释放了就行了。这里就不再赘述了。
朝deque
任意位置插入元素
insert
的版本确实是最多的,不管是vector
还有
list
或者是deque
,这里只选择最通用的几个版本进行分析。
指定位置插入元素x
iterator insert(iterator position, const value_type& x) {
if (position.cur == start.cur) {
/* 如果插入位置是deque容器的最前面
* 则让push_front去做就行了
*/
push_front(x);
return start;
}
else if (position.cur == finish.cur) {
/* 如果插入位置是deque容器的末尾
* 则让push_back去做
push_back(x);
iterator tmp = finish;
--tmp;
return tmp;
}
else {
//否则调用insert_aux
return insert_aux(position, x);
}
}
指定位置插入n个x
template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>::insert(iterator pos,
size_type n, const value_type& x) {
/* 逻辑指定位置插入x类似
* 不过要借助uninitialized_fill函数进行插入
*/
if (pos.cur == start.cur) {
iterator new_start = reserve_elements_at_front(n);
uninitialized_fill(new_start, start, x);
start = new_start;
}
else if (pos.cur == finish.cur) {
iterator new_finish = reserve_elements_at_back(n);
uninitialized_fill(finish, new_finish, x);
finish = new_finish;
}
else
insert_aux(pos, n, x);
}
还有一种迭代器的范围插入其实和指定位置插入n个x这一类相似,只是插入元素时调用的函数不一样,但是逻辑都是一样的。
insert_aux
该函数供以上insert
调用,对应不同的insert
也有不同的版本,这里只分析最重要的一个,其他的都与之类似。
template <class T, class Alloc, size_t BufSize>
typename deque<T, Alloc, BufSize>::iterator
deque<T, Alloc, BufSize>::insert_aux(iterator pos, const value_type& x) {
//计算出插入点之前的元素个数
difference_type index = pos - start;
value_type x_copy = x;
/* 要在非头/尾位置插入元素,对于线性空间而言,势必涉及到移动元素
* 为了使得效率达到最优,需要尽量少移动元素
* 所以接下来我们比较插入点前的元素个数和插入点之后的元素个数
* 然后来选择移动哪一边的元素
*/
if (index < size() / 2) {
//插入点前的元素较少
//在首部插入一个与第一个元素相等的值
push_front(front());
iterator front1 = start;
++front1;
iterator front2 = front1;
++front2;
pos = start + index;
iterator pos1 = pos;
++pos1;
//移动元素
copy(front2, pos1, front1);
}
else {
//插入点后的元素较少
//在尾部插入一个与最后一个元素相等的值
push_back(back());
iterator back1 = finish;
--back1;
iterator back2 = back1;
--back2;
pos = start + index;
//移动元素
copy_backward(pos, back2, back1);
}
//赋值
*pos = x_copy;
return pos;
}
删除元素
erase
函数有两个不同的版本,一个是针对具体的位置删除元素,另一个是迭代器范围删除元素。
指定位置删除元素
iterator erase(iterator pos) {
/* 由于是线性空间
* 所以删除元素时也涉及到元素的移动
* 思想和insert_aux相同
* 选择较少的元素移动
*/
iterator next = pos;
++next;
difference_type index = pos - start;
if (index < (size() >> 1)) {
copy_backward(start, pos, next);
pop_front();
}
else {
copy(next, finish, pos);
pop_back();
}
return start + index;
}
迭代器范围删除
deque<T, Alloc, BufSize>::erase(iterator first, iterator last) {
//若是删除整个deque,则调用clear进行删除
if (first == start && last == finish) {
clear();
return finish;
}
else {
/* 这里同样也要判断移动哪一端的元素较少
* 来选择效率较高的方法
*/
difference_type n = last - first;
difference_type elems_before = first - start;
if (elems_before < (size() - n) / 2) {
copy_backward(start, first, last);
iterator new_start = start + n;
/* 移动完了之后,对空间进行释放 */
destroy(start, new_start);
for (map_pointer cur = start.node; cur < new_start.node; ++cur)
data_allocator::deallocate(*cur, buffer_size());
start = new_start;
}
else {
copy(last, finish, first);
iterator new_finish = finish - n;
destroy(new_finish, finish);
for (map_pointer cur = new_finish.node + 1; cur <= finish.node; ++cur)
data_allocator::deallocate(*cur, buffer_size());
finish = new_finish;
}
return start + elems_before;
}
}
删除整个deque,回到初始状态
负责该功能的是clear
函数。
template <class T, class Alloc, size_t BufSize>
void deque<T, Alloc, BufSize>::clear() {
/* 依次遍历所有结点
* 析构所有的元素
* 并释放线性空间
*/
for (map_pointer node = start.node + 1; node < finish.node; ++node) {
destroy(*node, *node + buffer_size());
data_allocator::deallocate(*node, buffer_size());
}
/* 若还存在两个缓冲区及以上
* 则对其元素进行析构
* 最后将第一个线性空间保留下来
*/
if (start.node != finish.node) {
destroy(start.cur, start.last);
destroy(finish.first, finish.cur);
data_allocator::deallocate(finish.first, buffer_size());
}
else
//将唯一的线性空间上的元素进行析构
destroy(start.cur, finish.cur);
//回归到deque的初始状态,至少有一个线性空间
finish = start;
}
小结
本小节介绍了大量的跟deque
容器紧密相关的一些操作。由于deque
容器本身的实现就比较复杂,所以它的操作也稍微复杂一些,其实最主要的就是需不需要跨结点的问题,以及释放空的结点。以及在增加/删除元素时,会选择移动元素次数最少的一端进行移动等等。
在下一小节中,我们将介绍stack
配接器。