Functions
A function gives a name to a piece of work, isolates its variables, and returns one value to the caller. The recursion in lesson 16 already used functions; this lesson is about their contract: the return type, the parameter list, and the declaration that lets the compiler check a call before it sees the body.
Defining and calling
A definition states the return type, the name, the parameters and the body. A call supplies one argument per parameter and uses the returned value where the type fits. The function below is defined before main, so no separate declaration is needed.
#include <stdio.h>
static int square(int value)
{
return value * value;
}
int main(void)
{
printf("%d\n", square(7));
printf("%d\n", square(square(2)));
return 0;
}
gcc -Wall -Wextra define.c -o define
./define
49
16
The inner call runs first, so square(square(2)) is square(4), then 16. The static makes the name local to this source file; lesson 18 explains the difference from an ordinary external function.
Declare before use
C reads a source file from top to bottom, so a call above the definition needs a declaration first. The prototype repeats the return type and the parameter types and ends with a semicolon; the definition below main must match it exactly.
#include <stdio.h>
int twice(int value);
int main(void)
{
printf("%d\n", twice(6));
return 0;
}
int twice(int value)
{
return value * 2;
}
gcc -Wall -Wextra proto.c -o proto
./proto
12
A prototype also documents the contract at the top of the file, where a reader looks first. In a header it serves every source file of a project, which is the subject of lesson 19.
Parameters are copies
C passes every argument by value: the parameter is a new object initialised from the argument. Changing the parameter inside the function changes the copy only. To let a function modify the caller's variable you need its address, which is the subject of Pointers in C.
#include <stdio.h>
static void bump(int n)
{
n++;
printf("inside: %d\n", n);
}
int main(void)
{
int x = 5;
bump(x);
printf("outside: %d\n", x);
return 0;
}
gcc -Wall -Wextra value.c -o value
./value
inside: 6
outside: 5
This is not a limitation to work around: a function that cannot change its caller's variables is easier to test. When a function must return more than one value, an out-parameter carries the second result, and the caller has to pass an address explicitly.
Functions calling functions
Small functions compose: one name states what a step does, and a second function reuses it. The call graph stays shallow, and a bug in square is found by testing one function instead of every caller. Arguments are evaluated before the call, so nested calls run from the inside out.
#include <stdio.h>
static int square(int value)
{
return value * value;
}
static int sum_of_squares(int a, int b)
{
return square(a) + square(b);
}
int main(void)
{
printf("%d\n", sum_of_squares(3, 4));
printf("%d\n", sum_of_squares(square(2), 1));
return 0;
}
gcc -Wall -Wextra compose.c -o compose
./compose
25
17
Three squared plus four squared is 25, and the second line passes 4 and 1 after squaring 2. Keeping the intermediate values in the caller, as the second call does, is fine, but a named local often reads better.
No prototype, conflicting types
A call with no visible declaration is an error in C23; before C99 it was silently assumed to return int. The compiler cannot check the arguments, so it refuses the call and says what is missing:
#include <stdio.h>
int main(void)
{
printf("%d\n", twice(6));
return 0;
}
int twice(int value)
{
return value * 2;
}
gcc -Wall -Wextra noproto.c -o noproto
noproto.c: In function 'main':
noproto.c:5:20: error: implicit declaration of function 'twice' [-Wimplicit-function-declaration]
5 | printf("%d\n", twice(6));
| ^~~~~
The second mistake is a prototype that disagrees with the definition. The compiler reports both types and the line of each, and nothing is built:
#include <stdio.h>
int twice(int value);
double twice(double value)
{
return value * 2.0;
}
int main(void)
{
printf("%d\n", twice(6));
return 0;
}
gcc -Wall -Wextra conflict.c -o conflict
conflict.c:5:8: error: conflicting types for 'twice'; have 'double(double)'
5 | double twice(double value)
| ^~~~~
conflict.c:3:5: note: previous declaration of 'twice' with type 'int(int)'
3 | int twice(int value);
| ^~~~~
conflict.c: In function 'main':
conflict.c:12:14: warning: format '%d' expects argument of type 'int', but argument 2 has type 'double' [-Wformat=]
12 | printf("%d\n", twice(6));
| ~^ ~~~~~~~~
| | |
| | double
| int %f
The format warning is a consequence: the compiler used the prototype, which promises an int, but the definition returns a double. Fix the declarations first and the warning usually disappears with them.
Exercises
- Write min3 and max3 for three integers and call each from the other to check the result.
- Give the factorial function from lesson 16 a prototype, move its definition below main, and check that the program still builds.
- Write a function that takes two integers and returns both their sum and their product in the way this lesson allows, then explain why the caller cannot see the second result.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What is the purpose of a function prototype?
Answer
It gives the compiler the types before the call
A prototype lets the compiler check a call before it sees the definition, and it documents the contract at the top of the file.
-
How are arguments passed to a C function?
Answer
By value, as copies
Each parameter is a new object initialised from the argument, so changing it changes only the copy.
-
What does the
statickeyword do to a function at file scope?Answer
It hides the name from other source files
A
staticfunction has internal linkage, so other translation units cannot call it by name. -
What does a function definition have that a prototype does not?
Answer
A body
The prototype ends with a semicolon and has no body; the definition supplies the statements.
-
What does this program print?
#include <stdio.h> static int square(int value) { return value * value; } int main(void) { printf("%d\n", square(7)); return 0; }Answer
49
The function multiplies its argument by itself, so 7 becomes 49.
-
What does this program print?
#include <stdio.h> static int square(int value) { return value * value; } int main(void) { printf("%d\n", square(square(2))); return 0; }Answer
16
The inner call runs first and gives 4, and squaring 4 gives 16.
-
What does this program print?
#include <stdio.h> static int square(int value) { return value * value; } static int sum_of_squares(int a, int b) { return square(a) + square(b); } int main(void) { printf("%d\n", sum_of_squares(3, 4)); return 0; }Answer
25
Three squared is 9 and four squared is 16, and their sum is 25.
-
What does this program print?
#include <stdio.h> static void bump(int n) { n++; printf("inside: %d\n", n); } int main(void) { int x = 5; bump(x); printf("outside: %d\n", x); return 0; }Answer
inside: 6 / outside: 5
The parameter is a copy, so the increment changes only the local copy and the caller still holds 5.
-
This program is built with
gcc -std=c17 -Wall -Wextra noproto.c -o noproto. Which error does GCC print?#include <stdio.h> int main(void) { printf("%d\n", twice(6)); return 0; } int twice(int value) { return value * 2; }Answer
implicit declaration of function
The call appears before any declaration, so the error is an implicit declaration.
-
This program is built with
gcc -std=c17 -Wall -Wextra conflict.c -o conflict. What does GCC report?#include <stdio.h> int twice(int value); double twice(double value) { return value * 2.0; } int main(void) { printf("%d\n", twice(6)); return 0; }Answer
error: conflicting types for 'twice'; have 'double(double)'
The prototype and the definition describe different functions, so the compiler rejects the conflicting types.
The code on this page was run with gcc (Alpine 15.2.0) 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev