Menu

Function Prototypes in C: Declaration vs Definition

Why C needs to see a function's shape before you call it - writing prototypes, fixing implicit-declaration errors, putting prototypes in header files, and the real difference between f(void) and f().

This page includes runnable editors - edit, run, and see output instantly.

The Compiler Reads Top to Bottom

A C compiler processes a source file in one pass, from the first line to the last. When it reaches a function call, it needs to already know three things: what the function returns, how many arguments it takes, and what their types are. Without that it cannot generate correct code or check the call.

So this is a problem:

#include <stdio.h>

int main(void) {
    printf("%d\n", add(2, 3));   /* add has not been seen yet */
    return 0;
}

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

The fix is a prototype: the function's signature, written before the call, with a semicolon where the body would go.

Now main can come first, which is how most C files are organised: the prototypes at the top say what the file offers, main reads as the outline, and the details follow below.

Declaration vs Definition

Two words that are easy to blur and worth keeping apart:

  • A declaration says a function exists and gives its type. It ends in a semicolon and has no body. You may write it as many times as you like.
  • A definition gives the body. It must appear exactly once in the whole program - two definitions of the same function is a linker error ("multiple definition of").
int add(int a, int b);                        /* declaration (prototype) */
int add(int a, int b) { return a + b; }       /* definition - also a declaration */

Parameter names are optional in a prototype; only the types matter to the compiler:

int add(int, int);              /* legal, and equivalent */
int add(int a, int b);          /* better: the names document the order */

Use the names. void drawRect(int, int, int, int); tells a reader nothing, while void drawRect(int x, int y, int width, int height); tells them everything.

What the Prototype Buys You

Not just the ability to reorder definitions - it is what makes the compiler check your calls.

With the prototype in scope, the 3 and the 2 are converted to double before the call. Without it, they would be pushed as ints and scale would read them as doubles - garbage, and no diagnostic in old C dialects.

The prototype also catches wrong argument counts and incompatible types at compile time:

scale(3.0);              /* error: too few arguments */
scale(3.0, "two");       /* error: passing char * where double is expected */

Those are the errors you want - loud, at compile time, pointing at the line.

Implicit Declaration Errors

Call a function the compiler has never heard of and you get one of the most common messages in C:

warning: implicit declaration of function 'add' [-Wimplicit-function-declaration]

In C89, the compiler would guess: assume the function returns int and accept whatever arguments you passed. That guess is wrong more often than not, and when the real function returns a double or a pointer the result is nonsense. C99 removed implicit declarations from the language, so this is an error in C99 and later - modern GCC and clang reject it by default in recent versions.

Two causes, two fixes:

Your own function, not yet declared. Add the prototype above the call, or move the definition earlier.

A library function whose header you forgot. The library's prototypes live in its header, so you need the #include:

printf, scanf, fopen        ->  #include <stdio.h>
malloc, free, exit, atoi    ->  #include <stdlib.h>
strlen, strcpy, strcmp      ->  #include <string.h>
sqrt, pow, sin, fabs        ->  #include <math.h>
isdigit, toupper            ->  #include <ctype.h>
bool, true, false           ->  #include <stdbool.h>

A related message, conflicting types for 'add', means the prototype and the definition disagree - a parameter type differs, or the return type does. Fix whichever one is wrong; they have to match exactly.

Mutual Recursion Needs a Prototype

Sometimes reordering cannot solve it: two functions that call each other cannot both come first.

One prototype breaks the circle. This is the same mechanism a plain recursive function relies on - a function's own name is in scope inside its body, so direct recursion needs no prototype, but mutual recursion does.

Prototypes in Header Files

Once a program spans several .c files, prototypes move into a header so every file sees the same declarations from one place.

mathutils.h - the interface:

#ifndef MATHUTILS_H
#define MATHUTILS_H

int add(int a, int b);
int multiply(int a, int b);
double average(const int values[], int count);

#endif

mathutils.c - the implementation:

#include "mathutils.h"

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

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

double average(const int values[], int count) {
    if (count == 0) return 0.0;
    int total = 0;
    for (int i = 0; i < count; i++) total += values[i];
    return (double) total / count;
}

main.c - a user:

#include <stdio.h>
#include "mathutils.h"

int main(void) {
    printf("%d\n", add(2, 3));
    return 0;
}

Then:

gcc main.c mathutils.c -o program

Two habits worth adopting here. The #ifndef / #define / #endif wrapper is an include guard: it stops the declarations being processed twice if the header gets included along two paths. And mathutils.c includes its own header - which looks redundant but is not, because it makes the compiler check every definition against the declaration other files will use. If they ever drift apart, you find out immediately rather than at link time. Header files goes into the rest.

Angle brackets (<stdio.h>) search the system include paths; quotes ("mathutils.h") search your project's directory first. Use quotes for your own headers.

f(void) vs f()

This one is genuinely surprising, and it is the reason every example in these docs writes int main(void).

void ping(void);      /* takes NO arguments - calls with arguments are rejected */
void pong();          /* says NOTHING about the parameters */

void ping(void); is a prototype: it declares that the function takes no parameters, so ping(1, 2, 3) is a compile error.

void pong(); is an old-style declaration inherited from pre-standard C. It declares the return type and nothing at all about the parameters, so the compiler cannot check calls - pong(1, 2, 3) compiles quietly and does something undefined.

int main(void) {      /* right: main takes no arguments */
int main() {          /* legal, but argument checking is off */

Always write (void) for a no-parameter function. C23 changes () to mean the same as (void), which finally removes the trap - but plenty of code and plenty of compilers are not there yet, and (void) is correct in every C standard.

The same distinction applies to the definition. void ping(void) { } is a prototype-style definition; void ping() { } is not, and does not enable checking of calls that appear before it.

Common Mistakes

  • Semicolon on the definition. int add(int a, int b); { return a + b; } declares add and then leaves a stray block. The error message is confusing; the cause is one character.
  • Missing semicolon on the prototype. The compiler reads on into whatever follows and reports something baffling several lines below.
  • Prototype and definition disagreeing. conflicting types for .... Make them identical - or better, include the header in the implementation file so the check is automatic.
  • Declaring a function inside another function. Legal (int add(int, int); inside main), but the declaration is then scoped to that function only. Put prototypes at file scope.
  • Defining a function in a header. Include it from two .c files and the linker reports a duplicate definition. Headers hold declarations; definitions go in a .c file.
  • Relying on () for checking. It does not check. Write (void).

Frequently Asked Questions

What is a function prototype in C?

A declaration of a function's signature - return type, name, and parameter types - ending in a semicolon instead of a body: int add(int a, int b);. It tells the compiler how the function is called so that calls can be checked, without saying what it does.

What is the difference between a declaration and a definition in C?

A declaration introduces the name and type (int add(int, int);) and can appear many times. A definition supplies the body (int add(int a, int b) { return a + b; }) and must appear exactly once in the whole program. Every definition is also a declaration.

How do I fix "implicit declaration of function" in C?

Add a prototype before the call. For your own functions, put returnType name(paramTypes); near the top of the file or in a header; for library functions, include the right header - #include <stdio.h> for printf, <stdlib.h> for malloc, <string.h> for strlen, <math.h> for sqrt.

What is the difference between f(void) and f() in C?

void f(void); declares a function that takes no arguments, and the compiler rejects any call that passes some. void f(); is an old-style declaration that says nothing about the parameters, so argument checking is switched off. Always write (void); C23 finally makes the two mean the same thing, but older code and compilers still differ.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED