Lesson 2 — Copies, Scope, and Prototypes

← Module home · ← Lesson 1 — The machine · Lesson 3 — References →

In this lesson you will learn


1. Pass-by-value — the copy rule

When you call a function, each argument is copied into its parameter. The function works on the copies:

void tryToChange(int x) {
    x = 999;                        // changes the COPY
    std::cout << "inside: " << x << '\n';
}

int main() {
    int n = 5;
    tryToChange(n);
    std::cout << "after:  " << n << '\n';   // still 5!
    return 0;
}
before call:      main's n ──► [ 5 ]
during call:      main's n ──► [ 5 ]     tryToChange's x ──► [ 999 ]  (separate box)
after return:     main's n ──► [ 5 ]     (x's box destroyed)

Output: inside: 999 then after: 5. The function cannot reach back into main’s box — the parameter is a new box that received a copy. Dry run:

step n (main) x (copy) effect
call 5 5 copy made
body 5 999 x reassigned
return 5 destroyed main unaffected

Why this is a feature, not a limitation. A caller can hand over a variable and trust it won’t be touched: printInvoice(total) can never corrupt your total. Big libraries are built on this guarantee. When a function genuinely must hand back more than one answer, C++ has a second tool — references — and it gets its own lesson (Lesson 3). Until then, one function = one answer via return.

2. Scope — where variables live

A variable’s scope is the region of code where its name is usable:

int globalCount = 0;              // global scope: the whole file

void tick() {
    int localTotal = 0;           // local: born each call, dies at return
    localTotal += 1;
    globalCount += 1;             // legal — and the beginning of trouble
}

int main() {
    tick();
    // std::cout << localTotal;   // ERROR: localTotal doesn't exist here
    return 0;
}

The rules:

Variable Born Dies Visible to
Local (in a function) at its declaration, each call at function return only that function
Local (in a loop body) each pass end of pass only inside the block
Parameter at the call at return only that function
Global at program start at program end every function below it

Shadowing (met in Iteration D6) applies inside functions too: a local named total hides a global total for the whole function — a bug factory. Two variables with the same name in one program is a design smell unless one deliberately shadows a parameter for a good reason (rare).

3. Global variables — why this course bans them

A global is readable and writable by every function in the file. That sounds convenient and is precisely the problem:

// WHY NOT — the tangle
int balance = 1000;

void withdraw(int amount) { balance -= amount; }
void audit() { std::cout << balance << '\n'; }
void resetDay() { balance = 0; }             // surprise!

After twenty calls, which function moved the balance? The answer requires reading every function. Globals:

The two honest alternatives cover every case:

  1. Data a function needs → pass it in (a parameter).
  2. Data a function produces → hand it back (a return, or a reference in Lesson 3).
// the same program, no globals
int withdraw(int balance, int amount) { return balance - amount; }

Every program in this course passes its own tests because every function declares its inputs and outputs openly. (One nuance for honesty: constexpr constants like constexpr double TAX_RATE = 0.15; at file scope are fine and encouraged — they’re fixed values, not changing state. The ban is on mutable globals.)

4. Prototypes — telling the compiler early

Lesson 1 §2 showed the order problem: calls must come after the compiler knows the machine. Real programs solve it with prototypes — the function’s signature without a body — collected above main:

#include <iostream>

// --- prototypes: the program's table of contents ---
double lineCost(double unitPrice, int qty);
double applyDiscount(double amount);
void   printInvoice(double unitPrice, int qty);

int main() {
    printInvoice(120.0, 10);        // compiler already knows all three
    return 0;
}

// --- definitions: the actual recipes, in any order ---
double lineCost(double unitPrice, int qty) {
    return unitPrice * qty;
}

double applyDiscount(double amount) {
    return (amount >= 1000) ? amount * 0.90 : amount;
}

void printInvoice(double unitPrice, int qty) {
    double sub = lineCost(unitPrice, qty);
    std::cout << "Net: " << applyDiscount(sub) << '\n';
}

Why professionals prefer this layout:

House style from here on: prototypes block → main → definitions, with one comment line per function.

One rule with teeth: the compiler accepts a prototype whose parameter names differ from the definition’s (void f(int a); defined as void f(int b)) — parameters’ names are ignored in signatures. But you shouldn’t: keep prototype and definition names identical; the names are your only documentation at the call site.

5. Decomposition — designing with functions

Decomposition is the unit’s central skill: given a problem, list the functions before writing any code. The method:

Step 1 — Underline the verbs and nouns. The problem statement’s verbs are functions; the nouns are parameters and returns.

“The program reads a customer’s units consumed, computes the bill from slab rates, prints an itemised receipt, and repeats until the clerk enters 0.”

Verbs: read, compute, print, repeat. Nouns: units, bill, receipt.

Step 2 — Draft the function list (name, parameters, return):

Function Takes Returns Job
readUnits — int validated input (0 = quit signal handled by caller)
computeBill int units double the slab ladder — pure computation
printReceipt int units, double bill void itemised output

Step 3 — Check each function against the two tests:

Step 4 — Wire main as the conductor:

int main() {
    int units = readUnits();
    while (units != 0) {
        printReceipt(units, computeBill(units));
        units = readUnits();
    }
    std::cout << "Day closed.\n";
    return 0;
}

Read main aloud: it is the problem statement. That is the goal of decomposition — main narrates; functions act.

The grain of the decomposition is a judgement call, and two heuristics keep it sane: a function longer than about a screen is usually two functions; a function called once and trivial (double double_it(double x) { return 2*x; }) is usually noise. Between those poles, name-by-responsibility.

Practice

Key takeaways

→ Next: Lesson 3 — References and testing