Module 1 — Worked Examples¶
Every example here exists as a real, compiling program in examples/module-01/.
Each one opens with a prediction prompt: write your answer down before you run
anything. The gap between your prediction and the output is the only thing in this
page that teaches you something.
Example 1 — a program that works and is already fragile [L2 Apply]¶
The problem. A catalogue of four items with three parallel arrays and four free functions. Nothing is broken, and everything is expensive to change.
Before you read on, predict: how many places would you have to change to add a
publicationYear field? Write the number down, and a list.
// examples/module-01/01-procedural-baseline/main.cpp (abridged)
const int kItemCount = 4;
std::string g_titles[kItemCount] = {"C++ Primer", "Effective Modern C++",
"Clean Code", "UML Distilled"};
int g_ids[kItemCount] = {1001, 1002, 1003, 1004};
bool g_onLoan[kItemCount] = {false, true, false, true};
bool lend(int id) {
int index = 0;
if (!findById(id, index)) {
return false;
}
if (g_onLoan[index]) {
return false; // already out — but the caller cannot tell why
}
g_onLoan[index] = true;
return true;
}
Output (verified — expected-output.txt in the same folder):
Catalogue (4 items)
#1001 C++ Primer [available]
#1002 Effective Modern C++ [on loan]
#1003 Clean Code [available]
#1004 UML Distilled [on loan]
Available: 2
Lending #1003 ... ok
Lending #1002 ... failed
After lending:
Catalogue (4 items)
#1001 C++ Primer [available]
#1002 Effective Modern C++ [on loan]
#1003 Clean Code [on loan]
#1004 UML Distilled [on loan]
Why it behaves that way. lend(1003) succeeds because item 1003 is available
and updates exactly one array. lend(1002) fails because item 1002 is already out —
and returns the same false it would return for an identifier that does not exist
at all. Two different failures, one indistinguishable answer.
What to take away. The program is correct and uninspectable: the reason for the second failure is nowhere in the output, because the function had only two things it could say.
Try it now. Add a fourth item that is already on loan, then make lend report
which failure occurred. Count the lines you changed and the places you had to
touch. That count is the module's argument.
Example 2 — the same data, with a type [L3 Modify]¶
The problem. Identical behaviour, one class instead of four globals.
Before you read on, predict: does the output change at all? Specifically, will item 1004 still be on loan at the end?
// examples/module-01/03-first-class/main.cpp (abridged)
class Catalogue {
public:
bool lendItem(int id) {
const int index = indexOf(id);
if (index < 0 || onLoan_[index]) {
return false;
}
onLoan_[index] = true;
return true;
}
private:
int count_ = 0;
int ids_[kMaxItems] = {0};
std::string titles_[kMaxItems];
bool onLoan_[kMaxItems] = {false};
int indexOf(int id) const { /* ... */ }
};
Output (abridged — run it for the full report):
Catalogue (4 items)
#1001 C++ Primer [available]
#1002 Effective Modern C++ [available]
#1003 Clean Code [available]
#1004 UML Distilled [available]
Available: 4
Lending #1003 ... ok
Lending #1002 ... ok
Why the output is different. The test program creates its own catalogue through
addItem, so nothing starts on loan. The lesson is not the difference in data — it
is that the first program's initial state came from an array literal that four
functions had to be trusted to respect, and the second one cannot be constructed in
an inconsistent state at all, because addItem writes every field in one place.
What to take away. Same features, same output shape, and one fewer thing to get
wrong. Note what has not changed: ids_, titles_ and onLoan_ are still three
parallel arrays, so the type is only hiding the alignment problem — not solving it.
Module 2's std::vector<Book> solves it properly, and Module 4 makes it
unbreakable.
Example 3 — a compile error, a link error and a runtime error [L1 Understand]¶
The problem. Three tiny programs, each broken one way, so you learn to tell the messages apart.
Before you read on, predict: which of these fails at compile time, which at link time, and which not at all until it runs?
// (a)
int main() {
int count = 5
return 0;
}
// (b)
#include <iostream>
double area(double width, double height); // declared
int main() { std::cout << area(3.0, 4.0); } // used, never defined
// (c)
#include <iostream>
int main() {
int* pointer = nullptr;
std::cout << *pointer << '\n'; // read through a null pointer
}
Output. (a) and (b) verbatim from the compiler; (c) is what this machine prints — your exact message will differ, and that variation is the lesson.
(a) error: expected ';' before 'return'
(b) /tmp/cc7x2LqF.o: in function `main':
main.cpp:(.text+0x1e): undefined reference to `area(double, double)'
collect2: error: ld returned 1 exit status
(c) Segmentation fault (core dumped)
Why each fails where it does. (a) is a syntax error, caught while the compiler is reading your file — stage 2. (b) is a promise the program made and never kept, so it survives compilation and dies at linking — stage 3. (c) is undefined behaviour: the language makes no promise about it at all, so it may crash, may print nothing, and may appear to work. It is the only one of the three where a successful run proves nothing.
What to take away. Read the message and ask which stage am I in. The fix for a failure in one stage never helps with the next.
Example 4 — values, references and pointers [L2 Apply]¶
The problem. Three functions with one job each: double the caller's counter.
Before you read on, predict: for each of the three calls, is the caller's variable changed afterwards? Write "yes" or "no" for each before reading further.
// examples/module-01/02-values-references-pointers/main.cpp (abridged)
void addOneByValue(int count) { count = count + 1; }
void addOneByReference(int& count) { count = count + 1; }
void addOneByPointer(int* count) { *count = *count + 1; }
int main() {
int a = 5; addOneByValue(a);
int b = 5; addOneByReference(b);
int c = 5; addOneByPointer(&c);
// each variable is then printed
}
Output (verified — expected-output.txt in the same folder):
a starts at 5
calling addOneByValue(a):
inside addOneByValue: 6
after the call, a is 5 <-- unchanged
b starts at 5
calling addOneByReference(b):
inside addOneByReference: 6
after the call, b is 6 <-- changed
c starts at 5
calling addOneByPointer(&c):
inside addOneByPointer: 6
after the call, c is 6 <-- changed
Why it behaves that way. addOneByValue received a copy; it doubled the
copy and destroyed it. addOneByReference received another name for b.
addOneByPointer received c's address and wrote through it. Two different
mechanisms, one effect.
What to take away. A parameter's type tells you whether the function can change
your variable: no & and no * means it cannot, whatever it does inside. You will
use this reading skill in every module from here — and in Module 9, when a reference
member outlives the thing it refers to, the same three ideas turn into a bug class.
Example 5 — the four pillars in eighty lines [L4 Analyze]¶
The problem. Read a program that uses abstraction, encapsulation, inheritance and polymorphism, and identify which construct serves which pillar.
Before you read on, predict: in the loop below, which describe() runs for the
Book object — LendableItem::describe or Book::describe? Write your answer and
your reason.
// examples/module-01/04-pillars-preview/main.cpp (abridged)
class LendableItem {
public:
virtual ~LendableItem() = default;
virtual std::string describe() const {
return "#" + std::to_string(id_) + " " + title_;
}
private:
int id_;
std::string title_;
};
class Book : public LendableItem {
public:
std::string describe() const override {
return LendableItem::describe() + " (book, " + std::to_string(pages_) + " pages)";
}
};
// ...
std::vector<const LendableItem*> items{&book, &dvd};
for (const LendableItem* item : items) {
std::cout << " " << item->describe() << '\n';
}
Output (abridged):
1. abstraction - a caller asks whether it is too hot, and never sees a double
reading 27.5 C, too hot? yes
2. encapsulation - the rule is in the type, so it cannot be skipped
on loan: 2
returning: ok, then ok, then failed
4. polymorphism - one loop, every kind
#1001 C++ Primer (book, 976 pages)
#2001 Design Patterns Explained (DVD, 95 min)
Why it behaves that way. item has static type const LendableItem* but its
real object is a Book, and the function is virtual, so the derived version
runs. Remove virtual and the output silently becomes two identical base-class
lines — which is exactly the failure that examples/module-02/ and Module 7 are
built around.
What to take away. Four ideas, four jobs: abstraction hides representation, encapsulation protects a rule, inheritance states what is common, polymorphism lets one loop handle every kind. Naming them is not the skill — knowing which one a design needs, and where it does not, is.
Stretch — a question with no single answer [L4 Analyze]¶
Look at LoanCounter in Example 5's program. returnOne() reports failure and
changes nothing, while borrowOne() cannot fail.
Is that the right shape? Consider: what should happen if a member tries to return an item they never borrowed? What should the caller be able to do with the failure? Write two sentences. Module 4 will give you vocabulary for this — for now, notice that you already have an opinion, which is the point.
Next: Misconceptions and mistakes — the wrong mental models this module exists to correct, and the errors you will actually make.