What is a function?

Real-World Analogy — Functions as Recipes

Think of a function as a recipe card. The recipe has a name ("Bake Chocolate Cake"), a list of ingredients you must provide (parameters — "2 cups flour, 3 eggs"), a set of instructions (the body), and an outcome it produces (the return value — "one finished cake"). When a recipe says "sift the flour" it can call another recipe ("How to Sift Flour") — that is one function calling another. Each recipe does one thing, has a clear interface, and can be reused any number of times. C functions work exactly the same way.

A function in C is a named block of code that takes zero or more inputs, performs some computation, and optionally returns a result. Functions let you break a large program into smaller, testable pieces. Every C program has at least one function: main, which is the entry point the operating system calls first.

There are two related concepts you must keep separate: a declaration (also called a prototype) and a definition.

A declaration tells the compiler the function's name, return type, and parameter types — but contains no body (no curly braces). It ends with a semicolon. Declarations live in header files (.h) or at the top of a .c file, before main. They let the compiler check that every call site passes the right arguments before it ever sees the actual implementation.

A definition includes the declaration plus the full function body — the curly braces, local variables, logic, and return statement. Each function name may be defined exactly once across an entire program (the "one definition rule"). Attempting to define the same function twice produces a linker error.

Pass by value is the key mental model for parameters in C. When you call add(x, y), C copies the values of x and y into fresh local variables inside add. The function never sees the originals — it sees clones. This means modifying a parameter inside a function has no effect on the caller's variable. This is fundamentally different from Python's object model and important to get right early.

Scope refers to where a variable is visible and accessible. Variables declared inside a function are local to that function — they exist only while the function is executing and are invisible to all other functions. Variables declared outside all functions are global and visible everywhere. Prefer local variables: global state makes programs hard to reason about and debug.

Python (what you know)
# Python function
def add(a, b):
    return a + b

# No type annotations required
def greet(name):
    print(f"Hello, {name}")  # no return value

result = add(3, 5)   # 8
C (what you are learning)
/* C function */
int add(int a, int b) {
    return a + b;
}

/* void = no return value */
void greet(char *name) {
    printf("Hello, %s\n", name);
}

int result = add(3, 5);  /* 8 */
Java (for reference)
// Java static method
public static int add(int a, int b) {
    return a + b;
}

// void return type
public static void greet(String name) {
    System.out.println("Hello, " + name);
}
Key C Differences
- No classes: functions are top-level
- Types are mandatory (no inference)
- void = no params OR no return
- Parameters: ALWAYS pass by value
- No overloading (one name = one fn)
- Forward declarations (prototypes)
- extern keyword for multi-file use
Concept Python Java C
Return type declared? No (dynamic) Yes (int, void, etc.) Yes — mandatory
Parameter types declared? No (dynamic) Yes Yes — mandatory
Pass by value? References (objects) Primitives by value, objects by ref Always by value
Overloading? No Yes No
Forward declaration needed? No No Yes (if calling before defining)
Void parameters def f(): void f() void f(void) — explicit
Common Mistake 1 — Forgetting the Forward Declaration

In C, the compiler processes code from top to bottom. If you define add() after main() and call it from main() without a prior declaration, the compiler assumes a default (often wrong) type and may warn or error. The fix: either put a prototype at the top, or define helper functions above the functions that call them.

Common Mistake 2 — Expecting Pass-by-Reference Behaviour

In C, parameters are always copies. If you write void double_it(int x) { x *= 2; } and call double_it(n), n in the caller is unchanged. To modify the caller's variable you must pass a pointer to it — covered in Topic 08. This surprises almost every beginner.

Common Mistake 3 — Returning a Pointer to a Local Variable

A local variable lives on the stack frame of its function. Once the function returns, that stack frame is gone. If you return a pointer to a local variable, the pointer is immediately dangling (points to freed memory). This is undefined behavior and a very common source of bugs. Never do: int *f(void) { int x = 5; return &x; }

Mental Model Summary

A function = name + return type + parameter list + body. Declaration = the contract (no body). Definition = the implementation. Parameters = copies of arguments (pass by value). Local variables = live only during the call. void = nothing (either no return, or no parameters).

Anatomy of a C function

Every part of a function declaration and definition has a precise name. Here is the annotated breakdown:

/* ── FUNCTION DECLARATION (prototype) ── */

int  add(int a, int b);
 ^    ^   ^           ^
 |    |   |           └── semicolon: ends a declaration (no body)
 |    |   └────────────── parameter list: type + name pairs (names optional in decl)
 |    └────────────────── function name (identifier)
 └─────────────────────── return type: what the function hands back to its caller

/* ── FUNCTION DEFINITION ── */

int  add(int a, int b)  {
    int result = a + b;   /* local variable — lives only during this call */
    return result;           /* return statement: sends value back to caller  */
}
 ^                      ^
 |                      └── closing brace ends the function body
 └─────────────────────── opening brace starts the function body

/* ── VOID FUNCTION (no return value) ── */

void  print_double(int x)  {
    printf("%d\n", x * 2);
    /* no return statement needed (or use bare "return;") */
}

/* ── VOID PARAMETERS (no parameters at all) ── */

int  get_magic_number(void)  {
    return 42;
    /* "void" in parameter list means "this function takes NO arguments" */
}
Return type colours: blue = type keyword, green = function name, orange = parameter names, pink = keywords/punctuation. The void keyword serves double duty: as a return type it means "returns nothing"; in the parameter list it means "accepts nothing".

The extern keyword — multi-file functions

/* foo.h — the header file declares the interface */
extern int foo(int x);
 ^
 └── "extern" = defined in another .c file, linker will find it

/* foo.c — the .c file defines the implementation */
int foo(int x) { return x * 2; }

/* main.c — includes the header to know about foo */
#include "foo.h"
int main(void) { foo(5); return 0; }
Function declarations (without bodies) in header files are the standard way to share a function's interface across multiple .c files. The linker resolves the reference at link time. This is the pattern used by every C library.

Scope rules in C

int global_count = 0;  /* global scope — visible in ALL functions in this file */

void increment(void) {
    int step = 1;        /* local scope — exists ONLY while increment() runs */
    global_count += step;
}                      /* 'step' is destroyed here — memory returned to stack */

void show(void) {
    /* step is invisible here — "undeclared identifier" if you try to use it */
    printf("%d\n", global_count);  /* global_count IS visible */
}

static int secret = 7;  /* file scope only — invisible to other .c files */
Scope hierarchy: block scope (inside {}) < function scope < file scope (static global) < program scope (extern global). Local beats global when names clash — the inner declaration shadows the outer one.

The Call Stack

Every time a function is called, the operating system pushes a stack frame onto the call stack. The frame holds the function's local variables, its parameters (copies), the return address (where to jump back to), and the saved state of the caller. When the function returns, its frame is popped off and that memory is reclaimed. This is why local variables do not persist between calls.

Call Stack State During: factorial(3) calls factorial(2) calls factorial(1)
HIGH ADDRESS (stack grows downward)
Stack Frame: factorial(n=1) ← currently executing
n = 1    [local param copy]
result = 1    [local variable]
return address → factorial(2) + resume point
Stack Frame: factorial(n=2) ← waiting for factorial(1)
n = 2
result = ?   [not yet computed]
return address → factorial(3) + resume point
Stack Frame: factorial(n=3) ← waiting for factorial(2)
n = 3
result = ?   [not yet computed]
return address → main() + resume point
Stack Frame: main()
argc, argv, local variables of main
return address → OS (program entry point)
LOW ADDRESS
Stack Overflow

Each stack frame takes memory. Infinite or very deep recursion (thousands of frames) exhausts the stack and causes a stack overflow — the program crashes with a segmentation fault. This is why recursion must always have a base case that terminates the chain.

Function Pointer Preview

/* A function pointer stores the address of a function */
/* Type: "pointer to function taking (int,int) and returning int" */

int (*fp)(int, int);
 ^    ^  ^
 |    |  └── parameter types of the function being pointed to
 |    └───── * inside parens = this is a function POINTER, not a function call
 └────────── return type of the pointed-to function

fp = add;          /* assign — function name without () is its address */
int r = fp(3, 4);  /* call via pointer — r == 7 */
Function pointers are covered fully in Topic 15. For now, know they exist and that a function name without parentheses evaluates to the function's address — this is how callbacks and qsort comparators work.

Variadic Functions — va_list and va_args

Some functions must accept a variable number of arguments of unknown types — like printf. C supports this via the variadic function syntax (...) and the macros in <stdarg.h>: va_list, va_start, va_arg, and va_end.

#include <stdarg.h>   /* provides: va_list, va_start, va_arg, va_end */

/* Variadic function: accepts variable number of arguments         */
/* The '...' means "zero or more additional arguments of any type" */
/* You MUST have at least one fixed parameter before '...'         */

int my_sum(int count, ...) {         /* count = how many ints follow */
    va_list args;
    va_start(args, count);           /* initialize; 'count' = last fixed param */

    int total = 0;
    for (int i = 0; i < count; i++) {
        total += va_arg(args, int);  /* read next arg as specified type */
    }

    va_end(args);    /* REQUIRED — clean up va_list before returning */
    return total;
}
/* my_sum(3, 10, 20, 30)   → 60  */
/* my_sum(5, 1, 2, 3, 4, 5) → 15 */

/* This is exactly how printf() works:                      */
/* int printf(const char *format, ...)                      */
/* When printf sees "%d" it calls va_arg(args, int)         */
/* When it sees "%s" it calls va_arg(args, char *)          */
Key rules: (1) No compile-time type checking on the ... args — calling va_arg(args, double) for an int argument is undefined behavior. (2) va_end must be called before returning — resources may not be freed otherwise. (3) You must track argument count yourself (via a fixed count parameter, a sentinel NULL at the end, or a format string like printf uses). (4) va_start's second argument must be the last named parameter before the ....
No type safety on variadic arguments

The compiler cannot check types in the ... part. If the caller passes a double but the function calls va_arg(args, int), the bytes are misread — silent undefined behavior. This is why printf format strings must exactly match the argument types.

Random Numbers — rand() and srand()

The standard library functions rand() and srand() from <stdlib.h> generate pseudo-random integers. They appear in the Week 3 password generator exercise and are useful for simulations, games, and randomised testing.

#include <stdlib.h>   /* rand(), srand(), RAND_MAX */
#include <time.h>     /* time() — needed for time-based seed */

/* rand() — returns a pseudo-random int in [0, RAND_MAX] */
/* RAND_MAX is at least 32767; typically 2147483647 on 64-bit systems */
int r = rand();                              /* 0 to RAND_MAX */
int die = (rand() % 6) + 1;               /* 1 to 6 */
double frac = (double)rand() / RAND_MAX;  /* 0.0 to 1.0 */

/* srand(seed) — seeds the generator; same seed → same sequence */
srand(42);           /* fixed seed: reproducible (useful for testing) */
srand(time(NULL));    /* time-based seed: different each run */

/* Helper: random int in closed range [lo, hi] */
int random_in_range(int lo, int hi) {
    return lo + rand() % (hi - lo + 1);
}
Key points: Call srand() once at the start of your program — never inside a loop. Without a call to srand(), most implementations default to seed 1, producing the same sequence every run. The % modulo trick introduces a slight bias for large ranges (where RAND_MAX is not evenly divisible by the range size) — acceptable for games and exercises, not for cryptography.
Never use rand() for cryptographic purposes

rand() is a pseudo-random generator — given the same seed, it produces the same sequence. An attacker who knows the seed can predict all future values. For passwords, tokens, or keys, use /dev/urandom (on Linux/macOS) or a crypto library. rand() is fine for games, simulations, and testing.

Command-Line Arguments — argc and argv

Every C program's main is itself a function. Its full signature accepts two parameters: argc (argument count) and argv (argument vector). This is how programs receive input from the shell — like ./myprogram file.txt 42.

int main(int argc, char **argv)
           ^           ^
           |           └── array of strings (char*); each argv[i] is one argument
           └───────────── count of arguments INCLUDING the program name itself

/* Running: ./hello world 42 */
/* argc == 3 */
/* argv[0] == "./hello"   (always the program name) */
/* argv[1] == "world"     (first user argument) */
/* argv[2] == "42"        (second user argument — still a STRING, not int!) */
/* argv[3] == NULL        (sentinel — array is NULL-terminated) */

#include <stdio.h>
#include <stdlib.h>   /* for atoi() */

int main(int argc, char **argv) {
    if (argc != 3) {
        fprintf(stderr, "Usage: %s <name> <number>\n", argv[0]);
        return 1;       /* non-zero exit = error */
    }
    int n = atoi(argv[2]);   /* convert string "42" to integer 42 */
    printf("Hello, %s! Number is %d\n", argv[1], n);
    return 0;
}
Key rules: (1) argv[0] is always the program name — user arguments start at argv[1]. (2) All arguments arrive as strings — use atoi() or strtol() to convert to integers. (3) Always check argc before accessing argv[i] — accessing beyond argc-1 is undefined behavior. (4) char **argv and char *argv[] are equivalent in this context.

Complete C functions you can compile and run

Example 1 — Recursive factorial (from Week 2 Lecture) Lecture Page 24
#include <stdio.h>

/* FORWARD DECLARATION (prototype) — tells compiler about factorial before main */
int factorial(int n);

int main(void) {
    /* Call factorial for several values */
    printf("0! = %d\n", factorial(0));
    printf("1! = %d\n", factorial(1));
    printf("5! = %d\n", factorial(5));
    printf("10! = %d\n", factorial(10));
    return 0;
}

/* FUNCTION DEFINITION — the full implementation */
/*
 * factorial(n) - computes n! (n factorial)
 *
 * Parameters:
 *   n  - the non-negative integer to factorial
 *
 * Returns:
 *   n! = n * (n-1) * ... * 2 * 1
 *   Special case: 0! = 1 by mathematical convention
 *
 * How recursion works here:
 *   factorial(3)
 *     = 3 * factorial(2)
 *         = 3 * 2 * factorial(1)
 *             = 3 * 2 * 1 * factorial(0)
 *                         = 3 * 2 * 1 * 1  <-- base case reached
 *             = 3 * 2 * 1
 *         = 3 * 2
 *     = 6
 */
int factorial(int n) {
    int result;

    if (n > 1) {
        /* Recursive case: n! = n * (n-1)! */
        result = n * factorial(n - 1);
    } else {
        /* Base case: 0! = 1! = 1 — stops the recursion */
        result = 1;
    }

    return result;  /* hand the computed value back to the caller */
}
Output
0! = 1
1! = 1
5! = 120
10! = 3628800
Example 2 — swap: demonstrating pass-by-value vs pass-by-pointer Tutorial — Functions Exercise
#include <stdio.h>

/* ─── ATTEMPT 1: pass by value — does NOT work ─── */
void swap_wrong(int a, int b) {
    /* a and b are COPIES of the caller's variables.
       Modifying them here has zero effect on the caller. */
    int tmp = a;
    a = b;
    b = tmp;
    /* When this function returns, a and b are destroyed.
       The caller's originals are completely unchanged. */
}

/* ─── ATTEMPT 2: pass by pointer — DOES work ─── */
/*
 * swap(pa, pb) - swaps the values at two memory addresses
 *
 * Parameters:
 *   pa  - pointer to the first integer  (address of x)
 *   pb  - pointer to the second integer (address of y)
 *
 * Returns:
 *   void — modifies values in-place via pointers
 *
 * The * operator (dereference) reads/writes the value AT the address.
 */
void swap(int *pa, int *pb) {
    int tmp = *pa;   /* read value at address pa, store in tmp */
    *pa = *pb;       /* write value at pb into the location at pa */
    *pb = tmp;       /* write tmp into the location at pb */
}

int main(void) {
    int x = 10, y = 20;

    printf("Before swap_wrong: x=%d, y=%d\n", x, y);
    swap_wrong(x, y);   /* pass VALUES — function gets copies */
    printf("After swap_wrong:  x=%d, y=%d\n", x, y);  /* unchanged! */

    printf("\nBefore swap: x=%d, y=%d\n", x, y);
    swap(&x, &y);       /* pass ADDRESSES — function can modify originals */
    printf("After swap:  x=%d, y=%d\n", x, y);  /* swapped! */

    return 0;
}
Output
Before swap_wrong: x=10, y=20
After swap_wrong: x=10, y=20

Before swap: x=10, y=20
After swap: x=20, y=10
Example 3 — Custom string_length: understanding arrays and loops in functions Tutorial — Functions + Strings
#include <stdio.h>

/*
 * string_length(s) - counts characters in a null-terminated string
 *
 * Parameters:
 *   s  - pointer to the first character of a null-terminated C string
 *        (const means we promise not to modify the string via this pointer)
 *
 * Returns:
 *   number of characters before the '\0' terminator
 *   (same result as the standard strlen() from <string.h>)
 *
 * Algorithm: walk pointer forward until we hit '\0', count steps.
 */
int string_length(const char *s) {
    int count = 0;
    /* s[count] is equivalent to *(s + count) — array index notation */
    while (s[count] != '\0') {
        count++;
    }
    return count;
    /* Note: '\0' has integer value 0, so the condition can also be written:
       while (s[count])  -- because 0 is falsy in C */
}

/*
 * count_upper(s) - counts uppercase ASCII letters in a string
 *
 * Parameters:
 *   s  - null-terminated string to scan
 *
 * Returns:
 *   number of characters in the range 'A'..'Z'
 */
int count_upper(const char *s) {
    int count = 0;
    for (int i = 0; s[i] != '\0'; i++) {
        if (s[i] >= 'A' && s[i] <= 'Z') {
            count++;
        }
    }
    return count;
}

int main(void) {
    char greeting[] = "Hello, World!";
    char empty[]    = "";
    char all_caps[] = "COMP2017";

    printf("Length of \"%s\" = %d\n", greeting, string_length(greeting));
    printf("Length of \"%s\" = %d\n", empty,    string_length(empty));
    printf("Length of \"%s\" = %d\n", all_caps, string_length(all_caps));

    printf("Uppercase in \"%s\": %d\n", greeting, count_upper(greeting));
    printf("Uppercase in \"%s\": %d\n", all_caps, count_upper(all_caps));

    return 0;
}
Output
Length of "Hello, World!" = 13
Length of "" = 0
Length of "COMP2017" = 8
Uppercase in "Hello, World!": 2
Uppercase in "COMP2017": 7

Practice problems with solutions

P1 — Write a function: power(base, exp) Tutorial — Functions Exercise

Write a C function int power(int base, int exp) that computes base raised to the power exp using a loop (not recursion, not pow() from <math.h>). Assume exp >= 0. In main, call it to verify: power(2, 10) should return 1024, power(3, 0) should return 1, power(5, 3) should return 125.

#include <stdio.h>

/*
 * power(base, exp) - raises base to the power exp
 * Returns: base^exp (1 when exp==0, by any-number^0 = 1 rule)
 */
int power(int base, int exp) {
    int result = 1;          /* Start at 1 — the identity for multiplication */
    for (int i = 0; i < exp; i++) {
        result *= base;      /* Multiply result by base, exp times */
    }
    return result;
}

int main(void) {
    printf("2^10 = %d\n", power(2, 10));  /* 1024 */
    printf("3^0  = %d\n", power(3, 0));   /* 1   */
    printf("5^3  = %d\n", power(5, 3));   /* 125 */
    return 0;
}
Key ideas: The result starts at 1 (the multiplicative identity) so that exp == 0 works correctly — the loop body never executes, and 1 is returned. The loop runs exactly exp times, each time multiplying result by base. This is an O(n) algorithm. An O(log n) approach using "fast exponentiation" (exponentiation by squaring) is a classic interview question once you understand this version.
P2 — Trace the call stack: what does this print? Lecture Page 24 — extended

Without running the code, trace each function call step by step and predict the exact output. Draw the call stack for the moment just before add_three returns.

#include <stdio.h>

int add(int a, int b) {
    printf("  add(%d, %d)\n", a, b);
    return a + b;
}

int add_three(int x, int y, int z) {
    printf(" add_three(%d, %d, %d)\n", x, y, z);
    int partial = add(x, y);
    return add(partial, z);
}

int main(void) {
    printf("main starts\n");
    int result = add_three(1, 2, 3);
    printf("result = %d\n", result);
    return 0;
}
main starts
 add_three(1, 2, 3)
  add(1, 2)
  add(3, 3)
result = 6
Trace: main calls add_three(1,2,3), which prints its banner. Inside, add(1,2) is called — prints " add(1, 2)" and returns 3, stored in partial. Then add(3,3) is called — prints " add(3, 3)" and returns 6. add_three returns 6 to main, which prints "result = 6".

Call stack at the second call to add: (bottom to top) main frame → add_three(x=1,y=2,z=3,partial=3) frame → add(a=3,b=3) frame. The first add frame has already been popped when partial was assigned.
P3 — Spot and fix the bugs: three function mistakes Tutorial — Functions Bug Hunt

The code below has three distinct bugs related to functions. Identify each bug, explain why it is wrong, and write the corrected version.

#include <stdio.h>

/* Bug 1: increment is supposed to add 1 to the caller's variable */
void increment(int x) {
    x = x + 1;
}

/* Bug 2: this function should return the larger of a and b */
int max_of(int a, int b) {
    if (a > b) {
        return a;
    }
    /* missing return when a <= b */
}

/* Bug 3: attempting to use result before it is defined */
int main(void) {
    int n = 5;
    increment(n);
    printf("n = %d\n", n);   /* expects 6 */

    printf("max = %d\n", max_of(3, 7));

    printf("%d\n", result);  /* Bug 3: 'result' is not declared */
    return 0;
}
#include <stdio.h>

/* Fix 1: accept a POINTER, dereference it to modify the original */
void increment(int *x) {
    *x = *x + 1;   /* write through the pointer to the caller's variable */
}

/* Fix 2: add the missing return for the else branch */
int max_of(int a, int b) {
    if (a > b) {
        return a;
    }
    return b;      /* was missing — without this, returning garbage */
}

int main(void) {
    int n = 5;
    increment(&n);             /* Fix 1: pass address, not value */
    printf("n = %d\n", n);    /* now prints 6 */

    printf("max = %d\n", max_of(3, 7));   /* prints 7 */

    /* Fix 3: declare result before using it */
    int result = max_of(10, 20);
    printf("%d\n", result);   /* prints 20 */
    return 0;
}
Bug 1: increment receives a copy of x. Modifying the copy has no effect on n in main. Fix: take a pointer (int *x), call with &n, dereference with *x.

Bug 2: max_of only has a return in the if branch. If a <= b, execution falls off the end of the function with no return — undefined behavior in C. Compilers with -Wall will warn: "control reaches end of non-void function". Always ensure every code path through a non-void function has a return statement.

Bug 3: result is used but never declared. In C, every variable must be declared before first use. This is a compile-time error: "undeclared identifier 'result'".
P4 — Write a recursive function: sum of digits Tutorial — Recursion Preview

Write a recursive C function int digit_sum(int n) that returns the sum of all digits in a non-negative integer n. For example, digit_sum(1234) should return 1+2+3+4 = 10. Hint: n % 10 gives the last digit; n / 10 removes the last digit. The base case is when n == 0.

#include <stdio.h>

/*
 * digit_sum(n) - returns sum of all decimal digits of n
 *
 * Recursion strategy:
 *   digit_sum(1234) = (1234 % 10)   + digit_sum(1234 / 10)
 *                   =      4         + digit_sum(123)
 *                   =      4         + 3 + digit_sum(12)
 *                   =      4         + 3 + 2 + digit_sum(1)
 *                   =      4         + 3 + 2 + 1 + digit_sum(0)
 *                   =      4         + 3 + 2 + 1 + 0   <-- base case
 *                   = 10
 */
int digit_sum(int n) {
    if (n == 0) {
        return 0;              /* Base case: zero has no digits to sum */
    }
    return (n % 10) + digit_sum(n / 10);
    /*      ^^^^^^^^^  last digit   ^^^^^^^^^^^^^^^^^  rest of number */
}

int main(void) {
    printf("digit_sum(1234) = %d\n", digit_sum(1234));   /* 10 */
    printf("digit_sum(999)  = %d\n", digit_sum(999));    /* 27 */
    printf("digit_sum(0)    = %d\n", digit_sum(0));      /* 0  */
    printf("digit_sum(100)  = %d\n", digit_sum(100));    /* 1  */
    return 0;
}
Why this works: Integer division n / 10 drops the last digit. Modulo n % 10 extracts the last digit. The recursion peels off one digit per call. The base case n == 0 stops the recursion — when all digits have been stripped, nothing remains and the sum contribution is 0. The call stack depth equals the number of digits (at most 10 for a 32-bit int), so there is no risk of stack overflow here.
P5 — Forward declaration and multi-function program Lecture Page 21-22 — Declarations

The code below tries to call is_even and is_odd from main, but both functions are defined after main and they mutually call each other (even numbers are those where removing 1 makes them odd). Add forward declarations so the code compiles, and explain why mutual recursion requires them.

#include <stdio.h>

/* ??? — add forward declarations here ??? */

int main(void) {
    for (int i = 0; i <= 6; i++) {
        printf("%d is %s\n", i, is_even(i) ? "even" : "odd");
    }
    return 0;
}

int is_even(int n) {
    if (n == 0) return 1;    /* 0 is even — base case */
    return is_odd(n - 1);    /* n is even if n-1 is odd */
}

int is_odd(int n) {
    if (n == 0) return 0;    /* 0 is not odd — base case */
    return is_even(n - 1);   /* n is odd if n-1 is even */
}
#include <stdio.h>

/* Forward declarations (prototypes) — parameter names optional */
int is_even(int n);
int is_odd(int n);

int main(void) {
    for (int i = 0; i <= 6; i++) {
        printf("%d is %s\n", i, is_even(i) ? "even" : "odd");
    }
    return 0;
}

int is_even(int n) {
    if (n == 0) return 1;
    return is_odd(n - 1);
}

int is_odd(int n) {
    if (n == 0) return 0;
    return is_even(n - 1);
}
Why forward declarations are mandatory here: When the compiler sees is_even(i) in main, it needs to know the function's return type and parameter list to generate correct machine code. Without the prototype it either guesses (old C89 behavior, assumed int with unknown args) or errors. Mutual recursion (is_even calling is_odd and vice versa) is impossible to resolve by just reordering definitions — no matter which you put first, it will call the other before it is defined. Prototypes at the top break this circular dependency.

Output: 0 is even, 1 is odd, 2 is even, 3 is odd, 4 is even, 5 is odd, 6 is even.

Key concepts to memorize

Card 1 of 12
Question — click to flip
Answer
Click card to flip • Use buttons to navigate

Test your understanding

Topic 06 Quiz — Functions Score: 0 / 8
1
What is the difference between a function declaration and a function definition in C?LO1
multiple choice
2
True or False: In C, function parameters are always passed by value — the function receives a copy, so modifying a parameter inside the function does not change the caller's variable.LO1
true / false
3
What does void mean in the parameter list of int get_value(void)?LO1
multiple choice
4
Fill in the blank: A ___ is a region of memory on the call stack that holds a function's local variables, parameters, return address, and saved registers. It is created when the function is called and destroyed when it returns.LO1
fill in the blank
5
Spot the bug: what is wrong with this function?LO1
int *create_value(void) {
    int x = 42;
    return &x;   /* return address of local variable */
}
spot the bug — multiple choice
6
A recursive function must always have which of the following to avoid infinite recursion and stack overflow?LO1
multiple choice
7
You call srand(time(NULL)) inside a loop that runs 1000 times very quickly. What is the most likely problem?LO2
multiple choice
8
Which statement about variadic functions in C is correct?LO2
multiple choice
0/8
Quiz complete!