C / FUNCTIONS
Declarations versus definitions and why prototypes matter
Tell a declaration from a definition, write prototypes the compiler actually checks, and predict how each argument is converted at a call.
What you will learn
- Declare a function as many times as you like; define it exactly once per program.
- Put a prototype above the first call so arguments convert to the declared types.
- Write f(void), never f(), for a function that takes no arguments.
- Read a definition as a declaration plus a body, and keep both signatures identical.
Understanding Declarations versus definitions and why prototypes matter
A declaration introduces a name and tells the compiler what type it has; a definition is a declaration that also produces the thing itself. For a function, double average(double, double); is a declaration and nothing more: it emits no machine code, it only teaches the compiler what a call to average looks like. The version with a body is the definition, and the linker insists on exactly one of those per program while happily accepting any number of declarations. That asymmetry is the entire reason headers work: every file can be told the function exists, but only one file has to supply it.
A prototype is a declaration that spells out the parameter types, and those types are what let the compiler check and fix up each call. Because it knows average expects two doubles, average(3, 4) silently converts the int literals to 3.0 and 4.0 before the call, and average(3) is rejected immediately. With no prototype in scope the compiler has no target type to convert to, so it applies only the default argument promotions (small integer types to int, float to double) and passes whatever the expression already was. Nothing is verified at runtime, so the function decodes those bytes as doubles anyway and you get nonsense or a crash: undefined behavior, not a conversion.
The rules feel arbitrary until you know that pre-C89 declarations carried only the return type, which is why calling an undeclared function used to be legal and assumed to return int. C99 removed that fallback, so a call with no declaration in scope is now a constraint violation that current compilers reject or warn loudly about. One leftover trap survives: in C17 and earlier an empty parameter list, int f();, means "unspecified arguments, check nothing", so int f(void) is how you actually say a function takes none. Treat a declaration as a contract the compiler enforces at every call site, and the definition as the single place that honours it.
Keeping the two in sync is mechanical: the declaration names the types, the definition repeats them and adds the body.
<stdio.h>
/* Declarations only: no bodies, so no code is generated here.
They exist to tell the compiler the types used in every call. */
double average(double a, double b);
void show(const char *name, double value);
int main(void)
{
/* The prototype turns these int literals into doubles. */
show("average(3, 4)", average(3, 4));
show("average(1.5, 2.5)", average(1.5, 2.5));
return 0;
}
/* Definitions: the same signatures, plus the bodies the linker needs. */
double average(double a, double b)
{
return (a + b) / 2.0;
}
void show(const char *name, double value)
{
printf("%s -> %.2f\n", name, value);
}
A declaration supplies the types the compiler needs to check and convert calls; the definition is the one place that supplies the code.
Worked examples
Prototype types drive the conversion
The declared parameter type, not the callee, is what converts an argument at the call site.
<stdio.h>
void takes_double(double x);
void takes_int(int n);
int main(void)
{
takes_double(7); /* int 7 converted to 7.0 at the call */
takes_int(7.9); /* double 7.9 truncated to 7 at the call */
return 0;
}
void takes_double(double x) { printf("double: %.1f\n", x); }
void takes_int(int n) { printf("int: %d\n", n); }
Example explained
Line 1takes_double(7) hands over a double because the prototype makes the caller convert int to double first.
Line 2takes_int(7.9) compiles for the same reason, and the conversion truncates toward zero instead of rounding.
Line 3Delete either declaration and the call has no target type, which is exactly the situation C99 turned into an error.
Forward declaration for functions that call each other
Two functions referring to each other cannot both be defined first, so one needs a declaration.
<stdio.h>
int is_odd(unsigned n); /* forward declaration: is_even needs it */
int is_even(unsigned n)
{
if (n == 0) return 1;
return is_odd(n - 1);
}
int is_odd(unsigned n)
{
if (n == 0) return 0;
return is_even(n - 1);
}
int main(void)
{
printf("is_even(4) = %d\n", is_even(4));
printf("is_even(7) = %d\n", is_even(7));
return 0;
}
Example explained
Line 1Line 3 declares is_odd without defining it, which is all the body of is_even needs to compile.
Line 2is_odd calls is_even with no extra declaration because the definition of is_even above it already declared the name.
Line 3No reordering of the two definitions removes the need for that one declaration, since each function names the other.
Important notes
A definition is also a declaration, so a function defined above its first call needs no separate prototype, but that ordering trick collapses the moment two functions call each other.
C23 redefines int f() to mean int f(void); until every compiler you target follows it, keep writing void explicitly.
Common mistakes
Defining a function that returns double below main with no prototype: the old implicit rule assumed int, and a modern compiler either errors on the implicit declaration or reports conflicting types when it finally reaches the definition.
Writing void reset(); to mean "takes no parameters": in C17 and earlier that turns argument checking off, so reset(1, 2) compiles and passes junk. Only void reset(void) states that there are no parameters.
Putting the function body in a header instead of a prototype: every .c file that includes it gets its own definition and the link fails with "multiple definition of".
Try it yourself
Change, predict, then run
Copy the average program, delete the two declarations at the top and compile it, noting the exact diagnostic. Then make it build again by moving both definitions above main, and explain why that works with no prototype at all.
Open the C workspaceCheck your understanding
A file has void log_value(double v); at the top and calls log_value(5);. What happens to the argument, and what changes if that declaration is removed?
- The int 5 is converted to 5.0 in the caller; with no declaration there is no target type, so an int is passed and log_value reads those bytes as a double
- 5 is passed as an int either way, and log_value converts it on entry because its parameter is declared double
- 5 is converted to 5.0 either way, because the default argument promotions turn integers into doubles
- The call is rejected as written, because C never converts int to double implicitly
Show answer
The conversion happens at the call site and is driven entirely by the declared parameter type, which is why deleting the declaration breaks it. Option 2 is tempting because the parameter really is a double, but the callee performs no check: it just interprets the incoming bytes according to its own parameter type. Option 3 misstates the default argument promotions, which promote float to double and small integer types to int, never int to double.