Recursion and Function-Call Mechanics
This chapter covers recursion in C and the mechanics of the call stack that make it possible. It connects the abstract idea of a function call to the concrete reality of stack frames.
Why This Matters
Recursion is a natural way to express many algorithms (tree traversal, parsing, divide-and-conquer), but it consumes stack space. Understanding how the stack grows and shrinks lets you predict when recursion is safe and when it will overflow the stack.
Prerequisites
c.core.17— functions.c.core.7— lifetime and storage duration.
Core Concept
A function may call itself, directly or indirectly. Each call creates a new stack frame (activation record) holding the function's parameters, local variables, and return address. When the function returns, its frame is reclaimed.
Recursion requires:
1. A base case that does not recurse. 2. A recursive case that makes progress toward the base case.
Without a base case (or if progress is not guaranteed), recursion never terminates and eventually overflows the stack.
Syntax
unsigned factorial(unsigned n)
{
if (n <= 1)
return 1; /* base case */
return n * factorial(n - 1); /* recursive case */
}
Examples
Factorial (recursive)
#include <stdio.h>
unsigned factorial(unsigned n)
{
if (n <= 1)
return 1;
return n * factorial(n - 1);
}
int main(void)
{
printf("%u\n", factorial(5));
return 0;
}
Expected output: 120.
Factorial (iterative, for comparison)
unsigned factorial_iter(unsigned n)
{
unsigned result = 1;
for (unsigned i = 2; i <= n; i++)
result *= i;
return result;
}
Fibonacci (showing exponential blowup)
unsigned fib(unsigned n)
{
if (n <= 1)
return n;
return fib(n - 1) + fib(n - 2); /* O(2^n) */
}
This is correct but inefficient. A memoized or iterative version is preferred for large n.
How It Works
Each call pushes a frame onto the call stack. The frame contains, roughly:
- return address (where to resume after the call);
- saved registers;
- local variables and parameters;
- (for large locals or VLAs) space for arrays.
When a function returns, the stack pointer moves back, reclaiming the frame. The stack has a finite size (implementation-defined, often a few MB on hosted systems), so deep recursion can overflow it.
Variations
Tail recursion
A function is tail-recursive if the recursive call is the last thing before returning, with no pending computation. Some compilers optimize tail recursion into a loop (tail-call optimization), avoiding stack growth. C does not *require* this optimization.
int gcd(int a, int b)
{
if (b == 0) return a;
return gcd(b, a % b); /* tail call */
}
Mutual recursion
Two or more functions call each other. This requires a forward declaration for at least one.
int is_even(int n);
int is_odd(int n) { return n == 0 ? 0 : is_even(n - 1); }
int is_even(int n) { return n == 0 ? 1 : is_odd(n - 1); }
Common Mistakes
- Missing or incorrect base case.
- Not making progress toward the base case.
- Deep recursion causing stack overflow.
- Assuming tail-call optimization always happens.
Undefined Behavior
- Recursion itself is not UB. But if it causes the stack to overflow and the
program then accesses memory beyond the stack, the behavior is undefined. (There is no portable way to catch stack overflow in ISO C.)
- Signed overflow in a recursive computation is UB (e.g.,
factorialwith a
value that overflows unsigned is fine — it wraps — but with int it is UB).
Portability
- Stack size is implementation-defined and may be configurable (e.g.,
ulimit -s on POSIX).
- Tail-call optimization is compiler-dependent; do not rely on it for
correctness.
Under the Hood
On x86-64, call pushes the return address and jumps; ret pops it. The stack grows downward (toward lower addresses) on x86 and most ARM. Local variables are accessed via the base pointer or stack pointer. Part 7 covers this in detail.
Practical Usage
- Use recursion for naturally recursive structures (trees, graphs, parsers).
- Use iteration when the problem is linear and recursion would add stack
pressure.
- Consider an explicit stack (heap-allocated) for deep or unbounded recursion
(e.g., tree traversal of very deep trees).
Exercises
1. Write recursive and iterative factorial functions and compare results. 2. Write a recursive binary search and prove it terminates. 3. Write a recursive tree traversal and measure the depth at which stack overflow occurs (carefully, on a small program). 4. Convert a tail-recursive function to an explicit loop and compare.
Deep Challenge
Implement an iterative post-order tree traversal using an explicit stack, and explain why this approach avoids stack overflow for very deep trees where a recursive traversal would fail. Discuss the trade-offs (complexity vs. stack safety).
Related Concepts
c.cpu.2— stack frames.c.alg.recursion— recursion in algorithms.c.ds.binary-tree— recursive data structures.c.opt.inlining— how compilers transform calls.
References
- ISO/IEC 9899:2018 §6.5.2.2 (function calls), §6.8.6.4 (return).
Verification
- Recursion creates a new frame per call.
VERIFIED(as a model) - Tail-call optimization is not required by ISO C.
VERIFIED - Stack overflow leading to UB is a consequence of the abstract machine, not a
standard-guaranteed check. VERIFIED
- No example was executed during generation unless noted. `Execution not
verified.`
Progress
- [ ] Read
- [ ] Understand
- [ ] Complete examples
- [ ] Complete exercises
- [ ] Complete deep challenge
Concept checkboxes
- [ ] Recursion and base case
- [ ] Stack frames
- [ ] Tail recursion
- [ ] Mutual recursion
- [ ] Stack overflow risk
Mastery levels
| Concept | Current level (0–8) | Target level |
|---|---|---|
| c.func.recursion | 0 | 5 |