c-foundations Lesson 9 22 min read

How Do I Organize Code with Functions and Recursion?

Function prototypes, pass-by-value, and why swap doesn't work — yet

Reading: C Text: Ch. 6 (Functions), Ch. 9 §1–4 (Recursion)

After this lesson, you will be able to:

  • Write C functions with a return type, parameter list, and body
  • Write function prototypes above main and definitions below main
  • Explain pass-by-value and why modifying a parameter does not change the caller’s variable
  • Explain local variable scope across functions
  • Write a recursive function with a base case and trace its execution on the call stack

The Swap That Doesn’t Swap

Here’s one of the most instructive bugs in C:

Before you read further: look at the code below and predict what it outputs. Does swap(x, y) actually swap the values? Then read the explanation.

void swap(int a, int b)
{
    int temp = a;
    a = b;
    b = temp;
}

int main(void)
{
    int x = 5, y = 10;
    swap(x, y);
    printf("x = %d, y = %d\n", x, y);    // x = 5, y = 10 — DIDN'T SWAP!
}

The swap worked perfectly inside the function — but x and y didn’t change. Why? Because C passes everything by value. The function got copies of x and y, swapped the copies, and threw them away.

You’ll fix this in Series 3 with pointers. But understanding why it fails teaches you how C functions actually work.

Check Your Understanding
After calling swap(x, y) where swap is defined as void swap(int a, int b), what are the values of x and y?
A Unchanged — a and b are copies, so swapping them doesn't affect x and y
B Swapped — x gets y's value and vice versa
C Both become 0 — the function resets them
D Undefined behavior — you can't pass two variables to a function
Answer: A. C is pass-by-value. The function receives copies of x and y in parameters a and b. Swapping the copies has no effect on the originals. To actually swap the caller's variables, you need pointers (Series 3).

Functions from the Ground Up

Writing Functions

A C function has four parts:

return_type function_name(parameter_list)
{
    // body
    return value;
}

Example:

double calculate_average(double sum, int count)
{
    return sum / count;
}

From Java: C functions are like Java methods, minus the class. No public, no static, no this. Just the return type, name, and parameters. If you’ve written Java methods, you can write C functions.

Function Prototypes

C reads top-to-bottom. You must declare a function before calling it:

// Prototype (declaration) — goes at top of file
double calculate_average(double sum, int count);

int main(void)
{
    double avg = calculate_average(95.0, 3);    // Call — compiler knows about it
    printf("Average: %.2f\n", avg);
    return 0;
}

// Definition — goes after main
double calculate_average(double sum, int count)
{
    return sum / count;
}

Key Insight: Prototypes let you organize your code with main first (for readability) and helper functions after. The compiler just needs to know the function’s signature before it sees a call.

Check Your Understanding
What happens if you call a function in main without declaring a prototype or defining it above main?
A The program compiles but crashes at runtime
B C automatically searches the entire file for the function
C The compiler issues an error because it doesn't know the function's signature
D The compiler assumes the function returns void and takes no arguments
Answer: C. C reads top-to-bottom. If the compiler encounters a function call it hasn't seen declared, it can't verify argument types or return type. In C99 and later, this is an error. (In ancient C89, the compiler would silently guess — option D was actually the old behavior — but modern compilers reject it.)

Pass-by-Value

When you call a function, C copies each argument into the parameter:

void double_it(int x)
{
    x = x * 2;           // Modifies the LOCAL copy
    printf("Inside: %d\n", x);   // 10
}

int main(void)
{
    int num = 5;
    double_it(num);
    printf("Outside: %d\n", num); // Still 5!
}

Key Insight: Every function parameter is a copy. Modifying a parameter inside a function does NOT affect the caller’s variable. This is why swap(x, y) doesn’t work — it swaps copies. To modify the caller’s variable, you need pointers (Series 3).

From Java: Java primitives are also pass-by-value. But Java objects are passed by reference-value — a copy of the reference. C has no objects and no implicit references. In C, if you want to modify something in another scope, you must explicitly pass its memory address with &.

void Functions

Functions that don’t return a value use void:

void print_header(void)
{
    printf("====================\n");
    printf("  Grade Report\n");
    printf("====================\n");
}

Common Pitfall: void greet() and void greet(void) are different in C! Empty parentheses () means “unspecified number of arguments” (a legacy C89 feature). (void) means “no arguments.” Always use (void) when you mean no parameters.

Check Your Understanding
You write void double_it(int x) { x = x * 2; } and call double_it(num). After the call, what is the value of num?
A Unchanged — still its original value
B Doubled
C Zero
D Undefined — depends on the compiler
Answer: A. C is pass-by-value. The function receives a copy of num. Modifying x inside the function only changes the local copy. The caller's num is untouched. To actually modify the caller's variable, you'd need pointers (Series 3).

Scope

Variables declared inside a function are local to that function:

void function_a(void)
{
    int x = 5;            // Local to function_a
}

void function_b(void)
{
    printf("%d", x);      // ERROR: x doesn't exist here
}

The Trick: Avoid global variables. They make code harder to reason about because any function can change them at any time. Pass data through parameters and return values instead.

Why does this matter?

Pass-by-value and local scope might feel restrictive compared to Java’s object references, but they make C functions predictable. When you read a function, you know exactly what data it can access — its parameters and its locals. No hidden side effects. This predictability is why C is used in safety-critical systems like medical devices and aircraft software.

Introduction to Recursion

A function that calls itself:

int factorial(int n)
{
    if (n <= 1)            // Base case: stop here
    {
        return 1;
    }
    return n * factorial(n - 1);    // Recursive case
}

Let’s trace factorial(4):

factorial(4) = 4 * factorial(3)
             = 4 * (3 * factorial(2))
             = 4 * (3 * (2 * factorial(1)))
             = 4 * (3 * (2 * 1))
             = 4 * (3 * 2)
             = 4 * 6
             = 24

Key Insight: Every recursive function needs a base case (when to stop) and a recursive case (how to get closer to the base case). Without a base case, recursion runs forever and crashes with a stack overflow.

Each call creates a new stack frame — a block of memory holding that call’s local variables. When the call returns, its frame is removed. This is the call stack, and you’ll visualize it in detail in Series 3.

Deep dive: Visualizing the call stack for factorial(4)

Each recursive call adds a frame to the stack. The frames build up until the base case is reached, then unwind as each function returns:

Step 1: factorial(4) called       Step 2: factorial(3) called
┌─────────────────┐               ┌─────────────────┐
│ factorial(4)    │               │ factorial(3)    │
│  n = 4          │               │  n = 3          │
│  waiting...     │               │  waiting...     │
└─────────────────┘               ├─────────────────┤
                                  │ factorial(4)    │
                                  │  n = 4          │
                                  └─────────────────┘

Step 3: Base case reached         Step 4: Unwinding
┌─────────────────┐               ┌─────────────────┐
│ factorial(1)    │               │ factorial(2)    │
│  n = 1          │               │  n = 2          │
│  returns 1      │  ← base!     │  returns 2*1=2  │
├─────────────────┤               ├─────────────────┤
│ factorial(2)    │               │ factorial(3)    │
│  n = 2          │               │  n = 3          │
├─────────────────┤               ├─────────────────┤
│ factorial(3)    │               │ factorial(4)    │
│  n = 3          │               │  n = 4          │
├─────────────────┤               └─────────────────┘
│ factorial(4)    │
│  n = 4          │
└─────────────────┘

At the deepest point (step 3), there are 4 stack frames. Each frame has its own n. When factorial(1) returns 1, the stack unwinds: factorial(2) computes 2*1=2, factorial(3) computes 3*2=6, factorial(4) computes 4*6=24.

This is why infinite recursion crashes — each call adds ~16-32 bytes to the stack. A million recursive calls could consume tens of megabytes, exceeding the typical 1-8 MB stack limit.

Power Function (Recursion Example)

double power(double base, int exp)
{
    if (exp == 0)
    {
        return 1.0;
    }
    return base * power(base, exp - 1);
}
printf("%.0f\n", power(2.0, 10));    // 1024
Check Your Understanding
What happens if a recursive function is missing its base case?
A The compiler catches the error and refuses to compile
B The function returns 0 by default
C The function runs exactly once and stops
D It calls itself indefinitely, creating stack frames until the stack overflows and crashes
Answer: D. Each recursive call creates a new stack frame. Without a base case to stop the recursion, frames pile up until the program runs out of stack memory. The compiler can't detect this — it's a runtime problem, not a syntax error. The crash usually shows up as a segmentation fault.
Quick Check: Why doesn't the swap(int a, int b) function work?

C passes arguments by value — a and b are copies of the caller’s variables. Swapping the copies has no effect on the originals. To swap the caller’s variables, you need to pass their memory addresses using pointers (covered in Series 3).

Quick Check: What happens if a recursive function has no base case?

It calls itself infinitely, creating new stack frames until the call stack overflows. This crashes the program with a “segmentation fault” or “stack overflow” error.

Quick Check: Why use function prototypes instead of just defining functions before main?

Prototypes let you put main at the top of the file for readability. Without prototypes, you’d have to define all helper functions before main, making the program’s entry point hard to find. Prototypes also enable mutual recursion (function A calls B, B calls A).


Functions Are Your Building Blocks

Functions let you break a program into manageable, reusable pieces. But the pass-by-value limitation is real — you can’t modify a caller’s variable from inside a function. Not yet.

In Series 3, you’ll learn pointers — the mechanism that lets you pass memory addresses to functions, finally making swap work. Pointers are the most important concept in C, and functions are where they become essential.

Next: input handling. You’ll learn the buffer-clearing pattern and how to build reliable input loops for your labs.

Big Picture: C doesn’t have function overloading (multiple functions with the same name but different parameters). Each function name must be unique. This is simpler than Java but means you’ll see names like print_int, print_double, print_string instead of three print methods.