std::queue 详解:队列适配器的实现、底层容器选择、应用场景

队列这东西,说白了就是「先进先出」——你排过队吧?先来的人先服务。在 C++ 里,std::queue 就是帮我们干这个的。我最早接触它是在写一个简单的任务调度器时,那时候还自己手写数组模拟队列,后来才发现 STL 早就给你准备好了。

队列适配器是什么?

std::queue 不是一个独立的容器,它是一个适配器。什么意思呢?它底层不自己存数据,而是包装了另一个容器(比如 dequelist),只对外暴露队列的接口——pushpopfrontback。你想想看,这就像给一个普通容器穿了一件「队列马甲」,只让你从一头进、另一头出。

核心特点:
  • 先进先出(FIFO)
  • 不能遍历,只能访问队头和队尾
  • 底层容器可替换,但必须支持 front()back()push_back()pop_front()

底层容器选择

默认情况下,std::queue 使用 std::deque 作为底层容器。为什么是 deque?因为 deque 既支持 push_back,又支持 pop_front,而且效率都不错。我个人习惯在大多数场景下直接用默认的 deque,省心。

但你也可以换成 std::list。什么时候用 list?我记得有一次我在做一个内存敏感的项目,deque 的块状分配策略导致内存碎片有点多,换成 list 后问题就缓解了。不过 list 的缺点是每个元素都有额外指针开销,而且缓存局部性不如 deque。

底层容器 优点 缺点 推荐场景
std::deque(默认) 随机访问快,内存连续性好 内存块管理略复杂 通用场景,绝大多数情况
std::list 插入删除无内存重分配 额外指针开销,缓存不友好 元素大、频繁插入删除
小技巧:如果你不确定用什么,就用默认的 deque。我见过太多人为了「优化」而换成 list,结果性能反而更差。除非你明确测出 deque 是瓶颈,否则别瞎换。

基本用法

#include <queue>
#include <iostream>

int main() {
    std::queue<int> q;

    q.push(10);
    q.push(20);
    q.push(30);

    std::cout << "队头: " << q.front() << "\n";  // 10
    std::cout << "队尾: " << q.back() << "\n";   // 30

    q.pop();  // 移除队头
    std::cout << "弹出后队头: " << q.front() << "\n";  // 20

    std::cout << "队列大小: " << q.size() << "\n";
    std::cout << "是否为空: " << q.empty() << "\n";

    return 0;
}

嗯,这里要注意:pop() 不会返回被移除的元素。如果你想拿到队头再移除,得先 front()pop()。我曾经在代码 review 时看到有人写 int val = q.pop();,编译不过还一脸懵——记住,pop 是 void 的。

应用场景一:BFS(广度优先搜索)

BFS 是队列最经典的应用,没有之一。你想想看,BFS 的流程就是一层一层往外扩,先访问的节点先处理它的邻居——这不就是队列的 FIFO 特性吗?

#include <queue>
#include <vector>
#include <iostream>

void bfs(int start, const std::vector<std::vector<int>>& graph) {
    std::queue<int> q;
    std::vector<bool> visited(graph.size(), false);

    q.push(start);
    visited[start] = true;

    while (!q.empty()) {
        int node = q.front();
        q.pop();
        std::cout << "访问节点: " << node << "\n";

        for (int neighbor : graph[node]) {
            if (!visited[neighbor]) {
                visited[neighbor] = true;
                q.push(neighbor);
            }
        }
    }
}

我在项目中用 BFS 做过一个简单的社交关系推荐系统——从某个用户出发,找他的朋友、朋友的朋友,一层层往外推。队列在这里就是核心数据结构,没有它,BFS 根本没法写。

应用场景二:任务调度

另一个常见场景是任务调度。比如你有一个生产者线程不断产生任务,一个消费者线程不断处理任务。队列就是它们之间的缓冲区。

#include <queue>
#include <mutex>
#include <condition_variable>

std::queue<int> task_queue;
std::mutex mtx;
std::condition_variable cv;

void producer() {
    for (int i = 0; i < 100; ++i) {
        std::lock_guard<std::mutex> lock(mtx);
        task_queue.push(i);
        cv.notify_one();  // 通知消费者
    }
}

void consumer() {
    while (true) {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, []{ return !task_queue.empty(); });
        int task = task_queue.front();
        task_queue.pop();
        lock.unlock();
        // 处理任务...
    }
}
避坑指南:我曾经在生产环境遇到过一个问题——消费者处理太慢,队列越积越长,最后内存爆了。解决方案是给队列加一个最大长度限制,超过就阻塞生产者。这叫「有界队列」,很多工业级任务调度系统都这么干。

队列的局限性

说实话,std::queue 的功能非常有限。你不能遍历,不能随机访问,甚至不能清空(得一个个 pop)。如果你需要这些功能,建议直接用 std::dequestd::list,自己控制接口。

另外,std::queue 不是线程安全的。多线程环境下你必须自己加锁,或者用 TBB、Boost 等库提供的并发队列。

知识结构图

std::queue 适配器模式 包装底层容器 底层容器 deque(默认)/ list 核心操作 push / pop / front / back BFS 广度优先搜索 图遍历、最短路径 任务调度 生产者-消费者模型 消息缓冲 异步通信 ⚠ 非线程安全 · 不能遍历 · pop不返回值

这张图把 queue 的核心脉络理清楚了:适配器模式、底层容器选择、核心操作、三大应用场景,以及那些容易踩的坑。我个人建议你把这张图存下来,写代码前瞄一眼,思路会清晰很多。

一句话总结:std::queue 是 STL 里最「老实」的容器——它只做一件事,但把这件事做到极致。用对场景,它就是利器;用错场景,它就是鸡肋。
公众号:蓝海资料掘金营,微信deep3321