标准模板库deque
在C++ STL的序列容器家族中,vector凭借极致的随机访问性能成为了绝大多数场景的首选,但它有一个致命短板:头部插入与删除操作的时间复杂度为O(n),在需要频繁双端操作的场景下效率极低。而deque(double-ended queue,双端队列)正是为解决这个问题而生的容器,它既保留了vector的随机访问能力,又实现了头尾两端O(1)时间复杂度的增删操作,是STL中平衡性能与灵活性的经典设计,也是很多开发者容易忽略却实用性极强的核心组件。
deque属于STL序列容器,采用分段连续存储的设计思路,核心设计目标十分明确,既要兼顾随机访问的便利性,又要解决vector头部操作低效的问题,同时优化扩容性能。它与vector最本质的区别在于内存布局:vector要求所有元素存储在一整块连续的内存空间中,而deque的元素分散存储在多块固定大小的连续缓冲区里,再通过一个小型主控结构维护这些缓冲区的逻辑顺序,这也是它能实现双端高效操作的核心原因。
依托这样的设计,deque完美兼顾了三大核心优势:保留vector级别的O(1)随机访问能力,实现容器头部与尾部的O(1)插入与删除操作,同时彻底规避vector扩容时必须拷贝全部元素的高昂代价,在双端操作频繁的场景中,性能优势远超vector。
存储架构
deque采用双层存储架构,整体结构清晰且兼顾效率,分为实际存储数据的缓冲区,与负责统筹管理的主控Map两部分。缓冲区是多块固定大小的连续内存块,每一块缓冲区可存放若干个元素,是deque存储数据的主体;主控Map则是一块小型连续内存,内部每个元素都是指针,分别指向对应的缓冲区,核心作用是维护所有缓冲区的逻辑顺序,对外把分段的内存伪装成一整块连续内存,让使用者可以像操作vector一样正常访问元素。
这种分段式设计带来了显著优势:头部或尾部插入元素时,无需移动任何已有元素,只需分配新的缓冲区并挂接到Map两端即可;扩容时仅需分配新缓冲区,或是对Map进行扩容(仅拷贝指针,不拷贝实际元素),完全避开了vector扩容时全量拷贝数据的性能损耗;更关键的是,元素所在的缓冲区一旦分配就不会被移动,因此元素的指针和引用永远不会因插入操作失效,这也是deque与vector的核心差异之一。
简化版类结构定义
不同编译器的STL实现细节略有差异,但deque的核心结构逻辑一致,简化后的核心代码如下,贴合标准实现逻辑,能清晰体现其底层组成:
1 | template <class _Ty> |
迭代器底层实现
deque的迭代器是它能实现 “整体连续” 假象的核心,也是它比vector迭代器复杂的原因。每个迭代器包含 4 个核心指针:
1 | class const_iterator { |

这四个指针的作用:_First/_Last:标记当前迭代器所在缓冲区的边界,用于判断是否需要跳转到前 / 后一个缓冲区;_Next:指向当前访问的元素,解引用迭代器时直接返回*_Next;_Map:指向主控 Map 中当前缓冲区的指针,用于缓冲区跳转和随机访问计算。
正是这个迭代器结构,让deque实现了随机访问:当你访问dq[i]时,迭代器会先计算i落在哪个缓冲区,再计算在缓冲区中的偏移量,最终定位到元素,整个过程仅需两次指针跳转,时间复杂度为 O (1)。
缓冲区与Map的扩容规则
缓冲区大小
不同编译器的 STL 实现对缓冲区大小有不同的定义,以 GCC 的 libstdc++ 为例:
- 当元素类型
sizeof(T)小于 512 字节时,单个缓冲区的大小为512 / sizeof(T)个元素; - 当元素类型
sizeof(T)大于等于 512 字节时,单个缓冲区仅存放 1 个元素。
这种设计平衡了内存利用率和操作效率:小元素用大缓冲区减少 Map 的开销,大元素用单元素缓冲区避免内存浪费。
Map 的扩容
当主控 Map 的指针槽位用尽时,deque会对 Map 进行扩容:
- 分配一块更大的连续内存(通常是原大小的 2 倍);
- 将原 Map 中的缓冲区指针拷贝到新 Map 的中间位置(为头部和尾部预留相同的增长空间);
- 释放原 Map 的内存,更新
_Map和_Mapsize。
Map 扩容仅需拷贝指针数组,无需移动任何元素,代价极低,这也是deque比vector扩容效率高的核心原因。
存储模型的状态变化
deque的内存状态会随着元素操作发生规律性变化,空对象状态下,Map指针为空,Mapsize与元素总数均为0,首尾迭代器的所有指针也全部为空,未分配任何内存。向尾部插入元素时,先在当前尾部缓冲区写入数据,若缓冲区已满,则分配新缓冲区并将地址挂接到Map尾部,同步更新尾部迭代器;向头部插入元素时,若当前头部缓冲区有空闲位置,则从后往前写入数据,若已满则分配新缓冲区,挂接到Map头部并更新头部迭代器,全程不触动已有元素。
- 空对象状态:
_Map为nullptr,_Mapsize和_Size均为 0,首尾迭代器的所有指针均为nullptr; - 尾部插入元素:先在当前尾部缓冲区写入元素,若缓冲区已满,则分配新缓冲区,将其地址挂到 Map 的尾部,更新
_Last迭代器; - 头部插入元素:若当前头部缓冲区有空闲位置,则从后往前写入;若已满,则分配新缓冲区,挂到 Map 的头部,更新
_First迭代器。
deque的使用
构造函数
deque提供了多种构造方式,适配不同的初始化场景,从空容器初始化、指定数量元素初始化,到拷贝、移动初始化一应俱全,用法和vector完全一致,上手无门槛。包含默认构造的空deque、指定数量默认值或指定值的填充构造、基于数组或其他容器迭代器的范围构造、C++11支持的初始化列表构造、拷贝构造,以及移动构造。其中移动构造是C++11的重要特性,可实现零拷贝转移容器所有权,原容器会变为空,大幅提升大数据量转移的性能。
1 |
|
元素访问
deque支持四种常用的元素访问方式,和vector完全兼容,不同方式在安全性与效率上各有侧重,可根据场景选择。
下标访问operator[]不做边界检查,效率最高,但越界访问会触发未定义行为,适合确定索引有效的场景;at()方法自带边界检查,越界时会抛出std::out_of_range异常,安全性更高,适合索引不确定的场景,只是有轻微性能损耗;迭代器访问兼容所有STL算法,通用性强;范围for循环是C++11的语法糖,写法简洁,配合const引用还能避免不必要的拷贝,是日常遍历的首选。除此之外,deque也支持front()和back()方法快速访问首尾元素,使用前需确保容器非空,避免访问空容器导致程序异常。
1 | int main() { |
访问方式选择建议:
- 确定索引有效时,用
operator[]追求极致效率; - 索引不确定、需要安全检查时,用
at()避免未定义行为; - 遍历容器时,优先用范围 for 循环,简洁且不易出错;
- 配合 STL 算法时,用迭代器访问。
增删操作
deque最核心的亮点就是头尾两端的O(1)增删效率,这是vector完全无法比拟的,尾部增删接口与vector完全一致,新增的头部增删接口同样高效,均为常数时间复杂度。日常开发中优先使用emplace_front和emplace_back,这两个C++11特性的接口可直接在容器内存中原地构造元素,避免临时对象的拷贝与析构,效率远高于push_front和push_back。
需要注意的是,deque的中间插入与删除操作效率较低,和vector一样需要移动后续元素,时间复杂度为O(n),实际开发中应尽量避免中间操作,充分发挥其双端操作的优势。下面补充完整的双端增删+中间操作实操代码,覆盖所有常用场景。
1 | void PrintDeque(const deque<int>& dq, const string& desc) { |
emplace_front/emplace_back是 C++11 引入的特性,直接在容器的内存中构造元素,避免了临时对象的拷贝和析构,效率远高于push_front/push_back,日常开发中优先使用。
容量与内存管理
由于deque采用分段存储设计,无需像vector那样预分配整块连续内存,因此没有reserve()和capacity()方法,容量管理接口更为精简。size()用于获取当前元素总数,empty()判断容器是否为空,效率比size()==0更高,max_size()返回容器理论可容纳的最大元素数量。resize()可调整元素数量,元素不足时补充默认值,超出时删除末尾元素;shrink_to_fit()是C++11新增接口,可释放未使用的缓冲区,减少内存占用;swap()方法可交换两个deque的内容,仅交换内部指针,时间复杂度为O(1),效率极高。
1 | int main() { |
迭代器失效
| 操作类型 | 迭代器失效情况 | 指针 / 引用失效情况 |
|---|---|---|
| 头部 / 尾部插入 | 所有迭代器失效 | 不会失效(元素所在缓冲区不会被移动) |
| 中间插入 / 删除 | 所有迭代器失效 | 被删除元素的指针 / 引用失效,其他有效 |
| 头部 / 尾部删除(pop_front/pop_back) | 仅被删除元素的迭代器失效,其他迭代器有效 | 仅被删除元素的指针 / 引用失效 |
| clear/resize | 所有迭代器失效 | 被删除元素的指针 / 引用失效 |
头部或尾部插入元素时,所有迭代器都会失效,但元素的指针和引用不会失效;中间插入或删除操作会导致所有迭代器失效,仅被删除元素的指针和引用失效;头部或尾部删除操作,仅被删除元素的迭代器、指针和引用失效,其余迭代器保持有效;clear或resize操作会让所有迭代器失效,被删除元素的指针和引用也会失效。先看经典的迭代器失效错误示例,直接复用旧迭代器会触发未定义行为,再给出修复后的正确写法,从实操层面规避问题。
1 | int main() { |
push_front操作可能分配了新的缓冲区,甚至触发了主控 Map 的扩容,导致迭代器中的_Map指针和缓冲区边界指针_First/_Last都指向了旧的内存结构,迭代器已经无法正确计算元素位置,因此完全失效。
但要注意:元素的指针和引用仍然有效,比如你在插入前保存了&dq[0],插入后这个指针仍然能正确访问原来的元素,因为元素所在的缓冲区没有被移动,这是和vector最核心的区别。
插入操作后,务必重新获取迭代器:任何插入操作后,不要使用之前保存的迭代器,必须重新调用begin()/end()获取新的迭代器;
循环中删除元素,使用 erase 的返回值:erase会返回指向被删除元素下一个位置的有效迭代器,正确写法如下:
1 | // 正确的循环删除写法 |
优先使用范围 for 循环:范围 for 循环会在编译期确定迭代范围,避免手动管理迭代器的失误;
需要长期保存元素地址时,用指针而非迭代器:deque的元素指针不会因插入失效,比迭代器更稳定。
与vector对比
deque和vector是STL中最常用的两个序列容器,接口高度兼容,但底层设计差异决定了适用场景完全不同,理清两者的核心差异,才能在开发中做出最优选择。下面用清晰的对比维度梳理差异,搭配核心结论,方便快速记忆。
| 特性 | vector | deque |
|---|---|---|
| 内存布局 | 整块连续内存 | 多块分段连续的缓冲区,通过主控 Map 维护 |
| 随机访问 | O (1),极快(一次指针跳转) | O (1),稍慢(两次指针跳转) |
| 尾部增删 | O (1)(均摊,扩容时 O (n)) | O (1),无扩容开销 |
| 头部增删 | O (n),需移动所有元素 | O (1),直接分配新缓冲区 |
| 中间增删 | O (n),需移动元素 | O (n),需移动元素,效率略低于 vector |
| 扩容代价 | 极高,需拷贝所有元素,旧内存完全失效 | 极低,仅分配新缓冲区 / 拷贝 Map 指针,旧元素地址不变 |
| 迭代器失效 | 扩容后所有迭代器 / 指针 / 引用全部失效 | 插入后迭代器失效,但指针 / 引用始终有效 |
| 内存利用率 | 高,无额外开销 | 中等,有主控 Map 和缓冲区空闲空间的开销 |
| 缓存友好性 | 极好,整块连续内存,CPU 缓存命中率高 | 一般,分段内存会降低缓存命中率 |
| C 接口兼容 | 支持,可通过 data () 传递给 C 函数 | 不支持,内存不连续,无 data () 方法 |
扩容机制的本质区别:vector的连续内存设计,决定了它扩容时必须申请更大的整块内存,然后把所有元素拷贝过去,再释放旧内存,这个过程的时间复杂度是 O (n),元素越多代价越高。而deque的分段设计,扩容时只需申请新的缓冲区,不需要移动任何已有元素,即使主控 Map 扩容,也只是拷贝指针数组,代价可以忽略不计。因此在需要频繁插入未知数量元素的场景下,deque的效率远高于vector。
随机访问的性能差异:vector的随机访问只需一次指针偏移,而deque需要先计算元素所在的缓冲区,再计算缓冲区中的偏移,多了一次指针跳转,因此随机访问性能比vector低 20%-30%。在需要频繁随机访问的场景下,vector仍然是首选。
缓存友好性的差异:CPU 的高速缓存对连续内存的访问有极强的优化,vector的整块连续内存可以充分利用 CPU 缓存,遍历效率极高。而deque的分段内存会导致缓存行频繁失效,遍历性能比vector低。
容器选择
若需要频繁随机访问、仅尾部增删、兼容C接口、追求极致缓存性能时,优先选vector;需要频繁头尾双端操作、元素数量未知需频繁扩容、需要稳定的元素指针或引用时,优先选deque,同时deque也是STL中queue和stack的默认底层容器,适配性极佳。
deque和vector是STL中最常用的两个序列容器,接口高度兼容,但底层设计差异决定了适用场景完全不同,理清两者的核心差异,才能在开发中做出最优选择。
内存布局上,vector是整块连续内存,deque是分段连续缓冲区+主控Map;随机访问方面,vector一次指针跳转即可完成,速度极快,deque需要两次指针跳转,速度略慢;尾部增删两者均为O(1),但vector扩容时会有O(n)损耗,deque无额外损耗;头部增删vector是O(n),需移动全部元素,deque是O(1),直接分配缓冲区;扩容代价vector极高,需全量拷贝元素,deque极低,仅拷贝指针;迭代器与指针失效方面,vector扩容后全部失效,deque仅迭代器易失效,指针和引用稳定;另外vector支持通过data()兼容C接口,deque因内存不连续无法兼容,且vector的缓存友好性更高,遍历速度更快。
容器选择遵循黄金法则即可:需要频繁随机访问、仅尾部增删、兼容C接口、追求极致缓存性能时,优先选vector;需要频繁头尾双端操作、元素数量未知需频繁扩容、需要稳定的元素指针或引用时,优先选deque,同时deque也是STL中queue和stack的默认底层容器,适配性极佳。
应用场景
依托双端高效操作的特性,deque在工程开发中应用场景广泛,覆盖业务开发、数据流处理、算法实现等多个领域。在排队系统与任务调度场景中,deque可实现队尾新增任务、队头处理任务的逻辑,两端操作均为O(1),适用于银行叫号、线程池任务队列、消息缓冲等场景;数据流处理中,可作为固定大小缓冲区,队尾新增数据、队头移除已处理数据,保证音视频流、网络数据流的实时处理;在文本编辑器、设计软件的撤销重做功能中,用deque存储操作历史,双端操作适配撤销与重做逻辑;浏览器历史记录、游戏战绩等固定长度记录管理,也可通过deque快速删除旧数据、新增新数据。
算法领域中,deque是实现单调队列的核心容器,可高效解决滑动窗口最值问题,同时也是广度优先搜索(BFS)的首选队列实现,效率远高于list。
顺序容器互转
实际开发中常需要结合deque与vector的优势,先通过deque完成双端高效插入,再转换为vector做高频随机访问,两者互转十分便捷,且支持移动语义优化,减少性能损耗。下面补充完整的容器互转可运行代码,覆盖常用转换场景。
1 |
|
使用deque的核心最佳实践,是优先使用emplace系列接口,尽量只在头尾操作,避免中间增删;利用其作为queue和stack的默认底层容器,无需手动替换;遍历优先用范围for循环,需要长期保存地址用指针。同时要避开常见陷阱:不要假设deque内存连续,不尝试用其兼容C接口;不忽略迭代器失效规则;不盲目用deque替代vector做高频随机访问;避免存储极小元素且高频遍历,减少缓存损耗与Map开销。
使用deque的核心最佳实践,是优先使用emplace系列接口,尽量只在头尾操作,避免中间增删;利用其作为queue和stack的默认底层容器,无需手动替换;遍历优先用范围for循环,需要长期保存地址用指针。同时要避开常见陷阱:不要假设deque内存连续,不尝试用其兼容C接口;不忽略迭代器失效规则;不盲目用deque替代vector做高频随机访问;避免存储极小元素且高频遍历,减少缓存损耗与Map开销。
deque 的主要应用场景
std::deque(双端队列)在 C++ 编程中有许多应用场景,尤其是当需要在序列的两端进行高效的插入和删除操作时。以下是 std::deque 的一些典型应用场景:
- 排队系统:std::deque 非常适合实现排队系统,如电影院售票或银行排队系统。可以使用 push_back () 在队列的末尾添加新客户,使用 pop_front () 从队列的头部移除已服务的客户。由于 std::deque 在两端操作具有高效的性能,这种实现方式比使用 std::vector 或 std::list 更加高效。
- 缓冲区处理:在处理数据流或需要维护一个固定大小的缓冲区时,std::deque 也非常有用。可以使用 push_back () 添加新数据,并使用 pop_front () 移除旧数据,以保持缓冲区的大小恒定。
- 撤销与重做功能:在实现如文本编辑器或图形设计工具的撤销与重做功能时,std::deque 可以存储用户的操作历史。使用 push_back () 添加新操作,使用 pop_front () 撤销最近的操作。由于 std::deque 在头部和尾部的操作都很高效,这可以提供快速且流畅的撤销与重做体验。
- 历史记录管理:在需要维护一个操作历史记录的系统中,如网页浏览器或游戏应用,std::deque 可以用于存储最近的访问历史或得分记录。
- 任务调度:在任务调度系统中,std::deque 可以用于存储待处理的任务。新的任务可以添加到队列的末尾,而处理完成的任务可以从队列的头部移除。
现代C++中的deque
随着C++标准的演进,deque也在不断获得新特性,让它更易用、更高效、更安全:
- C++11:新增
emplace_front/emplace_back/emplace原地构造接口,支持移动构造和移动赋值,支持初始化列表构造,新增cbegin()/cend()常量迭代器; - C++11:新增
shrink_to_fit()方法,支持手动释放未使用的内存; - C++17:为
swap()方法添加了noexcept保证,提升了异常安全和性能; - C++20:新增
std::erase()/std::erase_if()全局函数,简化了元素删除操作,无需再写复杂的迭代器循环:1
2
3std::deque<int> dq = {1, 2, 3, 4, 5, 2, 2};
std::erase(dq, 2); // 删除所有值为2的元素
std::erase_if(dq, [](int x) { return x % 2 == 0; }); // 删除所有偶数 - C++23:新增
assign_range/append_range/prepend_range方法,支持批量范围插入,进一步简化了双端批量操作。
deque是STL中被很多开发者低估的优质容器,它通过分段存储+主控Map的精巧设计,完美补全了vector的头部操作低效、扩容损耗大的短板,在保留随机访问能力的同时,实现了双端O(1)增删的核心优势,接口与vector高度兼容,学习成本极低。

