Functions, Scope & Linkage

Functions are how C composes behavior. Every executable statement lives inside one, arguments are always passed by value, and the C compiler needs an exact type signature — a prototype — before a function may be called. This page covers the mechanics, the storage classes that control a name's lifetime, and the variadic tail of printf.

Declarations & Definitions

A prototype (declaration) states the signature: return type, name, parameter types. A definition adds the body { ... }. A function called after only its definition works fine, but calling before any declaration is a compile error since C99. Headers exist precisely to share prototypes across files.

#include <stdio.h>

int add(int a, int b);      // prototype — promises this exact signature

int main(void) {
    printf("%d\n", add(2, 3));   // OK: prototype seen before the call
    return 0;
}

int add(int a, int b) {     // definition — the promise is fulfilled here
    return a + b;
}

Parameters — Always By Value

Arguments are copied into parameters. Mutating a parameter inside the function never changes the caller's variable; to modify the caller's data you must pass its address and dereference it. This is the out-parameter pattern, and it is the mechanical root of every "reference-like" behavior in C.

#include <stdio.h>

void bump(int b) {      // b is a copy — caller unaffected
    b += 10;
}

void bump_it(int *p) {  // now we modify what the caller points to
    *p += 10;
}

int main(void) {
    int n = 5;
    bump(n);            // n stays 5 — copy was incremented, then discarded
    printf("after bump:    %d\n", n);
    bump_it(&n);        // the address travels; *p += 10 mutates n itself
    printf("after bump_it: %d\n", n);   // 15
    return 0;
}

The same rule covers structs: pass a small struct by value for simplicity, a large one by pointer to avoid copying megabytes per call.

Recursion

A function may call itself — this is recursion. Each call gets its own stack frame with fresh locals, so a recursive function is just a loop wearing a function call. Depth costs stack memory, so deep recursion (say, 100,000 calls) can exhaust the stack; iterative or tail-style versions are often preferable for production.

#include <stdio.h>

// factorial: the canonical recursion — n! = n * (n-1)!
unsigned long factorial(unsigned int n) {
    if (n <= 1) {        // base case: stop unwinding
        return 1;
    }
    return n * factorial(n - 1);   // recursive step: call with smaller n
}

int main(void) {
    for (unsigned int i = 0; i <= 10; i++) {
        printf("%2u! = %lu\n", i, factorial(i));
    }
    return 0;
}

The same idea powers divide-and-conquer algorithms — quicksort, mergesort, tree traversal — which the algorithms page explores in depth.

Scope & Storage Classes

Every name has a scope (where it is visible) and a storage duration (how long its storage lives). Four storage classes matter daily:

StorageLives whereWhen
autoblock/stackInside its block, alive for one call (the default for locals)
static (local)global memoryFirst call initializes once; value persists between calls
externglobal memoryDefined in another translation unit; visible here
registerhint onlyAsk the compiler to keep it in a CPU register (rarely needed)
#include <stdio.h>

int next_id(void) {
    static int counter = 0;   // initialized ONCE, at program start
    return ++counter;         // and it keeps its value across calls
}

int main(void) {
    printf("%d\n", next_id());   // 1
    printf("%d\n", next_id());   // 2
    printf("%d\n", next_id());   // 3 — state survived between calls
    return 0;
}

Linkage — static vs External

A file-scope function or variable can be shared across files (external linkage) or kept private to its translation unit (static). Marking a helper static prevents name collisions across a large project and lets the compiler inline more aggressively.

static int private_helper(int v) {   // visible only in this .c file
    return v * 2;
}

int public_api(int v) {               // external linkage — callable elsewhere
    return private_helper(v) + 1;
}

In practice: declare shared functions in a header (extern by default), include the header where used, and keep implementation helpers static.

Variadic Functions

printf accepts any number of arguments — the prototype ends with .... Your own variadic function reads them with the <stdarg.h> macros. The format string (or another argument) must tell the callee how many arguments follow; C cannot count them for you — and this is exactly how a wrong %d becomes a crash.

#include <stdarg.h>   // va_list, va_start, va_arg, va_end
#include <stdio.h>

// sum n integers: the caller passes the count first
int sum_all(int n, ...) {
    va_list args;
    va_start(args, n);        // begin reading after parameter n
    int total = 0;
    for (int i = 0; i < n; i++) {
        total += va_arg(args, int);   // pull the next int
    }
    va_end(args);             // mandatory cleanup
    return total;
}

int main(void) {
    printf("%d\n", sum_all(3, 10, 20, 30));   // 60
    return 0;
}

Next: control flow — conditions and loops that steer the program.