Functions & Scope
Declaring & calling
A function declaration states the return type, the name, and the parameter list. When the program reaches a call — the name followed by parentheses — it jumps into the function, runs its body, and resumes where it left off.
#include <iostream>
// Declaration and definition in one piece.
int add(int a, int b) { // returns int, takes two ints
return a + b; // send the result back to the caller
}
int main() {
int sum = add(3, 4); // call: 3 and 4 arrive as a and b
std::cout << sum << "\n"; // prints 7
return 0;
}
The int before add promises an integer result; the
void placeholder means "returns nothing". A function that performs an action but
returns no value is declared void.
Parameters & arguments
The values a caller passes are arguments; the names inside the function are parameters. The count and order must match the declaration.
void greet(std::string name, int times) { // two parameters
for (int i = 0; i < times; i++) {
std::cout << "Hi " << name << "\n";
}
}
int main() {
greet("Ada", 2); // two arguments, in the same order
return 0;
}
Default arguments
A parameter can have a default value; callers may then omit that argument. Defaults are declared once, in the header of the function — put them after all non-default parameters.
void greet(std::string name, int times = 1) { // times defaults to 1
for (int i = 0; i < times; i++) {
std::cout << "Hi " << name << "\n";
}
}
greet("Ada"); // valid: times is 1
greet("Bob", 3); // valid: times is 3
Pass by value
By default, arguments are passed by value: the function receives a copy. Modifying a parameter never changes the caller's variable. This is safe but can be expensive for large objects — the reference lesson shows how to avoid the copy.
void mutate(int x) {
x = 999; // changes only the local copy
}
int main() {
int n = 1;
mutate(n);
std::cout << n; // still 1 — the original is untouched
return 0;
}
Return values
A function announces its result with return. The returned value flows back to the
call site, where it can be stored, printed, or passed to another function.
double average(double a, double b) {
return (a + b) / 2.0; // compute and hand the result back
}
int main() {
double mid = average(3.0, 5.0); // mid becomes 4.0
std::cout << average(10.0, 20.0); // print the returned value directly
return 0;
}
Early return
A function may return from anywhere, not only at the end. Early returns are a clean way to handle invalid input before doing real work:
double division(double a, double b) {
if (b == 0.0) {
return 0.0; // guard: reject the invalid case first
}
return a / b; // normal path
}
Overloading
Two functions may share a name if their parameter lists differ. The compiler picks the right one from the arguments at each call site — this is overloading.
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
int main() {
std::cout << add(2, 3) << "\n"; // calls the int version
std::cout << add(2.5, 1.5) << "\n"; // calls the double version
return 0;
}
The return type alone cannot distinguish overloads — the parameters must differ. Use overloading for functions that do the same job with different types; do not create two names for the same conceptual operation.
Scope & lifetime
Variables are visible only inside the block where they are declared (their scope) and live until that block ends (their lifetime). Parameters behave like local variables of the function.
int result = 0; // global scope: visible everywhere below
void update() {
int local = 5; // local: visible only inside update()
result += local; // globals are readable from anywhere
} // local dies here
int main() {
update();
std::cout << result; // 5
// std::cout << local; // error: local is out of scope
return 0;
}
Global variables are tempting but dangerous — any function can alter them, which makes bugs hard to trace. Pass values through parameters instead.
Recursion
A function that calls itself is recursive. Recursion is the natural way to express problems that contain smaller copies of themselves, like tree traversal and divide-and-conquer. Every recursive function needs a base case that stops the calls, or it recurses forever.
int factorial(int n) {
if (n <= 1) return 1; // base case: stop here
return n * factorial(n - 1); // recursive step: shrink the problem
}
int main() {
std::cout << factorial(5); // 120 = 5*4*3*2*1
return 0;
}
Each call gets its own copy of the parameters, so state does not leak between levels. When a problem fits recursion, the code is often shorter and clearer than an iterative version.
Practice
- Write
is_even(int)returningbool, and print results for 0, 3 and 8. - Write
max_of_threethat returns the largest of threedoublevalues. - Add a default argument to a greeting function and call it with and without it.
- Overload
areafor a square (double side) and a rectangle (double w, double h). - Rewrite
factorialas a loop and compare the two versions for clarity.