【C++】deque的使用和源码解析

前言

这周开始实习了,白天的时间都要花在实习上,所以自己学习的时间就变少了很多。目前我还没有想到很好的平衡实习与自己学习的办法,原本计划的在四月份之前看完STL的主要容器和容器适配器的任务也不知道能不能完成,3月29号是周五,还有一个周末,目前今晚不管睡多晚,我都一定把deque看完。看明天后天两天能不能把stack/queue给看了,这个周末就隔离一切干扰,把AVLTree和RBTree给搞懂,可以的话自己写一写map和set,四月份再制定新计划了~

——2024年3月27日

deque的使用

swap();//std::swap的特化版本
size();//返回容器的大小
empty();//检查容器是否为空
insert(iterator pos, const T& x);//在pos处插入x
erase(iterator pos);//删除pos处的元素

更多使用参考:Standard library header <deque> - cppreference.com

源码解析

迭代器解析

我们先来看deque的迭代器设计,与vector的设计不同,deque因为要提供两端进出的能力,所以不像vector那样采用原生的raw指针实现迭代,deque的迭代器要复制一些。

vector因为其只在容器末尾添加元素的特性,所以原生的raw指针的性能会比较高。而deque则不行,STL里deque的设计是一种逻辑上的连续,而不是像vector那样物理意义上的连续。deque的内存管理是通过分段空间模拟“连续”空间来实现的。所以在上图中我们可以看到,SGI中deque定义了三个T*类型的指针cur/first/last以及一个指向指针的指针T** node;

(上图中第一个圈中是的函数是一种Kludge的,用以处理一些编译器可能出现的常量表达式处理报错的方案,我觉得很有趣,就圈出来了)


迭代器的初始化,可以看到first和last都是用node来初始化的,所以deque的迭代器实现种map_pointer这个指向指针的指针是控制的中心。


我们来看一下deque这种通过指针和双重指针的非连续容器的设计思路。可以看到,当对迭代器执行++/--操作时,我们需要判断其所在的node是否到达了末尾/开头,如果达到了,就要切换缓冲区,即进行node+1(node - 1)操作,然后再重新设置cur节点。对于多个node来说,每个node都有其first和last,而我们的迭代器相当于操纵cur这个T*类型的指针在这些node之间“跳跃”。就像一个一个连起来的空岛,构成了deque容器。通过迭代器的这种设计,在用户看来deque是一段连续的空间。


我们来看一下这种多个缓冲区(后文buffer)相连的怎么实现随机读取,其中最基础的函数即上面这个。我们通过传参获取迭代器跳跃“距离”,再将该距离与(cur - first)相加即可得到目前移动的相对距离。如果相对距离小于一个buffer的大小,直接操作cur指针移动即可。若超出了buffer的大小,就要根据正负进行判断它具体能跳跃几个“buffer”(可以跳过中间一整个完整的buffer,这里有点像取模操作,即一个很大的数对一个较小的数取模,也差不多是跳跃多个“小数的值”最后得到模值)。

后面的+  -  +=  -=  [] 这些操作符都是调用的上面这个函数,仅仅是调用方式有些许差异。所以deque的随机访问就是基于上面这个操作符重载实现的。


主类解析

主类的基本类型大多与其他相同,除了这个 difference_type类型,该类型是deque的距离移动的表示。下面是迭代器的定义,即上文所讲内容。


deque在具体实现的时候,start和finish是逻辑上的头和尾,具体到每一个buffer上又另有first和last,从而实现了从两端加减元素的功能。

在随机访问的时候,deque调用的是迭代器的 operator [],而迭代器的 operator[] 调用是 self& operator+=(difference_type n)。同理,deque的 ++/-- 也是调用的迭代器的 ++ / --。

构造函数的重载比较多,但总体来说是调用的create_map_and_nodes()这个函数,所以我们搞懂create_map_and_nodes()即可。


在deque 的构造中,我们要根据在deque内部设置的默认buffer_size()大小来决定配置几个node , 即用元素的数量/ 每个节点的元素数量 得到节点数量,再将map_pointer指向map的中间区域,而不是最边上得两个节点。随后通过for ()循环为每个node 单独分配内存,最后设置整个deque的start和finish指针,即第一个nstart和nfinish。

析构函数通过map_pointer对每一个node进行挥手空间,并释放map自身即可。


swap

我没有想到swap居然是直接调用std::swap对两个元素的首尾迭代器各swap一下就完了(😂)


push

push又分两种情况讨论,即还有备用空间,那直接调用construct构建并移动cur指针即可。如果没有备用空间或者只剩下一个元素的备用空间,那么就调用push_back_aux(t);

我们先来看第一个红框:

push_back_aux是通过重新分配一个新的map_pointer空间来实现的,而不是我们认为的在原有空间上添加node来实现的,这是因为在原有空间上添加node可能导致map_pointer失效,进而导致迭代器失效。这是因为在构造函数的时候,就已经定义好了map_pointer的范围,所以当范围扩大时,就需要重新为map_pointer分配,这样做的好处是,我们不需要重新为pointer分配内存,于是deque就不会出现迭代器失效的情况(两端插入删除时)。而vector由于是使用的原生raw指针而不是像deque这种一个类里面封装了两重指针,所以会有较多的迭代器失效的场景。

第二个红框:在重新分配map_pointer后,我们就可以直接添加node分配一个新的buffer了,然后我们再construct元素,设置finish/start迭代即可。


在插入元素时,deque会像vector那样移动元素。不同于vector那种需要移动pos后面的所有元素(最坏情况下每次插入到0处,所有元素都需要移动,时间复杂度O(n)),deque由于其内部基于多个buffer相连的实现,所以最坏情况下是O(n/2),好吧其实也是O(n),不太适合中间插入,但至少比vector的最坏情况快一倍。其中copy操作大概就是遍历移动,也是调用的std的全局函数。


结束语

deque实现起来有点复杂,我估计今晚是完不成了,而且后面也不一定会去实现。我觉得deque就是属于那种理解起来不复杂,但是实现起来细节特别多的类型,很容易让人掉头发。所以看情况吧,我后面可能看看stack/queue,然后直接基于stl::deque去模拟stack和queue了~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值