Skip to content
DSA stacks queues 7 min read

Circular Queue: Fixed-Size FIFO with a Ring Buffer

A circular queue (or ring buffer) is a fixed-capacity queue that reuses its array slots: when the rear reaches the end of the array, it wraps back to index 0. This avoids the wasted space and shifting of a plain array queue while keeping O(1) enqueue and dequeue. Ring buffers are the backbone of streaming, audio, and networking buffers where memory is fixed.

The problem it solves

A simple array queue with a moving head pointer keeps abandoning the front slots — after many dequeues, the used region marches off the end even though the front is empty. A circular queue fixes this by treating the array as a ring: index after the last wraps around to the first using modulo arithmetic.

capacity = 5, after some enqueue/dequeue:

index:   0    1    2    3    4
        [ _ ][ C ][ D ][ E ][ _ ]
               ^head          ^tail(next write) wraps to 0

How wrap-around works

Keep a head (front index), a count (number of items), and a fixed array of size capacity. The rear write position is computed, not stored: (head + count) % capacity. After dequeuing, advance head = (head + 1) % capacity. The modulo is what makes the indices loop.

  • isFull: count == capacity
  • isEmpty: count == 0
  • enqueue: write at (head + count) % capacity, then count++
  • dequeue: read at head, then head = (head + 1) % capacity, count--
#include <vector>
#include <stdexcept>
class CircularQueue {
    std::vector<int> buf;
    int head = 0, count = 0, cap;
public:
    explicit CircularQueue(int capacity) : buf(capacity), cap(capacity) {}
    bool isFull()  const { return count == cap; }
    bool isEmpty() const { return count == 0; }
    void enqueue(int x) {
        if (isFull()) throw std::overflow_error("queue full");
        buf[(head + count) % cap] = x;
        count++;
    }
    int dequeue() {
        if (isEmpty()) throw std::underflow_error("queue empty");
        int x = buf[head];
        head = (head + 1) % cap;
        count--;
        return x;
    }
    int front() const { return buf[head]; }
};

Complexity: every operation is O(1) time; space is O(capacity), fixed up front.

Why store count instead of a tail pointer? With only head and tail, a full ring and an empty ring look identical (head == tail). Tracking count (or leaving one slot empty) removes that ambiguity cleanly.

Common pitfalls

  • Forgetting the modulo. head + count can exceed capacity; you must wrap with % cap on both the write index and when advancing head.
  • Off-by-one on full/empty. Decide your disambiguation strategy (count vs. sacrificial slot) and apply it everywhere.
  • Overwriting live data. Always check isFull() before enqueue, or you’ll clobber the front element.

When to use a circular queue

Use it whenever capacity is bounded and known: audio/video buffers, keyboard input buffers, network packet queues, sliding-window streaming, and producer/consumer pipelines. If you need unbounded growth, use the dynamic queue implementation; if you need add/remove at both ends, use a deque.

Next: the deque generalizes this to both ends, or test yourself on the exercises.

Last updated June 25, 2026
Was this helpful?