Functions
Recursion & Storage Classes
Master C Recursion — what/why, applications, head vs tail recursion, types of recursion — plus Storage Classes (scope, lifetime, local/global, auto, register, static, extern) using LEARNCODEGURU’s 6-level learning model.
Level 1 Basics Content
C Recursion Overview
Recursion is a function calling itself to solve a smaller version of the same problem. This level covers what recursion is, why it is used, its applications, the essential base case and recursive case, head vs tail recursion, types of recursion, and recursion vs iteration.
The idea: solve a big problem by assuming you can already solve a slightly smaller one, and stop at a case so small the answer is obvious.
What is recursion?
A recursive function is one whose body contains a call to itself. Every recursive function needs two parts:
- Base case — the stopping condition; answers directly without recursing
- Recursive case — solves the problem using a smaller input
long fact(int n) {
if (n <= 1)
return 1; /* base case */
return n * fact(n - 1); /* recursive case */
}
Tracing fact(4):
Why recursion?
- Natural fit — some problems are defined recursively (factorial, Fibonacci, trees)
- Shorter, clearer code — mirrors the mathematical definition
- Divide and conquer — split the problem, solve halves, combine (merge sort, binary search)
- No manual stack — the call stack tracks state for you
- Elegant tree/graph traversal — visiting children is just another call
Applications of recursion
| # | Application | Example |
|---|---|---|
| 1 | Mathematical definitions | Factorial, power, Fibonacci, GCD |
| 2 | Divide and conquer algorithms | Binary search, merge sort, quick sort |
| 3 | Tree and graph traversal | Preorder / inorder / postorder, DFS |
| 4 | String / array processing | Reverse a string, palindrome check, array sum |
| 5 | Backtracking puzzles | Towers of Hanoi, N-Queens, maze solving |
| 6 | Parsing nested structures | Expressions with brackets, directory trees |
Head recursion vs tail recursion
The difference is where the recursive call sits relative to the other work.
Head recursion — the call comes first; work happens while returning:
void headPrint(int n) {
if (n == 0)
return;
headPrint(n - 1); /* recurse FIRST */
printf("%d ", n); /* then do the work */
}
/* headPrint(5) prints: 1 2 3 4 5 (ascending) */
Tail recursion — the work comes first; the call is the last action:
void tailPrint(int n) {
if (n == 0)
return;
printf("%d ", n); /* do the work FIRST */
tailPrint(n - 1); /* then recurse — nothing after */
}
/* tailPrint(5) prints: 5 4 3 2 1 (descending) */
| Aspect | Head recursion | Tail recursion |
|---|---|---|
| Position of the call | First / before the work | Last / after the work |
| Work happens | While unwinding (on the way back) | Before recursing (on the way down) |
| Output order | Reversed relative to the calls | Same order as the calls |
| Pending work in frames | Yes — each frame waits | No — nothing left to do |
| Optimizable to a loop | Not directly | Yes (compiler may reuse the frame) |
| Typical use | Printing digits / lists in reverse | Accumulator-style computation |
return n * fact(n-1); is head-style because the multiply waits for the call.
Types of recursion
| # | Type | Meaning | Example |
|---|---|---|---|
| 1 | Direct recursion | A function calls itself | fact() calls fact() |
| 2 | Indirect (mutual) recursion | Two functions call each other | isEven() ↔ isOdd() |
| 3 | Head recursion | Recursive call before the work | Print 1..n ascending |
| 4 | Tail recursion | Recursive call is the last action | Print n..1 descending |
| 5 | Linear recursion | One recursive call per invocation | Factorial, array sum |
| 6 | Tree (multiple) recursion | Two or more calls per invocation | Fibonacci, Towers of Hanoi |
| 7 | Nested recursion | A call whose argument is itself a call | f(f(n)) — Ackermann style |
/* Indirect recursion — each needs the other */
int isOdd(int n); /* forward prototype */
int isEven(int n) {
if (n == 0) return 1;
return isOdd(n - 1);
}
int isOdd(int n) {
if (n == 0) return 0;
return isEven(n - 1);
}
Recursion vs iteration
| Aspect | Recursion | Iteration (loops) |
|---|---|---|
| How repetition happens | Function calls itself | for / while repeats a block |
| Termination | Base case | Loop condition |
| Memory | One stack frame per call | Constant — a few variables |
| Speed | Call overhead per step | Usually faster |
| Risk | Stack overflow if too deep | Infinite loop if condition never fails |
| Best for | Trees, divide and conquer, backtracking | Simple counted or sequential repetition |
Sample program — factorial plus head and tail printing
/* Recursion basics: factorial + head/tail order demo */
#include <stdio.h>
long fact(int n);
void headPrint(int n);
void tailPrint(int n);
int main(void) {
int n;
printf("Enter n: ");
scanf("%d", &n);
printf("Factorial of %d = %ld\n", n, fact(n));
printf("Head recursion (ascending): ");
headPrint(n);
printf("\n");
printf("Tail recursion (descending): ");
tailPrint(n);
printf("\n");
return 0;
}
long fact(int n) {
if (n <= 1)
return 1;
return n * fact(n - 1);
}
void headPrint(int n) {
if (n == 0)
return;
headPrint(n - 1);
printf("%d ", n);
}
void tailPrint(int n) {
if (n == 0)
return;
printf("%d ", n);
tailPrint(n - 1);
}
printf flips the output order.
Storage Classes in C
A storage class tells the compiler four things about a variable: its scope (where it is visible), its lifetime (how long it exists), its default value when uninitialized, and where it is stored. This closely relates to recursion, because each recursive call gets fresh automatic variables but shares any static ones.
Local vs global variables
| # | Kind | Declared where | Visible where | Lives how long | Default value |
|---|---|---|---|---|---|
| 1 | Local (automatic) | Inside a function or block | Only in that block | Until the block/function ends | Garbage (indeterminate) |
| 2 | Global (external) | Outside all functions | Every function in the file (and other files with extern) | Whole program run | 0 |
#include <stdio.h>
int g = 10; /* global — visible to all functions */
void show(void) {
int g = 99; /* local hides the global inside show() */
printf("local g = %d\n", g);
}
Scope and lifetime
- Block scope — visible from declaration to the closing
} - Function scope — parameters and locals of one function
- File scope — globals; visible to every function below in the file
- Program (whole-run) lifetime — globals and
staticvariables - Automatic lifetime — created on entry to the block, destroyed on exit
static local has narrow scope (one block)
but full program lifetime.
Types of storage classes
C has four storage class specifiers:
| # | Storage class | Keyword | Scope | Lifetime | Default value | Stored in |
|---|---|---|---|---|---|---|
| 1 | Automatic | auto | Block (local) | Until block ends | Garbage | Stack |
| 2 | Register | register | Block (local) | Until block ends | Garbage | CPU register (hint) or stack |
| 3 | Static | static | Block (local) or file (internal linkage) | Whole program | 0 | Data segment |
| 4 | External | extern | Whole program (all files) | Whole program | 0 | Data segment |
void demo(void) {
auto int a = 1; /* default for locals — keyword rarely written */
register int r = 2; /* hint only; &r is not allowed */
static int s = 0; /* initialized once; survives calls */
s++;
printf("a=%d r=%d s=%d\n", a, r, s);
}
auto is the default for local variables, so you almost never
type it. register is effectively obsolete — and you cannot apply &
to a register variable.
static and extern in detail
static local — remembers its value across calls:
void counter(void) {
static int count = 0; /* runs only once */
int plain = 0; /* reset every call */
count++; plain++;
printf("count=%d plain=%d\n", count, plain);
}
/* Three calls print: 1 1 , 2 1 , 3 1 */
extern — refers to a variable defined in another file:
/* file1.c */
int total = 100; /* definition */
/* file2.c */
extern int total; /* declaration — no memory allocated here */
| Where used | static means | extern means |
|---|---|---|
| Inside a function | Keeps its value between calls (lifetime) | Refers to a global defined elsewhere |
| At file scope (global) | Private to this file (internal linkage) | Declares a global from another file |
| On a function | Callable only within this file | Callable from other files (the default) |
Storage classes and recursion
This is where the two topics meet. In a recursive function:
- Automatic locals — each call has its own separate copy in its own frame
- Static locals — one shared copy for all calls, so you can count invocations
- Globals — also shared across all frames and all functions
void rec(int n) {
static int calls = 0; /* shared by every frame */
int local = n; /* fresh in every frame */
calls++;
if (n == 0) {
printf("total calls = %d\n", calls);
return;
}
rec(n - 1);
printf("unwinding local = %d\n", local);
}
local still holds the right value after the recursive call returns —
proof that every frame has its own automatic storage.
Level 2 Advanced Content
Going deeper into recursion
Level 1 covered what/why/applications, head vs tail, types, and recursion vs iteration. Level 2 explains the call stack in detail, converting head to tail with an accumulator, tail-call optimization, complexity, memoization, recursion on arrays and strings, memory segments, choosing a storage class, and common mistakes.
The call stack during recursion
Each call pushes a frame with its own parameters, locals, and return address. Frames stack up until the base case, then unwind in reverse (LIFO).
- Winding phase — calls go deeper; nothing is computed yet in head recursion
- Base case — the deepest frame returns a concrete value
- Unwinding phase — pending work in each frame completes
- Depth × frame size ≈ stack used; exceeding the limit is a stack overflow
Converting head recursion to tail recursion
Carry the running result in an extra parameter (an accumulator) so nothing remains to do after the call.
/* Head style: multiply waits for the call to return */
long factHead(int n) {
if (n <= 1) return 1;
return n * factHead(n - 1);
}
/* Tail style: the call IS the return value */
long factTail(int n, long acc) {
if (n <= 1) return acc;
return factTail(n - 1, acc * n);
}
/* call as factTail(5, 1) */
Tail-call optimization — what C guarantees
- Standard C makes no promise about eliminating tail calls
- GCC / Clang often do it with optimization enabled (
-O2) - Without it, tail recursion still uses one frame per call
- For guaranteed constant memory, write an explicit loop
Complexity — linear vs tree recursion
| Function | Calls per invocation | Time | Stack depth |
|---|---|---|---|
fact(n) | 1 | O(n) | O(n) |
fib(n) naive | 2 | O(2^n) | O(n) |
fib(n) memoized | 2 (cached) | O(n) | O(n) |
| Binary search | 1 | O(log n) | O(log n) |
Memoization — caching repeated subproblems
long memo[100]; /* zero-initialized globally */
long fib(int n) {
if (n <= 1)
return n;
if (memo[n] != 0)
return memo[n]; /* already computed */
memo[n] = fib(n - 1) + fib(n - 2);
return memo[n];
}
Recursion on arrays and strings
/* Array sum: shrink the length */
int sum(int a[], int n) {
if (n == 0)
return 0;
return a[n - 1] + sum(a, n - 1);
}
/* Palindrome: move both ends inward */
int isPal(char s[], int i, int j) {
if (i >= j)
return 1;
if (s[i] != s[j])
return 0;
return isPal(s, i + 1, j - 1);
}
- The array is not copied — only the pointer and index/length change
- Pass indices when you need to work from both ends
- Base cases: empty range (
n == 0) or crossed indices (i >= j)
Memory segments — where each storage class lives
| Segment | Holds | Lifetime |
|---|---|---|
| Stack | Automatic locals, parameters, return addresses | Per call / per block |
| Data (initialized) | Globals and statics with non-zero initializers | Whole program |
| BSS (uninitialized) | Globals and statics left uninitialized — set to 0 | Whole program |
| Heap | Memory from malloc / calloc | Until you free it |
| Registers | Values the compiler chooses to keep in the CPU | Very short — as needed |
Choosing the right storage class
| Need | Use | Why |
|---|---|---|
| A temporary value inside one function | Plain local (automatic) | Cheapest; cleaned up automatically |
| Remember a count between calls | static local | Narrow scope, program lifetime |
| Share data between functions in one file | static global | Shared but hidden from other files |
| Share data across multiple files | Global + extern declaration | One definition, many declarations |
| Micro-optimize a hot loop counter | Nothing — trust the compiler | register is obsolete in practice |
Common mistakes
| Mistake | Why it hurts | Fix |
|---|---|---|
| No base case | Infinite recursion → stack overflow | Handle the smallest input first |
| Base case unreachable | Argument never shrinks toward it | Ensure each call reduces the problem |
| Base case forgets to return a value | Caller uses an indeterminate result | Return on every path |
Calling fact(n) instead of fact(n-1) | Same input repeats forever | Decrement / divide the argument |
| Assuming tail recursion is free | C does not guarantee optimization | Use a loop when depth is large |
| Recomputing subproblems | Exponential blow-up (naive Fibonacci) | Memoize or go bottom-up |
| Expecting a plain local to persist between calls | It is re-created (garbage) every call | Declare it static |
Re-initializing a static mentally each call | The initializer runs only once | Reset it explicitly if you need a fresh start |
| Defining a global in a header | Multiple definitions at link time | Define once in a .c, declare extern in the header |
Taking & of a register variable | Compile error — not addressable | Drop the register keyword |
Level 3 C Tricky Interview Points
Advanced tricky interview points on base cases, head vs tail recursion, stack depth, tail-call optimization, complexity, indirect recursion, and storage classes.
Tricky Interview Point-1:
Every recursion needs a base case AND progress toward it
A base case alone is not enough — each call must move closer to it. fact(n-1) shrinks n; fact(n) would recurse forever. Missing either condition gives infinite recursion and a stack overflow at runtime, not a compile error.
Tricky Interview Point-2:
Head recursion does work AFTER the call; tail recursion does it BEFORE
In head recursion the recursive call is the first action and the pending work happens while unwinding, so output appears in reverse order. In tail recursion the call is the last action and nothing remains to do after it returns.
Tricky Interview Point-3:
Tail calls are not guaranteed to be optimized in C
Standard C does not require tail-call optimization. GCC/Clang often do it at -O2, but you cannot rely on it for correctness. Deep tail recursion may still overflow the stack unless the compiler eliminates the frame.
Tricky Interview Point-4:
Each call gets its own copy of parameters and locals
Recursive calls do not share locals — every frame has separate storage. That is why n keeps its value after the inner call returns. A static local, by contrast, IS shared across all frames.
Tricky Interview Point-5:
Naive recursive Fibonacci is exponential — O(2^n)
fib(n) = fib(n-1) + fib(n-2) recomputes the same subproblems repeatedly. fib(40) makes over a billion calls. Fix with memoization or an iterative loop, which is O(n).
Tricky Interview Point-6:
Recursion depth is limited by stack size, not by any language rule
Typical stacks are 1–8 MB. With large frames you may overflow after only a few thousand calls; with tiny frames, tens of thousands. There is no portable maximum depth — it is environment-specific.
Tricky Interview Point-7:
Indirect (mutual) recursion also counts as recursion
If f() calls g() and g() calls f(), that is indirect recursion. It needs a base case just like direct recursion, and one of the two functions needs a forward prototype so the first can see the second.
Tricky Interview Point-8:
Any recursion can be rewritten iteratively (with an explicit stack if needed)
Linear recursion converts to a simple loop. Tree recursion may need your own stack or queue. This equivalence is why recursion is a convenience for expressing logic, not a unique capability.
Tricky Interview Point-9:
return without a value in a non-void recursive function is a trap
If the base case forgets to return a value, the caller multiplies by an indeterminate result. Make sure every path — base case and recursive case — returns.
Tricky Interview Point-10:
Tail recursion behaves like a loop; head recursion behaves like a stack
Tail recursion carries state forward in parameters (accumulator pattern). Head recursion defers work, so it naturally reverses order — perfect for printing a number's digits or a list backwards.
Tricky Interview Point-11:
Recursion on arrays usually passes an index or a shrinking length
sum(a, n) → a[n-1] + sum(a, n-1), or sum(a, i, n) advancing i. The array itself is not copied; only the pointer and index change per call.
Tricky Interview Point-12:
A recursive function calling itself in main-like fashion is still one definition
A function may call itself even though the definition is not finished being compiled, because its own declaration is already in scope from the header line. No extra prototype is needed for direct recursion.
Tricky Interview Point-13:
Storage class controls scope, lifetime, default value, and storage location
auto, register, static, and extern are the four storage class specifiers. Each answers four questions: where is the variable visible, how long does it live, what is its initial value if uninitialized, and where is it kept.
Tricky Interview Point-14:
A static local keeps its value between calls — including recursive calls
static int count = 0; inside a function is initialized once and shared by every invocation. In recursion this means all frames see the same counter, unlike ordinary locals which are separate per frame.
Tricky Interview Point-15:
Uninitialized static and global variables are zero; automatic ones are garbage
Static-duration objects live in a zero-initialized area. Automatic (stack) variables hold indeterminate values until assigned — reading them first is undefined behavior.
Tricky Interview Point-16:
static on a global means file-private, not "keeps its value"
At file scope static changes linkage (internal, invisible to other files), not lifetime — globals already live for the whole program. Inside a function static changes lifetime. Same keyword, two different jobs.
Tricky Interview Point-17:
register is only a hint, and you cannot take its address
&x is illegal for a register variable, and modern compilers allocate registers themselves, so the keyword is effectively obsolete. Mentioning the &-restriction is what interviewers look for.
Level 4 Simple Programs
Short programs covering factorial, head vs tail order, Fibonacci, GCD, array recursion, and storage classes (local / global / static).
Program 1 — Factorial: head style vs tail style
/* Same result; different frame behaviour */
#include <stdio.h>
long factHead(int n);
long factTail(int n, long acc);
int main(void) {
int n;
printf("Enter n: ");
scanf("%d", &n);
printf("Head: %ld\n", factHead(n));
printf("Tail: %ld\n", factTail(n, 1));
return 0;
}
long factHead(int n) {
if (n <= 1)
return 1;
return n * factHead(n - 1);
}
long factTail(int n, long acc) {
if (n <= 1)
return acc;
return factTail(n - 1, acc * n);
}
Program 2 — Head vs tail printing (order demo)
/* Moving printf flips the output order */
#include <stdio.h>
void headPrint(int n);
void tailPrint(int n);
int main(void) {
printf("Head (1..5): ");
headPrint(5);
printf("\nTail (5..1): ");
tailPrint(5);
printf("\n");
return 0;
}
void headPrint(int n) {
if (n == 0) return;
headPrint(n - 1);
printf("%d ", n);
}
void tailPrint(int n) {
if (n == 0) return;
printf("%d ", n);
tailPrint(n - 1);
}
Program 3 — Fibonacci and GCD
/* Tree recursion (fib) and tail recursion (gcd) */
#include <stdio.h>
long fib(int n);
int gcd(int a, int b);
int main(void) {
int n, x, y;
printf("Enter n for Fibonacci: ");
scanf("%d", &n);
printf("fib(%d) = %ld\n", n, fib(n));
printf("Enter two numbers for GCD: ");
scanf("%d %d", &x, &y);
printf("gcd = %d\n", gcd(x, y));
return 0;
}
long fib(int n) {
if (n <= 1)
return n; /* fib(0)=0, fib(1)=1 */
return fib(n - 1) + fib(n - 2);
}
int gcd(int a, int b) {
if (b == 0)
return a;
return gcd(b, a % b); /* tail recursive */
}
Program 4 — Array sum and string reverse by recursion
/* Recursion over arrays and strings */
#include <stdio.h>
#include <string.h>
int sumArr(int a[], int n);
void reverse(char s[], int i, int j);
int main(void) {
int a[] = {4, 8, 15, 16, 23};
char s[] = "recursion";
printf("Sum = %d\n", sumArr(a, 5));
reverse(s, 0, (int)strlen(s) - 1);
printf("Reversed = %s\n", s);
return 0;
}
int sumArr(int a[], int n) {
if (n == 0)
return 0;
return a[n - 1] + sumArr(a, n - 1);
}
void reverse(char s[], int i, int j) {
char t;
if (i >= j)
return;
t = s[i]; s[i] = s[j]; s[j] = t;
reverse(s, i + 1, j - 1);
}
Program 5 — Storage classes: local, global, and static
/* Scope and lifetime in one program */
#include <stdio.h>
int gCount = 0; /* global: whole program, default 0 */
void demo(void);
int main(void) {
int i;
for (i = 0; i < 3; i++)
demo();
printf("global gCount = %d\n", gCount);
return 0;
}
void demo(void) {
int plain = 0; /* automatic: reset each call */
static int kept = 0; /* static: initialized once */
plain++;
kept++;
gCount++;
printf("plain=%d kept=%d gCount=%d\n", plain, kept, gCount);
}
/* Output:
plain=1 kept=1 gCount=1
plain=1 kept=2 gCount=2
plain=1 kept=3 gCount=3
global gCount = 3 */
Program 6 — Counting recursive calls with a static variable
/* A static local is shared by every recursive frame */
#include <stdio.h>
void rec(int n);
int main(void) {
rec(4);
return 0;
}
void rec(int n) {
static int calls = 0; /* one copy for all frames */
int local = n; /* fresh copy per frame */
calls++;
if (n == 0) {
printf("base reached; total calls = %d\n", calls);
return;
}
rec(n - 1);
printf("unwinding: local=%d calls=%d\n", local, calls);
}
Level 5 Problem Solving Tricks
How to master recursion while solving problems
Most recursion bugs come from a missing or unreachable base case, arguments that never shrink, and repeated subproblems. These habits make recursive solutions easy to write and easy to trust.
Write the base case first
Ask: what is the smallest input where the answer is obvious? factorial → n <= 1 gives 1; sum of array → n == 0 gives 0. Code that line before anything else.
Express the answer in terms of a smaller answer
State the recurrence in words: "factorial of n is n times factorial of n-1." If you cannot say that sentence, recursion is the wrong tool for the problem.
Check that every call shrinks the problem
The argument must strictly move toward the base case — n-1, n/2, i+1, or a shorter string. Passing n unchanged (or growing) means infinite recursion.
Trace two or three levels on paper
Write fact(3) → 3 * fact(2) → 3 * (2 * fact(1)) → 3 * 2 * 1. Seeing the unwind step makes head vs tail order obvious and exposes wrong base cases.
Use an accumulator to convert head into tail recursion
Instead of returning n * fact(n-1), carry the running result: factTail(n-1, acc * n). The recursive call becomes the final action, and nothing waits on the return.
Prefer iteration when depth can be large
Summing a million elements recursively risks stack overflow; a loop does not. Use recursion where the structure is naturally recursive (trees, divide and conquer) or depth is small.
Watch for repeated subproblems
If the same argument is computed more than once (classic Fibonacci), cache results in an array (memoization) or switch to a bottom-up loop. Otherwise the cost explodes.
Print inside the function to see the order
Add printf before the call and after it. Output before shows the descending path (tail-style order); output after shows the unwinding path (head-style, reversed).
Use a static local when you need to count calls
A plain local resets every call, so it can never total anything. static int calls = 0; is initialized once and shared by all frames — ideal for counting recursive invocations or cache hits.
Ask "scope or lifetime?" before reaching for static
Inside a function static extends lifetime. At file scope it restricts visibility. Naming which of the two you actually want prevents the most common storage-class mistake.
Quick success checklist
- Before run: base case present? argument shrinks? every path returns a value?
- Debug order: print the argument on entry → trace 3 levels on paper → check the unwind order
- Design habit: state the recurrence in words → head or tail? → is depth safe, or use a loop?
Level 6 Practice Programs List
Programming questions only — focused on recursion (base cases, head vs tail style, linear and tree recursion, recursion over arrays and strings) and storage classes (local, global, static).
- 1. Write a C program that computes the factorial of n using recursion.
- 2. Write a C program that prints numbers from 1 to n using tail recursion, then from n down to 1 using head recursion.
- 3. Write a C program that computes the sum of the first n natural numbers recursively.
- 4. Write a C program that computes the nth Fibonacci number recursively and prints the number of calls made.
- 5. Write a C program that computes the GCD of two numbers using recursion (Euclid's algorithm).
- 6. Write a C program that reverses a string using recursion.
- 7. Write a C program that computes the sum of all elements in an array using recursion.
- 8. Write a C program that checks whether a string is a palindrome using recursion.
- 9. Write a C program with a function containing a static local counter that prints how many times the function has been called.
- 10. Write a C program that declares a global variable and a local variable with the same name, and prints both to demonstrate scope.
- 11. Write a C program that demonstrates the difference between an automatic local variable and a static local variable across three calls.
- 12. Write a C program with two functions sharing a global variable, where one modifies it and the other prints it.
- 13. Write a C program that uses a static counter inside a recursive function to count the total number of recursive calls made.