深入解析:浅谈deque的底层机制
目录
③头部 / 尾部删除(pop_front/ pop_back)
在C++STL序列式容器中,deque(双端队列)是一个兼顾了vector随机访问和list双端高效操作的容器,支持首尾O(1)增删,也支持随机访问,其底层通过分段连续内存实现。
deque示意图:

deque和vector的最大差异,一是deque允许在常数时间内对两端元素进行插入或删除,二是deque没有所谓容量(capacity)观念,因为它是动态地以分段连续空间组合而成,随时可以增加一段新的空间并链接起来。换句话说,像vector那样“因为旧空间不足而重新配置一块更大空间,然后复制元素,再释放旧空间”这样的事情在deque是不会发生的。
一、deque底层结构:分段连续内存
deque 既不像 vector 那样是单一连续内存块,也不像 list 那样完全离散,而是由 多个固定大小的连续内存块(缓冲区)和一个中控数组(map,不是STL中的map容器)组成的 “分段连续” 结构。
1️⃣核心组成
GCC 标准库中 deque 的底层结构包含三个关键部分:
- 缓冲区(buffer):大小固定的、连续的内存块,存储实际元素
- 中控数组(map):指针数组,每个元素都是指针,分别指向不同缓冲区的起始地址
- 迭代器(iterator):封装了指向当前缓冲区的指针、当前元素指针、缓冲区首尾指针,以及中控数组指针,支持跨缓冲区移动
2️⃣迭代器、中控数组与缓冲区的关联图解

注意:
_M_cur可能会认为它是可以指向缓冲区已使用空间内的任一元素,其实描述的不太准确,容易产生范围上的歧义——会被误解为“整个缓冲区的物理空间中已经被使用的部分”,而忽略了deque的有效元素范围是多个缓冲区的整体,而不是单个缓冲区
假设,deque有两个缓冲区:
- 缓冲区A(物理范围0 ~ 2):存放了三个有效元素,填满了
- 缓冲区B(物理范围0 ~ 2):只存放了一个有效元素,在下标0位置
此时:
- 如果说 “
_M_cur指向缓冲区已使用空间的任意元素”,对缓冲区 A 来说没问题(指向 0-2),但对缓冲区 B 来说,容易让人误以为 “缓冲区 B 的已使用空间是 0,所以_M_cur只能指向缓冲区 B 的 0”—— 但deque的有效元素是 “缓冲区 A 的 0-2 + 缓冲区 B 的 0”,_M_cur还能指向缓冲区 A 的任意元素,而非局限于单个缓冲区的已用空间。
所以,更严谨的表达是:_M_cur指向deque整体有效元素序列中的任意一个元素(该元素同时属于某个缓冲区的物理范围)。
3️⃣deque迭代器的实现
template
struct _Deque_iterator {
T* _M_cur; // 指向当前元素
T* _M_first; // 指向当前缓冲区的起始
T* _M_last; // 指向当前缓冲区的末尾(尾后)
T** _M_node; // 指向中控数组中当前缓冲区的指针
// 重载 ++ 运算符:跨缓冲区处理
_Deque_iterator& operator++() {
++_M_cur;
if (_M_cur == _M_last) { // 当前缓冲区已到末尾
_M_set_node(_M_node + 1); // 切换到下一个缓冲区
_M_cur = _M_first; // 指向新缓冲区的起始
}
return *this;
}
// 重载 +n 运算符:随机访问
_Deque_iterator operator+(size_t n) const {
_Deque_iterator tmp = *this;
tmp._M_cur += n;
// 处理跨缓冲区的情况(省略具体逻辑)
return tmp;
}
// 切换缓冲区的辅助函数
void _M_set_node(T** new_node) {
_M_node = new_node;
_M_first = *new_node;
_M_last = _M_first + BufSiz;
}
};
- 迭代器通过
_M_node关联中控数组,通过_M_first/_M_last标记当前缓冲区的边界 - 跨缓冲区移动时,自动切换中控数组的指针并重置缓冲区边界,保证迭代器的连续性
二、deque核心机制:双端扩容与内存管理
1、双端扩容机制
deque 的中控数组支持两端扩容:
- 头部不足时,在中控数组头部分配新空间,新增缓冲区;
- 尾部不足时,在中控数组尾部分配新空间,新增缓冲区;
- 中控数组扩容时,只需分配新的指针数组(通常扩大一倍),将原指针拷贝到新数组中间位置,无需移动缓冲区的元素(因为缓冲区是独立的)。
示例:初始中控数组大小为 8,当头部新增缓冲区时,中控数组扩容到 16,原指针从索引 4 开始存放,头部留出索引 0-3 用于新增缓冲区。
2、元素存储的“伪连续”特性
deque 虽然对外表现为 “连续”(支持随机访问),但底层是分段连续的:
- 访问
deque[i]时,先通过i / 缓冲区大小计算中控数组的索引(node_idx),再通过i % 缓冲区大小计算缓冲区内的偏移(elem_idx),最终定位到元素:*(map[node_idx] + elem_idx)- 随机访问的时间复杂度仍为 O (1)(计算索引是常数操作),但比
vector多了缓冲区索引计算的开销
三、deque的核心操作与性能
1、双端的增删:O(1)的复杂度
deque的头部和尾部的增删操作都为O(1) (没有扩容的情况)
①头部插入(push_front)
- 先检查头部缓冲区是否还有空闲时间(如果头部缓冲区已满,则分配新缓冲区并添加到中控数组头部)
- 然后将元素构造到头部缓冲区的空闲位置
- 更新头部迭代器
②尾部插入(push_back)
类似头部插入,检查尾部缓冲区是否有空闲,无则分配新缓冲区,构造元素并更新尾部迭代器
③头部 / 尾部删除(pop_front/ pop_back)
直接移动头部 / 尾部迭代器,销毁元素(若缓冲区为空,释放缓冲区并调整中控数组),时间复杂度 O (1)
2、随机访问:O(1)
deque支持operator[]和at(),但是底层需要计算缓冲区索引和偏移,所以效率没有vector高
reference operator[](size_type n) {
return *(_M_start + n); // 调用迭代器的 +n 运算符
}
3、中间插入 / 删除:O(n)
与vector类似,中间插入 / 删除需要移动元素(若跨缓冲区,需移动多个缓冲区的元素),时间复杂度 O (n),且效率低于 list(list 中间操作 O (1))
四、deque与vector、list的差异
特性 | deque(双端队列) | vector(动态数组) | list(双向链表) |
内存布局 | 分段连续(中控数组 + 缓冲区) | 单一连续内存块 | 完全离散(节点 + 指针) |
随机访问 | 支持(O (1),但略慢) | 支持(O (1),最快) | 不支持(O (n)) |
头部增删 | O (1)(无扩容时) | O (n)(需移动元素) | O(1) |
尾部增删 | O (1)(无扩容时) | O (1)(无扩容时)/ O (n)(扩容时) | O(1) |
中间增删 | O (n)(需移动元素) | O (n)(需移动元素) | O (1)(仅调整指针) |
迭代器失效规则 | 插入时所有迭代器失效(中控数组扩容);删除时仅被删元素迭代器失效 | 扩容时所有失效;插入 / 删除中间时部分失效 | 仅被删元素迭代器失效 |
内存利用率 | 缓冲区可能有少量浪费;中控数组有指针开销 | 连续内存,可能有预留空间浪费 | 节点有指针开销(每个节点 2 个指针) |
缓存友好性 | 较好(分段连续) | 最好(完全连续) | 较差(完全离散) |
- 需双端高效增删+随机访问(如实现队列 / 栈的混合操作),选
deque - 需极致随机访问性能(如大量
[]操作),选vector - 需频繁中间增删(如链表结构场景),选
list
⚠️五、deque使用需注意的点
1、老生常谈的迭代器失效的情况
deque 迭代器失效规则比 vector 复杂:
- 插入元素时:
- 若中控数组未扩容,迭代器可能有效(但不保证)
- 若中控数组扩容,所有迭代器、指针、引用失效(中控数组地址改变)
- 删除元素时:
- 若删除的是头部 / 尾部元素,仅指向被删元素的迭代器失效
- 若删除的是中间元素,被删元素之后的迭代器失效(需重新获取)
deque d = {1,2,3,4};
auto it = d.begin() + 2; // 指向 3
d.push_front(0); // 若中控数组扩容,it 失效
d.erase(d.begin() + 1); // 删除 1,it 仍指向 3(有效)
2、避免频繁的中间操作
deque 中间插入 / 删除的效率低于 list(需移动元素),且高于 vector(分段移动,无需整体拷贝),但总体仍为 O (n),频繁操作会导致性能下降
六、总结
deque 以 “分段连续内存” 为核心,通过中控数组、缓冲区和复杂迭代器的配合,兼顾了 vector 的随机访问和 list 的双端高效操作,是 STL 中功能最均衡的序列式容器之一。其底层的 “中控数组 + 缓冲区 + 迭代器” 设计,决定了它在双端操作场景中的优势,同时也带来了迭代器实现复杂、随机访问效率略低的特点。
浙公网安备 33010602011771号