Functions in C Part 2: Call by Value, Call by Reference, Scope, and Recursion

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 of x and y, not their values.
  • Inside swap, the parameters a and b are pointers — variables that store an address rather than an ordinary value.
  • *a and *b (the dereference operator) mean “go to the address stored in a/b, and access the actual value there.”
  • Because a holds x‘s real address, writing to *a writes directly into x‘s memory — that’s why the change is visible back in main(), 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 why localValue is always 101 at print time, never accumulating across calls, and why main() 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.

Leave a Comment