Why this matters
Two questions come up the moment programs have more than a couple of functions. First, how does the compiler check a call to a function it has not reached yet, or one that lives in another file or a library? The answer, the prototype, is also what every header file is made of. Second, what exactly does a function receive when you pass it a variable? C's answer, a copy, is simple and strict, and it explains why scanf needs that & you have been typing, and why a whole module is devoted to pointers.
The compiler reads top to bottom
The compiler processes a file in one pass from the top. When it meets a call, it must already know the function's return type and parameter types so it can check the arguments and generate the right machine code. So far you have satisfied this by defining every function above its first use. That stops working as soon as two functions call each other, when you prefer main at the top, or when the function is in another file entirely (like printf).
Declarations and prototypes
A declaration introduces a name and its type without providing the body. For a function, a declaration with full parameter types is called a prototype:
int area(int w, int h); /* prototype: a declaration, note the semicolon */
int area(int w, int h) /* definition: provides the body */
{
return w * h;
}
- A program may declare a function many times (consistently), but must define it exactly once.
- Parameter names in a prototype are optional and are only documentation:
int area(int, int);is equivalent. Good names make prototypes self-explanatory, so most people keep them. - The prototype and definition must agree. If they do not, gcc refuses:
q.c:4:5: error: conflicting types for 'half'; have 'int(int)'
q.c:2:8: note: previous declaration of 'half' with type 'double(double)'
This is exactly what the headers you include contain. <stdio.h> contains a line like int printf(const char *format, ...); and <math.h> contains double sqrt(double x);. The definitions are already compiled, in the C library, and the linker connects your calls to them, as you saw in the module 1 pipeline. Later in the track you will write your own headers in exactly this way.
A common layout for a single-file program:
#include <stdio.h>
/* prototypes: the table of contents */
int read_number(void);
int is_prime(int n);
void print_report(int n, int prime);
int main(void)
{
/* the outline of the program */
}
/* definitions, in any order */
What a prototype buys you
With a prototype in scope, the compiler checks every call:
- Argument count.
f(1)forint f(void);giveserror: too many arguments to function 'f'. - Argument types. Passing something that cannot be converted, such as a string to an
intparameter, is diagnosed. - Conversions. Arguments are converted to the parameter types, as in an assignment. With
double half(double x);in scope,half(3)passes3.0and returns1.5.
The trap of empty parentheses
In C17 and earlier, empty parentheses in a declaration do not mean "no parameters". They mean "parameters not specified", an old-style declaration from the 1970s that turns off checking:
int old_style(); /* no information about parameters */
int main(void)
{
printf("%d\n", old_style(1, 2, 3)); /* compiles silently! */
}
int old_style(int a) { return a; }
With -Wall -Wextra this compiles with no diagnostic at all and, here, prints 1. Calling a function with the wrong arguments is undefined behavior; it only happened to work. Always write (void) for a function without parameters: int new_style(void);. (C23 changes () to mean (void), but much existing code and this track use C17.)
Pass-by-value
When you call a function, each argument expression is evaluated, and the value is copied into the parameter, which is a brand new local variable of the called function. The function works on its copy. Nothing it does to the copy affects the caller.
#include <stdio.h>
void bump(int n)
{
n = n + 1;
printf("inside bump: n = %d\n", n);
}
int main(void)
{
int n = 5;
bump(n);
printf("in main: n = %d\n", n);
return 0;
}
inside bump: n = 6
in main: n = 5
In memory, there are two separate boxes that happen to share a name:
main's variables bump's variables
+-------+ +-------+
| n = 5 | -- copy 5 --> | n = 5 | then n = n + 1
+-------+ | n = 6 |
+-------+
still 5 gone when bump returns
Consequences worth internalizing:
- A function cannot change the caller's variables through its parameters. Write
x = f(x);to update a variable with a computed value. - A function returns at most one value. When you need more (a quotient and a remainder), the options are a struct (module 9) or passing addresses (module 7).
- Parameters are cheap scratch variables. Since they are copies, a function may modify its parameters freely, as Euclid's algorithm did with
aandbin the previous lesson.
That is also why scanf("%d", &n) has the &. scanf must store a number in your variable, and a copy of n's current value would be useless to it. &n passes the variable's address instead: a value that tells scanf where n lives. Passing addresses is still pass-by-value (the address itself is copied), and it is the whole subject of the pointers module.
Order of evaluation
C does not specify the order in which the arguments of a call are evaluated. Usually it does not matter, but it does when the arguments have side effects:
int counter = 0;
int next(void) { return ++counter; }
printf("%d %d\n", next(), next());
With gcc on x86-64 this prints 2 1: the second argument was evaluated first. Another compiler, or another optimization level, may print 1 2. Both are correct. If order matters, evaluate into variables first:
int first = next();
int second = next();
printf("%d %d\n", first, second); /* always 1 2 */
Common mistakes
- Prototype missing a semicolon.
int area(int w, int h)followed byint main(void)makes the compiler think a definition with old-style parameter declarations is beginning, and it produces confusing errors pointing atmain's opening brace:error: expected '=', ',', ';', 'asm' or '__attribute__' before '{' token. Check the line above. - Prototype and definition disagree:
conflicting types for .... Change one to match the other. int f();instead ofint f(void);: no checking at all in C17.- Expecting a function to modify its argument:
reset(count);leavescountunchanged. Return the new value and assign it. - Relying on argument evaluation order, such as
f(i++, i). Besides being unspecified, modifying and reading the same variable like this is undefined behavior. Split it up.
Recap
- A prototype (declaration) gives a function's return and parameter types; a definition gives the body. Declare as often as you like, define once.
- Prototypes let you call functions defined later or elsewhere; headers are collections of prototypes.
- With a prototype, calls are checked and arguments converted. Write
(void)for "no parameters":()means "unspecified" in C17. - C passes arguments by value: parameters are copies, and changes to them never reach the caller.
- The order in which arguments are evaluated is unspecified.
Going further: run grep -n 'sqrt' /usr/include/x86_64-linux-gnu/bits/mathcalls.h | head in the sandbox to see how the C library declares math functions (through a few macros). Look up "C23 function declarations without prototypes" to see why the committee finally removed () declarations.