Week 3 of 8 · C++

Functions and Collections

Can faith be reasoned about — and is the Bible reliable?

Chapter 3 — Functions and Collections

“Come now, let us reason together, says the LORD.” — Isaiah 1:18

“The grass withers, the flower fades, but the word of our God will stand forever.” — Isaiah 40:8

“Don’t repeat yourself.” — every senior engineer who has ever lived

This week merges Coding 1 chapters 5 and 6. Everything in that book’s “Repetition II: Functions” and “Collections: Arrays and Strings” is here, resequenced into one week and one running example.


Your Week at a Glance

Twelve honest hours, four sessions of about three. Do them on four different days if you can — this material compounds overnight in a way cramming cannot reproduce.

#~TimeWhat you doDone when
13 hrsRead §3.1–§3.8. Type and run function_basics.cpp, prototypes.cpp, scope_rules.cpp, pass_by_reference.cpp, composition.cpp, guard_clauses.cpp, mini_toolkit.cpp. Reps 1–5 in the exercises.You can write a function — return type, parameters, body, return — from a blank editor, no copying.
23 hrsRead §3.9–§3.14. Run array_basics.cpp, array_count.cpp, array_functions.cpp. Reps 6–10.You can declare an array, traverse it with a loop that does not run off the end, and pass it to a function with its size.
33 hrsRead §3.15–§3.21, including the bug catalogue in §3.20 — read it once now so the error text is familiar when you hit it. Run string_ops.cpp, getline_fix.cpp, linear_search.cpp, selection_sort.cpp, manuscript_report.cpp. Reps 11–16. Then take the Checkpoint in §3.22, honestly.You pass the Checkpoint at 6 of 8 or better. If not, you re-drill instead of starting the project.
43 hrsBuild Project 3 — Reasoning Toolkit & Manuscript Database, Normal tier (Project 3). Submit.Zero warnings under -Wall -Wextra, and you exercised every menu option once before submitting the link.

Two honest warnings. Session 4 is three hours for the Normal tier only — Medium and Hard are extra credit and extra hours, budgeted on top of the twelve. And this is the heaviest of the three C++ front-half weeks, because it merges the most important abstraction in the course with the first data structure. If a session runs long, let session 4 slip a day rather than skipping reps. The reps are what make the project fast.


Why This Matters

For two weeks your programs have grown by getting longer. Another calculation? Add lines to main. Another case? Another else if. Another value to remember? Another variable — score1, score2, score3. That strategy has a ceiling, and you felt it in Project 2 when the same three lines of output formatting turned up in four branches.

This week you break through in two directions at once.

Functions let the programmer stop repeating himself. A function is a chunk of code with a name: write it once, use it by saying its name. The cost of not having them is quiet and enormous. When the same logic lives in three places and a bug appears in one, the bug is sitting in the other two, waiting. When a requirement changes you must change it everywhere, and you will miss one.

Collections let the program stop repeating itself. An array is one name holding many values. Instead of score1 through score20, you write scores[20]; instead of twenty near-identical lines, a three-line loop. “How many” becomes a variable, and the same program handles five records or five hundred.

The apologetics frame this week is the pair of questions in the syllabus: can faith be reasoned about, and is the Bible reliable?

The first half answers the first in code. The Christian intellectual tradition is itself a toolkit of named, reusable arguments. Aquinas did not reinvent the Cosmological Argument each time he needed it; he named it, structured it, reused it. Pascal did not rebuild the wager in every pensée. Lewis returned to the moral argument book after book, because once an argument is carefully built, reusing it well is a virtue and not laziness. A function is exactly that move in code.

The second half answers the second with data. The New Testament textual tradition runs to roughly 5,800 known Greek manuscripts, plus tens of thousands in Latin, Coptic, Syriac, Armenian, and Ge’ez. The earliest fragments — such as P52, a scrap of John 18 — sit within a handful of generations of the originals; P52’s traditional dating is around AD 125, though palaeographers such as Nongbri argue the evidence supports anywhere in AD 125–225. Homer’s Iliad, the next best-attested classical work, survives in about 1,700 manuscripts, the earliest more than four centuries after composition. After Homer the counts fall into the hundreds. Whatever you make of it, that data exists — and data that exists can be held in a program: searched, counted, sorted, filtered.

Nothing you write this week proves anything about the Bible. What it does is make a dataset legible to a human being who runs it. That is a real and modest thing, and it is enough for Week 3.


3.1 — What a Function Is

A function is three things: a name, a signature (what it takes in and what it gives back), and a body.

You have already used functions other people wrote — static_cast<double>(x), getline(cin, line), s.length(). This chapter is about writing your own. Here is the simplest one:

int add(int a, int b) {
    return a + b;
}

Read it left to right. int — this function returns an int. add — its name. (int a, int b) — it takes two parameters, both int. { return a + b; } — the body; return sends a value back to whoever called it.

Now, anywhere in your program:

int sum1 = add(3, 4);              // 7
int sum2 = add(10, 20);            // 30
int sum3 = add(sum1, sum2);        // 37
cout << add(1, add(2, add(3, 4))) << endl;   // nested calls

Stare at that last line. The innermost add(3, 4) runs first and produces 7; then add(2, 7) produces 9; then add(1, 9) produces 10. A function call is an expression — it evaluates to a value, so it is legal anywhere a value is legal.

That is the whole concept. You wrote add once and used it five times, so a fix lands in one place.


3.2 — Function Anatomy in Full

<return_type> <name>(<parameter_list>) {
    <body>
    return <value>;
}

Return type. The type of value handed back: int, double, bool, char, string — or void, meaning nothing.

void greet(string name) {
    cout << "Hello, " << name << "." << endl;
}

A void function needs no return statement. Because it hands nothing back, int x = greet("Maya"); is illegal — there is nothing to assign.

Name. Any valid identifier: letters, digits, underscores, not starting with a digit. This book uses snake_casebayes_update, list_manuscripts. Names should be verbs, because functions do things: add, compute_margin, is_valid_probability, print_report. A noun-only name like data reads like a variable and will confuse you in six weeks.

Parameter list. Comma-separated, each written <type> <name>:

double compute_margin(double measured, double low, double high) {
    return (measured - low) / (high - low);
}

Parameters are local variables, born when the function is called and dead when it returns. Their names need not match the caller’s:

double m = compute_margin(student_score, class_low, class_high);

Inside the function those are measured, low, high. What matters is order — first argument into first parameter — and C++ will not warn you if you swap two arguments of the same type. That is a bug you will write at least once. A function may also take zero parameters: ().

Body. Anything you could write in main — output, input, loops, conditionals, calls to other functions. One restriction: you cannot define a function inside another function.

return. It sends a value back and immediately ends the function; code after it on the same path never runs. Several returns are fine:

double safe_divide(double a, double b) {
    if (b == 0.0) {
        return 0.0;   // early exit on bad input
    }
    return a / b;
}

A non-void function must return on every path. If any route reaches the closing brace without a return, the compiler warns and you get garbage.

code/function_basics.cpp has one function of every shape above. Actual output:

square(7)                      = 49
add(3, 4)                      = 7
add(1, add(2, add(3, 4)))      = 10
compute_margin(5.0, 1.0, 11.0) = 0.4
is_valid_probability(0.7)      = true
is_valid_probability(1.5)      = false
course_name()                  = Accelerated Coding 1
Hello, Marcus.

cout << boolalpha; is what makes bool print as true/false instead of 1/0.


3.3 — Where Functions Live, and Prototypes

A C++ program is a list of functions. Exactly one must be named main — that is where the operating system starts. Everything else is defined outside main, normally above it, because the compiler reads the file top to bottom once. By the time it reaches main it has already seen the helpers and knows their names, parameter types, and return types.

To call a function before the compiler has seen its body, supply a prototype — the signature, a semicolon, no body:

double compute_margin(double measured, double low, double high);   // prototype
void print_report(string name, double margin);                     // prototype

int main() {
    double m = compute_margin(5.0, 1.0, 11.0);
    print_report("Fine-tuning test", m);
    print_report("Second sample", compute_margin(8.0, 1.0, 11.0));
    return 0;
}

double compute_margin(double measured, double low, double high) {  // definition
    return (measured - low) / (high - low);
}

void print_report(string name, double margin) {
    cout << name << ": margin = " << margin << endl;
}

That is code/prototypes.cpp, and it prints:

Fine-tuning test: margin = 0.4
Second sample: margin = 0.7

Prototypes let main sit at the top so a reader gets the story before the details. Either organization is fine; pick one per file. This book mostly defines helpers above main and skips prototypes, because in a single file that is one less thing to keep in sync — change a function’s parameters, forget its prototype, and you get a confusing error about a function that “was not declared.”

Coach’s Note — In professional codebases prototypes live in header files (.h) and definitions in .cpp files, because big programs compile in pieces. We do not need multi-file builds until Java forces the issue in Chapter 7. One .cpp file per program until then.


3.4 — Scope: Where Variables Live

A variable’s scope is the region where its name means anything.

Local scope. A variable declared inside a function — or a loop body, or an if block — exists only there.

double compute_margin(double measured, double low, double high) {
    double range = high - low;        // local to compute_margin
    return (measured - low) / range;
}

int main() {
    double m = compute_margin(5.0, 1.0, 11.0);
    cout << range << endl;            // ❌ ERROR: range does not exist here
    return 0;
}

range is created when the function starts and destroyed when it returns. The same is true of a loop counter — after for (int i = 0; ...) ends, i is gone.

Parameters are local too. measured, low, and high are ordinary locals that happen to be initialized from what the caller passed. That is the mental model for the next section.

Global scope. A variable declared outside every function is visible to every function below it:

#include <cmath>                 // for std::abs

const double EPSILON = 0.0001;   // global constant, visible everywhere

bool close_enough(double a, double b) {
    return std::abs(a - b) < EPSILON;
}

Use globals for constants only. The rule for this course is not negotiable: global const is fine; global mutable variables are forbidden. A changeable global can be written by any function in the file, so when its value goes wrong every line is a suspect. A const cannot be written, so it cannot be the culprit.

code/scope_rules.cpp demonstrates all of it and compiles cleanly; the two illegal lines are commented out and marked ❌ so you can uncomment them one at a time and see the error yourself.

Coach’s Note — Students breeze past scope in Week 3 and panic in Week 6, when a pointer escapes a function and the program starts printing nonsense. Learn it now, while it is cheap. Local means local. Parameters are local. A function call copies its arguments into fresh locals — unless you say otherwise, which is next.


3.5 — Pass by Value vs. Pass by Reference

By default a function gets its own private copy of what you pass in:

void try_to_double(int x) {
    x = x * 2;     // modifies the local copy
}

int main() {
    int n = 5;
    try_to_double(n);
    cout << n << endl;     // 5, not 10
    return 0;
}

try_to_double doubled its own x; main’s n never moved. This is pass by value, it is the default, and it is almost always what you want, because a function cannot damage the caller’s data by accident.

When the function genuinely must change the caller’s variable, use pass by reference, marked &:

void actually_double(int& x) {
    x = x * 2;     // modifies the caller's variable
}

void swap_ints(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
}

int& x means “x is another name for whatever the caller passed.” Write swap_ints without the &s and watch it do absolutely nothing — the classic Week 3 head-scratcher, and Bug 5 in §3.20.

A third form matters once your data gets bigger: const reference, const string& — “do not copy this, and do not let me modify it.”

void announce(const string& label, double value) {
    cout << label << " = " << value << endl;
}

Three rules of thumb that carry the whole course:

  • Small and read-only (int, double, bool, char): pass by value. The default and the majority case.
  • Must modify the caller’s variable: pass by reference, int&. Project 3’s add_manuscript needs this for its count.
  • Large and read-only (a long string, later a struct or object): pass by const reference.

code/pass_by_reference.cpp shows all three:

after try_to_double(n):   n = 5
after actually_double(n): n = 10
after swap_ints(x, y):    x = 99, y = 1
posterior = 0.92

Coach’s Note — & means something different in Chapter 6 (address-of) and different again for reference variables in general. This week, one meaning only: int& in a parameter list means “not a copy — the caller’s actual variable.”


3.6 — Composition

Functions can call other functions, and that is where the power stops being additive:

double bayes_update(double prior, double L_H, double L_notH) {
    double numerator = L_H * prior;
    double denominator = numerator + L_notH * (1.0 - prior);
    return numerator / denominator;
}

double chained_bayes(double prior, double L1_H, double L1_notH,
                     double L2_H, double L2_notH) {
    double first = bayes_update(prior, L1_H, L1_notH);
    return bayes_update(first, L2_H, L2_notH);
}

chained_bayes weighs two pieces of evidence in succession: update once from the original prior, then feed that result back in as the prior for the second update. It contains no arithmetic of its own. The formula lives in exactly one place, so one fix repairs both functions.

That is composition, the shape of all durable code. Build small functions you trust; combine them into larger ones; those become parts for larger ones still. By the time you write a 500-line program, almost none of it is fresh logic — it is orchestration of pieces you already believe in.

code/composition.cpp runs the classic rare-condition scenario: a hypothesis you initially give a 1% chance, tested by evidence that appears 99% of the time when it is true and 1% of the time when it is false.

Prior:             0.01
After 1 positive:  0.5
After 2 positives: 0.99

Look hard at the middle line. One piece of very strong evidence took a 1% hypothesis only to a coin flip — because the prior was so low. Two took it to 99%. That is not a quirk of the code; it is what the arithmetic of evidence does, and it is why careful people are slow to be moved by a single striking argument in either direction.

Coach’s Note — Composition separates a beginner from a working programmer. Beginners write one enormous function that does everything. Working programmers write twenty small ones that combine. The second kind is easier to read, easier to test, and vastly easier to change. Practice it now, while your programs still fit in your head.


3.7 — Function Style, and Guard Clauses

Six standards. They cost nothing to adopt in Week 3 and are expensive to retrofit in Week 6.

  1. One function, one job. If you cannot describe it in one sentence with no “and,” split it.
  2. Name it for what it does, not how. compute_margin beats divide_and_subtract. Functions returning bool read beautifully as questions: is_, has_, can_.
  3. Parameter order matters. Group related parameters; inputs first, output references last.
  4. No magic numbers in the body. If 0.6 means “imbalance threshold,” make it a parameter or a named const.
  5. Keep functions short. One screen is reasonable. Eighty lines is telling you something wants to be broken out.
  6. Return early on bad input. Do not wrap the whole body in if (good_input) { ... }:
// ❌ nested — the real work is buried
double safe_divide_nested(double a, double b) {
    double result = 0.0;
    if (b != 0.0) {
        result = a / b;
    }
    return result;
}

// ✅ guard clause — handle the bad case, leave, then work
double safe_divide(double a, double b) {
    if (b == 0.0) {
        return 0.0;         // bail
    }
    return a / b;           // main logic, not nested
}

Adopt the second shape permanently. With one guard the difference looks cosmetic; with three, the nested version is four levels deep and unreadable while the guarded version is still flat.

Guard clauses need a way to say “that didn’t work,” and you have two, both in code/guard_clauses.cpp:

  • A sentinel return value — one the function could never legitimately produce. validated_bayes returns -1.0 for non-probability inputs, safe because a real probability is never negative. Linear search returns -1 for “not found” for the same reason: no valid index is negative.
  • A bare return; in a void function, which just stops early.
safe_divide_nested(10, 4) = 2.5
safe_divide(10, 4)        = 2.5
safe_divide(10, 0)        = 0
Posterior: 0.9
Invalid input - probabilities must be in [0, 1].
Invalid input - probabilities must be in [0, 1].

The sentinel has one hard requirement: the caller must check it. A function that returns -1.0 to a caller who prints it verbatim has helped nobody.


3.8 — Worked Example: A Mini Toolkit

code/mini_toolkit.cpp assembles the first half of the week: six functions — bayes_update, chained_bayes, complement, conjunction_probability, is_valid_probability, print_result — each doing one thing, driven by a main short enough to read in one breath:

int main() {
    double prior = 0.5;
    double L_E_given_H = 0.9;
    double L_E_given_notH = 0.1;

    if (!is_valid_probability(prior)) {
        cout << "Invalid prior." << endl;
        return 1;
    }

    double posterior = bayes_update(prior, L_E_given_H, L_E_given_notH);

    print_result("Prior", prior);
    print_result("Posterior", posterior);
    print_result("1 - Posterior", complement(posterior));
    print_result("Both, if independent", conjunction_probability(posterior, 0.5));
    print_result("Posterior after a second identical piece of evidence",
                 chained_bayes(prior, L_E_given_H, L_E_given_notH,
                               L_E_given_H, L_E_given_notH));
    return 0;
}

Actual output:

Prior: 0.5
Posterior: 0.9
1 - Posterior: 0.1
Both, if independent: 0.45
Posterior after a second identical piece of evidence: 0.987805

Notice what main does not contain: not one arithmetic operator. Every calculation lives in a named function. main decides what; the functions know how. That division is the grading criterion behind “no business logic in main” in Project 3, and it is not busywork — it is the property that lets you add a seventh operation next week without touching the first six.

Run it, then change prior to 0.01 and run it again. Watch how much of the answer was being carried by that one number.


3.9 — The Bridge: A Function Is a Verb, a Collection Is a Noun

Stop for a minute, because this is the seam of the week.

You have just learned to name a behavior. The rest of the chapter is about naming a group of things. These are not two unrelated topics that happened to land in the same week — they are two halves of one idea, and neither is much use alone.

Consider what your functions can do so far. bayes_update takes three numbers; compute_margin takes three numbers; every function this week takes a small, fixed count of individual values. To average five test scores you would need average5(double a, double b, double c, double d, double e), then average6 for six, and the whole point of functions — write it once — collapses. Functions without collections cannot be written once for a variable amount of data.

Now the reverse. Suppose you have twenty manuscripts in an array and no functions. Every time you print the list you write a for loop. Every time you search it, another loop. Every time you count something, a third. main becomes a wall of near-identical loops, each one a fresh chance to type <= where you meant <. Collections without functions force you to rewrite the same traversal forever.

Put them together and you get the single most-used shape in programming. This next block is a template, not a programreturn_type and element_type are placeholders you replace with real types, so do not try to compile it as written:

return_type do_something(element_type arr[], int size) {
    for (int i = 0; i < size; i++) {
        // ... one element at a time ...
    }
    // ... and a return, unless return_type is void ...
}

A named verb wrapped around a loop over a named collection. sum_array. print_roster. find_index. sort_by_century. Every one is that shape with the middle filled in differently, and once it is in your fingers an enormous amount of programming becomes recognizable: you are almost always writing a function that walks a collection.

This week’s graded item — P3, the Reasoning Toolkit & Manuscript Database — is deliberately built on both halves. The Toolkit is functions with no collections; the Database is collections that would be unbearable without functions. Feel the difference between them, and you will stop thinking of “functions” and “arrays” as two chapters and start thinking of them as one tool.


3.10 — Arrays: One Name, Many Values

int scores[5];        // room for 5 ints, contents undefined

That reserves a run of memory big enough for five ints and names the whole run. You reach elements by index, in square brackets:

scores[0] = 90;
scores[1] = 85;
scores[2] = 72;
scores[3] = 88;
scores[4] = 95;

Indices start at 0. A five-element array has valid indices 0, 1, 2, 3, 4. There is no scores[5].

Initialize at declaration, all at once — or partially, where everything omitted becomes zero — or let the compiler count:

int scores[5] = {90, 85, 72, 88, 95};
int partial[5] = {90, 85};             // 90, 85, 0, 0, 0
int inferred[] = {90, 85, 72, 88, 95}; // size 5, inferred

Two hard rules:

The size is fixed at compile time. You cannot grow or shrink an array. The size must be a compile-time constant — const int SIZE = 5; works; an int the user typed does not. (Dynamically sized arrays arrive in Chapter 6 with new. Growable containers like vector are outside this course.)

The array does not know its own length. There is no scores.length(). If a function needs the size, you hand it the size separately — which is why every array function in this chapter takes two parameters.

Coach’s Note — C++ arrays are spartan on purpose: no length, no bounds checking, no printing. That bareness is exactly why we start here. An array is a row of numbered mailboxes and nothing more, and every fancy container you will ever meet is built on that row. Learn the row.


3.11 — Traversing an Array

The canonical pattern counts from 0 up to but not including the size:

const int SIZE = 5;
int scores[SIZE] = {90, 85, 72, 88, 95};

for (int i = 0; i < SIZE; i++) {
    cout << "scores[" << i << "] = " << scores[i] << endl;
}

Four things to internalize. Use const int SIZE, never a bare literal — if the size changes you edit one line, not six loops. The condition is i < SIZE, never i <= SIZE (§3.12). You can write as well as readscores[i] += 2; curves every score. Traversal plus an accumulator gives you statistics, the same accumulator pattern from Chapter 2, now fed by an array:

int sum = 0;
for (int i = 0; i < SIZE; i++) {
    sum += scores[i];
}
double average = static_cast<double>(sum) / SIZE;

The static_cast<double> is there because of the integer-division trap from Chapter 1: 430 / 5 in int arithmetic truncates. code/array_basics.cpp demonstrates all of it:

All scores:
  scores[0] = 90
  scores[1] = 85
  scores[2] = 72
  scores[3] = 88
  scores[4] = 95
Sum:     430
Average: 86
After a 2-point curve: 92 87 74 90 97 
partial: 90 85 0 0 0 
inferred: 3 1 4 1 5 

3.12 — Off-by-One and Out-of-Bounds

Two different bugs with one cause, and between them they account for most of the pain in Week 3.

Off-by-one is arithmetic: your loop runs one iteration too many or too few. i <= SIZE runs six times over a five-element array; i < SIZE - 1 runs four times and silently drops your last element. Neither is an error the compiler can see — both are perfectly legal loops.

Out-of-bounds is what happens next: you index a slot that isn’t yours. scores[5] reads whatever bytes sit past the end. In C++ this is undefined behavior, a term of art meaning the standard imposes no requirement at all on what happens. In practice you see one of four things:

  1. A plausible-looking garbage number, and you never notice.
  2. A wildly implausible number, and you do.
  3. Segmentation fault — the operating system killed your program for touching memory it did not own.
  4. Nothing today, and a corruption bug three functions away tomorrow.

Outcome 4 is why this matters more than it looks. There is no bounds checking at runtime. C++ hands you the bytes and moves on.

The compiler helps a little. Write a literal out-of-range index — scores[5] typed by hand — and g++ may warn with something in the -Warray-bounds family. But that warning depends on optimization settings, so at default settings you may get nothing, and the compiler categorically cannot catch the general case where the index is computed while the program runs. That case is one hundred percent yours.

Three habits that make the whole class of bug rare:

  • Write i < SIZE so automatically that <= next to an array size looks wrong on sight.
  • When you compute an index instead of looping, guard it: if (i >= 0 && i < size) { ... }.
  • When output looks almost right but the first or last item is missing or duplicated, suspect your bounds before anything else. That symptom is off-by-one until proven otherwise.

3.13 — Capacity vs. Logical Size

Here is the pattern that carries you through this chapter, Project 3, and all of Chapter 4. A fixed-size array is usually bigger than what you are using, so you track the in-use portion with a separate integer:

const int MAX_ENTRIES = 20;    // capacity     — fixed forever
string entries[MAX_ENTRIES];
int count = 0;                 // logical size — changes as you go

Adding means writing into the next free slot, then growing the count:

entries[count] = "P52";
count++;

Assign, then increment. The other order puts your first entry in slot 1 while slot 0 stays empty, and every listing starts with an unexplained blank line.

Traversing stops at count, not MAX_ENTRIES — loop to MAX_ENTRIES and you will faithfully print fifteen empty strings after your five real ones.

Removing is the interesting one, because a C++ array cannot shrink. “Remove” means shift the tail down over the hole, then decrement the count:

void remove_entry(string entries[], int& count, string target) {
    int at = find_index(entries, count, target);
    if (at == -1) {
        cout << "Not found: " << target << endl;
        return;                             // guard clause
    }
    for (int j = at; j < count - 1; j++) {
        entries[j] = entries[j + 1];        // slide the tail left
    }
    count--;
}

Three things in that one function. find_index is the linear search of §3.16 — it returns the position of target, or -1 if target isn’t there. You have not written it yet; a plain case-sensitive version of it sits in code/array_count.cpp so that this program runs, and §3.16 upgrades it to the case-insensitive one Project 3 needs. Note j < count - 1, not j < count: the final copy is entries[count - 2] = entries[count - 1], and reading entries[count] would be exactly the out-of-bounds read from §3.12. Note also int& count — the caller’s count must change, so it is passed by reference. That is §3.5 earning its keep for the first time.

code/array_count.cpp runs the whole life cycle — empty, five adds, a successful removal, a failed one:

Empty database:
(empty)
Added P52. count = 1
Added Codex Sinaiticus. count = 2
Added Codex Vaticanus. count = 3
Added Codex Bezae. count = 4
Added Bodmer Papyri. count = 5
Current list:
  1. P52
  2. Codex Sinaiticus
  3. Codex Vaticanus
  4. Codex Bezae
  5. Bodmer Papyri
Removed Codex Vaticanus. count = 4
Not found: Codex Ephraemi
After removal:
  1. P52
  2. Codex Sinaiticus
  3. Codex Bezae
  4. Bodmer Papyri

That listing prints 1. through 4. while indexing 0 through 3cout << (i + 1) << ". ". Humans count from one, arrays from zero; the + 1 belongs in the display and nowhere else.

Coach’s Note — “Fixed-size array plus a count variable” is your bread and butter this week and next. It is clunky next to a modern container, and it is exactly how memory works underneath every modern container. You are not learning the primitive version of the skill; you are learning the thing the convenient version is made of.


3.14 — Passing Arrays to Functions

Arrays travel to functions a little differently: you pass the name and the size as two separate arguments, because the array cannot report its own length.

int sum_array(const int arr[], int size) {
    int total = 0;
    for (int i = 0; i < size; i++) {
        total += arr[i];
    }
    return total;
}

Call it with the name alone — no brackets: sum_array(scores, SIZE). sum_array(scores[], SIZE) is a syntax error; forgetting the size is a “too few arguments” error. Both are in §3.20.

The critical fact: a function can modify the array you pass it. Arrays are effectively passed by reference, with no & anywhere to warn you:

void zero_out(int arr[], int size) {
    for (int i = 0; i < size; i++) {
        arr[i] = 0;
    }
}

int main() {
    int scores[3] = {90, 85, 72};
    zero_out(scores, 3);
    cout << scores[0] << endl;   // 0 — main's array really changed
    return 0;
}

(What actually gets copied into the parameter is the address of the first element, not the elements. Chapter 6 tells that story properly; for now take the behavior as a rule.)

This is powerful and dangerous, and the defense is one keyword: mark array parameters const when the function only reads. const int arr[] documents intent and makes the compiler reject an accidental write inside the body:

void print_array(const int arr[], int size);      // reads only
int  sum_array(const int arr[], int size);        // reads only
void zero_out(int arr[], int size);               // writes — no const

Here is §3.9 in practice. average_array is a guard clause plus a composition — it does not sum anything itself, it asks sum_array to:

double average_array(const int arr[], int size) {
    if (size <= 0) {
        return 0.0;             // guard clause: never divide by zero
    }
    return static_cast<double>(sum_array(arr, size)) / size;   // composition
}

code/array_functions.cpp:

scores:  [ 90 85 72 88 95 ]
sum:     430
average: 86
max:     95
after zero_out(scores, SIZE): [ 0 0 0 0 0 ]

3.15 — Strings as Collections

A string is technically a class, not an array, but this week you can treat it as a collection of characters with useful operations attached. Every string operation below runs in code/string_ops.cpp; the cin/getline fix at the end of the section has its own file, code/getline_fix.cpp, because it needs input.

Length. name.length() — and .size() is the same function under another name.

Indexing. name[0] is the first character; name[name.length() - 1] is the last. Zero-based, same out-of-bounds rules as an array.

Comparison. == compares full contents, which is what you want: if (name == "Maya"). And <, >, <=, >= compare alphabetically (strictly, lexicographically — by character code, so all uppercase sorts before all lowercase):

string a = "apple", b = "banana";
bool a_first = (a < b);   // true

One trap: this works when at least one side is a real std::string. Write ("apple" < "banana") with two bare literals and you are comparing memory addresses, not text; the answer is meaningless and g++ -Wall -Wextra will warn about it. Put your text in a string variable first.

find.

string verse = "1 Peter 3:15";
size_t colon = verse.find(":");     // 9

.find(pattern) returns the index of the first occurrence, or string::npos if it is not there. size_t is the unsigned type the standard library uses for sizes and indices; treat it like an int for arithmetic, but never store a .find() result in an int. Always test “not found” with != string::npos, never with != -1. Here is why, straight out of string_ops.cpp:

find("Q"):        18446744073709551615
string::npos:     18446744073709551615

substr.

verse.substr(0, colon)     // "1 Peter 3"  — start at 0, take `colon` chars
verse.substr(2, 5)         // "Peter"      — start at 2, take 5 chars
verse.substr(colon + 1)    // "15"         — start at colon+1, take the rest

One argument runs to the end; two takes at most that many characters. If start is past the end of the string, substr throws and your program dies — Bug 10 in §3.20.

Concatenation. + joins, += appends:

string kind = "Codex";
string which = "Sinaiticus";
string full = kind + " " + which;    // "Codex Sinaiticus"

Note that "Codex" + " " — literal plus literal — is not legal; at least one operand must be a std::string. Start the expression with your variable and it always works.

Case-insensitive comparison. C++ gives you none, so you build one — a genuinely useful rep in functions-plus-loops:

#include <cctype>

string to_lower(string s) {
    for (size_t i = 0; i < s.length(); i++) {
        s[i] = static_cast<char>(std::tolower(s[i]));
    }
    return s;             // s was a COPY — the caller's string is untouched
}

bool same_ignore_case(string a, string b) {
    return to_lower(a) == to_lower(b);
}

Three details worth naming. to_lower takes its parameter by value on purpose — it modifies its own copy and returns it, so the caller’s original is safe (§3.5 used deliberately). The loop counter is size_t to match what .length() returns; use int and you get a -Wsign-compare warning (Bug 7). And std::tolower returns an int, so the static_cast<char> is what keeps -Wall -Wextra silent.

Project 3’s search must be case-insensitive. Write this helper once and reuse it — that is the whole lesson of the chapter in one function.

Mixing cin >> and getline — the cin.ignore() fix. This has cost more student hours than anything else in this section. Read a number with cin >> n;, then a line with getline(cin, line);, and the getline appears to be skipped — it returns an empty string instantly. The reason: cin >> n consumes the digits and leaves the newline you pressed in the buffer, and getline reads from there to the next newline, which is zero characters away.

code/getline_fix.cpp does it wrong and then right in one program. Fed the four lines 5, Codex Sinaiticus, 7, Codex Vaticanus:

no ignore:   n=5 line=[]
with ignore: n=7 line=[Codex Vaticanus]
the line Part 1 lost was: [Codex Sinaiticus]

The name was never destroyed — it was still sitting in the buffer, one read later than the program expected. That is what “the input is off by one read” feels like. The fix is one line at the boundary between the formatted read and the line read:

int n;
cin >> n;
cin.ignore();          // throw away the leftover newline
string line;
getline(cin, line);    // now reads the actual line

You need it only at that boundary. Project 3 needs exactly one, in the menu loop, because you read the choice with >> and the manuscript name with getline.

Full output of code/string_ops.cpp:

verse:            1 Peter 3:15
length:           12
first char:       1
last char:        5
position of ':':  9
before the colon: 1 Peter 3
after the colon:  15
substr(2, 5):     Peter
find("Q"):        18446744073709551615
string::npos:     18446744073709551615
concatenated:     Codex Sinaiticus
a == b:           false
a < b:            true
case-insensitive Codex vs CODEX:  true
case-insensitive Codex vs Bodmer: false
to_lower(full):   codex sinaiticus
full is untouched: Codex Sinaiticus

The simplest search there is: walk the collection, compare each element to the target, stop when you find it.

int find_index(const string roster[], int size, string target) {
    string t = to_lower(target);
    for (int i = 0; i < size; i++) {
        if (to_lower(roster[i]) == t) {
            return i;       // found — return immediately
        }
    }
    return -1;              // fell off the end — not found
}

Note the parameter order, and use it everywhere from here on: collection first, then its size, then whatever you are looking for. That is the same order as sum_array(arr, size) in §3.14, and it is the order remove_entry in §3.13 already assumed. Pick one order and never vary it, because C++ will happily accept a call with two strings swapped and hand you nonsense at runtime. The array parameter is const because this function only reads (§3.14).

Four things to notice, each a general technique. return inside the loop is an early exit — the moment you find it you are done, and no break is needed because return leaves the whole function. -1 is the conventional “not found” sentinel for an index-returning function, safe because no valid index is negative (§3.7). The caller must checkif (idx >= 0) before using it as an index, always. This is O(n) — ten elements, at most ten comparisons. Sorted data admits faster algorithms; for twenty manuscripts linear search is not merely acceptable, it is the correct engineering choice, because it is the one you can write correctly on the first try.

Often you want contains, not equals. Same loop, different test — swap == for .find(...) != string::npos:

int search_substring(const string roster[], int size, string query) {
    string q = to_lower(query);
    int matches = 0;
    for (int i = 0; i < size; i++) {
        if (to_lower(roster[i]).find(q) != string::npos) {
            cout << "  " << roster[i] << endl;
            matches++;
        }
    }
    return matches;
}

code/linear_search.cpp runs both flavors against the five-manuscript starter set. With codex typed in, the actual run is:

Search term: 
Exact match:
  no exact match.

Substring matches:
  Codex Sinaiticus
  Codex Vaticanus
  Codex Bezae
Matches: 3

There is the distinction in one screen. codex matches nothing exactly — no manuscript is named just “Codex” — but it appears inside three of them. Project 3’s Normal tier wants the substring flavor, because that is what a human searching a database expects.


3.17 — Selection Sort

Sorting means rearranging a collection into order. There are dozens of algorithms; this course teaches exactly one, and teaches it properly: selection sort.

Repeatedly find the smallest element in the unsorted part and move it to the front of the unsorted part.

void selection_sort(int arr[], int size) {
    for (int i = 0; i < size - 1; i++) {
        // find the index of the smallest element in arr[i .. size-1]
        int min_index = i;
        for (int j = i + 1; j < size; j++) {
            if (arr[j] < arr[min_index]) {
                min_index = j;
            }
        }
        // swap arr[i] with arr[min_index]
        int temp = arr[i];
        arr[i] = arr[min_index];
        arr[min_index] = temp;
    }
}

Read the structure before the details. The outer loop walks a boundary left to right: everything left of i is finished, everything from i rightward is not. The inner loop searches that unsorted part for its smallest element — the linear search of §3.16, hunting a minimum instead of a match. Then a three-line swap puts that element at position i and the boundary moves right.

Two details that trip people. The outer loop stops at size - 1 — when one element remains it is by definition the largest and already in the only slot left. The swap needs temp — writing arr[i] = arr[min_index]; arr[min_index] = arr[i]; destroys the first value before saving it and leaves you the same number twice.

code/selection_sort.cpp runs it on {3, 1, 4, 1, 5}, printing after every pass:

start:  [ 3 1 4 1 5 ]
  pass i=0: smallest was at index 1 -> [ 1 3 4 1 5 ]
  pass i=1: smallest was at index 3 -> [ 1 1 4 3 5 ]
  pass i=2: smallest was at index 3 -> [ 1 1 3 4 5 ]
  pass i=3: smallest was at index 3 -> [ 1 1 3 4 5 ]
sorted: [ 1 1 3 4 5 ]

centuries before: [ 4 2 5 3 4 ]
centuries after:  [ 2 3 4 4 5 ]

Follow the first two lines by hand. Pass i=0 searched all five slots, found the smallest value 1 at index 1, swapped it to index 0. Pass i=1 searched slots 1–4, found the other 1 at index 3, swapped. By pass i=3 the smallest remaining element was already in place, so the swap exchanged an element with itself and nothing visibly changed — normal, not a bug.

Selection sort is O(n²): double the input, roughly quadruple the work. It is a poor choice for a million records, a completely fine choice for twenty manuscripts, and the simplest correct sort a human can write from memory. That last property is why it is here.


3.18 — Parallel Arrays

Real data has more than one field per thing. A manuscript has a name and a century and a type. With only arrays, the way to store all three is parallel arrays:

const int MAX = 20;
string names[MAX];
int centuries[MAX];
string types[MAX];
int count = 0;

The contract is simple and entirely unenforced: names[i], centuries[i], and types[i] describe the same manuscript. Index all three with the same i, always.

It works, and it is brittle in a memorable way: every operation must touch every array, in lockstep. Add and you write all three. Remove and you shift all three. Sort by century and you swap all three on every swap:

int temp_century = centuries[i];
centuries[i] = centuries[min_index];
centuries[min_index] = temp_century;

string temp_name = names[i];              // and the name...
names[i] = names[min_index];
names[min_index] = temp_name;

string temp_type = types[i];              // ...and the type
types[i] = types[min_index];
types[min_index] = temp_type;

Nine lines to move one row. Forget one block and your arrays drift out of alignment — names[2] says “Codex Bezae” while centuries[2] holds some other manuscript’s century, nothing crashes, nothing warns, and every report from that moment on is quietly wrong. That is the worst class of bug there is: silent, plausible, permanent.

Feel that pain deliberately this week. In Chapter 4 a struct bundles all three fields into one value, arrays of structs replace parallel arrays entirely, and the nine-line swap collapses to three. Chapter 4 will feel like a gift in exact proportion to how much Chapter 3 annoyed you.


3.19 — Worked Example: The Manuscript Report

code/manuscript_report.cpp is the whole week in one file: functions with parameters and return values, a reference parameter, guard clauses, composition, three parallel arrays with a count, linear search, substring search, and a lockstep selection sort. It takes no input, so you can run it and compare against the transcript exactly.

The data is the real starter set: P52 (2nd century, papyrus), Bodmer Papyri (3rd, papyrus), Codex Sinaiticus (4th, uncial), Codex Vaticanus (4th, uncial), Codex Bezae (5th, uncial). Centuries are approximate; the four standard manuscript types are papyrus, uncial, minuscule, and lectionary.

Its main contains no loops, no comparisons, and no arithmetic — just declarations and a script of calls:

string names[MAX_MANUSCRIPTS];
int centuries[MAX_MANUSCRIPTS];
string types[MAX_MANUSCRIPTS];
int count = 0;

add_manuscript(names, centuries, types, count, "Codex Sinaiticus", 4, "uncial");
add_manuscript(names, centuries, types, count, "P52", 2, "papyrus");
// ... three more, then print, search, compute statistics, sort, print again

Note that count is passed with no & at the call site even though the parameter is declared int& count. That is how references work: the & appears in the parameter list, never in the call. The array parameters get no & either and are modified anyway (§3.14). Read the signature, not the call.

Actual output:


=== Database as entered ===
  1. Codex Sinaiticus (century 4, uncial)
  2. P52 (century 2, papyrus)
  3. Codex Bezae (century 5, uncial)
  4. Bodmer Papyri (century 3, papyrus)
  5. Codex Vaticanus (century 4, uncial)

=== Exact lookup ===
  codex bezae -> index 2 (Codex Bezae)

=== Substring search for "codex" ===
  Codex Sinaiticus
  Codex Bezae
  Codex Vaticanus
Matches: 3

=== Statistics ===
  entries:          5
  earliest century: 2
  average century:  3.6
  papyri:           2
  uncials:          3

=== Sorted by century ===
  1. P52 (century 2, papyrus)
  2. Bodmer Papyri (century 3, papyrus)
  3. Codex Sinaiticus (century 4, uncial)
  4. Codex Vaticanus (century 4, uncial)
  5. Codex Bezae (century 5, uncial)

Three things to verify with your own eyes, because verifying is the skill this book is built around:

  1. The exact lookup worked on lowercase input. "codex bezae" found Codex Bezae because find_index lowercases both sides first — to_lower from §3.15 doing its job inside a search from §3.16.
  2. The sort moved all three columns together. After sorting, P52 still says century 2 and papyrus. Had the lockstep swap been missing one block, names and centuries would have shuffled independently and the table would be plausible-looking nonsense.
  3. average century: 3.6 is (4 + 2 + 5 + 3 + 4) / 5 = 18 / 5, computed with static_cast<double> so it does not truncate to 3. The Chapter 1 integer-division trap, still hunting you in Week 3.

One small trap in that file is worth stealing. This line looks reasonable and misbehaves:

cout << "Matches: " << search_names(names, count, "codex") << endl;   // ❌

search_names prints its matches as it runs, but "Matches: " was already sent to cout before the call, so the output interleaves and the count lands after the list with no label. Separate the jobs:

int hits = search_names(names, count, "codex");   // ✅ call first
cout << "Matches: " << hits << endl;              //     THEN print

The general rule: do not call a printing function from inside a cout chain that also prints.


3.20 — Common Bugs (Week 3 Edition)

These are what OnlineGDB’s g++ prints. Other browser compilers word the same errors differently — clang says “use of undeclared identifier” where GCC says “was not declared in this scope” — but the meanings and the fixes are identical. Turn -Wall -Wextra on in your compiler settings and leave them on; half this list is a warning you would otherwise never see.


Bug 1 — you called a function the compiler hasn’t met yet.

error: 'compute_margin' was not declared in this scope

You called it on a line above its definition, and the compiler reads top to bottom. Move the definition above the call or add a prototype (§3.3). Check spelling and capitalization too — Compute_Margin is a different name.


Bug 2 — a path through the function forgets to return.

warning: control reaches end of non-void function [-Wreturn-type]

Some route through your function hits the closing brace without a return — classically if (n > 0) { return 1; } with nothing for the else case. What you get back is garbage. Make every path end in return <value>;. Guard-clause style (§3.7) makes this easy to eyeball, because the returns line up down the left margin.


Bug 3 — wrong number of arguments.

error: too few arguments to function 'double bayes_update(double, double, double)'

or error: too many arguments to function .... Exactly what it says, and it prints the signature it expected. This bites hardest with array functions, where dropping the size argument is the single most common Week 3 typo: sum_array(scores) instead of sum_array(scores, SIZE).


Bug 4 — using a local variable outside its function or block.

error: 'range' was not declared in this scope

range is local to another function, or to a loop body that has ended (§3.4). If main needs the value, return it. If several functions need it, pass it as a parameter. Do not fix this by making it a global — that trades a compiler error you can see for a logic bug you cannot.


Bug 5 — the function changed its parameter and it didn’t stick.

No error, no warning. Just this:

after try_to_double(n):   n = 5

You passed by value, so the function modified its own copy (§3.5). This is the number-one silent bug of the week and the reason a hand-written swap with no & does nothing at all. If the caller’s variable must change, put & on the parameter. Otherwise return the new value and let the caller assign it — do not reach for & reflexively.


Bug 6 — a parameter you declared but never used.

warning: unused parameter 'size' [-Wunused-parameter]

-Wextra noticed your function takes size and never reads it. In an array function that almost always means you hard-coded a length instead of using the one you were handed — fine on your test array, broken on every other one. Use the parameter, or delete it from the signature.


Bug 7 — comparing an int against a length.

warning: comparison of integer expressions of different signedness: 'int' and
'std::__cxx11::basic_string<char>::size_type' {aka 'long unsigned int'} [-Wsign-compare]

You wrote for (int i = 0; i < s.length(); i++). .length() returns an unsigned type and i is signed. Use size_t i — the version this book uses — or cast at the comparison: i < static_cast<int>(s.length()). Pick one and be consistent within a file.


Bug 8 — indexing past the end of an array.

Usually no compiler message at all, and at runtime one of:

Segmentation fault

or an implausible number where your data should be, or — worst — nothing wrong today and corrupted data tomorrow. You read or wrote arr[size] when the valid indices are 0 through size - 1; this is undefined behavior and C++ does no bounds checking (§3.12). Use i < size, never i <= size. Guard computed indices with if (i >= 0 && i < size). When a listing is missing its first or last element, check bounds before anything else. A literal bad index like scores[5] may draw an -Warray-bounds warning, but only sometimes — never rely on it.


Bug 9 — .find() returned an enormous number.

find("Q"):        18446744073709551615

That is string::npos, the “not found” marker — the largest value a size_t can hold, which is why it prints as twenty digits rather than -1. Always test if (pos != string::npos) before using a find result. Never test != -1, and never store the result in an int.


Bug 10 — substr blew up at runtime.

terminate called after throwing an instance of 'std::out_of_range'
  what():  basic_string::substr: __pos (which is 20) > this->size() (which is 12)

You asked for a substring starting past the end of the string, and the message gives you both numbers: position 20 of a 12-character string. Nine times in ten the offending position is an unchecked .find() result — see Bug 9. Check the position first. The length argument is forgiving (asking for more characters than remain just gives you what is left); the start argument is not.


Bug 11 — getline seems to be skipped.

No error. The program races past your prompt and stores an empty string:

no ignore:   n=5 line=[]

A previous cin >> something left a newline in the buffer, and getline read the zero characters before it (§3.15). Put cin.ignore(); between the >> read and the getline. Every menu loop that reads an int choice and then a text value needs exactly one.


Bug 12 — array syntax at the call site, or writing to a const array.

error: expected primary-expression before ']' token

means you wrote sum_array(scores[], SIZE). The brackets belong in the parameter list of the definition, never in the call — pass the name alone.

error: assignment of read-only location

means you assigned into a parameter declared const int arr[], so the compiler is enforcing your own promise (§3.14). Decide which you meant: drop the const if the function should modify the array, or find and remove the assignment if it should not.


Bug 13 — the item lands in the wrong slot.

No error. Your first entry appears at position 2 with position 1 blank, or your last entry silently overwrites the previous one. You incremented count before assigning instead of after (§3.13). The order is arr[count] = value; then count++; — assign, then grow. Print count after every add while debugging; one cout finds this in seconds.


Bug 14 — parallel arrays drifted apart.

No error, no warning, no crash. Your report simply attributes the wrong century to the wrong manuscript, forever. Some operation — add, remove, or a sort swap — updated one array and not the others (§3.18). Audit every function that writes to any of the arrays and confirm it writes to all of them, then print the full table after each operation while testing. This is the bug Chapter 4’s structs delete from existence.


3.21 — Reps

Three teasers; the full set of sixteen is in the exercises. Type them by hand. Do not paste, and do not ask an AI — the point is the movement, not the answer.

Rep 1. Write int square(int x) that returns x * x. Call it from main on five different values, printing each result on its own line. Check yourself: call it on 1, 2, 3, 4, 5 and you must get exactly 1, 4, 9, 16, 25, in that order, five lines.

Rep 2. Write bool is_even(int n). Use it in a loop that prints 1 odd, 2 even, … through 10, one per line, with boolalpha on and the boolean itself as a third column. Check yourself: ten lines, numbered 1 through 10; the five lines for 2, 4, 6, 8, 10 say even and true, and the five for 1, 3, 5, 7, 9 say odd and false. If you see 1/0 instead of true/false, you forgot cout << boolalpha;.

Rep 3. Declare int values[6] = {12, 5, 30, 7, 19, 3};. Write int max_of(const int arr[], int size) and int min_of(const int arr[], int size), print both results, then check them by hand — if your function and your eyes disagree, your function is wrong. Check yourself: the max is 30 and the min is 3. Getting 12 back from either function means you seeded your running best with arr[0] and then never actually compared it against the rest.

Full set in the exercises, with exact expected output for every rep so you can grade yourself.


3.22 — Checkpoint: Can You Do This Yet?

Close the book. Open a blank OnlineGDB file. Write each of these from memory — no scrolling back, no autocomplete, no AI. Thirty minutes.

  1. double average_of(const int arr[], int size) returning the mean, with a guard clause returning 0.0 when size is zero. (Bounds, guard clause, static_cast.)
  2. A void function taking string names[], int& count, and a string, appending the string if there is room. (Reference parameter, capacity vs. logical size.)
  3. A function that takes an int by reference and triples it, plus the two lines in main that prove it worked.
  4. A for loop printing every element of a five-element array, numbered 1. through 5. for the human reader.
  5. A case-insensitive bool same_ignore_case(string a, string b), including the to_lower helper it depends on.
  6. A linear search over a string array returning the index or -1 — and the three lines in main that call it and handle both outcomes.
  7. Selection sort on int arr[], both loops and the three-line swap.
  8. Say out loud, without looking: why does try_to_double(n) leave n unchanged, and what one character fixes it?

Pass bar: 6 of 8 written correctly, compiling with zero warnings under -Wall -Wextra.

Items 1, 2, and 6 are not optional — they are the literal skeleton of Project 3. If you missed any of those three, you did not fail the checkpoint, you found the thing to drill: item 1 → §3.7 and §3.14; item 2 → §3.5 and §3.13; item 6 → §3.16. Then redo Reps 8–12.

If you score below 6, do not start the project yet. Not as punishment — as arithmetic. A student who starts P3 without these fluent spends six hours fighting the language. A student who drills ninety more minutes first spends three hours writing the program. The drilling is the shortcut.


3.23 — When You’re Stuck (and Nobody’s in the Room)

It is 11:40 p.m., your sort is producing garbage, and there is no hand to raise. Good — that is the normal condition of every working programmer, and this ladder is what they actually do. Work it in order; do not skip to the bottom.

Rung 1 — Read the actual error, not the first one you see. Scroll to the top of the compiler output. One missing brace produces a cascade of twenty errors and only the first is real; the rest are the compiler flailing. Fix the first, recompile, repeat. Take the line number literally, but check the line above it too — a missing ; is reported on the line after the one that lacks it.

Rung 2 — Match the message to §3.20. Fourteen entries up there cover the great majority of what Week 3 throws at you. Search this page for a distinctive fragment: "not declared in this scope", "too few arguments", "different signedness", "out_of_range".

Rung 3 — Shrink it to a minimal reproduction. The highest-value debugging skill in the course, and this is the perfect week to learn it. Start a new file. Copy in the single misbehaving function and nothing else. Write a five-line main that calls it once with hard-coded values you can verify by hand — for a sort, {3, 1, 2}; for a search, a three-element array. Run it. Either the bug reproduces, and you are now debugging fifteen lines instead of two hundred; or it does not, and you know the function is fine and the bug is in how you call it. Both are wins.

Rung 4 — Print the state; don’t stare at the code. Nothing this week is too complicated to debug with cout:

cout << "i=" << i << " count=" << count << " arr[i]=" << arr[i] << endl;

Off-by-one bugs confess instantly — you will see i reach a value you did not expect, or count grow when nothing was added. For parallel arrays, print all three columns on one line every time you change any of them; drift shows up in a glance. Delete the prints when you are done.

Rung 5 — Trace it on paper, then rubber-duck it out loud. Take three elements and walk the loop by hand, writing every variable’s value after each iteration in a little table. Four minutes, and faster than an hour of squinting. Then explain the function to something that cannot help you — a pet, a plant, an empty chair — one line at a time, in full sentences. The bug surfaces in the sentence you cannot finish. Not a joke; standard professional practice.

Rung 6 — Re-read the section, then re-run the book’s code. Every concept here has a working program in code/. If your sort is wrong, run selection_sort.cpp and diff its trace against yours pass by pass. If your search is wrong, run linear_search.cpp. These files compile clean and their outputs are printed in this chapter, so any difference is a difference in your code, not your environment.

Rung 7 — Post to the discussion board, correctly. A vague post gets a vague answer in three days. Post this shape and you usually get something useful the same evening — and often you solve it while writing, which is Rung 5 in disguise:

Week 3 — selection sort leaves the last two elements swapped What I’m trying to do: sort centuries[] ascending with selection sort (§3.17). Minimal code: (the function alone plus a five-line main with a hard-coded array — Rung 3) Expected: [ 2 3 4 4 5 ] Actually got: [ 2 3 4 5 4 ] Already tried: printed the array after each pass; first two passes correct, third diverges. Checked that my swap uses a temp.

Rung 8 — Email the instructor. No penalty, no apology needed, reply within one business day. Use this shape so the answer can be specific:

Subject: Accelerated Coding 1 — Week 3 — [one-line symptom]

Section I’m on: §3.__ What I’m trying to do: [one sentence] Expected output: [paste] Actual output or full error text: [paste the whole thing, from the first line] What I’ve already tried: [Rungs 1–6, specifically] My code: [OnlineGDB share link, not a screenshot]

Send the link, never a photo of your screen — a link can be run, a photo cannot. And send it at Rung 8, not Rung 1: rungs 1 through 6 solve four out of five Week 3 problems in less time than writing the email takes.

One last thing that belongs here more than anywhere else. Being stuck for forty minutes is normal. Being stuck for four hours is not, and it means you skipped a rung. When you notice you are circling, stop, stand up, and go to Rung 3 — the minimal reproduction is almost always the rung people skip and almost always the one that breaks the deadlock.


3.24 — This Week’s Project

You are ready for Project 3 — Reasoning Toolkit & Manuscript Database, in Project 3. Due at the end of Week 3, the third of six projects, part of the 50% of your grade that projects carry. Submit an OnlineGDB link; the workflow is in Appendix A.

It is one program in two halves, because this was one week in two halves.

The Reasoning Toolkit is functions with no collections: a menu-driven set of named probability and logic operations — Bayesian update, complement, conjunction, a modus ponens check, a probability validator — each its own function, with main reduced to a menu loop and a dispatcher. The criterion that matters most is the one from §3.8: no business logic in main. If a calculation happens between the cin and the cout, it is in the wrong place.

The Manuscript Database is collections that would be miserable without functions: a fixed-size array plus a count, seeded with real manuscripts, supporting add, remove-by-shift, list, and case-insensitive search — with parallel arrays for centuries and types at the higher tiers, and a selection sort that keeps them in lockstep. Every operation is a function taking the array and its size.

Everything the Normal tier asks for is in this chapter and in code/. array_count.cpp has add and remove-by-shift. linear_search.cpp has both search flavors. manuscript_report.cpp has the lockstep sort. mini_toolkit.cpp has the toolkit’s function shapes. You are not being asked to invent anything — you are being asked to assemble things you have already run.

The apologetics frame ties the halves together. The formal Christian intellectual tradition is itself a toolkit of named, reusable arguments, and the manuscript record is a dataset that rewards being looked at carefully. Your program models both moves. But a warning the source course makes and this one repeats: the toolkit does not settle anything. A Bayesian update fed sloppy priors produces confident garbage, and a database of five manuscripts proves nothing about a tradition of thousands. What writing this code buys you is the felt sense of how much every conclusion depends on its inputs. Humility is part of the lesson, and it is the part that transfers outside of programming.


3.25 — Coach’s Final Word for Week 3

This was the hinge week.

Functions are the first real abstraction in this course, and everything after is built out of them. A struct with functions attached is a class. A function inside a class is a method. Object-oriented programming — all of Weeks 5 through 8 — is largely the question of which functions belong with which data. If functions are still fuzzy on Sunday night, Week 5 will not be hard, it will be impossible.

Collections are the first data structure, and the same holds. An array of structs is Week 4. A dynamically allocated array is Week 6. A linked list is an array’s replacement, and you will only appreciate why after you have felt an array’s fixed size press against you. Every container you meet for the rest of your career is a row of slots with something clever wrapped around it.

So here is the honest measure of the week, and it is not your project grade: can you sit at a blank file and write a function that walks an array, from memory, without hesitating? Return type, parameters, the loop with i < size, the work, the return. If yes, you have what this week was for, and the rest of the course is downhill from a skills standpoint even as the ideas get bigger. If not yet, the reps in the exercises are the fastest path there — not rereading, not watching anything, not asking a chatbot. Reps.

You are three weeks in. Next week you get structs, which make the parallel-array pain of §3.18 disappear in a single move, and then the midterm — cumulative, and closer to being within reach than you think.

Go do the reps.


Up next: Read the exercises and run every rep — all sixteen, checking your output against the expected output. Then take the Checkpoint in §3.22 honestly. Then open Project 3 and build the Reasoning Toolkit & Manuscript Database. After that, Chapter 4 — structs, and the midterm.

Check Your Reps

Week 3 Knowledge Check

Question 1 of 6
What does this print?
void tryDouble(int x)    { x = x * 2; }
void reallyDouble(int& x) { x = x * 2; }

int n = 5;
tryDouble(n);    cout << n << " ";
reallyDouble(n); cout << n;
Why: tryDouble receives a copy, so the caller's n is untouched. reallyDouble takes `int&` — a reference to the caller's own variable — so the change sticks. That single `&` is the whole difference.
Question 2 of 6
What does this print?
int total(int a[], int n) {
    int s = 0;
    for (int i = 0; i < n; i++) s += a[i];
    return s;
}
int a[3] = {1, 2, 3};
cout << total(a, 3);
Why: 1+2+3 = 6. Note the function needs `n` passed in: the array decays to a pointer at the call, so its size is not available inside the function.
Question 3 of 6
Which loop correctly visits every element of `int a[N];` exactly once?
Why: Valid indices are 0 through N-1. Using `<=` walks one past the end into memory you do not own; starting at 1 silently skips the first element. This one pattern prevents more bugs than any other habit in the course.
Question 4 of 6
What happens when you read `nums[5]` from an array declared `int nums[5];`?
Why: C++ does no bounds checking at all. The dangerous part is that it often appears to work during testing and fails later. Java, by contrast, throws ArrayIndexOutOfBoundsException — a difference you will meet in Week 7.
Question 5 of 6
What does `s.substr(2, 4)` return?
Why: The second argument is a length, not an ending index. Reading it as a range is a common off-by-something bug in string work.
Question 6 of 6
A function declared `int findIndex(...)` has one path that reaches the closing brace without returning. What happens?
Why: You get whatever garbage was lying around, and only a warning. This bug hides until the one input that takes the unreturned path — which is why the warning flags are not optional here.
YOU FINISHED. NICE WORK.