C
Functions
A function is a named, reusable block of code that takes some inputs, does something with them, and
optionally hands a result back. Every C program you've seen in this domain already has one —
main — this chapter is about writing your own, and understanding exactly what happens in
memory every time one is called.
- What it is
- A named block of code, with its own parameters and (optionally) a return value, that can be called from anywhere it's visible.
- Mental model
- A small, self-contained machine: you feed it inputs (parameters), it does work, and it either hands back one result (return value) or nothing at all (
void). - Key numbers
- Every function call adds one new "stack frame" to the call stack; that frame is freed the instant the function returns.
- Reach for it when
- The same logic is needed in more than one place, or a block of code is complex enough that giving it a name makes the calling code easier to read.
Anatomy of a function
int add(int a, int b) {
int sum = a + b;
return sum;
}
int(first one)- The return type — the type of value this function hands back to its caller.
voidmeans it returns nothing. add- The function's name — how it's called elsewhere in the program.
(int a, int b)- The parameter list — the inputs this function accepts, each with its own type and name, local to this function only.
return sum;- Ends the function immediately and hands
sumback to whoever called it.
int main(void) {
int result = add(3, 4); // "call" add with arguments 3 and 4
printf("%d\n", result); // 7
return 0;
}
Declaration vs. definition
A function can be declared (just its signature, ending in a semicolon, promising "this exists") separately from where it's defined (the actual body). This matters because C reads top to bottom — a function has to be declared before it's used, so a declaration up top lets you call it before its full definition appears later in the file:
int add(int a, int b); // declaration ("prototype") — no body
int main(void) {
printf("%d\n", add(3, 4)); // legal — the compiler already knows add's signature
return 0;
}
int add(int a, int b) { // definition — the actual body
return a + b;
}
This is exactly why header files (.h) exist — they hold declarations so multiple
.c files can call each other's functions, with the actual definitions living in exactly one
.c file. See Embedded C's Compilation &
Linking for the full mechanism.
Parameters and return values
C passes arguments by value by default — a function receives its own private copy of each argument, not the original variable. Changing a parameter inside the function has zero effect on the caller's variable:
void try_to_change(int x) {
x = 100; // only changes this function's own local copy
}
int main(void) {
int n = 5;
try_to_change(n);
printf("%d\n", n); // still 5 — n was never actually touched
return 0;
}
Actually modifying a caller's variable from inside a function requires passing its address instead of its value — the subject of the next chapter, Pointers.
The call stack
Every function call reserves a small block of stack memory — a stack frame — holding its parameters and local variables. Calling another function from inside one pushes a new frame on top; returning pops it back off:
add doesn't exist before it's called and stops existing the instant it returns — its entire storage is that stack frame.Recursion
A function that calls itself is recursive. Each call gets its own independent stack frame, so each "copy" of the function has its own separate parameters and locals:
int factorial(int n) {
if (n <= 1) {
return 1; // base case — stops the recursion
}
return n * factorial(n - 1); // recursive case
}
// factorial(4) → 4 * factorial(3) → 4 * 3 * factorial(2) → 4 * 3 * 2 * factorial(1) → 4 * 3 * 2 * 1 = 24
Every recursive function needs a base case — a condition that stops the recursion without calling itself again. Without one, it calls itself forever, pushing a new stack frame every time, until it runs out of stack memory entirely (a stack overflow).
Real-world applications
- Driver APIs — every hardware driver exposes its functionality as a set of named functions (
uart_init(),uart_send()) rather than inline code repeated everywhere it's needed. - Interrupt Service Routines — the function a microcontroller jumps to automatically when an interrupt fires, covered fully in Embedded's Interrupts chapter.
- Tree and linked-list traversal — recursion is the natural way to walk a structure that contains smaller versions of itself, like the structures in Embedded C's Linked Lists.
Gotchas
- Forgetting a function's declaration before its first use (no prototype, and it's defined later in the file) causes a compiler error or warning — always declare or define a function before calling it.
- A recursive function with no base case, or a base case that's never actually reached, causes a stack overflow — a real crash, not a slow-down, and especially dangerous on a microcontroller with only a few KB of stack space total.
- Returning the address of a local variable from a function hands the caller a pointer into a stack frame that no longer exists the instant the function returns — a dangling pointer, covered fully in Embedded C's Storage Classes & Scope.
- A function declared to return a value (anything other than
void) that reaches its closing brace without an explicitreturnis undefined behavior — the compiler often warns about this, and the warning should never be ignored.