By Safa Mulani, Bradley Kouchi and Manikandan Kurup
A stack represents a fundamental data structure built on the Last-In, First-Out (LIFO) principle. Imagine a stack of plates where you naturally add and remove from the top. In C programming, you’ll need to implement stack functionality yourself, as it’s not a built-in feature. This gives you direct control over how your stack behaves and performs. Stacks play a dual role in your programming toolkit: they quietly manage function calls through the system’s call stack, while serving as powerful algorithmic tools for expression evaluation and backtracking challenges.
This article will take you through a complete journey to understanding the stack in C programming. You’ll start by understanding the core operations such as push
, pop
, and peek
, that define stack behavior. From there, you’ll build working implementations using two distinct approaches: array-based structures for fixed-size needs and dynamic linked lists for flexible memory management. You’ll also explore the performance characteristics and trade-offs of each method, equipping you with the knowledge to make informed architectural decisions based on your specific requirements and the practical scenarios where stacks deliver optimal solutions.
push
(add to top), pop
(remove from top), and peek
(view top).At its core, a stack maintains a collection of elements with strictly controlled access patterns. You can only interact with the element at the “top” of the stack, making it a restricted but highly predictable data structure. This limitation becomes a strength, providing clear behavioral guarantees that make stacks invaluable for specific computational tasks.
Stacks solve everyday programming problems in surprisingly simple ways. Some common examples include:
Understanding how a stack works centers on mastering its fundamental operations. These operations define your stack’s behavior and provide the interface for manipulating the data it stores. Each operation follows the same LIFO principle we’ve been discussing, creating a consistent and predictable programming experience.
push()
The push
operation is used to add a new element to the stack. This element is placed at the very top, becoming the new “topmost” item. Think of it as placing another book on top of a pile. This operation increases the stack’s size by one. In a fixed-size stack, it’s essential to first check if the stack is full to avoid a stack overflow, an error that occurs when there’s no space for the new element.
pop()
The pop
operation performs two actions at once: it removes the topmost element from the stack and returns that element’s value. This is the only way to remove elements. After a pop
, the element that was previously second from the top becomes the new top. If you try to pop
from an empty stack, you’ll encounter a critical error known as a stack underflow, as there is no element to remove or return.
peek()
The peek
operation (sometimes called top
) allows you to inspect the value of the topmost element without removing it. It’s a read-only action, like looking at the title of the top book in a pile without lifting it off. This is incredibly useful when you need to know what the next item is to make a decision, but you aren’t ready to remove it from the stack yet. Like pop
, attempting to peek
at an empty stack will also result in a stack underflow.
isEmpty()
The isEmpty
operation is a simple utility function that returns True
if the stack contains zero elements and False
otherwise. It serves as a crucial safety check. Before calling pop
or peek
, you should always use isEmpty
to confirm the stack actually has an element to act upon, thereby preventing underflow errors and potential program crashes.
isFull()
The isFull
operation is the counterpart to isEmpty
. It checks if the stack has reached its maximum capacity and returns True
if it has, and False
if there is still room. This operation is only relevant for stacks with a fixed, predefined size, such as those implemented using an array. It acts as a guard for the push
operation, allowing you to prevent stack overflow errors.
In summary:
Operation | What It Does | Speed |
---|---|---|
push |
Add item to top | O(1) |
pop |
Remove top item | O(1) |
peek /top |
View top item (no removal) | O(1) |
isEmpty |
Check if stack is empty | O(1) |
isFull |
Check if stack is full | O(1) |
These operations form your complete stack toolkit. Each one serves a specific purpose, and together they give you everything you need to implement stack-based solutions in your programs. The beauty lies in their simplicity—five straightforward operations that can solve surprisingly complex programming challenges.
One of the most straightforward ways to implement a stack is by using an array. This approach is simple to understand and efficient for many use cases. We use a static, fixed-size array to store the elements and an integer variable, often called top
, to keep track of the last inserted element.
struct
and LogicTo represent our stack, we’ll define a C struct
. This structure will bundle together all the necessary components:
top
: An integer that holds the index of the topmost element in the array. We initialize top
to -1
to signify that the stack is initially empty.capacity
: An unsigned integer that stores the maximum number of elements the stack can hold. This is the fixed size of our array.array
: A pointer to an integer (int*
). This pointer will point to the block of memory we dynamically allocate for our array, where the stack elements will be stored.The core logic revolves around the top
index:
top
, then place the new element at array[top]
.array[top]
and then decrement top
.top
is -1
.top
is equal to capacity - 1
.This array-based implementation is highly efficient because accessing elements by their index is a very fast operation.
Here is a complete C program that defines an array-based stack and demonstrates its core operations in a main
function.
#include <stdio.h>
#include <stdlib.h> // For malloc and free
#include <limits.h> // For INT_MIN
// A structure to represent a stack
struct Stack {
int top;
unsigned capacity;
int* array;
};
// Function to create a stack of given capacity.
struct Stack* createStack(unsigned capacity) {
struct Stack* stack = (struct Stack*)malloc(sizeof(struct Stack));
if (!stack) {
return NULL;
}
stack->capacity = capacity;
stack->top = -1;
stack->array = (int*)malloc(stack->capacity * sizeof(int));
if (!stack->array) {
free(stack);
return NULL;
}
return stack;
}
int isFull(struct Stack* stack) {
return stack->top == stack->capacity - 1;
}
int isEmpty(struct Stack* stack) {
return stack->top == -1;
}
void push(struct Stack* stack, int item) {
if (isFull(stack)) {
printf("Stack Overflow\n");
return;
}
stack->array[++stack->top] = item;
printf("%d pushed to stack\n", item);
}
int pop(struct Stack* stack) {
if (isEmpty(stack)) {
printf("Stack Underflow\n");
return INT_MIN;
}
return stack->array[stack->top--];
}
int peek(struct Stack* stack) {
if (isEmpty(stack)) {
printf("Stack is empty\n");
return INT_MIN;
}
return stack->array[stack->top];
}
void freeStack(struct Stack* stack) {
if (stack) {
free(stack->array);
free(stack);
}
}
int main() {
struct Stack* stack = createStack(5);
if (!stack) {
printf("Failed to create stack.\n");
return 1;
}
push(stack, 10);
push(stack, 20);
push(stack, 30);
printf("\nTop element is %d\n", peek(stack));
printf("%d popped from stack\n", pop(stack));
printf("After popping, top element is %d\n\n", peek(stack));
push(stack, 40);
push(stack, 50);
push(stack, 60);
push(stack, 70); // This will cause a stack overflow
printf("\nPopping all elements:\n");
while(!isEmpty(stack)) {
printf("%d popped from stack\n", pop(stack));
}
pop(stack); // This will cause a stack underflow
freeStack(stack);
return 0;
}
The code uses a struct Stack
to group three things: a pointer to an integer array (used to store the elements), the stack’s maximum size, and an index called top
to track the most recent item. The createStack
function allocates memory and sets top
to -1
to show the stack is empty. To add an item, top
is increased, and the item is stored at that position. To remove an item, the value at the current top
index is returned, and then top
is decremented. All stack operations are done by updating this top
index, and the freeStack
function cleans up the memory to avoid leaks.
Output:
10 pushed to stack
20 pushed to stack
30 pushed to stack
Top element is 30
30 popped from stack
After popping, top element is 20
40 pushed to stack
50 pushed to stack
60 pushed to stack
Stack Overflow
Popping all elements:
60 popped from stack
50 popped from stack
40 popped from stack
20 popped from stack
10 popped from stack
Stack Underflow
While an array is a simple way to build a stack, it has a significant limitation: a fixed size. We must define its capacity beforehand, which can lead to wasted memory or a "Stack Overflow"
error if we exceed the limit.
A linked list provides a more flexible and dynamic solution. A linked list-based stack can grow and shrink as needed at runtime, limited only by the available system memory.
Node
struct and Pointer LogicThe foundation of this implementation is the Node
. Each node in our linked list will be a small structure containing two things:
data
: The actual value (e.g., an integer) we want to store.next
: A pointer to the next Node
in the stack.To manage the stack, we only need a single pointer, let’s call it top
. This top
pointer always points to the head of the linked list, which represents the most recently added element. If top
is NULL
, the stack is empty.
The logic for the stack operations becomes about manipulating the head of the list:
push
:
next
pointer of this new node to point to the current top
node.top
pointer to point to our newly created node.pop
:
top == NULL
).data
from the top
node to be returned later.top
node so we don’t lose it.top
pointer to the next node in the list (top = top->next
).top
node using the temporary pointer.isEmpty
: The stack is empty if top
is NULL
.isFull
: This concept doesn’t apply. The stack is never “full” in the traditional sense. A push
operation only fails if the system runs out of memory (malloc
returns NULL
).The following C program implements a stack using a linked list and demonstrates its core functions.
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
struct StackNode {
int data;
struct StackNode* next;
};
struct StackNode* newNode(int data) {
struct StackNode* stackNode = (struct StackNode*)malloc(sizeof(struct StackNode));
if (!stackNode) {
printf("Heap Overflow\n");
return NULL;
}
stackNode->data = data;
stackNode->next = NULL;
return stackNode;
}
int isEmpty(struct StackNode* top) {
return !top;
}
void push(struct StackNode** top_ref, int data) {
struct StackNode* stackNode = newNode(data);
if (!stackNode) return;
stackNode->next = *top_ref;
*top_ref = stackNode;
printf("%d pushed to stack\n", data);
}
int pop(struct StackNode** top_ref) {
if (isEmpty(*top_ref)) {
printf("Stack Underflow\n");
return INT_MIN;
}
struct StackNode* temp = *top_ref;
int popped = temp->data;
*top_ref = (*top_ref)->next;
free(temp);
return popped;
}
int peek(struct StackNode* top) {
if (isEmpty(top)) {
printf("Stack is empty\n");
return INT_MIN;
}
return top->data;
}
int main() {
struct StackNode* top = NULL;
push(&top, 10);
push(&top, 20);
push(&top, 30);
printf("\nTop element is %d\n", peek(top));
printf("%d popped from stack\n", pop(&top));
printf("Top element is now %d\n", peek(top));
printf("\nPopping all elements...\n");
while(!isEmpty(top)) {
printf("%d popped from stack\n", pop(&top));
}
pop(&top);
return 0;
}
This code builds a stack using a linked list of StackNode
structures. The top
pointer always points to the first node, which holds the most recent item. If top
is NULL
, the stack is empty. To push an item, a new node is created and added to the front of the list. To pop, top
is moved to the next node, and the old node is freed from memory. This approach allows the stack to grow and shrink as needed, using only as much memory as required.
Output:
10 pushed to stack
20 pushed to stack
30 pushed to stack
Top element is 30
30 popped from stack
Top element is now 20
Popping all elements...
20 popped from stack
10 popped from stack
Stack Underflow
Choosing between an array-based and a linked list-based stack depends entirely on the specific requirements of your application. Each implementation has distinct trade-offs in terms of memory, flexibility, and performance.
This table summarizes the key differences between the two approaches.
Feature | Array-Based Stack | Linked List-Based Stack |
---|---|---|
Size | Fixed size, defined at creation. | Dynamic size, grows and shrinks as needed. |
Memory Usage | Can lead to wasted memory if the stack is often under-full. | Has a small memory overhead for the next pointer in every single node. |
Stack Overflow | A primary concern. Occurs when pushing to a full stack. | Not an issue. The limit is available system memory (a “Heap Overflow”). |
Memory Allocation | A single, contiguous block of memory is allocated once. | Memory for each node is allocated individually and can be scattered across the heap. |
Implementation Complexity | Simple to implement. | Slightly more complex due to pointer management. |
Note: Heap overflow can occur when the system runs out of memory for dynamic allocations (e.g., due to too many malloc
calls). It’s less predictable than array-based overflow, which happens when a fixed limit is reached.
While both implementations are highly efficient, there are subtle but important performance differences to consider.
The primary stack operations (push
, pop
, and peek
) are all O(1) (constant time) operations in both array and linked list implementations.
top
index. This is a direct memory access, which takes the same amount of time regardless of the stack’s size.top
node (the head of the list). Since we always have a direct pointer to the head, we never need to traverse the list. This also takes a constant amount of time.Theoretically, in terms of complexity, both are equally fast.
The significant performance difference comes from cache locality. Modern CPUs use a small, fast memory cache to speed up access to frequently used data. It’s much faster for a CPU to access data that is stored sequentially in memory (good cache locality) than data that is scattered randomly.
Array-Based Stack: This implementation has excellent cache locality. Since the entire stack is stored in one contiguous block of memory, when one element is accessed, its neighbors are often loaded into the CPU cache automatically. This makes subsequent operations on nearby elements extremely fast.
Linked List-Based Stack: This implementation typically has poor cache locality. Each node is allocated separately on the heap and can reside anywhere in memory. Accessing the next node in memory (top = top->next
) can still result in a cache miss.
While both are O(1) on paper, the array-based stack is often faster in practice due to its superior cache locality. If you can predict the maximum size of your stack and performance is critical, an array is usually the better choice. If you need dynamic sizing and cannot risk a stack overflow, a linked list is the safer, more flexible option.
Writing a correct stack implementation goes beyond just the basic logic. Paying attention to edge cases and memory management is crucial for creating robust, bug-free code.
Many of the bugs in stack implementations stem from a few common, easy-to-make errors. Here’s what to watch out for.
top
IndexThis is the most frequent bug in array-based stacks. Since arrays are 0-indexed, the top
variable must be managed precisely.
top
should be initialized to -1
, not 0
. This correctly signifies an empty stack. Starting at 0
implies there is already one element at index 0
.top
is equal to capacity - 1
, not capacity
. Forgetting the -1
leads to attempting to write one element past the allocated memory boundary.++top
) with post-increment (top++
) can be disastrous. The correct logic is to increment top
before adding the element (stack[++top] = item;
) and to decrement top
after retrieving the element (item = stack[top--];
).These checks are not optional suggestions; they are critical for program stability.
push
an element onto a full array-based stack will write data out of bounds. This is a classic buffer overflow, which can corrupt adjacent memory, leading to unpredictable crashes and creating serious security vulnerabilities.pop
or peek
from an empty stack is even more immediately fatal. It results in trying to access an invalid memory address (e.g., array[-1]
or NULL->data
), causing a segmentation fault that will crash your program instantly.This mistake is specific to linked list-based stacks. Every push
operation allocates memory with malloc()
. It is your responsibility to free that memory during a pop
.
top
pointer to the next node (top = top->next;
) and forget about the original top
node. When you do this, the pointer to the old node is lost, but its memory remains allocated. This is a memory leak. Over time, repeated leaks will consume all available memory and crash the application.top
node, advance the real top
pointer, and then call free()
on the temporary pointer.NULL
PointersThis is the primary danger in a linked list implementation. Dereferencing a NULL
pointer, i.e., trying to access a member of a pointer that is NULL
(e.g., NULL->data
), causes an immediate program crash.
pop()
or peek()
on an empty stack where top
is already NULL
. The isEmpty()
check is your first line of defense.malloc()
fails to allocate memory (returns NULL
) and your code doesn’t check for this failure before trying to use the newly created node.Following established conventions and practices will make your code more reliable, reusable, and easier for others to understand.
Consistent Error Handling: Decide on a single, consistent strategy for handling errors like underflow or memory allocation failures. For example, you could have pop
and peek
return a special value like INT_MIN
on error. Another approach is to pass a pointer for an error code variable to your functions. The key is consistency. Mixing different error-handling methods (returning error codes, printing to the console, and exiting) makes your stack API unpredictable and difficult for other programmers to use correctly.
Using unsigned for Capacity/Size: A stack’s capacity or element count can logically never be negative. Representing these values with a standard int
is inefficient and less clear. Instead, use an unsigned int
or, even better, size_t
. The size_t
type is the standard C type for sizes of objects and memory regions. Using it clearly communicates that the value represents a size and is non-negative, making your code’s intent more explicit and robust.
Writing Modular Code: Don’t write your entire program in one file. Separate the stack’s implementation from its usage. The standard practice is to place the struct
definition and function declarations (the API) in a header file (e.g., stack.h
). The actual code for those functions goes into a source file (e.g., stack.c
). Any program that needs to use your stack can then simply #include "stack.h"
. This promotes reusability, improves maintainability, and provides a clean abstraction layer—the user of your stack shouldn’t have to care if it’s implemented with an array or a linked list.
A stack in C is a conceptual data structure, not a built-in type. It stores a collection of elements and operates on a Last-In, First-Out (LIFO) principle. This means the last element added to the stack is the first one to be removed. Think of it as a stack of plates; you add new plates to the top and also remove them from the top. You implement it yourself using an array or a linked list, creating functions like push()
to add items and pop()
to remove them.
Neither is universally “better”; the best choice depends on your needs:
You can prevent overflow in an array-based stack by always checking if the stack is full before pushing a new element. Implement and use an isFull()
function that checks if top == capacity - 1
. If it’s full, you refuse to push the element. Alternatively, use a linked-list implementation, which doesn’t have this overflow problem.
A stack and a queue are both abstract data structures, but they follow opposite principles.
push
) and remove from the top (pop
).enqueue
) and remove from the front (dequeue
).The stack and heap are two different regions of memory where a C program stores its data. They should not be confused with the stack data structure.
malloc()
and free()
. It’s more flexible and much larger than the stack but slightly slower to access.Our linked-list-based stack implementation stores its nodes on the heap. An array-based stack can be declared locally (on the call stack) or dynamically (on the heap) depending on how it is created.
Yes, and it’s an excellent hybrid approach. A dynamic array starts with an initial capacity. When a push
operation occurs on a full stack, the implementation automatically:
This provides the performance benefits of contiguous memory like an array while offering the flexible, growing nature of a linked list.
pop
and peek
?The difference is whether the stack is modified.
pop()
: Removes the topmost element from the stack and returns its value. It is an operation that changes the stack’s state.peek()
: Returns the value of the topmost element without removing it. It is a read-only operation that leaves the stack unchanged.Stack overflow is an issue specific to fixed-size data structures.
capacity
number of elements, the array is full. Trying to push one more element causes an “overflow” because there is no allocated space for it.Learning how to implement a stack in C is a great way to build skills in memory management, performance trade-offs, and clean code design. This article took you through everything: from the basic LIFO concept to building stacks from scratch.
We covered two main methods: the fast but fixed-size array-based stack, and the more flexible linked list-based stack. Along the way, you learned how to compare their performance, manage memory safely, and avoid common mistakes. With these skills, you’re now ready to write stack code that’s both efficient and reliable—an important step toward mastering more advanced data structures and real-world programming.
Ready to keep learning? Now that you understand stacks, expand your C programming skills with these excellent articles:
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
With over 6 years of experience in tech publishing, Mani has edited and published more than 75 books covering a wide range of data science topics. Known for his strong attention to detail and technical knowledge, Mani specializes in creating clear, concise, and easy-to-understand content tailored for developers.
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.
Full documentation for every DigitalOcean product.
The Wave has everything you need to know about building a business, from raising funding to marketing your product.
Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.
New accounts only. By submitting your email you agree to our Privacy Policy
Scale up as you grow — whether you're running one virtual machine or ten thousand.
Sign up and get $200 in credit for your first 60 days with DigitalOcean.*
*This promotional offer applies to new accounts only.