Dataslope logoDataslope

The Stack and Function Calls

Call frames, recursion, stack overflow, and the lifetime of local variables

Every time you call a function in C, the runtime pushes a stack frame that holds the function's parameters, local variables, and the return address it needs to come back to its caller. When the function returns, that frame is popped — and every local in it is gone.

This page makes that mechanism concrete enough that you can predict how recursion uses memory and where local pointers stop being safe to read.

Anatomy of a stack frame

A typical frame contains:

  • the return address (where to resume in the caller)
  • the saved frame pointer (so debuggers can walk the call chain)
  • the function's parameters (or in many ABIs, copies of register arguments)
  • the function's local variables
  • on some platforms, padding to keep alignment

The stack typically grows from high addresses to low — every call subtracts from the stack pointer, every return adds back. On WASI the direction is the same conceptually even if the linear memory layout differs.

See the stack with addresses

You can see the call chain by printing addresses of local variables.

Code Block
C 17 (201710L)

The addresses of x in main, middle, and inner should be decreasing (or sometimes increasing — the direction is platform-dependent), and the difference between consecutive frames shows roughly how big each frame is.

Recursion uses the stack

Each recursive call pushes a new frame. A function that recurses n times uses n frames at peak — so a recursion depth limit is really a stack size limit.

Code Block
C 17 (201710L)

fib(40) would push roughly 2 × 10⁸ frames in total over its run (though only ~40 are alive at once because the calls return between them). The total work explodes; the peak stack depth is only n.

Stack overflow

When recursion is too deep — or a function declares a huge local array — you hit the end of the stack region and the program crashes with a stack overflow.

Beware huge stack arrays

A local declaration like char buf[16 * 1024 * 1024]; allocates 16 MB on the stack. On many platforms the entire stack is only 1 MB, and this will crash before main runs a single line. Use the heap (malloc) for large buffers.

Code Block
C 17 (201710L)

Iteration vs recursion — same algorithm, different memory shape

Many recursive algorithms have iterative equivalents that use O(1) stack instead of O(n). Compare:

Code Block
C 17 (201710L)

Both compute the same value. The iterative version is friendlier on the stack and easier for an optimizer to reduce to a tight loop.

Tail calls

In return f(...) — where the recursive call is the very last thing the function does — a sufficiently aggressive optimizer can reuse the current frame instead of pushing a new one. This is called tail-call optimization (TCO). C compilers will sometimes perform it at higher optimization levels but the language does not guarantee it.

// Tail-recursive form of factorial: every recursive call is the function's
// last action, with the accumulated product passed as a parameter.
long fact_tail(int n, long acc) {
    if (n <= 1) return acc;
    return fact_tail(n - 1, n * acc);
}

If you depend on tail calls (e.g. for an interpreter loop), measure with -O2. Otherwise prefer an explicit loop.

Lifetimes: when a local stops being valid

The single most important stack rule:

A local variable lives only between the matching { and }. Any pointer to it becomes invalid the instant control leaves that scope.

Code Block
C 17 (201710L)

The pattern that is safe: write into a buffer the caller owns.

Code Block
C 17 (201710L)

This caller owns the buffer idiom is everywhere in C APIs: think fgets(buf, sizeof buf, stdin) or snprintf(buf, sizeof buf, ...).

Practice: iterative power

Challenge
C 17 (201710L)
Compute base^exp iteratively

Implement long my_pow(int base, int exp) for non-negative exp using a loop — not recursion. my_pow(2, 10) must return 1024. The provided main prints results.

Practice: greatest common divisor (iterative)

Challenge
C 17 (201710L)
Implement gcd iteratively

Implement int gcd(int a, int b) using the iterative Euclidean algorithm so that it uses O(1) stack frames regardless of input size. The provided main calls it for three pairs.

Test your understanding

QuestionSelect one

A function declares char buf[8 * 1024 * 1024]; as a local variable on a platform with a 1 MB stack. What is the most likely outcome?

The compiler silently moves the array to the heap.

The OS automatically grows the stack to fit.

The variable is allocated successfully but slowly.

The program runs out of stack and crashes (stack overflow).

QuestionSelect one

What is wrong with this function?

char *greeting(void) {
  char buf[16] = "Hello";
  return buf;
}

It allocates too few bytes for "Hello".

It returns a string in the wrong encoding.

It forgets to null-terminate.

It returns a pointer to a local array whose stack frame is destroyed the moment the function returns; the caller dereferences a dangling pointer.

QuestionSelect one

Compared to the recursive fact_rec(n), why is fact_iter(n) usually preferred in C?

It computes a different value (the iterative version is more accurate).

The compiler can vectorize iteration but never recursion.

It uses a constant number of stack frames regardless of n, eliminating the risk of stack overflow for large inputs.

The recursive version is undefined behavior in standard C.

On this page