Очередь (queue) — это структура данных, которая отдает элементы в порядке FIFO (first in — first out, «первым пришел — первым ушел»): извлечь можно только тот элемент, который добавили раньше всех остальных, сохранившихся в очереди. В C++ есть готовый контейнер-адаптер std::queue, приоритетная очередь std::priority_queue и способ собрать очередь вручную на массиве. Разберем все три варианта на рабочих примерах, сравним их и разберем типичную ошибку ручной реализации.
Содержание
Очередь, стек и дек: в чем разница
Очередь легко перепутать с соседними структурами, поэтому сначала разведем термины:
- Стек (stack) — работает по принципу LIFO (last in — first out): последним добавленный элемент извлекается первым. В C++ это
std::stack. - Дек (deque, double-ended queue) — позволяет добавлять и удалять элементы с обоих концов за O(1) и, в отличие от очереди, дает произвольный доступ по индексу через
operator[]. std::queue— это не отдельная структура данных, а контейнер-адаптер: обертка, которая ограничивает интерфейс нижележащего контейнера (по умолчаниюstd::deque) до операций push/pop/front/back, скрывая произвольный доступ.
Если в коде нужен доступ по индексу или проход по элементам в обе стороны, std::queue не подойдет — берите std::deque напрямую.
std::queue: интерфейс и пример
Очередь в C++ объявляется так: std::queue<T> q;, где T — тип элементов, а заголовок для подключения — <queue>. По умолчанию std::queue хранит данные в std::deque, но можно указать другой контейнер вторым параметром шаблона, например std::queue<int, std::list<int>>.
Методы, с которыми реально работают:
| Метод | Что делает | Сложность |
|---|---|---|
push(v) |
добавляет элемент в конец очереди | O(1) |
pop() |
удаляет первый элемент, значения не возвращает | O(1) |
front() |
ссылка на первый элемент | O(1) |
back() |
ссылка на последний элемент | O(1) |
empty() |
true, если очередь пуста | O(1) |
size() |
количество элементов | O(1) |
Важная деталь, которую часто путают: pop() не возвращает удаляемое значение (в отличие, например, от pop() в некоторых других языках). Чтобы получить значение и удалить его, сначала вызывают front(), потом pop().
Полный рабочий пример:
#include <iostream>
#include <queue>
int main() {
std::queue<int> q;
q.push(10);
q.push(20);
q.push(30);
std::cout << "front: " << q.front() << std::endl;
std::cout << "back: " << q.back() << std::endl;
q.pop();
std::cout << "after pop, front: " << q.front() << std::endl;
std::cout << "size: " << q.size() << std::endl;
std::cout << "empty: " << (q.empty() ? "true" : "false") << std::endl;
return 0;
}
Результат в консоли:
front: 10
back: 30
after pop, front: 20
size: 2
empty: false
Порядок предсказуем: первым положили 10 — он же первым и front(), пока его не удалят через pop().
Приоритетная очередь: std::priority_queue
std::priority_queue — отдельная структура, а не разновидность FIFO: она выдает не «того, кто пришел раньше», а элемент с наибольшим приоритетом. По умолчанию это максимум по оператору < (через std::less), хранилище — бинарная куча поверх std::vector.
У приоритетной очереди нет front()/back() — только top(). Push и pop работают за O(log n) из-за восстановления свойства кучи, обращение к вершине — за O(1).
#include <iostream>
#include <queue>
int main() {
std::priority_queue<int> pq;
pq.push(5);
pq.push(1);
pq.push(9);
pq.push(3);
while (!pq.empty()) {
std::cout << pq.top() << " ";
pq.pop();
}
std::cout << std::endl;
return 0;
}
Результат:
9 5 3 1
Если нужна очередь с минимумом на вершине (min-heap), меняют компаратор и хранилище явно: std::priority_queue<int, std::vector<int>, std::greater<int>> min_pq;. Такую конструкцию используют, например, в алгоритме Дейкстры для выбора вершины с минимальным текущим расстоянием.
Очередь на массиве: кольцевой буфер своими руками
STL доступен не везде: во встраиваемых системах или там, где нельзя аллоцировать память динамически, очередь реализуют вручную на массиве — это кольцевой буфер (circular buffer). Индексы начала и конца циклически «оборачиваются» через остаток от деления на емкость.
Частая ошибка — различать пустую и полную очередь только по равенству head == tail, без отдельного счетчика элементов:
// НЕВЕРНО: не различает "пусто" и "полностью заполнено"
class BadQueue {
public:
explicit BadQueue(int cap) : data(new int[cap]), capacity(cap), head(0), tail(0) {}
~BadQueue() { delete[] data; }
void push(int value) {
data[tail] = value;
tail = (tail + 1) % capacity;
}
bool empty() const { return head == tail; }
private:
int* data;
int capacity;
int head;
int tail;
};
int main() {
BadQueue bq(3);
bq.push(1);
bq.push(2);
bq.push(3); // tail снова стал равен 0, то есть head
std::cout << (bq.empty() ? "empty" : "not empty") << std::endl;
return 0;
}
Фактический результат:
empty
Хотя очередь заполнена под завязку, empty() вернул true: tail после третьей вставки совпал с head. Дальнейший push() молча перезапишет данные с начала буфера — это и есть баг, из-за которого в старой версии статьи ошибочно утверждалось, что «каждая новая запись заменяет последний элемент»: не свойство очереди, а следствие сломанной проверки заполненности.
Исправление — добавить отдельный счетчик элементов и проверять емкость явно:
#include <iostream>
class CircularQueue {
public:
explicit CircularQueue(int cap)
: data(new int[cap]), capacity(cap), head(0), tail(0), count(0) {}
~CircularQueue() { delete[] data; }
bool push(int value) {
if (count == capacity) return false; // очередь заполнена
data[tail] = value;
tail = (tail + 1) % capacity;
++count;
return true;
}
bool pop() {
if (count == 0) return false; // очередь пуста
head = (head + 1) % capacity;
--count;
return true;
}
int front() const { return data[head]; }
bool empty() const { return count == 0; }
private:
int* data;
int capacity;
int head;
int tail;
int count;
};
int main() {
CircularQueue q(3);
q.push(1);
q.push(2);
q.push(3);
std::cout << "push 4 into full queue: " << (q.push(4) ? "ok" : "queue full") << std::endl;
std::cout << "front: " << q.front() << std::endl;
q.pop();
std::cout << "front after pop: " << q.front() << std::endl;
std::cout << "push 4 after pop: " << (q.push(4) ? "ok" : "queue full") << std::endl;
std::cout << "front stays: " << q.front() << std::endl;
return 0;
}
Результат:
push 4 into full queue: queue full
front: 1
front after pop: 2
push 4 after pop: ok
front stays: 2
count однозначно определяет состояние: push()/pop() явно отказывают, когда это невозможно, вместо порчи данных.
Когда какую очередь выбрать
| Структура | Порядок извлечения | Доступ по индексу | Push/pop | Когда использовать |
|---|---|---|---|---|
std::queue |
FIFO | нет | O(1) | простая очередь задач или событий |
std::priority_queue |
по приоритету (по умолчанию максимум) | нет, только top() |
O(log n) | планировщики, алгоритм Дейкстры, топ-k элементов |
std::deque |
FIFO/LIFO с обоих концов | да, O(1) | O(1) с обоих концов | нужен и доступ по индексу, и вставка с обеих сторон |
| кольцевой буфер на массиве | FIFO | нет (можно добавить) | O(1) при фиксированной емкости | встраиваемые системы, буферы без динамической памяти |
Выводы
- Очередь работает по принципу FIFO: первым добавленный элемент извлекается первым.
std::queue— контейнер-адаптер надstd::deque, дает только push/pop/front/back/empty/size, без доступа по индексу.std::priority_queueизвлекает не по порядку добавления, а по приоритету, за O(log n) на бинарной куче.- Ручная реализация на массиве (кольцевой буфер) требует отдельного счетчика элементов, иначе
head == tailне различает пустую и полную очередь. - Выбор структуры зависит от задачи: для приоритетов нужна
std::priority_queue, для доступа по индексу —std::deque, для фиксированной памяти — свой кольцевой буфер.
Где применяется / связь с практикой
Очереди — одна из первых структур данных на пути разработчика C++: обработка событий, буферы сетевых пакетов, планировщики задач, обход графа в ширину (BFS) на std::queue, поиск кратчайшего пути (Дейкстра) на std::priority_queue. Разница между FIFO-очередью, приоритетной очередью и их устройством под капотом — база для более сложных структур и алгоритмов.
Освойте тему на практике
Если нужна системная база по языку, синтаксису и структурам данных C++ с практикой на реальных задачах, приходите на курс C++ Developer. Basic. А проверить формат и разобрать похожие темы с преподавателем можно на открытых уроках Otus — бесплатно и без предварительной подготовки.
Смежные темы: Сортировка Хоара и другие способы сортировки массивов, Строки в C.
FAQ
Можно ли обратиться к элементу std::queue по индексу, как в массиве?
Нет, у std::queue нет operator[] — доступны только front() и back(). Если нужен произвольный доступ, используйте std::deque напрямую вместо адаптера.
Чем std::queue отличается от std::stack по устройству?
Оба — контейнеры-адаптеры над одним и тем же нижележащим контейнером (по умолчанию std::deque), различие только в наборе разрешенных операций: std::queue дает FIFO через push/front/back, std::stack — LIFO через push/top.
Как реализовать очередь в чистом C, где нет STL?
Так же, как кольцевой буфер выше, но без классов: структура с массивом, head, tail, count и обычные функции queue_push()/queue_pop(), принимающие указатель на структуру. Логика с остатком от деления и отдельным счетчиком элементов остается точно такой же.



