Functions and Modularity
Functions as named, reusable units of behavior — parameters, return values, by-value vs by-reference, and overloading.
A function is a named, reusable chunk of code. It takes some inputs (called parameters), does something, and optionally returns a value. Functions are the single most important tool you have for managing complexity: they let you replace a tangle of inline code with a one-line call whose name documents intent.
Every C++ program already uses one function: main. Now let's
write our own.
Anatomy of a function
int add(int a, int b) { // return type, name, parameter list
return a + b; // body
}The parts:
- Return type (
int) — what kind of value flows back to the caller. Usevoidif the function returns nothing. - Name (
add) — what callers will use to invoke it. - Parameter list (
int a, int b) — typed slots for the inputs. - Body (between
{and}) — the code that runs when the function is called.
A function must have one definition (the body) and may have many declarations (signatures without a body, often in a header).
The declaration above main lets main call square before the
compiler has seen its body. Without the declaration, the compiler
would say "what is square?"
Parameters: by value, by reference, by const reference
Choosing how to pass parameters is one of the most important small decisions in C++.
By value: a copy
void doubled(int x) { x *= 2; }The function receives a copy of the caller's argument. Modifying
the parameter does not affect the caller. Use this for small,
cheap-to-copy types like int, double, bool, char.
By reference: an alias
void doubled(int& x) { x *= 2; }The function receives a reference — essentially a second name for the caller's variable. Modifying it modifies the caller's variable.
By const reference: a read-only alias
void print(const std::string& s) { std::cout << s; }A reference that you promise not to modify. No copy, no risk. This is the standard way to pass large objects (strings, vectors, classes) into functions when you only need to read them.
A quick rule of thumb:
| Argument type | How to take it |
|---|---|
| Small (fits in a register or two) and read-only | by value |
| Large and read-only | by const reference |
| Must be modified by the function | by reference |
| Optional / may be null | by pointer |
Return values
A function returns at most one value. Returning a large object used
to mean an expensive copy, but modern C++ optimizes this with
Return Value Optimization (RVO) and move semantics: returning
a std::string or std::vector is essentially free.
If you need to return multiple things, use a struct, std::pair,
or std::tuple. We will see structs soon.
Default arguments
Parameters can have default values. Callers may omit those arguments at the end of the list.
Defaults only go at the end of the parameter list. Once one parameter has a default, every parameter after it must too.
Function overloading
C++ lets multiple functions share a name as long as they differ in their parameter types. The compiler picks the right one at the call site based on the arguments.
This is compile-time polymorphism: one name, several implementations, chosen by static type. Don't confuse it with the runtime polymorphism we'll meet in the OOP chapter (virtual functions).
void, single responsibility, and good naming
A function that returns void is one that exists for its side
effects (printing, modifying things). A function that returns a
value but has no side effects is sometimes called pure, and is
the easiest kind to reason about and test.
A useful design principle: every function should do one thing,
and its name should describe that thing. Functions over ~30 lines
or with names like do_stuff are usually doing too much.
// Bad: vague name, multiple responsibilities
void process(int n) { ... 80 lines of code ... }
// Better: small, named, composable pieces
int parse(const std::string& s);
bool validate(int n);
void persist(int n);Recursion: a function that calls itself
A function can call itself. This is called recursion, and it is sometimes the most natural way to express problems that have a naturally recursive structure (trees, lists, mathematical sequences).
Every recursive function needs:
- A base case that returns without recursing (otherwise it recurses forever and overflows the stack).
- A recursive case that calls itself with a smaller problem.
For now, prefer loops when in doubt. Recursion shines when we hit trees and divide-and-conquer algorithms later.
A small multi-file project
In real code, related functions live in their own header / implementation pair. The example below splits string utilities out into their own file.
This pattern scales. Real C++ projects have hundreds of header/implementation pairs grouped into folders.
Challenges
Implement int min3(int a, int b, int c) that returns the smallest of three integers. main already prints min3(7, 2, 5), which should be 2.
Implement void swap_ints(int& a, int& b) that swaps the values of its two arguments in place. main will print the values after the swap and expects to see 9 1.
Open arith.cpp and implement gcd(a, b) using Euclid's algorithm — repeatedly replace (a, b) with (b, a % b) until b == 0. Return the absolute value of a at that point (assume positive inputs). main prints gcd(48, 18) which should be 6.
Test your understanding
A function takes a parameter as const std::string&. What does that mean?
The function gets a copy of the string that it cannot modify.
The function gets a pointer to the caller's string.
The function gets an alias for the caller's string, with a promise not to modify it.
The function will compile only if the string is a literal.
What is required for every recursive function to terminate?
A loop inside the function.
A base case that returns without making another recursive call, and a recursive case that progresses toward the base case.
A maximum-depth guard set by the compiler.
A pointer parameter.
When you write int square(int n); near the top of a file (no body), what is that?
A function call.
A function declaration: it tells the compiler the signature without providing the body.
A typo — function bodies are mandatory.
A way to make the function virtual.
Why can two overloads differ in their parameter types but not in return type alone?
Hint: think about what information is available at the call site when the compiler decides which overload to use.
The compiler chooses the overload by looking at the arguments you pass, and the arguments don't tell it what return type you expected.
Two functions with the same name are forbidden by the standard.
Overloading only works for functions that return void.
C++ inherited the rule unchanged from C.
Next: where do function calls actually live in memory? Meet the call stack.