Functions in C
October 7, 2026 • 6 min read

Table of contents
Part of the series:C Explained
A function is a named block of code you can call from anywhere else. It takes inputs, returns an output, and lets you write a piece of logic once and reuse it. main is itself a function, so you have been writing functions since the first post; this one teaches the full picture.
Anatomy of a function
A function definition has a return type, a name, a parameter list in parentheses, and a body in braces:
int add(int a, int b) {
return a + b;
}
Read it as: “function add takes two ints, returns an int, and its body returns their sum.” To use it, call it with matching arguments:
int result = add(3, 4); // result is 7
The pieces, in order:
int- the type of the value returned. A function that returns nothing usesvoid.add- the identifier.(int a, int b)- parameters: the inputs, each a type plus a name. A function with no inputs writesvoidinside:int main(void).{ return a + b; }- the body, withreturnsending a value back to the caller.- The caller’s values are arguments; the function’s names for them are parameters. The two often use different names; only positions matter.
Prototypes: declaring before using
In C you cannot call a function before the compiler has seen its definition, because the compiler needs to know its signature to type-check the call. If add is defined after main, this fails:
int main(void) {
int r = add(3, 4); // error: 'add' was not declared
return 0;
}
int add(int a, int b) {
return a + b;
}
The fix is a prototype (also called forward declaration): the signature with a ; instead of a body, placed before the call. Real programs put these in header files, but for now, top of the file works:
int add(int a, int b); // prototype
int main(void) {
int r = add(3, 4);
return 0;
}
int add(int a, int b) { // definition
return a + b;
}
In a strict C99 project the function must be declared before every use, which is why headers exist: the prototype lives in a .h file shared by every .c file that calls the function.
Pass by value
C passes arguments by value: the parameter is a copy of the argument. Changes inside the function never touch the caller’s variable:
void change(int x) {
x = 99;
}
int main(void) {
int n = 5;
change(n);
printf("%d\n", n); // still 5
return 0;
}
This is the single most important rule about functions in C, and it explains why the next post’s arrays and the pointers post’s & operator are indispensable: to let a function modify a caller’s variable, you must pass a pointer to it, because a copy of an address still names the original.
return and void
return exits the function and hands back a value of the declared return type:
double average(double a, double b) {
return (a + b) / 2.0;
}
Ending the function without a return when the type is not void is undefined behavior in most cases; the compiler may help you catch it only sometimes. void functions return no value; use a bare return; to leave early, or none at all:
void greet(const char *name) {
if (name == NULL) { // guard clause
return;
}
printf("Hello, %s\n", name);
}
Scope and lifetime
Where a variable exists is its scope, and how long it survives is its lifetime. The blocks from the syntax post are the boundaries:
- A variable declared inside a function is local: visible only inside its block, and destroyed when the block ends.
- The parameters are local to the function body.
- A variable declared outside any function is global: visible everywhere after its declaration, and alive for the whole program.
- A block
{}can open its own scope for a temporary variable.
int global = 10; // global, lives forever
void f(void) {
int local = 5; // local to f
{
int temp = local + 1; // block scope: only inside these braces
}
// temp does not exist here
}
Prefer local variables. Globals make functions hard to reason about, because any part of the program can change them.
Storage classes
Four keywords control storage class: where a variable lives and how long it lasts.
auto- the default for locals; you practically never write it.register- a hint to keep the value in a CPU register; the compiler largely ignores it today.static- on a local: the variable keeps its value between calls (and is initialized only once). On a global or function: it becomes visible only within its own file, hiding it from other files.extern- declares that the variable or function is defined in another file, letting you use it across files.
The static local is the useful one:
int nextId(void) {
static int id = 1000; // keeps its value across calls
return id++;
}
Each call returns 1000, then 1001, and so on, because the variable survives between calls even though it is local. const (from the syntax post) is not a storage class but is often grouped with these: it marks a value read-only.
Recursion
A function can call itself; that is recursion1. Its natural home is problems that break into smaller versions of themselves. The classic is factorial: n! = n * (n - 1)!, with 0! = 1:
int factorial(int n) {
if (n <= 1) {
return 1;
}
return n * factorial(n - 1);
}
factorial(4)
-> 4 * factorial(3)
-> 3 * factorial(2)
-> 2 * factorial(1)
-> 1
Every recursive function needs a base case (the n <= 1 branch) that stops the chain; without it, the calls never end and the program crashes with a stack overflow (the recursion story connects to the pointers/memory post later). Compared to loops, recursion shines on nested data like trees; use a loop for plain counting.
The big picture
A function is a named, reusable block: a return type, parameters passed by value, a body, and return to send results back. Prototypes let later definitions be called earlier; void means “returns nothing”; scope rules decide what each name means; storage classes tune lifetime and visibility; and recursion handles problems that fold into themselves. Functions combine with conditionals and loops to express any logic, and the next post applies them to the first built-in data structure: arrays and strings.
Footnotes
/sponsor
Enjoyed this post? You can sponsor me and this site.