In Part 1, we covered what functions are and how to declare, define, and call them. In this post, we go deeper into how values actually travel into and out of functions, where variables “live” (scope), and one of the most powerful ideas in programming: a function calling itself, known as recursion.
Call by Value: The Default in C
When you pass a variable to a function in C, by default, C copies the value of that variable into the function’s parameter. The function works with its own private copy — any changes made inside the function do not affect the original variable back in the caller.
#include <stdio.h>
void tryToDouble(int num) {
num = num * 2;
printf("Inside function, num = %d\n", num);
}
int main() {
int value = 10;
tryToDouble(value);
printf("Back in main, value = %d\n", value);
return 0;
}
Sample Output
Inside function, num = 20
Back in main, value = 10
Notice that value in main() is still 10, even though num was doubled inside the function. This is call by value: num and value are two completely separate memory locations. Changing one has zero effect on the other.
Call by Reference: Passing Addresses with Pointers
Sometimes you want a function to modify the caller’s actual variable — for example, a function that swaps two numbers, or updates several results at once (since a function can only return one value directly). C achieves this using pointers: instead of passing the value itself, you pass the address of the variable (using the & operator), and the function accepts a pointer parameter (using *) to reach back into the caller’s memory and modify it directly.
#include <stdio.h>
void swap(int *a, int *b) {
int temp = *a; // *a means "the value stored at the address a points to"
*a = *b;
*b = temp;
}
int main() {
int x = 5, y = 10;
printf("Before swap: x = %d, y = %d\n", x, y);
swap(&x, &y); // passing addresses, not values
printf("After swap: x = %d, y = %d\n", x, y);
return 0;
}
Sample Output
Before swap: x = 5, y = 10
After swap: x = 10, y = 5
Breaking this down
swap(&x, &y)passes the memory addresses ofxandy, not their values.- Inside
swap, the parametersaandbare pointers — variables that store an address rather than an ordinary value. *aand*b(the dereference operator) mean “go to the address stored ina/b, and access the actual value there.”- Because
aholdsx‘s real address, writing to*awrites directly intox‘s memory — that’s why the change is visible back inmain(), unlike the call-by-value example above.
A Practical Use Case: Returning Multiple Values
A function can only return one value. But with pointers, a function can update several variables at once — effectively returning multiple results.
#include <stdio.h>
void minMax(int arr[], int size, int *min, int *max) {
*min = arr[0];
*max = arr[0];
for (int i = 1; i < size; i++) {
if (arr[i] < *min) *min = arr[i];
if (arr[i] > *max) *max = arr[i];
}
}
int main() {
int numbers[] = {23, 5, 67, 12, 89, 1};
int smallest, largest;
minMax(numbers, 6, &smallest, &largest);
printf("Smallest: %d\n", smallest);
printf("Largest: %d\n", largest);
return 0;
}
Sample Output
Smallest: 1
Largest: 89
Note on arrays: unlike ordinary variables, when you pass an array to a function (like numbers above), C automatically passes it as a pointer to its first element — arrays are effectively always passed “by reference” in C, even without an explicit &. This is why modifying arr[i] inside a function affects the original array in the caller.
Scope: Where Do Variables Live?
Every variable in C has a scope — the region of code where it’s valid and accessible.
#include <stdio.h>
int globalCounter = 0; // global variable: visible to every function in this file
void increment() {
int localValue = 100; // local variable: only exists inside increment()
globalCounter++;
localValue++;
printf("Inside increment: localValue = %d, globalCounter = %d\n", localValue, globalCounter);
}
int main() {
increment();
increment();
increment();
// printf("%d", localValue); // ERROR: localValue doesn't exist here
printf("Final globalCounter (visible in main too): %d\n", globalCounter);
return 0;
}
Sample Output
Inside increment: localValue = 101, globalCounter = 1
Inside increment: localValue = 101, globalCounter = 2
Inside increment: localValue = 101, globalCounter = 3
Final globalCounter (visible in main too): 3
- Local variables (like
localValue) are created fresh each time the function runs and destroyed when the function returns — that’s whylocalValueis always 101 at print time, never accumulating across calls, and whymain()can’t access it at all. - Global variables (like
globalCounter) are declared outside any function, live for the entire program’s execution, and are visible to every function in the file — which is why the increments accumulate across all three calls. - Overusing global variables is generally considered poor practice in larger programs (functions become harder to reason about in isolation, and bugs can appear from unexpected places modifying shared state) — prefer passing values as parameters and returning results, reserving globals for genuinely program-wide constants or state.
Recursion: A Function That Calls Itself
Recursion is when a function calls itself to solve a smaller version of the same problem, until it reaches a base case simple enough to answer directly.
Example: Factorial
The factorial of n (written n!) is n × (n-1) × (n-2) × … × 1. Notice that n! = n × (n-1)!, and (n-1)! is just “factorial, but for a smaller number” — a perfect fit for recursion.
#include <stdio.h>
int factorial(int n) {
if (n == 0 || n == 1) { // base case: stops the recursion
return 1;
}
return n * factorial(n - 1); // recursive case: calls itself with a smaller n
}
int main() {
for (int i = 0; i <= 5; i++) {
printf("%d! = %d\n", i, factorial(i));
}
return 0;
}
Sample Output
0! = 1
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
Tracing factorial(4) step by step
factorial(4)
= 4 * factorial(3)
= 4 * (3 * factorial(2))
= 4 * (3 * (2 * factorial(1)))
= 4 * (3 * (2 * 1)) <- factorial(1) hits the base case, returns 1
= 4 * (3 * 2)
= 4 * 6
= 24
Every recursive call "pauses" waiting for the next call to finish, building up a chain, and then the results are multiplied together as the chain unwinds back to the original call. This waiting is managed using a data structure called the call stack — each call gets its own stack frame holding its own copy of n.
Every Correct Recursive Function Needs Two Things
- A base case — a condition simple enough to answer without recursing further (here,
n == 0 || n == 1). Without this, the function calls itself forever (in practice, it crashes with a stack overflow once memory runs out). - A recursive case that moves toward the base case — each call must work on a "smaller" version of the problem (here,
n - 1), so it's guaranteed to eventually reach the base case.
Example: Fibonacci Sequence
#include <stdio.h>
int fibonacci(int n) {
if (n == 0) return 0; // base case 1
if (n == 1) return 1; // base case 2
return fibonacci(n - 1) + fibonacci(n - 2); // recursive case
}
int main() {
printf("First 10 Fibonacci numbers:\n");
for (int i = 0; i < 10; i++) {
printf("%d ", fibonacci(i));
}
printf("\n");
return 0;
}
Sample Output
First 10 Fibonacci numbers:
0 1 1 2 3 5 8 13 21 34
Each Fibonacci call branches into two recursive calls instead of one, which is why this version becomes noticeably slow for larger n (it recomputes the same values repeatedly) — a good example of why recursion is elegant to write but not always the most efficient choice; this trade-off (and how to fix it with techniques like memoization) is a great topic to explore once you're comfortable with the basics covered here.
Recursion vs. Iteration
Anything recursion can do, a loop can also do (and vice versa) — they're different tools for expressing the same kind of repetition. As a rule of thumb:
- Use iteration (loops) when the problem is naturally a simple repeated count or accumulation — usually faster and uses less memory (no call stack overhead).
- Use recursion when the problem is naturally defined in terms of smaller versions of itself (factorial, Fibonacci, tree/directory traversal, divide-and-conquer algorithms like merge sort) — often much shorter and easier to read than the equivalent loop-based version.
Summary
- Call by value (the default): the function gets a copy; changes inside don't affect the caller's original variable.
- Call by reference (via pointers,
&and*): the function gets the actual address and can modify the caller's variable directly — also how a function can effectively "return" more than one value. - Arrays are always passed by reference in C, automatically.
- Local variables exist only during a function's execution; global variables exist for the whole program and are visible everywhere.
- Recursion requires a base case (to stop) and a recursive case that moves toward it (to make progress).
Coming up in Part 3: storage classes (static, extern, auto, register) and what they mean for functions and variables, function pointers, and how to split a program across multiple files using header files.