Function prototypes

Table of contents
  1. Function prototypes
    1. The compiler needs to see the function first
    2. Moving the function above main()
    3. The problem with several functions
    4. Solving the problem with prototypes
    5. Prototype, call and definition
    6. The prototype must match the definition
    7. Functions with no parameters
    8. Prototypes in header files

A function prototype tells the compiler about a function before the function is called.

For example:

int add(int a, int b);

This tells the compiler that add():

  • takes two int parameters
  • returns an int

The prototype ends with a semicolon because it does not contain the function body.

The compiler needs to see the function first

Consider this program:

#include <stdio.h>

int main(void)
{
    int total = add(10, 20); // we dont know anything about add() yet!
    printf("Total: %d\n", total);
    return 0;
}

int add(int a, int b)
{
    return a + b;
}

The function definition for add() appears below main().

When the compiler reaches:

int total = add(10, 20);

it has not seen add() yet, so it does not know:

  • whether the function exists
  • which parameters it takes
  • which type it returns

Modern C compilers will produce an error or warning because the function has been called before it was declared.

Moving the function above main()

One solution is to move the complete function definition above main():

#include <stdio.h>

int add(int a, int b)
{
    return a + b;
}

int main(void)
{
    int total = add(10, 20);
    printf("Total: %d\n", total);
    return 0;
}

This now compiles because the compiler sees the definition of add() before the function is called.

For a small program with one function, this approach works fine.

The problem with several functions

Moving every function above its caller becomes awkward as a program grows.

For example, suppose print_total() calls add():

#include <stdio.h>

void print_total(int a, int b)
{
    int total = add(a, b);
    printf("Total: %d\n", total);
}

int add(int a, int b)
{
    return a + b;
}

int main(void)
{
    print_total(10, 20);
    return 0;
}

This does not compile correctly because print_total() calls add() before the compiler has seen it.

We could move add() above print_total():

int add(int a, int b)
{
    return a + b;
}

void print_total(int a, int b)
{
    int total = add(a, b);
    printf("Total: %d\n", total);
}

However, functions may call several other functions. Constantly rearranging their definitions to keep the compiler happy quickly becomes a real hassle and makes our code a mess to read! Quite often we want main() near the top so that we can quickly see the overall order of the program.

Solving the problem with prototypes

Instead of moving the complete definitions, we can place function prototypes near the top of the file:

#include <stdio.h>

// Function prototypes
int add(int a, int b);
void print_total(int a, int b);

int main(void)
{
    print_total(10, 20);
    return 0;
}

void print_total(int a, int b)
{
    int total = add(a, b);
    printf("Total: %d\n", total);
}

int add(int a, int b)
{
    return a + b;
}

The compiler sees both prototypes before it reaches main():

int add(int a, int b);
void print_total(int a, int b);

It therefore knows how both functions should be called, even though their definitions appear later in the file.

This allows us to keep:

  • the prototypes near the top
  • main() near the start of the program
  • the complete function definitions underneath
  • our sanity

Prototype, call and definition

These three pieces look similar but do different jobs.

The prototype tells the compiler about the function:

int add(int a, int b); // ends with a semi colon

The call runs the function:

int total = add(10, 20);

The definition contains the code between {} that the function runs:

int add(int a, int b) // no semi colon
{
    return a + b;
}

The prototype must match the definition

The return type and parameter types in the prototype must match the function definition.

This prototype:

float calculate_average(float a, float b);

matches this definition:

float calculate_average(float a, float b)
{
    return (a + b) / 2.0f;
}

This prototype would not match:

int calculate_average(int a, int b);

The return type and parameter types are different.

Compiler errors about conflicting function types often mean that the prototype and definition do not match. Some of these you have seen in the debugging exercises in Unit 1.

Functions with no parameters

A prototype for a function with no parameters should use void inside the parentheses:

void print_title(void);

The matching definition is:

void print_title(void)
{
    printf("Function example\n");
}

For the code in this module, use:

void print_title(void);

rather than:

void print_title();

Using void makes it clear that the function intentionally has no inputs, rather than you forgot to add them.

Prototypes in header files

In larger projects, prototypes are normally placed in header files.

For example, math_utils.h might contain:

#ifndef MATH_UTILS_H
#define MATH_UTILS_H

int add(int a, int b);
int multiply(int a, int b);

#endif

Any source file that needs these functions can include the header:

#include "math_utils.h"

The function definitions remain in a .c file:

#include "math_utils.h"

int add(int a, int b)
{
    return a + b;
}

int multiply(int a, int b)
{
    return a * b;
}

The source file containing the definitions should include its own header. This allows the compiler to check that the prototypes and definitions match.

Header files, header guards and projects containing several .c files are covered in more detail later.


This site uses Just the Docs, a documentation theme for Jekyll.