Lesson 1 of 8
A function gives a name to a piece of work so you can do it from several places without writing it out again.
#include <iostream>
int add(int a, int b) {
return a + b;
}
int main() {
std::cout << add(2, 3) << "\n";
std::cout << add(10, -4) << "\n";
return 0;
}
5
6
Reading int add(int a, int b):
int— the return type: what this function hands back.add— the name.(int a, int b)— the parameter list: what it needs, and what those values are called inside.{ ... }— the body: the work itself.
Declaration and definition
You met these words in lesson 2.6 when linker errors were the subject. Now they matter directly.
A declaration states the function's interface and ends in a semicolon:
int add(int a, int b);
A definition supplies the body:
int add(int a, int b) {
return a + b;
}
Every definition is also a declaration. The reverse isn't true, and that asymmetry is the whole point:
- The compiler needs only a declaration to check a call. It verifies the argument types and
the use of the result, then emits "call whatever
addturns out to be." - The linker needs the definition, to fill in an actual address.
That's why a declaration with no matching definition compiles fine and fails at link time — the error from lesson 2.6.
Why separate them at all?
Because it lets you use a function before you've written it, and — more importantly — lets you
publish an interface without shipping the implementation. That's exactly what a header file is
(lesson 5.8), and it's how the standard library works: #include <iostream> pastes in
declarations, and the compiled library supplies the definitions at link time.
void: a function that returns nothing
#include <iostream>
#include <string>
void greet(const std::string& name) {
std::cout << "Hello, " << name << "!\n";
}
int main() {
greet("Ada");
greet("Grace");
return 0;
}
Hello, Ada!
Hello, Grace!
void means "no return value." You can still return; early to leave the function; you just
can't return a value.
Include what you use. Delete the
#include <string>above and it very likely still compiles on your machine —<iostream>happens to pull<string>in indirectly on many implementations. Don't rely on that. It's not guaranteed by the standard, and the day it stops being true (a compiler upgrade, a different platform) you get a wall of errors in a file you never touched. If you name a type, include its header.
Every path must return
// WILL NOT COMPILE
int classify(int n) {
if (n > 0) {
return 1;
}
// falls off the end when n <= 0
}
With -Wall this is a warning, and reaching that end is undefined behaviour. Make every path
explicit:
#include <iostream>
int classify(int n) {
if (n > 0) {
return 1;
} else if (n < 0) {
return -1;
}
return 0;
}
int main() {
std::cout << classify(7) << " " << classify(-7) << " " << classify(0) << "\n";
return 0;
}
1 -1 0
main is the one exception — it returns 0 implicitly (lesson 2.3).
Why functions are worth it
Beyond avoiding repetition:
- A name is documentation.
is_prime(n)says what a loop full of%does not. - They're independently testable. Module 18 relies on this entirely.
- They limit scope. Variables inside a function can't be touched from outside.
- They let you change one thing. Fix a bug in
addand every caller is fixed.
The practical guideline is that a function should do one thing and be short enough to read without scrolling. If you can't name it without using "and", it's probably two functions.
Try it
Write a function int max_of_three(int a, int b, int c) that returns the largest of its three
arguments, and call it with a few sets of values.
Checkpoint
- A function has a return type, a name, parameters, and a body.
- A declaration ends in
;and gives the interface; a definition supplies the body. - The compiler needs only the declaration; the linker needs the definition.
voidmeans no return value;return;still exits early.- Every path through a non-
voidfunction must return a value. - One function, one job, short enough to read at a glance.
Questions about this lesson
Log in to ask a question.