2.3.1.2 — очередь фиксированного размера без единой аллокации после старта2.3.1.2 — a fixed-size queue with zero allocations after startup

Кольцевой БуферRing Buffer

Связный список (2.3.1.1) платит указателем и отдельной аллокацией за каждый узел — гибко, но дорого для сценариев вроде очереди сообщений с предсказуемым максимальным размером. Кольцевой буфер — обычный массив фиксированной длины, но два указателя (head и tail) "оборачиваются" через конец обратно к началу, превращая линейный участок памяти в логически замкнутый круг без единой аллокации после инициализации.A linked list (2.3.1.1) pays a pointer and a separate allocation for every node — flexible, but expensive for scenarios like a message queue with a predictable maximum size. A ring buffer is an ordinary fixed-length array, but two pointers (head and tail) "wrap" past the end back to the start, turning a linear chunk of memory into a logically closed circle with zero allocations after initialization.

push/pop O(1) без аллокацийno allocations фиксированная ёмкостьfixed capacity

Идея и инвариантIdea & invariant

Массив фиксированной ёмкости cap хранит элементы, а два индекса — head (следующий элемент на извлечение) и tail (следующая свободная позиция для вставки) — определяют, какая часть массива логически "занята". Вставка (push) записывает значение в buf[tail] и сдвигает tail на (tail + 1) % cap — операция по модулю и есть "оборачивание": если tail дошёл до конца массива, следующая позиция — снова индекс 0. Извлечение (pop) читает buf[head] и аналогично сдвигает head. Отдельный счётчик count (или иногда — соглашение "оставлять одну ячейку пустой") нужен, чтобы различить состояния "буфер пуст" и "буфер полон", которые иначе неразличимы по одному условию head == tail.A fixed-capacity array cap stores elements, and two indices — head (the next element to remove) and tail (the next free slot for insertion) — define which part of the array is logically "occupied." Insertion (push) writes the value into buf[tail] and advances tail to (tail + 1) % cap — the modulo operation is exactly the "wrapping": once tail reaches the array's end, the next position is index 0 again. Removal (pop) reads buf[head] and advances head the same way. A separate count (or, in some conventions, "always leave one cell empty") is needed to distinguish "buffer empty" from "buffer full," which otherwise both look like head == tail.

0 1 2 3 4 5 head tail tail=4, следующий push пойдёт в ячейку 4, затем tail станет 5, затем 0 — оборот

физически это обычный массив — "кольцо" существует только в арифметике индексов по модулюphysically it's an ordinary array — the "ring" exists only in the modulo index arithmetic

template<typename T>
class RingBuffer {
    std::vector<T> buf;
    size_t head = 0, tail = 0, count = 0;
public:
    explicit RingBuffer(size_t cap) : buf(cap) {}

    bool push(const T& val) {
        if (count == buf.size()) return false;  // полон// full
        buf[tail] = val;
        tail = (tail + 1) % buf.size();          // оборачивание через модуль// wrapping via modulo
        ++count;
        return true;
    }

    std::optional<T> pop() {
        if (count == 0) return std::nullopt;      // пуст// empty
        T val = buf[head];
        head = (head + 1) % buf.size();
        --count;
        return val;
    }
};

Демонстрация вживуюLive demonstration

Ячейки массива фиксированной ёмкости расположены по кругу. Оранжевая метка — head, синяя — tail. Переключатель режима меняет поведение при заполнении: bounded — push отклоняется, пока не освободится место (типично для очередей между потоками); overwrite — push всегда успевает, затирая самый старый элемент и сдвигая head вместе с tail (типично для циклических логов вроде dmesg).The fixed-capacity array's cells are laid out in a circle. Orange marks head, blue marks tail. The mode switch changes what happens when the buffer is full: bounded — push is rejected until space frees up (typical for inter-thread queues); overwrite — push always succeeds, evicting the oldest element and advancing head along with tail (typical for circular logs like dmesg).

заполнено:occupied: 0/6

выберите операцию, чтобы выполнить её сразуpick an operation to execute it immediately

Почему это не просто "массив вместо списка"Why this isn't just "an array instead of a list"

Обычный массив без оборачивания индекса реализует стек или очередь только пока не достигнут конец памяти — после этого нужно либо остановиться, либо сдвинуть все элементы к началу (O(n)). Кольцевой буфер решает это раз и навсегда арифметикой по модулю: логическая позиция элемента отделена от его физического адреса в памяти, что позволяет переиспользовать освободившееся пространство в начале массива без единого перемещения существующих данных. Это тот же общий приём "разделить логическую и физическую адресацию", что лежит и в основе хеш-таблиц (2.3.1.3+) — там позиция тоже вычисляется, а не задаётся порядком вставки.A plain array without index wrapping implements a stack or queue only until memory runs out — after that, either stop or shift every element to the start (O(n)). A ring buffer solves this once and for all with modulo arithmetic: an element's logical position is decoupled from its physical memory address, allowing freed space at the array's start to be reused without moving a single existing element. This is the same general "separate logical from physical addressing" trick underlying hash tables (2.3.1.3+) too — there, position is also computed rather than set by insertion order.

Где встречаетсяWhere it shows up

01

Аудио/видеобуферы и очереди сообщений между потоками (lock-free SPSC)Audio/video buffers and inter-thread message queues (lock-free SPSC)

Single-producer-single-consumer очередь между потоками часто реализуется кольцевым буфером фиксированного размера без блокировок — атомарные операции над head/tail заменяют мьютекс, а фиксированный размер устраняет необходимость в динамических аллокациях в критичном по задержке пути.A single-producer-single-consumer queue between threads is often implemented as a lock-free fixed-size ring buffer — atomic operations on head/tail replace a mutex, and the fixed size eliminates the need for dynamic allocations on the latency-critical path.

02

Циклические логи (kernel ring buffer, dmesg)Circular logs (the kernel ring buffer, dmesg)

Логи ядра Linux (dmesg) хранятся именно в кольцевом буфере фиксированного размера — новые записи вытесняют самые старые, что даёт предсказуемое потребление памяти независимо от того, как долго работает система, ценой автоматической потери старых записей при переполнении.The Linux kernel log (dmesg) is stored precisely in a fixed-size ring buffer — new entries evict the oldest ones, giving predictable memory use regardless of how long the system has been running, at the cost of automatically losing old entries on overflow.