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

  1. Write min3 and max3 for three integers and call each from the other to check the result.
  2. Give the factorial function from lesson 16 a prototype, move its definition below main, and check that the program still builds.
  3. 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.

  1. 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.

  2. 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.

  3. What does the static keyword do to a function at file scope?

    Answer

    It hides the name from other source files

    A static function has internal linkage, so other translation units cannot call it by name.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

  8. 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.

  9. 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.

  10. 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

Banner image