Functions, defaults and overloading
Naming a piece of work so it can be reused: the return type, the parameters, the void function that prints instead of answering, and two ways to make one name serve several calls.
A function is a name for work
A function has a return type, a name, and a list of parameters. int add(int a, int b) takes two whole numbers and hands back one. The return statement is the answer, and its type has to fit the one in the signature — the compiler checks that for you at every call.
The function has to be known before it is used, so it goes above main, or its signature is declared near the top and the body written later. For a program in one file, defining it above main is the simpler habit and the one every example here follows.
Returning a value is not the same as printing it. A function that returns can be used inside another expression, stored in a variable, or passed to another function; a function that prints has done its work and left nothing behind. When you are unsure which to write, return the value and let main print it — the function is then testable and reusable.
A function that prints and hands nothing back
Some work has no answer worth returning: it prints, or it changes something. Those functions have the return type void, and they simply end when their last statement runs. Calling one is a statement on its own, with no answer to store.
Splitting printing into its own small void function is worth doing early. A program that prints from twenty places is hard to change; a program where every message goes through one function can have its output reshaped in one place.
Two names, one meaning
A default argument gives a parameter a value used when the call leaves it out: std::string greet(std::string name, std::string opening = "Hello"). Calls with one argument and with two are both allowed, and the defaults always sit at the end of the parameter list.
Overloading is the other trick: two functions may share a name if their parameter lists differ. add(int, int) and add(int, int, int) are different functions, and the compiler picks the right one from the call. Note the limit — they must differ in parameters, not in return type, because the call alone has to say which one you meant.
Pass by value, and the copy it makes
Every parameter in this chapter is passed by value: the function receives a copy, changes to it stay inside the function, and the caller value is untouched. For an int that copy costs nothing. For a std::string it is an allocation and a character-by-character copy, which is why the next chapters pass large values by reference — const std::string& — when a function only needs to read them.
The signature says which one you are doing: a plain type copies, an ampersand lends. There is no hidden third option, and a reader who wants to know whether their value can change has to look no further than the parameter list.
Four small functions
#include <iostream>
#include <string>
int add(int a, int b) {
return a + b;
}
int add(int a, int b, int c) {
return a + b + c;
}
std::string greet(std::string name,
std::string opening = "Hello") {
return opening + ", " + name + "!";
}
void show(std::string label, int value) {
std::cout << label << ": " << value << '\n';
}
int main() {
show("two", add(2, 3));
show("three", add(2, 3, 4));
std::cout << greet("Ada") << '\n';
std::cout << greet("Ada", "Welcome") << '\n';
return 0;
}One name, several calls
Run it and watch which function each call picks. Then add a third add that takes two doubles and a void function that prints a divider line, and call both from main.
Parameters are copies. add(2, 3) receives its own two ints and cannot change the caller values, which is exactly what you want for arithmetic like this.
Declaration, definition, and the linker
A function can be declared before it is defined. The declaration is the signature plus a semicolon — int add(int a, int b); — and it tells the compiler the name, the parameters and the return type. The definition is the body. Splitting them lets main call a function written further down the file, and it is how a header shares a function with other files.
The compiler checks each call against the declaration; the linker later finds the body. That is why a missing body is a link error rather than a compile error, and why the message often names a symbol instead of a line. In a single file the two steps are invisible, but the distinction explains many confusing errors later.
Overload resolution happens in the compiler, using the argument types at the call. It does not look at the return type, and it does not guess from what you assign the result to — which is why two functions that differ only in return type cannot coexist.
Three function mistakes: a non-void function that reaches its closing brace without returning (undefined behaviour, not zero), two overloads that differ only in return type (refused, because the call cannot choose), and a default argument written in both the declaration and the definition (an error — write it once).
Try these next
- Write
bool isEven(int n)and call it from anif. - Give a function a default parameter and call it with and without that argument.
- Write two overloads of
area, one for a square and one for a rectangle, and print both answers.
Areas, labels and a default argument
Run it and match each call to the overload it chose. Then add a `perimeter` function for a rectangle, and call `label` with a different unit.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
What return type does a function have if it prints but answers nothing?
