Implementing a Circular Buffer in C for Streaming
A circular buffer, or ring buffer, is an essential data structure for handling real-time data streaming efficiently in resource-constrained environments. This article demonstrates how to implement a fixed-capacity circular buffer queue in the C programming language, utilizing static or one-time heap allocation to achieve continuous reads and writes with deterministic \(O(1)\) time complexity and zero runtime memory reallocations.
Core Concepts
A circular buffer manages a continuous stream of data using a fixed-size array and two indices:
- Head (Write pointer): Points to the location where the next incoming byte will be written.
- Tail (Read pointer): Points to the location of the oldest unread byte.
- Wrap-around: When an index reaches the capacity of
the array, it wraps around to the beginning
(
index = (index + 1) % capacity).
By pre-allocating the underlying storage, the system avoids
malloc and realloc calls in latency-sensitive
processing loops, eliminating heap fragmentation and dynamic allocation
overhead.
Data Structure Definition
To manage buffer state without ambiguity between "full" and "empty" conditions, track the current element count alongside the head and tail pointers:
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
typedef struct {
uint8_t *buffer;
size_t head;
size_t tail;
size_t capacity;
size_t count;
} CircularBuffer;Initialization
The buffer is initialized by providing a pre-allocated memory block—either from the stack, static storage, or a one-time heap allocation.
void cb_init(CircularBuffer *cb, uint8_t *storage, size_t capacity) {
cb->buffer = storage;
cb->capacity = capacity;
cb->head = 0;
cb->tail = 0;
cb->count = 0;
}
bool cb_is_empty(const CircularBuffer *cb) {
return cb->count == 0;
}
bool cb_is_full(const CircularBuffer *cb) {
return cb->count == cb->capacity;
}Enqueue Operation (Writing Data)
When streaming data arrives, write operations check whether space is available. If full, the function can either reject the data or overwrite old data depending on system design requirements. The implementation below rejects writes to prevent data loss:
bool cb_push(CircularBuffer *cb, uint8_t data) {
if (cb_is_full(cb)) {
return false; // Buffer overflow: reject write
}
cb->buffer[cb->head] = data;
cb->head = (cb->head + 1) % cb->capacity;
cb->count++;
return true;
}Dequeue Operation (Reading Data)
The consumer consumes data from the tail pointer:
bool cb_pop(CircularBuffer *cb, uint8_t *data) {
if (cb_is_empty(cb)) {
return false; // Buffer underflow: no data to read
}
*data = cb->buffer[cb->tail];
cb->tail = (cb->tail + 1) % cb->capacity;
cb->count--;
return true;
}Complete Usage Example
Below is a complete, working example demonstrating continuous data streaming through the buffer:
#include <stdio.h>
#define BUFFER_CAPACITY 8
int main(void) {
uint8_t raw_memory[BUFFER_CAPACITY];
CircularBuffer cb;
cb_init(&cb, raw_memory, BUFFER_CAPACITY);
// Stream incoming bytes
printf("Enqueuing: ");
for (uint8_t i = 1; i <= 5; i++) {
cb_push(&cb, i);
printf("%d ", i);
}
printf("\n");
// Read partial data
uint8_t value;
printf("Dequeued: ");
for (int i = 0; i < 3; i++) {
if (cb_pop(&cb, &value)) {
printf("%d ", value);
}
}
printf("\n");
// Write additional data to trigger wrap-around
printf("Enqueuing more (wrapping): ");
for (uint8_t i = 6; i <= 9; i++) {
cb_push(&cb, i);
printf("%d ", i);
}
printf("\n");
// Flush remaining buffer
printf("Remaining in buffer: ");
while (cb_pop(&cb, &value)) {
printf("%d ", value);
}
printf("\n");
return 0;
}Optimization Note
If capacity is enforced to be a power of two (\(2^n\)), the expensive modulo operation
(%) can be replaced with a faster bitwise AND
operation:
cb->head = (cb->head + 1) & (cb->capacity - 1);
cb->tail = (cb->tail + 1) & (cb->capacity - 1);This replacement accelerates throughput in high-frequency hardware drivers, audio processing, and networking pipelines.