04 · Functions & Overloading¶
Functions package up reusable logic behind a name, parameters, and a return type.
Declaring and calling functions¶
#include <iostream>
int add(int a, int b) {
return a + b;
}
int main() {
int result = add(3, 4);
std::cout << result << std::endl; // 7
}
int add(int a, int b) declares a function named add that takes two int
parameters and returns an int. The body executes when the function is
called, not when it's defined.
Void functions (no return value)¶
void printBanner(const std::string& title) {
std::cout << "=== " << title << " ===" << std::endl;
}
int main() {
printBanner("Report");
// === Report ===
}
void means the function performs an action but doesn't hand a value back to
the caller. A bare return; (with no value) can still be used to exit early.
Parameters: pass by value vs. pass by reference¶
void increment(int x) { // pass by value -- x is a COPY
x++;
}
void incrementRef(int& x) { // pass by reference -- x IS the original
x++;
}
int main() {
int a = 10;
increment(a);
std::cout << a << std::endl; // 10 -- unchanged, the function modified its own copy
incrementRef(a);
std::cout << a << std::endl; // 11 -- changed, since x refers to a directly
}
By default C++ passes arguments by value — the function receives a copy.
Adding & to the parameter type makes it a reference parameter, which
lets the function modify the caller's variable directly. References are
covered in depth in Module 8.
Passing larger objects efficiently with const&¶
#include <string>
// Passing std::string by value would copy the whole string on every call.
// const& avoids the copy while still preventing the function from modifying it.
void greet(const std::string& name) {
std::cout << "Hello, " << name << "!" << std::endl;
}
A good default rule: pass small, cheap-to-copy types (int, double,
char, bool) by value, and pass larger types (std::string,
std::vector, your own classes) by const& unless the function needs to
modify the caller's copy.
Default arguments¶
double calculatePrice(double base, double taxRate = 0.08) {
return base + (base * taxRate);
}
int main() {
std::cout << calculatePrice(100.0) << std::endl; // 108 -- uses default 0.08
std::cout << calculatePrice(100.0, 0.05) << std::endl; // 105 -- overrides the default
}
Default arguments must be trailing (you can't have a defaulted parameter followed by a non-defaulted one).
Function overloading¶
C++ lets you define multiple functions with the same name as long as their parameter lists differ in type or count — the compiler picks the right one based on the arguments at the call site.
int multiply(int a, int b) {
return a * b;
}
double multiply(double a, double b) {
return a * b;
}
int multiply(int a, int b, int c) {
return a * b * c;
}
int main() {
std::cout << multiply(3, 4) << std::endl; // 12 -- calls int version
std::cout << multiply(2.5, 4.0) << std::endl; // 10 -- calls double version
std::cout << multiply(2, 3, 4) << std::endl; // 24 -- calls 3-arg version
}
Overload resolution is based purely on the parameter list — the return type alone cannot distinguish two overloads.
Function declarations vs. definitions¶
So far each function has been defined where it's used. In larger programs you typically declare a function's signature (often in a header file) and define its body separately:
// Declaration (a "prototype") -- tells the compiler this function exists
int square(int n);
int main() {
std::cout << square(5) << std::endl; // 25 -- can call before the definition appears
}
// Definition -- the actual implementation, can appear later in the file
int square(int n) {
return n * n;
}
This becomes essential once code is split across multiple .cpp and .h
files, which you'll do starting in Module 10.
How It Actually Works¶
Calling a function is a real, costed operation in C++: the CPU pushes a
stack frame. Concretely, calling f(a, b) involves pushing the
arguments (or loading them into designated argument registers per the ABI —
Application Binary Interface, the agreed-upon calling convention), pushing
the return address (where execution should resume after f finishes),
jumping to f's code, and inside f, reserving stack space for its local
variables. Returning pops that frame and jumps back to the saved return
address. Every level of function-call nesting adds another frame to the call
stack — which is exactly why unbounded recursion eventually crashes with a
stack overflow: each frame consumes real memory in a fixed-size region.
Overloading is resolved entirely at compile time, not runtime — there's
no dispatch cost. When you write print(5) and print(5.0), the compiler
looks at the argument types, matches them against every print overload's
parameter list, and picks one using a ranked set of rules (exact match beats
promotion, promotion beats standard conversion, and so on). By the time the
program runs, the ambiguity is already gone — the generated machine code has
a direct call to one specific address; there's no runtime "which overload"
check like there is with virtual dispatch. This is also why overload
resolution can fail to compile ("ambiguous call") — the compiler must be
able to pick exactly one candidate using its rules, with no runtime fallback
to break a tie.
Passing arguments by value copies bytes onto the callee's stack frame
(cheap for an int, potentially expensive for a large struct); passing
by reference just passes the address, so the callee operates on the
caller's original memory with no copy at all.
🔀 See this in another language¶
Exercise¶
Write overloaded functions named area: one taking a single double (for a
square's side), one taking two doubles (for a rectangle's width and
height), and one taking a double radius that computes a circle's area using
3.14159. Call all three from main and print the results.