Circular Queues Explained
A circular queue is a fixed-size queue that treats its underlying array as a ring: when the rear reaches the end of the array, it wraps back around to the front to reuse slots that earlier dequeues freed up. This solves the wasted-space problem of a simple array queue while keeping enqueue and dequeue at O(1).
The problem it solves
In a plain array queue, front and rear only move forward. After several dequeues, the slots at the start of the array sit empty and unusable — the queue reports "full" even though there is free space at the beginning. A circular queue fixes this by letting the indices wrap around using the modulo operator, so freed slots get reused.
How wraparound works
Instead of rear = rear + 1, a circular queue does rear = (rear + 1) % capacity. When rear is at the last index, (rear + 1) % capacity brings it back to 0. The array behaves like a circle with no real "end". The same trick applies to front.
The tricky part is telling full from empty, because in both cases front and rear can point to the same slot. A common, clear solution is to track a separate count of stored items.
Implementing a circular queue in C++
#include <iostream>
using namespace std;
class CircularQueue {
int* data;
int capacity;
int front; // index of the front element
int rear; // index of the last element
int count; // how many elements are stored
public:
CircularQueue(int cap) {
capacity = cap;
data = new int[cap];
front = 0;
rear = -1;
count = 0;
}
~CircularQueue() { delete[] data; }
bool isFull() { return count == capacity; }
bool isEmpty() { return count == 0; }
// Add a value at the rear, wrapping around if needed. O(1).
bool enqueue(int value) {
if (isFull()) return false; // no room
rear = (rear + 1) % capacity; // wrap the rear index
data[rear] = value;
count++;
return true;
}
// Remove and return the front value, wrapping around. O(1).
bool dequeue(int& out) {
if (isEmpty()) return false; // nothing to remove
out = data[front];
front = (front + 1) % capacity; // wrap the front index
count--;
return true;
}
};
int main() {
CircularQueue q(3);
q.enqueue(10);
q.enqueue(20);
q.enqueue(30); // queue is now full
int x;
q.dequeue(x); // x = 10, frees a slot at the start
q.enqueue(40); // 40 wraps into the freed slot
while (q.dequeue(x)) {
cout << x << " "; // prints: 20 30 40
}
return 0;
}
Notice how 40 reuses the slot that 10 vacated. Without wraparound, that slot would be stranded.
Want to learn this properly?
Join the waitlist for our courses — beginner-friendly, project-first classes in Jalgaon.
Browse coursesWhere circular queues are used
This pattern is everywhere in systems programming under the name ring buffer or circular buffer: audio and video streaming buffers, keyboard input buffers, network packet queues, and producer-consumer pipelines. Any time you have a fixed amount of memory and a steady flow of data passing through, a circular queue is the natural fit.
Common mistakes
- Confusing full and empty. When
frontandrearcollide, you cannot tell full from empty by indices alone. Track acount(as above) or sacrifice one slot to disambiguate. - Forgetting the modulo. Incrementing
rearwithout% capacityruns off the end of the array. The wraparound is the whole point. - Off-by-one on capacity. If you use the "leave one slot empty" trick instead of a count, remember your real capacity is
size - 1. - Mixing the two strategies. Either use a
countor leave a sentinel slot — do not half-implement both, or your full/empty checks will disagree. - Resizing assumptions. A circular queue is fixed-size by design. If you need it to grow, you must allocate a bigger array and copy elements in order, which is more involved than a normal array because of the wrap.
FAQ
Is a circular queue the same as a ring buffer? In practice, yes — ring buffer and circular buffer are common names for the same idea, especially in systems and embedded programming.
Why not just use a normal queue? A linked queue grows freely but uses more memory per item and is less cache-friendly. A circular queue gives you O(1) operations in a fixed, contiguous block, which is ideal when memory is bounded.
Keep learning
Start with the basics in Queues Explained, revisit Arrays as a Data Structure to understand the contiguous storage underneath, and browse the full DSA hub.
Want to implement ring buffers with instructor feedback in Jalgaon? Join the waitlist for our Data Structures & Algorithms course.
Want to learn this properly?
Join the waitlist for our courses — beginner-friendly, project-first classes in Jalgaon.
Browse coursesFounder, Infoplanet
Atul Kabra founded Infoplanet in 2001 and has spent over two decades teaching programming — C, C++, Java, databases and more — to students across Maharashtra.
Related guides
Arrays as a Data Structure
A clear introduction to arrays: how contiguous storage gives O(1) random access, why inserting in the middle is O(n), and when to reach for an array versus another structure.
Big-O Notation & Time Complexity, Simply Explained
A plain-language guide to Big-O notation and time complexity, covering the common growth rates with worked examples so you can reason about how fast your code really is.
Binary Search Trees Explained
Understand the binary search tree: how the left-smaller, right-larger rule enables O(log n) search on a balanced tree, and why balance is the whole game.
