Part of C in 100s

C in 100 Seconds: Stack vs Heap — Where Variables Live

Celest KimCelest Kim

Video: C in 100 Seconds: Stack vs Heap — Where Variables Live | Episode 31 by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

C Stack vs Heap: Where Variables Live

Stack: fast, automatic, scope-limited. Heap: slow, manual, lives until you free. Returning a pointer to a stack local is a dangling-pointer disaster.

C exposes the memory hierarchy directly. Knowing where each variable lives — stack, heap, or static — is the difference between a working program and a use-after-free crash.

The basic shape

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

int *stack_danger() {
  int x = 42;
  return &x;   // dangling pointer!
}

int *heap_safe() {
  int *p = malloc(sizeof(int));
  *p = 42;
  return p;    // caller owns it
}

int main() {
  int a = 10;             // stack
  int arr[5] = {1, 2, 3, 4, 5};   // stack
  int *h = malloc(sizeof(int));    // heap
  *h = 99;
  printf("Stack: %d, Heap: %d\n", a, *h);
  free(h);

  int *safe = heap_safe();
  printf("Heap return: %d\n", *safe);
  free(safe);

  // int *bad = stack_danger();
  // printf("%d\n", *bad);   // undefined!

  return 0;
}

The four memory regions

+------------------+ high address
|       stack      |  ↓ grows down
|------------------|
|        ↓         |
|     (free)       |
|        ↑         |
|------------------|
|       heap       |  ↑ grows up
|------------------|
|     bss/data     |  globals & static
|------------------|
|       text       |  code
+------------------+ low address
  • Text — your compiled code. Read-only. Includes string literals.
  • Data/BSS — globals and static variables. data for initialized; bss for zero-initialized.
  • Heap — dynamically allocated (malloc). Grows toward higher addresses.
  • Stack — function-call frames, locals. Grows toward lower addresses (on most architectures).

Stack: fast and automatic

void f() {
  int x = 5;       // stack
  int arr[100];    // stack
}   // x and arr are gone

Each function call allocates a stack frame with space for its locals. When the function returns, the frame is popped — locals are gone.

Pros:

  • Fast. Allocation is just adjusting the stack pointer (one instruction).
  • Automatic. No free needed.
  • Cache-friendly. Stack frames are usually hot in cache.

Cons:

  • Limited size. Default stack is typically 1-8 MB. Big arrays blow it.
  • Scope-limited. Locals are gone when the function returns.
  • Size known in advance. (Mostly — VLAs let you allocate runtime-sized stack arrays.)

Heap: manual and flexible

void f() {
  int *p = malloc(sizeof(int));   // heap
  *p = 5;
  // ...
  free(p);
}

Pros:

  • Unlimited size (up to RAM).
  • Outlives the function. Returns can hand back heap pointers.
  • Runtime size. Allocate as much as you decide at runtime.

Cons:

  • Slow. malloc has to find a free block, update bookkeeping.
  • Manual. Forget free → leak. Free twice → crash.
  • Fragmentation. Long-running programs can fragment.

The dangling pointer trap

int *bad() {
  int x = 42;
  return &x;   // x is on the stack — destroyed when bad() returns
}

int *p = bad();
*p;   // UNDEFINED — x is gone; this address now belongs to nobody

When bad() returns, its stack frame is popped. The address &x is invalid. Reading or writing through p is undefined behaviour — sometimes works (data hasn't been overwritten yet), sometimes catastrophic.

The fix: heap allocation.

int *good() {
  int *p = malloc(sizeof(int));
  *p = 42;
  return p;
}

int *p = good();
*p;     // OK — heap memory persists
free(p);

Static and global variables

int global = 5;     // data segment — lives entire program
static int s = 10;  // also data; file-scoped or function-scoped

void f() {
  static int counter = 0;   // initialized once; persists across calls
  counter++;
}

static inside a function: the variable persists across calls. Initialized once, retains value.

Globals and statics are zero-initialized by default. Local stack variables are not.

String literals

char *s = "hello";        // pointer to read-only memory
char arr[] = "hello";     // copy in stack-or-data segment

"hello" lives in the text segment (read-only). char *s = "hello"s points there. char arr[] = "hello" copies the literal into a writable array (typically on the stack).

s[0] = 'H'; undefined. arr[0] = 'H'; OK.

sizeof on stack vs heap arrays

int stack[10];
int *heap = malloc(10 * sizeof(int));

sizeof(stack)   // 40 (knows the array size)
sizeof(heap)    // 8 (just a pointer)

sizeof is a compile-time operator. For stack-allocated arrays, the compiler knows the size. For heap arrays, all you have is a pointer.

This is why you must track heap array sizes manually:

typedef struct {
  int *data;
  size_t size;
} IntList;

Variable-length arrays (VLA)

void f(int n) {
  int arr[n];   // VLA — size determined at call time
  for (int i = 0; i < n; i++) arr[i] = i;
}

C99 added VLAs — stack arrays whose size isn't a compile-time constant. They use stack space, so beware large n (stack overflow). C11 made them optional; some platforms don't support.

For runtime-sized memory, prefer malloc.

The stack overflow

void recurse() {
  char buf[1000000];   // 1 MB per call
  recurse();
}

Each call adds a 1MB frame. Default stack ~8MB → ~8 levels deep before crash.

Same for deep recursion without large locals — eventually the stack fills.

Symptoms: segfault on what looks like an innocent function call.

Fixes:

  • Smaller per-call locals.
  • Iterative version.
  • Heap-allocate the buffer.
  • Increase stack size (ulimit -s).

Common mistakes

Returning pointer to local. Dangling pointer — undefined behaviour.

Passing pointer to local across thread boundaries. If the local goes out of scope before the other thread reads, dangling.

Allocating huge arrays on the stack. int arr[10000000]; overflows on most systems. Use heap.

Forgetting to free heap memory. Memory leak (episode 22).

Assuming static means thread-safe. It doesn't — multiple threads share static variables. Use locks or thread-local storage.

Mixing stack/heap in container ownership. A struct with a int *data could be either; the struct's destructor needs to know to call free only for heap.

What's next

Episode 32: string functions — strcat, strchr, strstr. More from the <string.h> toolkit.

Recap

Stack — fast, automatic cleanup, scope-limited, size limit (~8 MB), returning pointer to local = disaster. Heap — slow, manual free, unlimited, persists until freed. Static/global — lives entire program; zero-initialized. Text — code + string literals; read-only. Use stack for short-lived locals; heap for big or long-lived data. Returning data from a function: return by value (cheap), pointer to heap (caller frees), or fill caller's buffer (caller manages).

Next episode: more string functions.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.