Functions, Scope & Linkage
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:
| Storage | Lives where | When |
|---|---|---|
auto | block/stack | Inside its block, alive for one call (the default for locals) |
static (local) | global memory | First call initializes once; value persists between calls |
extern | global memory | Defined in another translation unit; visible here |
register | hint only | Ask 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.