C++ queue 全面解析与实战指南
在 C++ STL(标准模板库)中,queue(队列)是一种基于**先进先出(FIFO, First In First Out)**规则的容器适配器(container adapter),而非独立的容器。它通过封装底层容器(默认是deque)的接口,提供了符合队列逻辑的操作,屏蔽了底层容器的其他无关接口,让开发者能更便捷地实现队列相关的业务逻辑。本文将从queue的核心特性、底层适配机制、常用接口、实战案例等方面,带你全面掌握C++ queue的使用与设计思路。
一、queue 核心特性与适配机制
1.1 核心特性
-
先进先出(FIFO):这是queue最核心的特性,最先插入的元素会最先被删除,类似于日常生活中的排队场景------先排队的人先办事。
-
容器适配器:queue不直接管理内存,而是依赖底层的基础容器来存储数据。它本质上是对基础容器的"包装",只暴露符合队列逻辑的操作接口。
-
仅支持两端操作:只能从队尾(back)插入元素,从队头(front)删除元素,无法直接访问队列中间的元素,也无法遍历整个队列。
-
无迭代器支持:由于其FIFO的特性和适配器的设计,queue不提供迭代器,无法像vector、list那样遍历容器内的元素。
1.2 底层适配容器
queue的底层实现依赖于基础容器,C++标准规定,适配的容器需支持以下操作:
-
empty():判断容器是否为空
-
size():返回容器内元素个数
-
front():返回容器首元素的引用
-
back():返回容器尾元素的引用
-
push_back():在容器尾部插入元素
-
pop_front():删除容器首元素
STL中默认适配的容器是deque(双端队列),也可以手动指定使用list作为底层容器(vector不支持pop_front()操作,因此不能作为queue的底层容器)。指定底层容器的语法格式如下:
cpp
#include <queue>
#include <list>
using namespace std;
// 指定list作为queue的底层容器
queue<int, list<int>> q;
选择deque作为默认底层容器的原因:deque兼顾了vector和list的部分优势,支持高效的首尾插入/删除操作,且内存管理较为高效,能很好地满足queue的使用需求。
二、queue 常用接口详解
queue的接口设计简洁直观,所有接口都围绕FIFO规则展开,使用前需包含头文件 <queue>。以下是queue最常用的接口分类说明:
2.1 构造与析构
| 接口原型 | 功能说明 | 示例 |
|---|---|---|
| queue(); | 默认构造,创建一个空的queue(底层容器也为空) | queue q; // 默认使用deque作为底层容器 |
| explicit queue(const Container& cont); | 拷贝构造,用已有的基础容器cont初始化queue | deque dq = {1,2,3}; queue q(dq); // q的元素为1,2,3 |
| queue(queue&& other); | 移动构造,将other的资源转移到当前queue(C++11及以上) | queue q1 = {1,2,3}; queue q2(move(q1)); // q1为空,q2为1,2,3 |
| ~queue(); | 析构函数,释放底层容器占用的内存 | 无需手动调用,生命周期结束自动执行 |
2.2 元素插入与删除
queue仅支持队尾插入、队头删除,这是保证其FIFO特性的核心操作:
| 接口 | 功能说明 | 示例 |
|---|---|---|
| void push(const T& val); | 在队尾插入元素val(拷贝构造) | q.push(4); // 队尾添加4 |
| void push(T&& val); | 在队尾插入元素val(移动构造,C++11及以上) | string s = "hello"; q.push(move(s)); // 移动s到队尾,s变为空 |
| template <class... Args> void emplace(Args&&... args); | 在队尾直接构造元素(避免拷贝,效率更高,C++11及以上) | queue<pair<int, string>> q; q.emplace(1, "a"); // 直接构造pair元素 |
| void pop(); | 删除队头元素(注意:pop()不返回被删除的元素) | q.pop(); // 删除队头的1 |
| 注意:使用pop()前,务必先通过empty()判断队列是否为空,否则删除空队列会导致未定义行为。 |
2.3 元素访问
queue仅支持访问队头和队尾的元素,无法访问中间元素:
| 接口 | 功能说明 | 示例 |
|---|---|---|
| T& front(); | 返回队头元素的引用(非const版本,可修改元素) | q.front() = 10; // 将队头元素改为10 |
| const T& front() const; | 返回队头元素的const引用(只读,不能修改) | const queue cq; cout << cq.front(); |
| T& back(); | 返回队尾元素的引用(非const版本,可修改元素) | q.back() = 20; // 将队尾元素改为20 |
| const T& back() const; | 返回队尾元素的const引用(只读,不能修改) | cout << cq.back(); |
2.4 容量与状态判断
| 接口 | 功能说明 | 示例 |
|---|---|---|
| bool empty() const; | 判断队列是否为空(元素个数为0返回true,否则返回false) | if (q.empty()) cout << "队列空"; |
| size_t size() const; | 返回队列中元素的个数 | cout << q.size(); // 输出队列元素个数 |
2.5 赋值与交换
| 接口 | 功能说明 | 示例 |
|---|---|---|
| queue& operator=(const queue& other); | 拷贝赋值,将other的元素拷贝到当前队列 | queue q1 = {1,2}; queue q2; q2 = q1; // q2变为1,2 |
| queue& operator=(queue&& other); | 移动赋值,将other的资源转移到当前队列(C++11及以上) | q2 = move(q1); // q1为空,q2为1,2 |
| void swap(queue& other); | 交换两个队列的元素(底层容器的元素也会交换) | q1.swap(q2); // 交换q1和q2的元素 |
三、queue 基础使用示例
以下示例演示了queue的核心接口使用流程,包括元素插入、访问、删除、状态判断等操作:
cpp
#include <queue>
#include <iostream>
using namespace std;
int main() {
// 1. 创建空队列(默认deque为底层容器)
queue<int> q;
// 2. 队尾插入元素
q.push(10);
q.push(20);
q.push(30);
// 3. 查看队列状态
cout << "队列是否为空:" << (q.empty() ? "是" : "否") << endl;
cout << "队列元素个数:" << q.size() << endl;
cout << "队头元素:" << q.front() << endl;
cout << "队尾元素:" << q.back() << endl;
// 4. 队头删除元素(循环删除所有元素)
cout << "依次删除队头元素:";
while (!q.empty()) {
cout << q.front() << " ";
q.pop(); // 删除队头元素
}
cout << endl;
// 5. 再次查看队列状态
cout << "删除所有元素后,队列是否为空:" << (q.empty() ? "是" : "否") << endl;
return 0;
}
// 输出结果:
// 队列是否为空:否
// 队列元素个数:3
// 队头元素:10
// 队尾元素:30
// 依次删除队头元素:10 20 30
// 删除所有元素后,队列是否为空:是
四、queue 实战案例
案例 1:用queue实现生产者-消费者模型(简化版)
生产者-消费者模型是多线程编程中的经典场景,生产者不断生产数据并放入队列,消费者不断从队列中取出数据进行处理。queue的FIFO特性非常适合作为数据缓冲区。以下是单线程简化版实现(多线程需添加互斥锁和条件变量保证线程安全):
cpp
#include <queue>
#include <iostream>
#include <string>
using namespace std;
// 数据缓冲区(队列)
queue<string> dataQueue;
const int MAX_SIZE = 5; // 缓冲区最大容量
// 生产者:生产数据并放入缓冲区
void producer() {
string products[] = {"产品1", "产品2", "产品3", "产品4", "产品5", "产品6"};
for (auto& prod : products) {
// 缓冲区满时等待(简化版,未用条件变量)
while (dataQueue.size() == MAX_SIZE) {
cout << "缓冲区满,生产者等待..." << endl;
this_thread::sleep_for(chrono::seconds(1));
}
dataQueue.push(prod);
cout << "生产者生产:" << prod << endl;
this_thread::sleep_for(chrono::seconds(1)); // 模拟生产耗时
}
cout << "生产者完成生产!" << endl;
}
// 消费者:从缓冲区取出数据并处理
void consumer() {
while (true) {
// 缓冲区空时等待(简化版,未用条件变量)
while (dataQueue.empty()) {
cout << "缓冲区空,消费者等待..." << endl;
this_thread::sleep_for(chrono::seconds(1));
}
string prod = dataQueue.front();
dataQueue.pop();
cout << "消费者处理:" << prod << endl;
this_thread::sleep_for(chrono::seconds(2)); // 模拟处理耗时
// 生产完成且缓冲区空时,消费者退出
if (dataQueue.empty() && !producing) {
break;
}
}
cout << "消费者完成处理!" << endl;
}
int main() {
// 启动生产者和消费者线程(C++11及以上支持线程)
thread prodThread(producer);
thread consThread(consumer);
// 等待线程结束
prodThread.join();
consThread.join();
return 0;
}
说明:实际多线程场景中,需添加 <mutex>(互斥锁)保证队列操作的原子性,添加 <condition_variable>(条件变量)实现生产者和消费者的高效等待与唤醒,避免忙等浪费CPU资源。
案例 2:用queue实现约瑟夫环问题
约瑟夫环问题描述:n个人围成一圈,从第k个人开始报数,报到m的人出列,接着从下一个人开始继续报数,直到圈中只剩下最后一个人。queue的FIFO特性可简化该问题的实现:
cpp
#include <queue>
#include <iostream>
using namespace std;
// 约瑟夫环函数:n为总人数,k为起始位置(1-based),m为报数阈值
int josephus(int n, int k, int m) {
queue<int> q;
// 1. 将所有人加入队列(编号1~n)
for (int i = 1; i <= n; ++i) {
q.push(i);
}
// 2. 移动到起始位置k(将前k-1个人移到队尾)
for (int i = 0; i < k-1; ++i) {
q.push(q.front());
q.pop();
}
// 3. 循环报数,淘汰报到m的人
while (q.size() > 1) {
// 报到m-1的人移到队尾
for (int i = 0; i < m-1; ++i) {
q.push(q.front());
q.pop();
}
// 报到m的人出列
cout << "淘汰:" << q.front() << endl;
q.pop();
}
// 4. 返回最后剩下的人
return q.front();
}
int main() {
int n = 5, k = 1, m = 3; // 5个人,从第1个人开始报数,报到3的人出列
int last = josephus(n, k, m);
cout << "最后剩下的人是:" << last << endl;
return 0;
}
// 输出结果:
// 淘汰:3
// 淘汰:1
// 淘汰:5
// 淘汰:2
// 最后剩下的人是:4
五、queue 与 priority_queue 的对比
STL中还有一个常用的队列适配器------priority_queue(优先队列),它与queue的核心区别在于排序规则(priority_queue是优先级排序,非FIFO)。以下是两者的核心差异对比:
| 特性 | queue | priority_queue |
|---|---|---|
| 排序规则 | 先进先出(FIFO) | 优先级排序(默认大顶堆,最大值优先出列) |
| 底层容器 | 默认deque,可指定list | 默认vector,可指定deque(需支持随机访问) |
| 元素访问 | 支持front()(队头)、back()(队尾) | 仅支持top()(优先级最高的元素,即堆顶) |
| 核心操作 | push(队尾)、pop(队头) | push(任意位置,自动调整堆)、pop(堆顶) |
| 迭代器 | 不支持 | 不支持 |
| 适用场景 | 需要FIFO顺序的场景(如任务队列、缓冲区) | 需要按优先级处理的场景(如调度系统、TOP-K问题) |
六、常见问题与注意事项
-
pop() 不返回被删除元素 :这是queue的设计特点,若需要获取被删除的元素,需先通过front()访问队头元素,再调用pop()删除。示例:
int val = q.front(); // 先获取队头元素 q.pop(); // 再删除队头元素 -
避免操作空队列:调用front()、back()、pop()前,必须先用empty()判断队列是否为空,否则会导致未定义行为(程序崩溃或异常)。
-
底层容器的选择:默认的deque已能满足大部分场景需求;若需要更高效率的首尾插入/删除(且不介意list的内存碎片),可指定list作为底层容器;vector不支持pop_front(),因此不能作为queue的底层容器。
-
线程安全问题:STL的queue不是线程安全的,多线程环境下同时操作queue时,需手动添加互斥锁(mutex)和条件变量(condition_variable)保证线程安全。
-
emplace() 与 push() 的区别:emplace()直接在队尾构造元素,避免了拷贝或移动操作,效率比push()更高;push()是先构造元素,再将元素拷贝或移动到队尾。建议在C++11及以上环境中,优先使用emplace()。
七、总结
C++ queue是基于FIFO规则的容器适配器,通过封装deque(默认)或list的接口,提供了简洁直观的队列操作。它的核心优势是符合人类对"排队"场景的认知,接口简单易用,适合作为数据缓冲区、任务队列等场景的实现工具。
使用queue时,需注意其仅支持两端操作、无迭代器、pop()不返回元素等特点,避免因误用接口导致程序异常。同时,要根据实际需求选择合适的底层容器,多线程场景需额外保证线程安全。