C++ / FUNCTIONS
Declaring, defining, and calling functions
Split a function into a declaration the compiler checks calls against and a definition the linker needs, then read whichever error tells you which is missing.
What you will learn
- Put a prototype above main so definitions can appear in any order below it
- Tell 'not declared in this scope' apart from the linker's 'undefined reference'
- Declare a function as many times as convenient, but define it exactly once
- Match declaration and definition on types; parameter names are free to differ
Understanding Declaring, defining, and calling functions
A declaration announces a function's interface — return type, name, and parameter types — and ends in a semicolon instead of a body. That is all the compiler needs to accept a call: it can check the argument types, decide what conversions to apply, and know the type and size of the result, without ever seeing the body. A definition is that same header line followed by a braced body, and it is the only thing that actually produces machine code for the function. So every definition is also a declaration, but not the other way round.
This split is why the same missing function can produce two completely different errors. If no declaration is visible above the call, the compiler stops with something like "'foo' was not declared in this scope": name lookup failed while the file was being read top to bottom. If a declaration was visible but no definition exists anywhere in the program, compilation succeeds and the linker later reports "undefined reference to foo", because it searched for the body and found nothing. Knowing which tool complained tells you whether to add a prototype or write a body.
It follows that you may repeat a declaration freely, but the program as a whole must contain exactly one definition of the function — the one-definition rule. That is exactly the arrangement headers exist for: the declaration is included into every file that wants to call, while one source file holds the body. A call then means: find the declared name, convert the arguments to the declared parameter types, transfer control into the body, and hand the returned value back to the expression the call sits in. Because prototypes decouple visibility from placement, you can order definitions for a human reader instead of for the compiler.
Keeping the declaration in one place also means you change a signature once and every caller is re-checked against it, rather than each call site silently guessing.
<iostream>
// Declaration: return type, name, parameter types. No body, ends with a semicolon.
double celsius_to_fahrenheit(double celsius);
void print_row(double celsius);
int main() {
print_row(0.0);
print_row(37.0);
print_row(100.0);
}
// Definitions: the bodies the linker needs in order to resolve the calls above.
double celsius_to_fahrenheit(double celsius) {
return celsius * 9.0 / 5.0 + 32.0;
}
void print_row(double celsius) {
std::cout << celsius << " C = " << celsius_to_fahrenheit(celsius) << " F\n";
}
A declaration is a promise about a function's signature that lets the compiler accept calls; the definition is the body the linker must find exactly once.
Worked examples
Repeating a declaration
Shows that a function may be declared more than once and that prototype parameter names are ignored.
<iostream>
int clamp_to_byte(int value); // declaration
int clamp_to_byte(int); // the same declaration again: legal
int main() {
std::cout << clamp_to_byte(-40) << ' ' << clamp_to_byte(90) << ' '
<< clamp_to_byte(300) << '\n';
}
int clamp_to_byte(int n) { // definition; the parameter name may differ
if (n < 0) return 0;
if (n > 255) return 255;
return n;
}
Example explained
Line 1The second prototype is a redeclaration, not an error, because the return type and parameter types are identical.
Line 2The definition names the parameter n while the first declaration named it value; only types take part in identifying the function.
Line 3clamp_to_byte(300) returns from the second if, so the call site receives 255 rather than the argument that was passed.
Line 4main falls off the end without a return statement, which is allowed for main alone and yields exit status 0.
Definition order and forward declarations
Demonstrates that a definition placed before a call needs no prototype, while one placed after it does.
<iostream>
int cube(int n); // needed: the body is below main
int square(int n) { // a definition is also a declaration
return n * n;
}
int main() {
std::cout << "square(7) = " << square(7) << '\n';
std::cout << "cube(7) = " << cube(7) << '\n';
std::cout << "sum = " << square(2) + cube(2) << '\n';
}
int cube(int n) {
return square(n) * n; // square was already declared above
}
Example explained
Line 1cube requires the prototype at the top because the compiler processes the file in order and reaches the call in main first.
Line 2square needs no prototype: its definition appears above every call, and a definition declares the name too.
Line 3Inside cube's body, square is usable because its declaration was already seen earlier in the file.
Line 4square(2) + cube(2) evaluates to 4 + 8; + binds tighter than <<, so the sum 12 is what gets printed.
Important notes
A declaration and its definition must agree on types, not on parameter names; names in a prototype are documentation and may even be omitted.
main is a function you define but never call yourself: the runtime calls it, and C++ forbids your own code from calling main.
Common mistakes
Leaving a semicolon on the definition's header line, as in int f(int n); { return n * n; } — the semicolon ends a declaration and the leftover block is not valid at file scope, so the compiler errors on the brace instead of pointing at the real slip.
Defining a function below main with no prototype and then hunting for a missing #include; the error 'was not declared in this scope' is a name-lookup failure and is fixed by declaring the function above the call.
Declaring int scale(double); but defining int scale(int) — those are two different functions, so the call type-checks fine and the build dies at link time with undefined reference to scale(double).
Try it yourself
Change, predict, then run
In one file, declare int minutes_to_seconds(int); above main, call it with 3 and 90 and print both results, and put the definition after main. Then delete the prototype, rebuild, and note whether the compiler or the linker reports the failure.
Open the C++ workspaceCheck your understanding
A single file declares double area(double); at the top and calls area(2.0) inside main, but no body for area exists anywhere. What happens when you build it?
- The compiler rejects the call, since it cannot check a function that has no body
- The compiler accepts the call and the linker fails with an undefined reference to area
- It builds and runs, and area returns 0.0 because its body is treated as empty
- It builds and runs, and the call is skipped because there is nothing to jump to
Show answer
The declaration gives the compiler the argument and return types it needs to check area(2.0) and emit a call to the symbol, so compilation succeeds; only the linker goes looking for the body, which is why the failure surfaces as a link error. Option 0 is tempting because the program is obviously incomplete, but a compiler that trusted only what it could see would make it impossible to call a function defined in another source file.