Why this matters
So far every program has been a single main. That works for twenty lines and collapses at two hundred: the same logic gets copied in three places, a fix is applied to two of them, and nobody can tell what any block is for. Functions are the main tool for managing complexity. A function gives a piece of logic a name, lets you use it from anywhere, and lets you think about it in isolation: once is_prime works and is tested, you can call it without remembering how it works. Every large C program, including the Linux kernel's tens of millions of lines, is built from functions that each do one understandable job.
Anatomy of a function
int square(int x)
{
return x * x;
}
return type name parameter list
| | |
int square (int x)
{
return x * x; <- body: statements that run on each call
}
- The return type (
int) is the type of value the function hands back.voidmeans it returns nothing. - The name follows the same rules as variable names.
- The parameters are variables that receive the values passed in, each with its own type, separated by commas.
(void)means "no parameters". - The body runs each time the function is called.
return expression;ends the function immediately and sends the value back to the caller.
Calling it:
#include <stdio.h>
int square(int x)
{
return x * x;
}
void print_line(int width)
{
for (int i = 0; i < width; i++)
putchar('-');
putchar('\n');
}
int main(void)
{
int a = square(7);
printf("square(7) = %d\n", a);
printf("square(3) + square(4) = %d\n", square(3) + square(4));
printf("square(square(2)) = %d\n", square(square(2)));
print_line(20);
return 0;
}
square(7) = 49
square(3) + square(4) = 25
square(square(2)) = 16
--------------------
A call is an expression: square(7) is the value 49 wherever it appears, so it can be stored, printed, added, or passed straight into another call. A call to a void function such as print_line(20) has no value and is used as a statement on its own.
Parameters and arguments
The values in a call are arguments; the variables in the definition that receive them are parameters. On each call, each parameter is initialized with the corresponding argument:
call: square( 7 ) print_line( 20 )
| |
definition: int square(int x) void print_line(int width)
x starts as 7 width starts as 20
Arguments can be any expressions; they are fully evaluated before the function starts. In square(square(2)), the inner call runs first and returns 4, and then the outer call runs with x = 4. The number and types of the arguments must match the parameters. Passing the wrong number is an error:
o.c:6:13: error: too many arguments to function 'twice'
6 | int r = twice(3, 4);
| ^~~~~
o.c:2:5: note: declared here
If an argument has a different but convertible type, it is converted, just as in an assignment: passing 7.9 to an int parameter gives it the value 7.
What happens during a call
When main executes square(7):
- The argument
7is evaluated. - Control jumps to the start of
square, remembering where to come back to (the return address). The machine instruction is literallycall square. squaregets its own fresh parameterx = 7and runs its body.return x * x;computes 49, places it where the caller expects results (on x86-64, theraxregister), and jumps back to the return address.- In
main, the call expression evaluates to 49.
main square
---- ------
a = square(7) ---call-------> x = 7
return 49
a = 49 <---return-----
printf(...)
Each call is independent: square(3) and square(4) in the same expression each get their own x. The lesson on the call stack shows exactly where those variables live.
return
return does two things: it chooses the result and it ends the function right away. A function can have several returns, which is handy for handling special cases first:
int abs_value(int n)
{
if (n < 0)
return -n;
return n;
}
A void function may use a bare return; to finish early, or simply run to its closing brace.
Every path through a non-void function must return a value. If execution can reach the closing brace of an int function, the caller receives garbage (undefined behavior), and gcc warns:
o.c:3:44: warning: control reaches end of non-void function [-Wreturn-type]
(main is the one exception, as you saw in module 1: falling off its end returns 0.)
Designing functions
Good functions have a few properties in common:
- One job, describable in one sentence. "Returns the larger of two ints." If the description needs "and", consider two functions.
- A name that says what it returns or does:
square,is_leap_year,print_report. Functions that answer yes/no questions are often namedis_...orhas_...and return 0 or 1. - Inputs through parameters, outputs through the return value. A function that reads its input with
scanfand prints its result can only ever be used in one program; a function that takes parameters and returns a value can be used anywhere, and tested automatically. That is exactly how the exercises in this module check your work: a test program calls your function with many inputs and compares the results. - Short. If a function no longer fits on a screen, it probably contains smaller functions waiting to be extracted.
Breaking a problem into functions is called decomposition. A typical main ends up reading like an outline: read input, compute, print, each step a call.
Common mistakes
Calling a function above its definition without a declaration. The compiler reads top to bottom; at the call it does not yet know the function exists:
p.c:6:20: error: implicit declaration of function 'area' [-Wimplicit-function-declaration]
Define functions above main, or declare them first (next lesson).
Using the value of a void function:
p.c:5:13: error: void value not ignored as it ought to be
Returning a value from a void function:
p.c:2:43: error: 'return' with a value, in function returning void [-Wreturn-mismatch]
Ignoring a return value. square(5); on its own line compiles and computes 25, then throws it away. Functions do not change their arguments; to keep the result you must store or use it: n = square(n);.
Printing instead of returning. A function that should compute a value but prints it instead cannot be reused in a larger expression. Return the value; let the caller decide what to print.
Recap
- A function has a return type, name, parameter list and body;
voidmeans no value or no parameters. - Arguments are evaluated first, then copied into the parameters; the body runs;
returnends the function and supplies the call's value. - A call is an expression and can be nested inside other expressions.
- Every path of a non-void function must return a value (
-Wreturn-typewarns). - Prefer parameters in and return values out; it makes functions reusable and testable.
Going further: take the leap-year or prime program from the control-flow module and turn the test into a function int is_leap(int year) or int is_prime(int n) that returns 1 or 0, so that main only reads, calls and prints.