📚 What is a Stack?
A stack is a linear data structure that follows the Last In, First Out (LIFO) principle — the element inserted last is the one removed first. Think of a stack of plates: you always add a new plate on top, and you always pick up from the top. You cannot access the plate at the bottom without removing all the plates above it first.
In C, a stack is most commonly implemented using a fixed-size array with an integer variable called top that always holds the index of the topmost element. When the stack is empty, top = -1. Every push increments top and writes to arr[top]. Every pop reads from arr[top] and decrements top.
Stacks are used everywhere in computer science: function call management (the call stack), expression evaluation, bracket matching, undo/redo, backtracking algorithms, depth-first search, and syntax parsing in compilers.
struct containing the data array and the top index. All six operations are implemented as separate functions that take a pointer to the struct. The main function demonstrates every operation — normal pushes, overflow detection, pop-all, underflow detection, and a visual display of the stack contents after every meaningful change. Every edge case is tested so you can see exactly what happens in each scenario.
#include <stdio.h> #include <stdlib.h> #define MAX 6 /* ── Stack structure ── */ typedef struct { int data[MAX]; int top; /* index of topmost element; -1 = empty */ } Stack; /* ── Initialise ── */ void initStack(Stack *s) { s->top = -1; printf("Stack initialised (capacity = %d)\n", MAX); } /* ── Predicates ── */ int isEmpty(Stack *s) { return s->top == -1; } int isFull (Stack *s) { return s->top == MAX - 1; } int size (Stack *s) { return s->top + 1; } /* ── Push: add element to top ── */ int push(Stack *s, int val) { if (isFull(s)) { printf(" OVERFLOW — cannot push(%d), stack is full!\n", val); return 0; } s->data[++s->top] = val; printf(" push(%2d) top=%d size=%d\n", val, s->top, size(s)); return 1; } /* ── Pop: remove and return top element ── */ int pop(Stack *s) { if (isEmpty(s)) { printf(" UNDERFLOW — cannot pop, stack is empty!\n"); return -1; } int val = s->data[s->top--]; printf(" pop() -> %2d top=%d size=%d\n", val, s->top, size(s)); return val; } /* ── Peek: read top without removing ── */ int peek(Stack *s) { if (isEmpty(s)) { printf(" peek() — stack is empty!\n"); return -1; } printf(" peek() -> %d (top stays at %d)\n", s->data[s->top], s->top); return s->data[s->top]; } /* ── Display: print stack top-to-bottom ── */ void display(Stack *s) { if (isEmpty(s)) { printf(" [stack is empty]\n"); return; } printf(" Stack (TOP -> BOTTOM): "); for (int i = s->top; i >= 0; i--) printf("| %d ", s->data[i]); printf("|\n"); } int main() { Stack s; initStack(&s); /* 1. Normal pushes */ printf("\n--- Push 6 elements ---\n"); push(&s, 10); push(&s, 25); push(&s, 38); push(&s, 47); push(&s, 56); push(&s, 62); display(&s); /* 2. Overflow */ printf("\n--- Overflow test ---\n"); push(&s, 99); /* 3. Peek */ printf("\n--- Peek ---\n"); peek(&s); /* 4. Size and full/empty status */ printf("\n--- Status ---\n"); printf(" size() = %d\n", size(&s)); printf(" isEmpty() = %d\n", isEmpty(&s)); printf(" isFull() = %d\n", isFull(&s)); /* 5. Pop three elements */ printf("\n--- Pop 3 elements ---\n"); pop(&s); pop(&s); pop(&s); display(&s); /* 6. Push more after pop (reuse space) */ printf("\n--- Push 2 more after pop ---\n"); push(&s, 71); push(&s, 85); display(&s); /* 7. Drain the stack completely */ printf("\n--- Drain all elements ---\n"); while (!isEmpty(&s)) pop(&s); display(&s); /* 8. Underflow */ printf("\n--- Underflow test ---\n"); pop(&s); peek(&s); printf("\n=== Done ===\n"); return 0; }
Stack initialised (capacity = 6) --- Push 6 elements --- push(10) top=0 size=1 push(25) top=1 size=2 push(38) top=2 size=3 push(47) top=3 size=4 push(56) top=4 size=5 push(62) top=5 size=6 Stack (TOP -> BOTTOM): | 62 | 56 | 47 | 38 | 25 | 10 | --- Overflow test --- OVERFLOW — cannot push(99), stack is full! --- Peek --- peek() -> 62 (top stays at 5) --- Status --- size() = 6 isEmpty() = 0 isFull() = 1 --- Pop 3 elements --- pop() -> 62 top=4 size=5 pop() -> 56 top=3 size=4 pop() -> 47 top=2 size=3 Stack (TOP -> BOTTOM): | 38 | 25 | 10 | --- Push 2 more after pop --- push(71) top=3 size=4 push(85) top=4 size=5 Stack (TOP -> BOTTOM): | 85 | 71 | 38 | 25 | 10 | --- Drain all elements --- pop() -> 85 top=3 size=4 pop() -> 71 top=2 size=3 pop() -> 38 top=1 size=2 pop() -> 25 top=0 size=1 pop() -> 10 top=-1 size=0 [stack is empty] --- Underflow test --- UNDERFLOW — cannot pop, stack is empty! peek() — stack is empty! === Done ===
top is decremented but the old value still sits in the array — it is just invisible to the stack logic because nothing looks past top. When you push again, the new value simply overwrites that slot. This is why stack operations are O(1) — there is zero cleanup work.-1 for an empty-stack pop is a common convention. The caller should always check isEmpty() before calling pop(), or check that the return value is not the sentinel. A safer design in production code uses a boolean output parameter or a tagged return struct instead of a magic number.TYPE, DELETE, FORMAT, INSERT, and PASTE.
#include <stdio.h> #include <string.h> #define MAX 10 #define DLEN 40 /* ── Action struct — what was done ── */ typedef struct { char type[12]; /* "TYPE", "DELETE", etc. */ char data[DLEN]; /* the text involved */ } Action; /* ── Action Stack ── */ typedef struct { Action items[MAX]; int top; } AStack; void as_init (AStack *s) { s->top = -1; } int as_empty(AStack *s) { return s->top == -1; } int as_full (AStack *s) { return s->top == MAX-1; } void as_push (AStack *s, Action a) { if(!as_full(s)) s->items[++s->top]=a; } Action as_pop (AStack *s) { return s->items[s->top--]; } Action as_peek (AStack *s) { return s->items[s->top]; } /* ── Pretty-print a stack ── */ void showStack(const char *name, AStack *s) { printf(" %-12s[ ", name); for (int i = 0; i <= s->top; i++) printf("%s:%s ", s->items[i].type, s->items[i].data); printf("%s]\n", s->top == -1 ? "(empty) " : ""); } /* ── Editor operations ── */ void doAction(AStack *undo, AStack *redo, const char *type, const char *data) { Action a; strncpy(a.type, type, 11); strncpy(a.data, data, DLEN-1); /* New action clears the redo stack */ as_init(redo); as_push(undo, a); printf(" ACTION %s \"%s\"\n", type, data); } void doUndo(AStack *undo, AStack *redo) { if (as_empty(undo)) { printf(" UNDO nothing to undo!\n"); return; } Action a = as_pop(undo); as_push(redo, a); printf(" UNDO <- %s \"%s\" (moved to redo)\n", a.type, a.data); } void doRedo(AStack *undo, AStack *redo) { if (as_empty(redo)) { printf(" REDO nothing to redo!\n"); return; } Action a = as_pop(redo); as_push(undo, a); printf(" REDO -> %s \"%s\" (moved to undo)\n", a.type, a.data); } void status(AStack *undo, AStack *redo) { showStack("Undo stack:", undo); showStack("Redo stack:", redo); printf("\n"); } int main() { AStack undo, redo; as_init(&undo); as_init(&redo); printf("=== Text Editor — Undo / Redo Demo ===\n\n"); /* Perform 5 actions */ printf("--- Perform 5 actions ---\n"); doAction(&undo, &redo, "TYPE", "Hello"); doAction(&undo, &redo, "TYPE", "World"); doAction(&undo, &redo, "FORMAT", "Bold"); doAction(&undo, &redo, "INSERT", "Image"); doAction(&undo, &redo, "DELETE", "World"); status(&undo, &redo); /* Undo 3 times */ printf("--- Undo 3 times ---\n"); doUndo(&undo, &redo); doUndo(&undo, &redo); doUndo(&undo, &redo); status(&undo, &redo); /* Redo 2 times */ printf("--- Redo 2 times ---\n"); doRedo(&undo, &redo); doRedo(&undo, &redo); status(&undo, &redo); /* New action clears redo */ printf("--- New action (clears redo stack) ---\n"); doAction(&undo, &redo, "PASTE", "Ananta"); status(&undo, &redo); /* Try to redo after new action */ printf("--- Try redo after new action ---\n"); doRedo(&undo, &redo); /* Undo everything */ printf("\n--- Undo everything ---\n"); while (!as_empty(&undo)) doUndo(&undo, &redo); doUndo(&undo, &redo); /* one more — should warn */ status(&undo, &redo); return 0; }
=== Text Editor — Undo / Redo Demo === --- Perform 5 actions --- ACTION TYPE "Hello" ACTION TYPE "World" ACTION FORMAT "Bold" ACTION INSERT "Image" ACTION DELETE "World" Undo stack: [ TYPE:Hello TYPE:World FORMAT:Bold INSERT:Image DELETE:World ] Redo stack: [ (empty) ] --- Undo 3 times --- UNDO <- DELETE "World" (moved to redo) UNDO <- INSERT "Image" (moved to redo) UNDO <- FORMAT "Bold" (moved to redo) Undo stack: [ TYPE:Hello TYPE:World ] Redo stack: [ DELETE:World INSERT:Image FORMAT:Bold ] --- Redo 2 times --- REDO -> FORMAT "Bold" (moved to undo) REDO -> INSERT "Image" (moved to undo) Undo stack: [ TYPE:Hello TYPE:World FORMAT:Bold INSERT:Image ] Redo stack: [ DELETE:World ] --- New action (clears redo stack) --- ACTION PASTE "Ananta" Undo stack: [ TYPE:Hello TYPE:World FORMAT:Bold INSERT:Image PASTE:Ananta ] Redo stack: [ (empty) ] --- Try redo after new action --- REDO nothing to redo! --- Undo everything --- UNDO <- PASTE "Ananta" (moved to redo) UNDO <- INSERT "Image" (moved to redo) UNDO <- FORMAT "Bold" (moved to redo) UNDO <- TYPE "World" (moved to redo) UNDO <- TYPE "Hello" (moved to redo) UNDO nothing to undo! Undo stack: [ (empty) ] Redo stack: [ PASTE:Ananta INSERT:Image FORMAT:Bold TYPE:World TYPE:Hello ]
- A stack is a LIFO structure — Last In, First Out. You can only add or remove elements from the top. All operations (push, pop, peek, isEmpty, isFull, size) are O(1).
- Initialise with
top = -1. Push usesarr[++top] = val— increment first, then write. Pop usesval = arr[top--]— read first, then decrement. - Always guard against overflow (
top == MAX-1) before push, and underflow (top == -1) before pop. Skipping these guards causes silent buffer overflows or reading garbage values. - Pop does not erase memory — it only moves
topdown. The old value stays in the array until overwritten by a future push. The stack logic ignores anything pasttop. - Real-world use — Undo/Redo: perform action → push to undo stack, clear redo. Undo → pop from undo, push to redo. Redo → pop from redo, push to undo. New action always clears redo.
- Other classic stack applications: bracket matching (push opens, pop on close), postfix evaluation (push numbers, pop two on operator), function call management (call stack), DFS graph traversal, and compiler syntax parsing.