A stack is a last-in, first-out data structure. Every function call in your program uses one — the call stack. Implementing your own stack teaches you the underlying mechanics and gives you a structure useful for parsing, undo/redo, depth-first search, and expression evaluation.

Fixed-Size Array Stack

#include <stdio.h>
#include <stdlib.h>

#define MAX_SIZE 64

typedef struct {
    int data[MAX_SIZE];
    int top;   /* index of the last pushed element, -1 if empty */
} Stack;

void stack_init(Stack *s) {
    s->top = -1;
}

int stack_empty(const Stack *s) {
    return s->top == -1;
}

int stack_full(const Stack *s) {
    return s->top == MAX_SIZE - 1;
}

int stack_push(Stack *s, int val) {
    if (stack_full(s)) return -1;   /* overflow */
    s->data[++(s->top)] = val;
    return 0;
}

int stack_pop(Stack *s, int *out) {
    if (stack_empty(s)) return -1;  /* underflow */
    *out = s->data[(s->top)--];
    return 0;
}

int stack_peek(const Stack *s, int *out) {
    if (stack_empty(s)) return -1;
    *out = s->data[s->top];
    return 0;
}

int main() {
    Stack s;
    stack_init(&s);

    stack_push(&s, 10);
    stack_push(&s, 20);
    stack_push(&s, 30);

    int val;
    while (!stack_empty(&s)) {
        stack_pop(&s, &val);
        printf("%dn", val);   /* 30, 20, 10 */
    }

    return 0;
}

Dynamic Stack — Grows With Demand

#include <stdio.h>
#include <stdlib.h>

typedef struct {
    int    *data;
    int     top;
    size_t  capacity;
} DynStack;

DynStack *dynstack_new(void) {
    DynStack *s = malloc(sizeof(DynStack));
    s->data     = malloc(8 * sizeof(int));
    s->top      = -1;
    s->capacity = 8;
    return s;
}

int dynstack_push(DynStack *s, int val) {
    if ((size_t)(s->top + 1) == s->capacity) {
        size_t new_cap = s->capacity * 2;
        int   *tmp     = realloc(s->data, new_cap * sizeof(int));
        if (!tmp) return -1;
        s->data     = tmp;
        s->capacity = new_cap;
    }
    s->data[++(s->top)] = val;
    return 0;
}

int dynstack_pop(DynStack *s, int *out) {
    if (s->top == -1) return -1;
    *out = s->data[(s->top)--];
    return 0;
}

void dynstack_free(DynStack *s) {
    free(s->data);
    free(s);
}

int main() {
    DynStack *s = dynstack_new();

    for (int i = 1; i <= 20; i++) dynstack_push(s, i);

    int val;
    while (dynstack_pop(s, &val) == 0) {
        printf("%d ", val);   /* 20 19 18 ... 1 */
    }
    printf("n");

    dynstack_free(s);
    return 0;
}

Practical Application: Balanced Parentheses

#include <stdio.h>
#include <string.h>

#define MAX 128

int is_balanced(const char *expr) {
    char stack[MAX];
    int  top = -1;

    for (size_t i = 0; i < strlen(expr); i++) {
        char c = expr[i];

        if (c == '(' || c == '[' || c == '{') {
            if (top == MAX - 1) return 0;  /* stack overflow */
            stack[++top] = c;

        } else if (c == ')' || c == ']' || c == '}') {
            if (top == -1) return 0;  /* unmatched closing bracket */

            char open = stack[top--];
            if ((c == ')' && open != '(') ||
                (c == ']' && open != '[') ||
                (c == '}' && open != '{')) {
                return 0;  /* mismatched */
            }
        }
    }

    return top == -1;  /* empty stack = balanced */
}

int main() {
    printf("%dn", is_balanced("({[]})"    ));  /* 1 — balanced */
    printf("%dn", is_balanced("({[}])"    ));  /* 0 — mismatched */
    printf("%dn", is_balanced("((("       ));  /* 0 — unclosed */
    printf("%dn", is_balanced("int x = (a + b) * (c - d);"));  /* 1 */
    return 0;
}

Stack Frames — What You’re Actually Using

Every function call in C pushes a stack frame: the return address, saved registers, and local variables. The frame is popped when the function returns. Stack overflow happens when this depth exceeds the OS-allotted stack size — typically around 8 MB on Linux, 1 MB on Windows. Your explicit stack data structure allocates on the heap and can grow as large as available memory, making it suitable for algorithms that need deep traversal without the risk of overflowing the call stack.

For more on how the call stack relates to heap allocation, see our guide on linked list implementation — linked list nodes are heap-allocated and don’t have the depth limit a recursive implementation would. Test the parentheses checker in our c online compiler — paste in complex expressions and verify balanced detection.

TL;DR

  • Stack is LIFO: last pushed is first popped
  • Use top == -1 for empty, top == capacity - 1 for full
  • Fixed-size stack: simple, zero allocation, appropriate when max depth is known
  • Dynamic stack: grows on demand using realloc with doubling strategy
  • Always return an error code from push/pop — never silently overflow or underflow
  • Balanced brackets, undo/redo, DFS, and expression evaluation are the classic stack use cases
  • An explicit heap stack avoids call-stack overflow for deep recursive algorithms