在 STL 的三大容器适配器(stack / queue / priority_queue)中,queue 是「先进先出(FIFO,First In First Out)」语义的标准实现。它与 stack 共享几乎完全一致的设计哲学:自身不实现存储逻辑,完全委托底层顺序容器,仅通过接口裁剪与封装,强制约束队列的访问语义。
本文从源码视角拆解 std::queue 的底层结构、接口实现与设计考量,并附带完整的手写简易版实现,还原一个极简却严谨的工业级适配器实现。
一、标准定义与整体结构
1. 头文件与模板声明
std::queue 定义于头文件 <queue>,位于 std 命名空间,其标准模板签名如下:
cpp
template <class T, class Container = std::deque<T>>
class queue;
T:队列存储的元素类型Container:底层存储容器,默认使用std::deque<T>,可手动指定为满足接口要求的顺序容器
与 stack 完全一致,queue 是典型的容器适配器(Container Adapter):它不持有底层存储,不实现内存管理与数据操作,所有功能全部转发给底层容器对象,自身仅负责对外暴露符合队列语义的接口。
2. 底层容器的准入条件
能够作为 queue 底层的容器,必须同时支持头部删除 与尾部插入,并提供基础容量查询接口:
push_back():尾部插入元素(入队)pop_front():头部删除元素(出队)front():访问队首元素back():访问队尾元素empty()/size():判空与获取元素个数
STL 原生满足全部要求的容器有两个:
std::deque:默认选项,双端操作均为稳定 O(1)std::list:双向链表,双端操作 O(1),但缓存局部性差
注意:
std::vector无法作为queue的底层容器,因为它只提供 O(n) 的头部删除,不满足队列的性能要求。
3. 核心成员变量
在 libstdc++、MSVC STL 等主流实现中,queue 的内部结构极度精简,仅包含一个受保护的底层容器成员:
cpp
template <class T, class Container>
class queue {
protected:
Container c; // 底层容器对象,所有队列操作均委托给 c
public:
// 全部对外接口均通过调用 c 的对应方法实现
};
成员变量命名为 c(container 的缩写)是 STL 适配器的通用惯例,protected 权限保证派生类可以直接访问底层容器,同时对用户封闭内部实现。
二、为什么默认底层容器是 std::deque?
这是 queue 最经典的面试题之一。队列要求首尾两端都能高效操作,候选者天然落在 deque 与 list 之间,最终选择 deque 是性能、内存与稳定性综合权衡的结果。
1. 对比 std::list:更高的缓存效率与更低的内存开销
list每个节点都需要额外存储前后指针,单元素内存开销大;且节点地址离散,遍历时缓存命中率极低。deque采用分段连续存储,数据块内部内存连续,缓存局部性远优于链表;元信息开销仅集中在中控数组,单位元素的额外内存消耗远低于list。- 队列仅操作首尾,不需要
list的中间插入能力,使用链表属于无意义的性能浪费。
2. 对比 std::vector:头部操作的性能鸿沟
vector头部删除需要移动全部后续元素,时间复杂度 O(n),完全无法满足队列的高效出队要求。deque的头部与尾部插入/删除均为稳定 O(1),完美匹配队列的双端操作特性。
3. 综合结论
deque 在双端操作性能、内存利用率、缓存友好性上达到了最佳平衡,是通用场景下队列底层容器的最优解,因此被 STL 设为默认值。
三、核心接口的源码级实现
queue 的所有接口均为对底层容器的直接转发,无额外复杂逻辑。下面给出与标准实现语义等价的核心代码。
1. 构造与析构
cpp
// 默认构造:底层容器默认初始化
queue() : c() {}
// 拷贝构造:整体拷贝底层容器
queue(const queue& other) : c(other.c) {}
// 从已有容器构造,explicit 禁止隐式转换
explicit queue(const Container& cont) : c(cont) {}
// 析构:自动调用底层容器析构,内存管理完全由底层负责
~queue() = default;
explicit 修饰单参数构造函数是 STL 的通用规范,避免意外的隐式类型转换,保证语义严谨。
2. 元素访问
队列仅允许访问队首与队尾元素,不支持随机访问与中间元素访问。
cpp
// 访问队首元素(出队端)
T& front() {
return c.front();
}
const T& front() const {
return c.front();
}
// 访问队尾元素(入队端)
T& back() {
return c.back();
}
const T& back() const {
return c.back();
}
同时提供普通引用与 const 引用两个重载,保证 const 正确性:只读场景下 const queue 对象仍可安全访问元素。
3. 入队与出队
cpp
// 入队:尾部插入元素
void push(const T& value) {
c.push_back(value);
}
// 出队:头部删除元素,无返回值
void pop() {
c.pop_front();
}
经典设计问题:pop() 为什么不返回元素?
与 stack::pop() 完全一致的异常安全设计 :
若 pop() 返回被删除的元素,需要将元素拷贝到调用方栈上;若拷贝构造过程抛出异常,元素已经从队列中移除,将永久丢失。
拆分为 front() 读取引用 + pop() 执行删除后,即使读取时拷贝失败,元素仍保留在队列中,不会造成数据丢失。
4. 容量查询
cpp
bool empty() const {
return c.empty();
}
size_t size() const {
return c.size();
}
全部为 O(1) 操作,时间复杂度取决于底层容器对应接口;在默认 deque 下均为常数时间。
5. 比较运算符
队列的比较直接委托底层容器按字典序逐元素比较:
cpp
template <class T, class Container>
bool operator==(const queue<T, Container>& lhs, const queue<T, Container>& rhs) {
return lhs.c == rhs.c;
}
template <class T, class Container>
bool operator<(const queue<T, Container>& lhs, const queue<T, Container>& rhs) {
return lhs.c < rhs.c;
}
其余 !=、<=、>、>= 均可通过上述两个运算符推导实现。
四、手写简易版 queue 完整实现
理解了标准库的设计思想后,我们可以亲手实现一个语义一致的简化版 MyQueue,完整复现容器适配器的核心设计。
1. 完整代码实现
cpp
#pragma once
#include <deque>
#include <cassert>
#include <utility>
template <typename T, typename Container = std::deque<T>>
class MyQueue {
public:
// ========== STL 规范类型别名 ==========
using value_type = T;
using container_type = Container;
using size_type = size_t;
using reference = T&;
using const_reference = const T&;
// ========== 构造与析构 ==========
// 默认构造:底层容器默认初始化
MyQueue() = default;
// 从已有底层容器构造,explicit 禁止隐式转换
explicit MyQueue(const Container& cont) : c(cont) {}
explicit MyQueue(Container&& cont) : c(std::move(cont)) {}
// 析构:自动调用底层容器析构
~MyQueue() = default;
// ========== 核心队列操作 ==========
// 入队:尾部插入左值
void push(const T& value) {
c.push_back(value);
}
// 入队:尾部插入右值,支持移动语义
void push(T&& value) {
c.push_back(std::move(value));
}
// 原地构造:直接在队尾构造元素,零临时对象拷贝
template <typename... Args>
reference emplace(Args&&... args) {
return c.emplace_back(std::forward<Args>(args)...);
}
// 出队:删除队首元素,无返回值(异常安全设计)
void pop() {
assert(!empty() && "pop called on empty queue");
c.pop_front();
}
// ========== 元素访问 ==========
// 访问队首
reference front() {
assert(!empty() && "front called on empty queue");
return c.front();
}
const_reference front() const {
assert(!empty() && "front called on empty queue");
return c.front();
}
// 访问队尾
reference back() {
assert(!empty() && "back called on empty queue");
return c.back();
}
const_reference back() const {
assert(!empty() && "back called on empty queue");
return c.back();
}
// ========== 容量接口 ==========
bool empty() const { return c.empty(); }
size_type size() const { return c.size(); }
// ========== 交换操作 ==========
void swap(MyQueue& other) noexcept {
using std::swap;
swap(c, other.c);
}
// 友元声明:比较运算符可访问底层容器
template <typename U, typename C>
friend bool operator==(const MyQueue<U, C>& lhs, const MyQueue<U, C>& rhs);
template <typename U, typename C>
friend bool operator<(const MyQueue<U, C>& lhs, const MyQueue<U, C>& rhs);
protected:
Container c; // 底层容器,所有操作全部委托
};
// ========== 全局比较运算符 ==========
template <typename T, typename Container>
bool operator==(const MyQueue<T, Container>& lhs, const MyQueue<T, Container>& rhs) {
return lhs.c == rhs.c;
}
template <typename T, typename Container>
bool operator!=(const MyQueue<T, Container>& lhs, const MyQueue<T, Container>& rhs) {
return !(lhs == rhs);
}
template <typename T, typename Container>
bool operator<(const MyQueue<T, Container>& lhs, const MyQueue<T, Container>& rhs) {
return lhs.c < rhs.c;
}
2. 测试用例
cpp
#include <iostream>
#include <list>
#include <string>
#include "MyQueue.h"
using namespace std;
class Person {
public:
Person(string name, int age) : name_(move(name)), age_(age) {}
void show() const {
cout << name_ << ", " << age_ << "岁" << endl;
}
private:
string name_;
int age_;
};
int main() {
// 1. 基础队列操作
cout << "===== 基础队列操作 =====" << endl;
MyQueue<int> q;
q.push(10);
q.push(20);
q.push(30);
cout << "队列大小:" << q.size() << endl;
cout << "队首:" << q.front() << endl; // 10
cout << "队尾:" << q.back() << endl; // 30
q.pop();
cout << "pop 后队首:" << q.front() << endl; // 20
cout << "是否为空:" << boolalpha << q.empty() << endl << endl;
// 2. 自定义底层容器 list
cout << "===== list 底层队列 =====" << endl;
MyQueue<int, list<int>> list_q;
list_q.push(100);
list_q.push(200);
cout << "list 队列队首:" << list_q.front() << endl << endl;
// 3. emplace 原地构造
cout << "===== emplace 原地构造 =====" << endl;
MyQueue<Person> person_q;
person_q.emplace("张三", 25);
person_q.front().show();
return 0;
}
3. 实现要点说明
- 纯适配器设计 :整个类只有一个成员变量
Container c,所有逻辑全部转发,完美复现了 STL 适配器的核心思想。 - 现代 C++ 支持 :补充了右值引用版本的
push和emplace原地构造,支持移动语义,减少不必要的拷贝。 - 边界断言保护 :空队列调用
pop/front/back时触发断言,便于调试阶段快速定位问题。 - const 正确性 :所有只读接口均提供
const重载,符合 C++ 通用编码规范。
五、核心设计思想与语义约束
1. 接口极度裁剪,强制 FIFO 语义
queue 刻意移除了底层容器的绝大多数能力:
- 不支持随机访问:无
operator[]、无at() - 不支持中间插入与删除
- 不提供迭代器:无
begin()/end(),无法遍历队列 - 仅能访问队首与队尾,仅能从尾部入队、头部出队
这种「做减法」的设计并非功能缺失,而是通过接口约束强制使用者遵循先进先出的语义,从语法层面杜绝误用。
2. 迭代器失效的屏蔽
queue 不对外暴露任何迭代器,因此用户层面不存在迭代器失效问题。底层容器自身的迭代器失效规则仍然存在,但被适配器完全封装屏蔽,使用者无需关心。
3. 时间复杂度一览
| 操作 | 时间复杂度(默认 deque) |
|---|---|
push() |
O(1) |
pop() |
O(1) |
front() / back() |
O(1) |
empty() / size() |
O(1) |
| 拷贝构造 / 析构 | O(n) |
所有核心操作均为常数时间,性能稳定,无均摊复杂度带来的性能抖动。
六、自定义底层容器与适用场景
1. 语法示例
通过第二个模板参数可手动指定底层容器:
cpp
// 使用 list 作为底层的队列
std::queue<int, std::list<int>> list_queue;
2. 场景选型建议
- 通用场景 :保持默认
deque,综合性能最优 - 极端大对象 / 频繁移动节点 :可选用
list,避免扩容时的元素移动,但需承担缓存劣化的代价 - 禁止使用 vector:头部删除 O(n),完全不满足队列的性能要求
七、高频面试题总结
-
Q:std::queue 是容器吗?
A:不是,它是容器适配器。自身不实现存储,仅封装底层容器,对外提供队列语义。
-
Q:queue 默认底层容器是什么?为什么选择它?
A:默认是
std::deque。原因:双端插入删除均为稳定 O(1)、无扩容抖动、分段连续存储缓存表现优于 list、内存开销更低。 -
Q:queue 可以用 vector 做底层吗?为什么?
A:不可以。vector 头部删除为 O(n),无法满足队列高效出队的性能要求。
-
Q:pop() 为什么设计成 void 不返回值?
A:异常安全设计。若返回值时拷贝构造抛出异常,元素已出队会永久丢失;拆分为 front() 读引用 + pop() 删除,保证异常时数据不丢失。
-
Q:queue 支持迭代器吗?为什么?
A:不支持。队列的语义是「仅访问首尾、先进先出」,遍历不符合 FIFO 语义,因此通过接口裁剪刻意禁止。
-
Q:stack 和 queue 的底层设计有什么异同?
A:相同点:均为容器适配器,全部操作委托底层容器,默认底层均为 deque。不同点:stack 仅操作尾部(LIFO),queue 分别操作头尾(FIFO);因此 stack 可使用 vector 做底层,queue 不可。
-
Q:手写一个简易版 queue,说明核心设计思路。
A:采用容器适配器模式,模板参数指定底层容器(默认 deque),所有入队、出队、访问操作全部转发给底层容器的对应接口;不提供迭代器和随机访问,严格约束 FIFO 语义。