A Queue With No malloc: The Ring Buffer

A ring buffer: a fixed array used as a circle, with head and tail pointers that wrap around

A queue that grows by allocating nodes is a luxury you cannot always afford. In an audio callback, a network interrupt handler, or a microcontroller with kilobytes of RAM, you cannot call malloc, you cannot block, and you cannot have unbounded memory. What you can have is a fixed array and the discipline to treat it as a circle. That is a ring buffer (also called a circular buffer), and it is one of the most quietly important data structures in systems programming.

The whole structure

typedef struct {
    int *buf;
    size_t cap;
    size_t head;  // index of the next write
    size_t count; // number of elements currently stored
} RingBuffer;

Storage is provided by the caller, so the buffer itself never allocates. head is where the next value will be written. Instead of also tracking a tail, this design stores count, the number of live elements, and derives the read position from head and count. That choice quietly solves a classic ambiguity, which we will get to.

Push and pop, with wraparound

bool rb_push(RingBuffer *rb, int x) {
    if (rb->count == rb->cap) return false;     // full
    rb->buf[rb->head] = x;
    rb->head = (rb->head + 1) % rb->cap;        // wrap
    rb->count++;
    return true;
}

bool rb_pop(RingBuffer *rb, int *out) {
    if (rb->count == 0) return false;           // empty
    size_t tail = (rb->head + rb->cap - rb->count) % rb->cap;
    *out = rb->buf[tail];
    rb->count--;
    return true;
}

The % rb->cap is the entire trick. When head reaches the end of the array it wraps back to 0 and starts overwriting the slots that have already been popped. The data never moves; only the indices travel around the array in circles. A push advances head; a pop advances the derived tail by shrinking count. The oldest element is always at (head + cap - count) % cap, and the + cap before the modulo keeps the arithmetic positive without signed-underflow surprises.

The empty-versus-full problem, dodged

If you store head and tail instead of head and count, you hit a famous puzzle: an empty buffer has head == tail, but so does a completely full one. You cannot tell them apart from the indices alone. People solve it with an extra "full" flag, or by wasting one slot so full means head is one behind tail. By storing count instead, this design sidesteps the whole issue. Empty is count == 0, full is count == cap, and there is never any doubt. A small representation choice that erases an entire category of off-by-one bugs.

Where these run the world

The lessons

It is a structure you can implement in twenty lines and then find everywhere you look, from the kernel to your headphones. The circle is the idea: reuse the same slots forever by wrapping around.


Build it yourself. Solve the Ring Buffer challenge on barehands and get graded on correctness and speed. No libraries, just cc -std=c11.