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
152: Implementing a Queue
I want to build a system to handle print jobs. In a real-world scenario, you can't just print everything at once; the printer handles one document, then the next, and so on. This is the classic "First-In, First-Out" (FIFO) behavior of a queue. I've got a basic understanding of arrays, but a queue needs to be dynamic in how it handles its head and tail.
The Naive Approach: Shifting the Line
My first instinct is to just use a fixed-size array and a count variable. When a job comes in, I'll put it at the end. When a job finishes, I'll take it from the front. Let's see how that looks in code.
typedef struct {
int jobs[10];
int count;
} PrintQueue;
void enqueue(PrintQueue *q, int jobID) {
if (q->count < 10) {
q->jobs[q->count++] = jobID;
}
}
int dequeue(PrintQueue *q) {
if (q->count == 0) return -1;
int job = q->jobs[0];
for (int i = 0; i < q->count - 1; i++) {
q->jobs[i] = q->jobs[i + 1];
}
q->count--;
return job;
}
It works, but it's inefficient. Every single time I dequeue a job, I'm shifting every other job in the array one spot to the left. If I have a thousand jobs in the queue, that's a lot of wasted CPU cycles. I'm basically treating the array like a sliding window, but I'm physically moving the data. That's a rookie mistake. We can do better.
Stop Moving the Data, Move the Pointer
Instead of shifting the elements, why not just keep track of where the "front" of the queue is? I'll add a front index. When I dequeue, I just increment front. The data stays exactly where it is.
typedef struct {
int jobs[10];
int front;
int rear;
int count;
} PrintQueue;
void enqueue(PrintQueue *q, int jobID) {
if (q->count < 10) {
q->jobs[q->rear] = jobID;
q->rear++;
q->count++;
}
}
int dequeue(PrintQueue *q) {
if (q->count == 0) return -1;
int job = q->jobs[q->front];
q->front++;
q->count--;
return job;
}
This is much faster—O(1) time complexity. But I've just created a new problem. Let's say I enqueue 10 jobs and then dequeue 10 jobs. My front and rear pointers are both at index 10. Even though the queue is empty, the enqueue function will think the array is full because rear has hit the boundary. I've "walked" off the end of my memory.
Closing the Loop
The solution here is a circular buffer. Instead of letting rear and front march off into the sunset, I'll make them wrap back around to zero when they hit the end of the array. The modulo operator % is perfect for this.
I'll rewrite the logic so that the indices wrap around based on the capacity of the array. This way, as long as the count is less than the maximum size, I can keep adding and removing jobs indefinitely without ever needing to shift data.
#define MAX_JOBS 10
typedef struct {
int jobs[MAX_JOBS];
int front;
int rear;
int count;
} PrintQueue;
void initQueue(PrintQueue *q) {
q->front = 0;
q->rear = 0;
q->count = 0;
}
void enqueue(PrintQueue *q, int jobID) {
if (q->count == MAX_JOBS) {
// Queue is full!
return;
}
q->jobs[q->rear] = jobID;
q->rear = (q->rear + 1) % MAX_JOBS; // Wrap around
q->count++;
}
int dequeue(PrintQueue *q) {
if (q->count == 0) {
// Queue is empty!
return -1;
}
int job = q->jobs[q->front];
q->front = (q->front + 1) % MAX_JOBS; // Wrap around
q->count--;
return job;
}
Now we have a professional implementation. The data stays put, the pointers cycle around, and we have a constant-time operation for both adding and removing. It's clean, it's efficient, and it doesn't waste cycles moving memory around for no reason.
📋 Practical Task
Build a Circular Message Buffer
You are tasked with implementing a small messaging system for an embedded device. The device receives short integer-based "message codes" from a sensor and needs to process them in the order they arrived.
Implement a circular queue in C with the following requirements:
- A maximum capacity of 5 elements.
- An
enqueue_msgfunction that adds a message code to the queue. If the queue is full, it should print "Buffer Overflow" and not add the message. - A
dequeue_msgfunction that removes and returns the oldest message. If the queue is empty, it should return -1. - A
peek_msgfunction that returns the oldest message without removing it from the queue.
Test your implementation by enqueuing 6 messages (to trigger the overflow), dequeuing 3 of them, and then enqueuing 2 more to verify the circular wrapping logic is working correctly.
There are no comments for now.