Skip to content

Expert · Data structures in C · topic 48 of 60 · 30 min

Queues and circular buffers

Explanation

A queue is first-in, first-out (FIFO): enqueue at the tail, dequeue from the head. A plain array queue wastes space as the head moves forward, so C programs use a circular buffer: head and tail indexes wrap around with % capacity, and a count (or one empty slot) tells full from empty. Circular buffers are everywhere in embedded systems, for example receiving serial (UART) data.

FIFO

A queue serves items in arrival order, like a ticket counter line. Typical uses: print jobs, tasks waiting for a worker, messages between parts of a program, and bytes arriving from hardware faster than the program reads them.

Why a circular buffer?

output
naive array queue after some enqueues and dequeues:
[ x | x | x | 40 | 50 | 60 |    |    ]
              head            tail
the slots before head are wasted; soon tail reaches the end even though space is free

circular buffer: the indexes wrap around to the start
[ 70 | 80 |    | 40 | 50 | 60 ]
        tail      head          (capacity 6, count 5)

Implementation

c
#define QCAP 8

typedef struct {
    int buf[QCAP];
    int head;     /* index of the oldest item */
    int tail;     /* index where the next item goes */
    int count;    /* items stored */
} Queue;

void q_init(Queue *q) { q->head = q->tail = q->count = 0; }

int enqueue(Queue *q, int v) {
    if (q->count == QCAP) return -1;          /* full */
    q->buf[q->tail] = v;
    q->tail = (q->tail + 1) % QCAP;           /* wrap around */
    q->count++;
    return 0;
}

int dequeue(Queue *q, int *out) {
    if (q->count == 0) return -1;             /* empty */
    *out = q->buf[q->head];
    q->head = (q->head + 1) % QCAP;
    q->count--;
    return 0;
}

Without a count, head == tail is ambiguous (empty or full?). The two standard fixes are keeping a count, as above, or always leaving one slot unused so "full" means (tail + 1) % cap == head. When the capacity is a power of two, (i + 1) & (cap - 1) replaces the % and is a little faster, a trick common in firmware.

Embedded use: a UART receive buffer

A microcontroller's serial port can receive bytes at any moment. An interrupt routine (module 12) stores each byte into a circular buffer as it arrives, and the main loop reads bytes out when it has time. Neither side ever moves data around; they just advance their own index.

Careful: When an interrupt handler and the main loop share a circular buffer, the shared indexes must be declared volatile and updated carefully (single-producer, single-consumer designs where each side writes only its own index are the usual safe pattern). Otherwise the compiler may cache values or both sides may update count at the same time and corrupt it.

Linked-list queues

A queue can also be a linked list with head and tail pointers: enqueue appends at the tail in O(1), dequeue removes from the head in O(1), and there is no fixed capacity, at the cost of a malloc per item.

Think of it like this: A circular buffer is like a revolving sushi belt with a fixed number of plates: the chef puts dishes on the next empty plate as it passes (tail), customers take the oldest dish (head), and the belt goes round and round, so no plate is ever wasted.
Tip: Always decide what happens when the buffer is full: reject the new item (as above), overwrite the oldest (useful for "last N readings" logs), or block until space is free. Write it down; it is a design decision, not a detail.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Queues and circular buffers · 1/6

Queues

In this lesson you will build queues and circular buffers.

Prefer a video by a teacher? Find free videos on “circular buffer ring buffer queue in c embedded uart” on YouTube ↗

In real life: Keeping the last 60 heart-rate readings

A fitness band stores the most recent 60 heart-rate readings in a circular buffer that OVERWRITES the oldest value when full. The display can always draw the last minute, and memory use never grows.

c
#define N 60
uint8_t bpm[N];
int next = 0, filled = 0;
void add_reading(uint8_t v) {
    bpm[next] = v;
    next = (next + 1) % N;
    if (filled < N) filled++;
}

Practice (0/3 done)

Task 1 · Question

In a circular buffer of capacity 5 with tail = 4, where does the next item go after enqueue moves tail with (tail + 1) % 5?

Task 2 · Code · Javascript

The same circular-buffer algorithm in JavaScript, which runs here: write createQueue(capacity) returning an object with enqueue(v) (returns false when full), dequeue() (returns undefined when empty) and size(). Use a fixed array with head, tail and count indexes and % capacity, NOT array push/shift.

Task 3 · Practice

Implement the circular-buffer Queue in C with capacity 8 (count-based), plus a second version that leaves one slot empty instead of using count. Write a test that fills the queue, empties it, then interleaves enqueue/dequeue 20 times to force the indexes to wrap, checking the FIFO order every time.

Quick quiz

Question 1

1.Which order does a queue follow?

Question 2

2.Without a count, why is head == tail ambiguous in a circular buffer?

Question 3

3.Where are circular buffers common?

Question 4

4.With capacity 16, which expression can replace (i + 1) % 16?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

Which order does a queue follow?

Answer: FIFO

First in, first out.

Without a count, why is head == tail ambiguous in a circular buffer?

Answer: It can mean either empty or full

Use a count or keep one slot empty.

Where are circular buffers common?

Answer: Embedded systems, e.g. serial (UART) receive buffers

Interrupts fill them; the main loop drains them.

With capacity 16, which expression can replace (i + 1) % 16?

Answer: (i + 1) & 15

For powers of two, & (cap - 1) equals % cap.

A circular queue has capacity 4, head = 3 and count = 2. Which slot does the next enqueue use?Related

Answer: 1

(3 + 2) % 4 = 1. The items sit in slots 3 and 0; the next one goes in slot 1.

From Queues and deques

Which is the best tool for a large FIFO queue in Python?Related

Answer: collections.deque with append and popleft

deque adds and removes at both ends in O(1). list.pop(0) shifts every item, which is O(n).

From Stacks and queues

Which removes and returns the head of a Queue, or null if it is empty?Related

Answer: poll()

poll returns null when empty; remove throws.

From Queues, stacks and priority queues

Why does push_front take Node **head?Related

Answer: To change the caller's head pointer

The function must update the caller's pointer.

From Singly linked lists

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 2 more topics in “Data structures in C” before its exam: Binary search trees, Hash tables.
  2. Module 10 exam: Data structures in C · 12 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Debugging and tools. 60 topics left in the course before the final exam.

← Previous: Stacks