9. Functions¶
What & why¶
A function is a named, reusable block of code with inputs (parameters) and an output (return value). Functions let you break a big problem into small, testable pieces, and give each piece a name that explains what it does.
Defining and calling¶
#include <iostream>
// return_type name(parameters) { body }
int square(int x) {
return x * x;
}
void greet(const std::string& name) { // void = returns nothing
std::cout << "Hello, " << name << "!\n";
}
int main() {
std::cout << square(7) << '\n'; // 49
greet("Ada");
}
- Every non-
voidfunction mustreturna value on every path. Falling off the end is undefined behavior (-Wallwarns you). - A
voidfunction can use a barereturn;to exit early.
Declaration before use¶
The compiler reads top to bottom. You can't call a function before it's been declared. Either define it above main, or write a declaration (prototype) first:
int square(int x); // declaration: "this exists"
int main() {
std::cout << square(7); // OK
}
int square(int x) { // definition: "here's how it works"
return x * x;
}
This separation is the foundation of header files (next lesson).
How arguments are passed¶
This is the most important section in this lesson.
By value — the function gets a copy¶
void try_to_change(int n) {
n = 100; // changes the local copy only
}
int x = 5;
try_to_change(x);
// x is still 5
By reference — the function works on the caller's variable¶
By const reference — no copy, no modification¶
double average(const std::vector<double>& values) {
double sum = 0;
for (double v : values) sum += v;
return values.empty() ? 0.0 : sum / values.size();
}
Which one?¶
| Situation | Parameter type |
|---|---|
Small, cheap types (int, double, char, bool) |
by value: int x |
Large types you only read (std::string, std::vector, your classes) |
const T& |
| Read-only text | std::string_view |
| You need to modify the caller's object | T& |
Prefer returning results over modifying reference parameters — it's clearer:
std::vector<int> evens_only(const std::vector<int>& v); // good
void evens_only(const std::vector<int>& v, std::vector<int>& out); // older style
Returning a local vector is cheap: the compiler moves it or constructs it in place.
Returning multiple values¶
Return a struct (or std::pair / std::tuple) and unpack with structured bindings:
struct MinMax {
int min;
int max;
};
MinMax min_max(const std::vector<int>& v) {
auto [lo, hi] = std::ranges::minmax_element(v);
return {*lo, *hi};
}
auto [smallest, largest] = min_max({4, 9, 1, 7});
Default arguments¶
void log(const std::string& msg, const std::string& level = "INFO") {
std::cout << "[" << level << "] " << msg << '\n';
}
log("started"); // [INFO] started
log("disk full", "ERROR"); // [ERROR] disk full
Defaults must be the trailing parameters, and go in the declaration only (not repeated in the definition).
Overloading¶
Several functions can share a name if their parameter lists differ:
int area(int side) { return side * side; }
int area(int w, int h) { return w * h; }
double area(double radius) { return 3.14159 * radius * radius; }
area(4); // 16
area(3, 5); // 15
area(2.0); // 12.566...
The compiler picks the best match. Overloads that differ only in return type are not allowed.
Recursion¶
A function that calls itself, with a base case that stops:
long long factorial(int n) {
if (n <= 1) {
return 1; // base case
}
return n * factorial(n - 1); // recursive case
}
Each call uses stack space; very deep recursion (hundreds of thousands of calls) overflows the stack and crashes. Loops are usually safer for simple repetition.
[[nodiscard]]¶
Mark functions whose result should never be ignored:
[[nodiscard]] bool save(const std::string& path);
save("data.txt"); // warning: ignoring return value
Lambdas: tiny inline functions¶
You saw these with std::count_if. A lambda is a function you can write inline and store in a variable:
auto is_even = [](int x) { return x % 2 == 0; };
is_even(4); // true
int threshold = 10;
auto above = [threshold](int x) { return x > threshold; }; // captures threshold
Full treatment in the intermediate track.
Small, well-named functions¶
Good functions:
- do one thing, described by their name (
parse_date,is_valid_email), - are short enough to read without scrolling,
- avoid hidden side effects (global variables, printing inside a calculation),
- take what they need as parameters and return their result.
Common mistakes¶
- Passing big objects by value — silently copies a whole vector on every call.
- Returning a reference to a local variable — the local dies when the function returns: Return by value instead.
- Missing
returnon some path of a non-voidfunction. - Calling a function before declaring it.
- Recursion without a base case — stack overflow.
- Ambiguous overloads —
f(int)andf(double)called with along.
Exercises¶
- Write
bool is_prime(int n)and use it to print the primes below 100. - Write
void swap_values(int& a, int& b)and prove it works. Then try it withint a, int band explain why it doesn't. - Write
std::string repeat(std::string_view s, int times = 2). - Write
double average(const std::vector<double>&),double median(std::vector<double>)(why by value here?), anddouble stddev(const std::vector<double>&). - Write a recursive
int sum_digits(int n)and a recursivebool is_palindrome(std::string_view s). - Refactor your number-guessing game from lesson 6 into functions:
int read_int(std::string_view prompt),int pick_secret(),void play().
Recap¶
return_type name(params) { ... }; declare before use.- Pass small types by value, large read-only types by
const&, text bystd::string_view, and use&only when you must modify. - Return results (including structs for multiple values) rather than using out-parameters.
- Default arguments and overloading make APIs convenient.
- Never return a reference or pointer to a local.
- Keep functions small and single-purpose.