Skip to content
VLSI Mentor

C Part I · Module 5 · Functions and Program Decomposition

Function Declarations and Prototypes

Every function so far has been defined above its first call, because Chapter 5.2 showed that the alternative is a compile error. That constraint has been quietly deciding how the files are laid out: main has had to sit at the bottom, underneath helpers the reader meets before knowing what they are for, and the order of a file has been dictated by the compiler rather than by what makes it readable. This chapter removes the constraint with one line per function. More importantly, it explains what that line is actually for — because the compiler is doing something at every call site that it cannot do without it, and the failure mode when it is missing is not an error message but a program that builds and is wrong.

Foundation16 min readCPart IFundamentalsFunctions

Part I · Module 5 · Chapter 5.5 · Function Declarations and Prototypes

1. The Ordering Problem

Chapter 5.2 established the rule: a function must be declared before it is called, and defining it above the first call satisfies that. It works, and it has three costs that get worse as a file grows.

main ends up at the bottom. A reader opening the file meets clamp_code and code_to_millivolts before anything explains what the program is for. The most important function in the file is the last thing you reach.

The compiler decides the reading order. If report_sample calls format_value, then format_value must come first — regardless of which one a reader would rather meet first.

Some arrangements are impossible. If two functions each call the other, no ordering satisfies both. (One such case is mutual recursion, which Chapter 5.8 covers; the constraint applies to ordinary mutual calls just as much.)

2. Declaring Without Defining

The rule is declared before use, not defined. These are two different things, and C lets you supply them separately.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int square(int n);          /* a declaration — note the semicolon */

int square(int n)           /* a definition — a body instead */
{
    return n * n;
}

A declaration announces that a function exists and states its signature: the return type, the name, and the parameter types. It ends with a semicolon and has no body.

A definition supplies the body. A definition is also a declaration — it announces the signature too — which is why defining above the call has been working.

A declaration that includes the parameter types is called a prototype. That word matters, because a declaration that leaves them out is also legal and does something quite different; §5 is about that.

With declarations at the top, the ordering problem disappears:

Azvya Education Pvt. Ltd.VLSI Mentor
main first, helpers after
#include <stdio.h>

int square(int n);
void describe(int value, int squared);

int main(void)
{
    describe(7, square(7));

    return 0;
}

int square(int n)
{
    return n * n;
}

void describe(int value, int squared)
{
    printf("%d squared is %d\n", value, squared);
}

Output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
7 squared is 49

main is now first. The two declarations above it act as a summary of what the file provides — often the most useful four lines in a C source file, because they tell you the shape of the program before any of the detail.

The parameter names are optional

In a declaration, only the types are needed:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int scale(int, int);

This is exactly equivalent to int scale(int value, int factor);. Names in a declaration are ignored by the compiler — Chapter 5.2 §8 established that parameter names are private to the function.

Write them anyway. int scale(int, int) tells a reader nothing about which argument is which, and int scale(int value, int factor) tells them everything. The compiler does not care; the next person to call the function does.

3. What the Compiler Does With a Prototype

A prototype is not paperwork. It is the information the compiler needs in order to do two jobs at every call site.

It checks the call. Wrong number of arguments, or types that cannot be converted, become errors:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int scale(int n, int factor);
...
    printf("%d\n", scale(5));
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: too few arguments to function call, expected 2, have 1

It converts the arguments. This is the part that is easy to miss, and it is Chapter 5.2's step 2 doing real work:

Azvya Education Pvt. Ltd.VLSI Mentor
an argument that does not match the parameter
#include <stdio.h>

int scale(int n, int factor);

int main(void)
{
    printf("%d\n", scale(5, 2.9));

    return 0;
}

int scale(int n, int factor)
{
    return n * factor;
}

Output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
10
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
warning: implicit conversion from 'double' to 'int' changes value from 2.9
    to 2 [-Wliteral-conversion]

2.9 is a double. The parameter is an int. Because the prototype says so, the value is converted on the way in — to 2 — and 5 * 2 is 10. This is the same conversion Chapter 3.12 described for assignment, applied at the call boundary, and it is possible only because the compiler knew what the parameter type was.

A prototype tells the compiler what a call must look like. It then enforces that, and performs the conversions the parameter types imply.

4. The Declaration Must Agree With the Definition

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int square(int n);

double square(double n)
{
    return n * n;
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: conflicting types for 'square'

Two statements about the same function that cannot both be true. The compiler rejects the file rather than choosing one — which is the behaviour you want, because silently choosing would mean every call is checked against one signature and executed against another.

This is why duplicating a signature by hand is a maintenance hazard: change the definition and forget the declaration, and you get an error. That is the good case. The genuinely dangerous version appears when a program spans several files and the two copies are in different ones, where nothing compares them at all — which is what header files exist to prevent, and Chapter 11.3's subject.

5. The Dangerous Middle Ground

There are three states a call site can be in, not two.

No declaration at all

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int main(void)
{
    printf("%d\n", helper(3));
    return 0;
}

int helper(int n) { return n + 1; }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: call to undeclared function 'helper'; ISO C99 and later do not support
    implicit function declarations [-Wimplicit-function-declaration]

An error. The program does not build. This is the safe failure, and it is relatively recent: before C99, the compiler would assume an undeclared function returned int and accept whatever arguments you passed.

A declaration that is not a prototype

Azvya Education Pvt. Ltd.VLSI Mentor
this builds — and it should not
#include <stdio.h>

int scale();            /* no parameter types */

int main(void)
{
    printf("%d\n", scale(5));
    return 0;
}

int scale(int n, int factor)
{
    return n * factor;
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
warning: a function declaration without a prototype is deprecated in all
    versions of C [-Wstrict-prototypes]
warning: passing arguments to 'scale' without a prototype is deprecated in all
    versions of C and is not supported in C23 [-Wdeprecated-non-prototype]
warning: ... is treated as a zero-parameter prototype in C23, conflicting with
    a subsequent definition [-Wdeprecated-non-prototype]

Warnings only. The program builds.

Look at what that call does. scale needs two arguments. The call supplies one. There is no second argument, so the function's factor parameter has no value it was given — and the result is undefined behaviour: the standard imposes no requirements on what the program does. This chapter does not run it, because printing whatever number appeared and discussing it would be describing one accident rather than explaining C.

The compiler could not stop it. int scale(); does not say takes nothing; it says parameters unspecified, which Chapter 5.2 §3 established. Having been told nothing, the compiler had nothing to check the call against.

A full prototype

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int scale(int n, int factor);

Now the same mistake is caught before the program exists:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: too few arguments to function call, expected 2, have 1
What the compiler hasWrong argument countWrong argument typeArguments converted
Nothing— (error at the call)——
int f(); — no prototypenot checkednot checkedno
int f(int, int); — prototypeerrorchecked and convertedyes

As Chapter 5.2 §3 showed, C23 removed the middle row entirely by redefining () to mean no parameters. The third warning above says so in as many words. Until a project moves to C23, the middle row is still there, still legal, and still silent at ordinary warning levels.

6. A File Laid Out Deliberately

Azvya Education Pvt. Ltd.VLSI Mentor
declarations, main, then definitions
#include <stdio.h>

int scale(int value, int factor);
int add_then_double(int a, int b);

int main(void)
{
    printf("%d\n", scale(5, 3));
    printf("%d\n", add_then_double(2, 3));

    return 0;
}

int scale(int value, int factor)
{
    return value * factor;
}

int add_then_double(int a, int b)
{
    return scale(a + b, 2);
}

Output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
15
10

add_then_double calls scale, which happens to be defined above it — but it no longer has to be. The declarations at the top mean any function in the file may call any other, in any order, and the file can be arranged for a reader.

Note also what the two declarations do for someone opening this file: before reading a single line of implementation, they know the program has two operations, what each needs, and what each produces. That is not a side effect. In a larger file it is the main reason to write them even where the ordering does not require it.

7. Where This Goes Next

Everything in this chapter has been about one file. The declarations and the definitions sit in the same place, a few lines apart, and nothing stops them drifting apart except §4's error.

Real programs are built from several source files, and then the function being called and the function being defined are not in the same file at all — so there is nothing for the compiler to compare, and §4's protection vanishes. The mechanism that restores it is the header file, and it is the reason headers exist at all rather than being a filing convention. Chapter 11.3 covers it, along with what #include actually does to your file, which Chapter 1.6 demonstrated and deliberately did not explain further.

That also explains #include <stdio.h>, which has been at the top of every program in this curriculum. It supplies the prototype for printf, which is why calls to printf have been checked all along — and why removing that line produces errors about an undeclared function rather than a program that merely cannot print.

8. What You Should Be Able to Say Now

  1. A declaration states a function's signature and ends with a semicolon; a definition supplies the body.
  2. Every definition is also a declaration, which is why defining above the call worked.
  3. A prototype is a declaration that includes the parameter types.
  4. With declarations at the top, any function may call any other and the file can be ordered for a reader.
  5. Parameter names in a declaration are ignored by the compiler — write them for the human.
  6. A prototype lets the compiler check the call and convert the arguments.
  7. The declaration and the definition must agree, or the file is rejected.
  8. No declaration is an error since C99.
  9. int f(); is a declaration without a prototype: calls are not checked, and a wrong call builds. C23 removed this.
  10. #include <stdio.h> is how printf gets its prototype.

9. Exercises

Exercise 1 — write the declarations. Given these definitions, write the prototype for each, with useful parameter names.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
unsigned int clamp_code(unsigned int code) { ... }
void report_sample(unsigned int code, unsigned int millivolts) { ... }
int first_divisor(unsigned int value) { ... }
int get_default_limit(void) { ... }

Exercise 2 — reorder a file. Take Chapter 5.1 §2's decomposed program and rewrite it so that main is the first function in the file. State exactly what you had to add, and how many lines it cost.

Exercise 3 — classify three call sites. For each, say whether it is an error, a warning, or accepted silently, and what the compiler is able to check.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
/* (a) */  int f(int, int);   ...   f(1);
/* (b) */  int f();           ...   f(1);
/* (c) */  /* nothing */      ...   f(1);

Exercise 4 — explain the conversion. In §3, scale(5, 2.9) printed 10. Give the value each parameter held when the body ran, name the rule that produced them, and say what would have happened at that call if the declaration had been int scale(); instead.

Exercise 5 — find the mismatch. This file does not compile. Quote the kind of thing the compiler objects to, explain why rejecting it is better than choosing one of the two, and give the repair.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int average(int a, int b);

double average(int a, int b)
{
    return (a + b) / 2.0;
}

Exercise 6 — judge the four-character difference. Explain the difference between int get_limit(); and int get_limit(void); in C17, what C23 changed, and which you would write in a project that currently builds as C17 but expects to move. Give your reason.

Exercise 7 — debug the silent build. A colleague reports that their program builds with no errors and prints nonsense. Their file contains int compute(); near the top and int compute(int a, int b) { ... } at the bottom, and the call is compute(4). Explain what the compiler knew, what it could not check, how you would classify the behaviour of that call, and the two changes you would make — one to the code and one to the build command.

Exercise 8 — defend the top-of-file block. A reviewer says the declarations at the top of §6's file are redundant, since every function is defined later in the same file anyway. Give two arguments for keeping them that do not depend on the ordering being required.

10. Summary

  • A declaration states the signature; a definition supplies the body. Every definition is also a declaration.
  • A prototype is a declaration with parameter types — that is the distinction that matters.
  • Declarations at the top free the file's ordering and summarise what it provides.
  • The compiler uses a prototype to check the call and convert the arguments.
  • Declaration and definition must agree, or the translation fails.
  • There are three states, not two: no declaration (error), declaration without a prototype (builds, unchecked), and prototype (checked).
  • int f(); is the dangerous one. Write (void), write the types, and enable -Wstrict-prototypes.
  • C23 removed the middle case by redefining ().
  • #include <stdio.h> is where printf's prototype comes from.

Declarations have now been used to put main at the top of a file, and both chapters that discussed names — the one about parameter names being private, and this one about them being ignored in a declaration — have been circling the same question without answering it. Two functions can both have a parameter called value, a function can have a local called count while main has one too, and nothing has yet said which object a given use of a name refers to. The next chapter answers that, and the answer is one of the few places where a block's braces turn out to be doing something beyond grouping.

Standards & specifications

Governing standard
ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)

Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C Programming curriculum.