How Do I Organize Code with Functions and Recursion?
Function prototypes, pass-by-value, and why swap doesn't work — yet
After this lesson, you will be able to:
- Write C functions with a return type, parameter list, and body
- Write function prototypes above
mainand definitions belowmain - 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.
swap(x, y) where swap is defined as void swap(int a, int b), what are the values of x and y?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, nostatic, nothis. 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
mainfirst (for readability) and helper functions after. The compiler just needs to know the function’s signature before it sees a call.
main without declaring a prototype or defining it above main?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()andvoid 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.
void double_it(int x) { x = x * 2; } and call double_it(num). After the call, what is the value of num?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
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_stringinstead of three