C
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Arrays and Strings
-
Section 5: Pointers
-
Section 6: Memory Management
-
Section 7: Structures and Unions
-
Section 8: The Preprocessor and Build Process
-
Section 9: Standard Library: stdio.h
-
Section 10: Standard Library: stdlib.h
-
Section 11: Standard Library: string.h
-
Section 12: Standard Library: ctype.h and wctype.h
-
Section 13: Standard Library: math.h, complex.h, fenv.h, tgmath.h
-
Section 14: Standard Library: Type and Limit Headers
-
Section 15: Standard Library: Error Handling and Debugging
-
Section 16: Standard Library: Localization and Encoding
-
Section 17: Standard Library: time.h
-
Section 18: Standard Library: Concurrency (C11)
-
Section 19: POSIX and System Programming (unistd.h)
-
Section 20: More Data Structures
-
Section 21: Algorithms in C
-
Section 22: Bitwise Operations
-
Section 23: Command-Line Programs
-
Section 24: Debugging and Best Practices
-
Section 25: Compiler and Language Internals
-
Section 26: Embedded and Cross-Platform Considerations
-
Section 27: Networking Basics
-
Section 28: Practical Projects
-
Section 29: Interview Practice
-
Section 30: C23 Modern Features
-
Section 31: More Practice and Review
153: Implementing a Circular Buffer
Imagine a sushi conveyor belt. The chef places plates of sushi on the belt at one end, and customers pick them up at another. The belt is a loop; once a plate reaches the end of the track, it simply circles back to the start. If the chef keeps adding sushi faster than people eat it, the belt eventually fills up, and the chef has to stop or start replacing old plates. If the customers eat everything, they're just staring at an empty loop.
That's exactly how a circular buffer—or ring buffer—works in C. Instead of a physical belt, we have a fixed-size array. Instead of a chef and a customer, we have a "write pointer" (the head) and a "read pointer" (the tail). When either pointer reaches the end of the array, it doesn't crash the program or overflow into random memory; it just jumps back to index 0.
Tracking the Head and the Tail
To make this work, we can't just use a raw array. We need a structure to keep track of where we are. I usually bundle the array, the head, the tail, and the current count into a struct. This keeps things clean, especially if you end up needing multiple buffers in one project.
typedef struct {
int buffer[8]; // Our fixed-size "belt"
int head; // Where the next item is written
int tail; // Where the next item is read from
int count; // How many items are currently in the buffer
} CircularBuffer;
In this setup, the head always moves forward as we add data, and the tail follows behind as we consume it. If head == tail and the count is 0, the buffer is empty. If head == tail but the count is 8, it's full.
Handling the Wrap-Around with Modulo
This is the part where most people overthink things by using if statements to check if the index has hit the limit. While that works, the "pro" way to do this is using the modulo operator (%).
When you increment the head, you do this: head = (head + 1) % SIZE;. If the size is 8 and the head is at index 7, (7 + 1) % 8 equals 0. It snaps right back to the start automatically. It's elegant, concise, and avoids a bunch of branching logic that could slow down your code.
Managing the Full-Buffer Dilemma
One of the first decisions you have to make when implementing this is: what happens when the buffer is full? You have two real options. You can either return an error (telling the producer to wait), or you can "overwrite" the oldest data. In high-speed logging or audio streaming, we usually choose the latter—we just advance the tail forward one spot to make room, effectively dropping the oldest piece of data to make way for the newest.
Here is how I'd implement a basic push and pop for this:
void push(CircularBuffer *cb, int value) {
cb->buffer[cb->head] = value;
// If we are full, the tail must move forward to "drop" the oldest item
if (cb->count == 8) {
cb->tail = (cb->tail + 1) % 8;
} else {
cb->count++;
}
cb->head = (cb->head + 1) % 8;
}
int pop(CircularBuffer *cb) {
if (cb->count == 0) {
return -1; // Buffer is empty
}
int value = cb->buffer[cb->tail];
cb->tail = (cb->tail + 1) % 8;
cb->count--;
return value;
}
Notice that the pop function is much simpler. We just grab the value at the tail, nudge the tail forward, and decrement the count. I've used -1 as an error code here, but in a production environment, you'd likely use a boolean success flag or a pointer to return the value.
📋 Practical Task
Implementing a UART-style Log Buffer
In embedded systems, data often arrives from a UART (serial port) faster than the CPU can process it. To prevent data loss, we use a circular buffer to "stage" the incoming bytes.
Your task is to implement a circular buffer specifically for unsigned char (bytes). You must modify the provided template to include a function called flush_buffer. This function should not delete the data, but instead reset both the head and tail to 0 and set the count to 0, effectively clearing the buffer for a fresh start.
Requirements:
- Create a
structnamedLogBufferwith a capacity of 16 bytes. - Implement
push_byte: If the buffer is full, it should overwrite the oldest byte (advance the tail). - Implement
pop_byte: Return the oldest byte and update the tail. - Implement
flush_buffer: Reset the buffer state completely. - Write a
mainfunction that pushes 20 different bytes into the 16-byte buffer, then pops all remaining bytes to verify that only the last 16 were preserved.
There are no comments for now.