student@ubuntu:~$
c-foundations Lesson 5 10 min read

Functions & Recursion

Prototypes, pass-by-value, scope rules, and thinking recursively about the call stack

Based on content from Dr. Stu Steiner, Eastern Washington University.

Reading: Hanly & Koffman: §3.1–3.5 (pp. 108–145), §3.7 (pp. 163–168)

In a nutshell

A C function has a return type, a name, a parameter list, and a body. In Java you wrote methods that lived inside a class; in C, functions live at file scope and there is no class holding them together. Every argument is copied (pass-by-value), just like a Java primitive. Every function call allocates a stack frame for its parameters and locals, so recursion is literally a stack of frames.

Practice this topic: C Functions drill, or browse the practice gallery.

After this lesson, you will be able to:

  • Write function prototypes and definitions and say what each contributes
  • Explain pass-by-value and its implications for modifying a caller’s variable
  • Identify where a variable is in scope and why globals are usually the wrong answer
  • Write a recursive function with a correct base case and predict how the call stack grows
  • Say when recursion is the right choice and when a loop is clearer

Quick reference

Purpose Shape
Prototype int add(int a, int b);
Definition int add(int a, int b) { return a + b; }
void return void print_hi(void) { printf("hi\n"); }
Local variable declared inside a function body
Global variable declared outside any function (avoid)
Recursive call function calls itself with a smaller problem

Coming from CSCD 210

You wrote methods, you called them, you knew about local versus instance scope, and you probably wrote at least one recursive method. Three differences in C. Functions live at file scope; there are no classes. C is strictly pass-by-value, so a function cannot directly modify a caller’s int (pointers in week 6 solve this). And you must write a prototype before calling a function whose definition appears later in the file.


Defining and calling

Structure

#include <stdio.h>

/* prototype: declares the function before main */
int add(int a, int b);

int main(void)
{
    int result;
    result = add(3, 4);
    printf("%d\n", result);     /* 7 */
    return 0;
}

/* definition: implements the function */
int add(int a, int b)
{
    return a + b;
}

The prototype tells the compiler the function exists, its return type, and its parameter types. Without it, calling add before its definition causes an implicit-declaration warning under -Wall and can silently pick the wrong return type. Always declare before main, define after.

Pass-by-value

Every argument is copied. Modifying the copy does not affect the caller.

void try_to_change(int x)
{
    x = 99;
    printf("Inside: %d\n", x);    /* 99 */
}

int main(void)
{
    int num = 5;
    try_to_change(num);
    printf("Outside: %d\n", num); /* 5, unchanged */
    return 0;
}

Java was the same for primitives. The difference: Java let you pass an object reference and modify the object’s fields. C has no objects. To modify a caller’s variable, you either return a new value and let the caller assign it, or (in week 6) pass a pointer.

int doubled(int x)
{
    return x * 2;
}

int main(void)
{
    int val = 5;
    val = doubled(val);      /* caller updates its own variable */
    printf("%d\n", val);     /* 10 */
    return 0;
}

Check your understanding (predict the output)

#include <stdio.h>

void double_it(int x)
{
    x = x * 2;
}

int main(void)
{
    int val = 5;
    double_it(val);
    printf("%d\n", val);
    return 0;
}
Reveal answer

Prints 5, not 10. When double_it(val) is called, the value 5 is copied into x. The function doubles its local copy to 10 and returns. The caller’s val is never touched. Pass-by-value. To actually change a caller’s variable, pass a pointer (week 6) or return a new value.


Scope

Variables declared inside a function (or block) are local to that block. They come into existence when the block starts and vanish when it ends.

int main(void)
{
    int x = 10;

    if (x > 5) {
        int y = 20;            /* y exists only inside this if block */
        printf("%d\n", y);
    }
    /* printf("%d\n", y); would fail: y is out of scope */

    return 0;
}

Global variables (declared outside any function) are visible everywhere in the file. Avoid them when you can. They make code harder to test and invite bugs in multi-threaded code. Pass values through parameters instead.

int count = 0;                 /* global; avoid when possible */

void increment(void)
{
    count++;                   /* silently modifies shared state */
}

When a global is genuinely the right tool (a configuration constant, a real singleton), keep it obviously named (g_config, or all-caps for const) so every use is visible.


Recursion

A recursive function calls itself. Every recursive function needs a base case (when to stop) and a recursive case (when to call itself with a smaller input).

int factorial(int n)
{
    if (n <= 1) {                       /* base case */
        return 1;
    }
    return n * factorial(n - 1);        /* recursive case */
}

Each call gets its own stack frame. Recursive calls stack up literally:

factorial(4) waits for factorial(3)
  factorial(3) waits for factorial(2)
    factorial(2) waits for factorial(1)
      factorial(1) returns 1
    factorial(2) returns 2 * 1 = 2
  factorial(3) returns 3 * 2 = 6
factorial(4) returns 4 * 6 = 24

Without a base case (or with one never reached), the stack grows until it hits the OS stack limit (typically a few MB). The program dies with a stack overflow (signal SIGSEGV). For the full picture of what a stack frame contains and how the OS allocates it, see the machine-model deep-dive.

Not every problem wants recursion. The classic fib definition is mathematically clean and operationally awful:

int fib(int n)
{
    if (n <= 1) {
        return n;
    }
    return fib(n - 1) + fib(n - 2);
}

Each call makes two more. fib(30) does about a million duplicated calls. A plain for loop computes the same sequence in 30 steps. Recursion is a tool; reach for a loop when a loop is clearer.

Check your understanding (what is missing?)

int sum(int n)
{
    return n + sum(n - 1);
}

A classmate calls sum(4) expecting 10. The program crashes with a segmentation fault instead. What is missing, and what is the mechanism behind the crash?

Reveal answer

No base case. Every call to sum(n) makes a call to sum(n - 1), including for negative values. Each allocates a new stack frame. Eventually the process exhausts its stack, the OS refuses the next frame allocation, and the program dies with SIGSEGV.

Fix:

int sum(int n)
{
    if (n <= 0) {
        return 0;
    }
    return n + sum(n - 1);
}

What comes next

Select every statement that is true about C functions.
AC passes every argument by value, so a function cannot directly modify a caller's int variable.
BA function can always be called before its definition, without a prototype, as long as the definition is somewhere in the file.
CEach function call allocates a new stack frame; runaway recursion exhausts the stack.
DA variable declared inside an if block is in scope only inside that block.
EGlobal variables are the preferred way to share data between functions in C.
Correct: A, C, D.
  • B is wrong: calling a function before the compiler has seen any declaration triggers an implicit-declaration warning under -Wall and can silently use the wrong return type.
  • E is wrong: globals are sometimes necessary but should not be the default. Pass values explicitly.

Next, Arrays & Strings shows how C stores sequences and why a string is just a char array with a null terminator. Drill this page: C Functions or the practice gallery.