Functions & Scope

Scope: Functions are named, reusable blocks of code. They let you build a program from small pieces instead of one long list of statements. This lesson covers declaring and calling functions, parameters, return values, overloading, scope, and recursion.

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

  1. Write is_even(int) returning bool, and print results for 0, 3 and 8.
  2. Write max_of_three that returns the largest of three double values.
  3. Add a default argument to a greeting function and call it with and without it.
  4. Overload area for a square (double side) and a rectangle (double w, double h).
  5. Rewrite factorial as a loop and compare the two versions for clarity.