C++ queue 全面解析与实战指南

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问题)

六、常见问题与注意事项

  1. pop() 不返回被删除元素 :这是queue的设计特点,若需要获取被删除的元素,需先通过front()访问队头元素,再调用pop()删除。示例:
    int val = q.front(); // 先获取队头元素 q.pop(); // 再删除队头元素

  2. 避免操作空队列:调用front()、back()、pop()前,必须先用empty()判断队列是否为空,否则会导致未定义行为(程序崩溃或异常)。

  3. 底层容器的选择:默认的deque已能满足大部分场景需求;若需要更高效率的首尾插入/删除(且不介意list的内存碎片),可指定list作为底层容器;vector不支持pop_front(),因此不能作为queue的底层容器。

  4. 线程安全问题:STL的queue不是线程安全的,多线程环境下同时操作queue时,需手动添加互斥锁(mutex)和条件变量(condition_variable)保证线程安全。

  5. emplace() 与 push() 的区别:emplace()直接在队尾构造元素,避免了拷贝或移动操作,效率比push()更高;push()是先构造元素,再将元素拷贝或移动到队尾。建议在C++11及以上环境中,优先使用emplace()。

七、总结

C++ queue是基于FIFO规则的容器适配器,通过封装deque(默认)或list的接口,提供了简洁直观的队列操作。它的核心优势是符合人类对"排队"场景的认知,接口简单易用,适合作为数据缓冲区、任务队列等场景的实现工具。

使用queue时,需注意其仅支持两端操作、无迭代器、pop()不返回元素等特点,避免因误用接口导致程序异常。同时,要根据实际需求选择合适的底层容器,多线程场景需额外保证线程安全。

相关推荐
rgeshfgreh2 小时前
Java+GeoTools+PostGIS高效求解对跖点
java
鱼跃鹰飞2 小时前
DDD中的防腐层
java·设计模式·架构
计算机程序设计小李同学2 小时前
婚纱摄影集成管理系统小程序
java·vue.js·spring boot·后端·微信小程序·小程序
橘颂TA2 小时前
【剑斩OFFER】算法的暴力美学——力扣 394 题:字符串解码
数据结构·c++·结构与算法
DICOM医学影像2 小时前
2. go语言从零实现以太坊客户端-查询区块链账户余额
开发语言·golang·区块链·以太坊·web3.0·hardhat
Data_agent2 小时前
Python 编程实战:函数与模块化编程及内置模块探索
开发语言·python
new_zhou2 小时前
vs2019+qt工程中生成dump文件及调试
开发语言·qt·visual studio·dump调试
栈与堆2 小时前
LeetCode 19 - 删除链表的倒数第N个节点
java·开发语言·数据结构·python·算法·leetcode·链表
一路向北·重庆分伦2 小时前
03-01:MQ常见问题梳理
java·开发语言