Module 2 — Worked Examples¶
Five programs, in the order the module needs them. Each one is real: it builds and
runs with the course's command, and its captured output sits beside it in
examples/module-02/.
The rule for this page. Before each program's output, predict it in writing. Not "roughly" — the actual lines. Then run it and account for every difference. The prediction is the exercise; the output is only the marking scheme. This habit is what turns reading code into being able to write it.
Example 1 — the first class: a type that owns its behaviour¶
examples/module-02/01-book-class/main.cpp
The data is deliberately the same kind of thing as Module 1's parallel arrays: an identifier, a title, a page count, and a question — is this book a long read? What changed is where the rules live.
class Book {
public:
Book(int id, const std::string& title, int pages)
: id_(id), title_(title), pages_(pages) {
}
void describe() const { ... }
bool hasId(int id) const { return id_ == id; }
bool isLongRead() const { return pages_ > 500; }
void correctPagesTo(int pages) { if (pages > 0) { pages_ = pages; } }
private:
int id_;
std::string title_;
int pages_;
};
Four decisions worth naming, because you will make all four again in every class you write from now on:
hasIdexists instead ofint id() const. The caller wanted to know "is this the item with that identifier?", not what the identifier is. Answering the actual question means no caller ever compares identifiers itself.isLongReadis a method, not a storedbool. The rule "> 500 pages" lives in one place. If the library decides 400 is long, exactly one line changes.correctPagesTochecks its argument. It is the class's only way to change a page count, and it refuses nonsense. Compare with Module 4, where this becomes the central subject: a setter that accepts anything is barely better than a public field.describe()isconst;correctPagesTo()is not. The compiler now enforces the difference between asking and changing.
Now predict. main creates two books, corrects the second, and asks a question about
each:
const Book primer(1001, "C++ Primer", 976);
Book clean(1003, "Clean Code", 464);
clean.correctPagesTo(500);
Predict: after correctPagesTo(500), what does clean.isLongRead() return, and
does primer change at all? Write both answers down.
Output (run it first)
Two objects of the same type:
#1001 C++ Primer (976 pages)
#1003 Clean Code (464 pages)
Two objects, two independent sets of data:
after correcting the second book:
#1001 C++ Primer (976 pages)
#1003 Clean Code (500 pages)
Asking an object a question about itself:
does either book have id 1001? yes
is the corrected book a long read? no longer
The two things to carry forward:
primeris untouched. Two objects of one class hold two independent sets of data — this is ordinary variable semantics, and it is the answer to "where do the values go?".clean.pages_is now exactly 500, andisLongRead()isfalse, because the rule is> 500. If you predicted "yes", you learned something more useful than if you predicted correctly: the boundary was checked in one place, and you were reasoning about it in another. That gap is exactly what the method exists to close.
Now break it deliberately. Add this line to main:
Keep the error message. It is the compiler stating that main is not allowed to
depend on how a book stores its identifier — which is precisely what makes it safe to
rename id_ tomorrow.
Example 2 — const member functions: who may call what¶
examples/module-02/02-const-members/main.cpp
A Loan that can be renewed, and a free function that may only look at one:
void printSummary(const Loan& loan) { // no copy, no permission to modify
std::cout << " " << loan.summary() << '\n';
}
Predict, before reading on: three calls follow — a fresh loan, the same loan after
renew(7), and a const Loan. Which of them can printSummary accept? What does the
program print for the third?
Output (run it first)
Three lessons, in order of how often they matter:
1. const is contagious in exactly the right way. printSummary takes
const Loan&, so inside it only const member functions may be called. That is why
summary() is marked const. Remove the const from summary() and the caller
stops compiling:
The error is in printSummary, but the fix belongs in summary(). This is the single
most confusing error in the module, and §7 of
Concepts
walks through it. Read the message as: "you asked me to call a possibly-modifying
function on an object I was told not to modify."
2. A const object is still useful. fixed can be read for the rest of its life.
It just cannot be renewed. Marking an object const is a design statement — "this
loan's terms are settled" — and the compiler enforces it.
3. A temporary can be passed to a function that only reads.
printSummary(Loan(3)) is fine. The temporary lives until the end of the full
expression that created it, which is longer than the call. Module 3 makes that rule
precise.
Why you should care beyond this module. Everything that takes your type by
const& is relying on this. std::cout << will want your type's output function to be
const (Module 5). Standard containers require it (Module 14). A class that is not
const-correct is a class that cannot be used in the places that matter.
Example 3 — this: two situations, and only two¶
examples/module-02/03-this-pointer/main.cpp
Book& withPages(int pages) {
pages_ = pages;
return *this; // the object itself, not a pointer to it
}
Predict: this program calls
How many objects exist afterwards? What happens to other, the second book?
Output (run it first)
One object. Not three, not a copy per call. The chain works because each function
returns Book& — a reference to the same object — so each successive call operates on
exactly the object the first one did.
The &book != &other line prints 1 (true). It is there to show that comparing
addresses is not the same as comparing books, and to leave you mildly annoyed that
book == other does not compile at all. What should == mean for two books, and who
should write it? That is Module 5, and finding it annoying now is the point.
The name-hiding situation is in the same file, commented:
void setTitle(const std::string& title) {
title = title; // ✗ assigns the parameter to itself — compiles, does nothing
}
void setTitle(const std::string& title) {
this->title = title; // ✓ now it means the member
}
The course's own style avoids the collision entirely by naming members with a trailing
underscore. Both conventions are common in real code; recognise both, and if you use
this-> everywhere, know that you are following a convention rather than solving a
problem.
Example 4 — two objects, one conversation¶
examples/module-02/04-objects-interacting/main.cpp
A Course with room for three students, and four Students who want in. The design
question is the section's whole content: which object owns the capacity rule?
bool Student::enrolIn(Course& course) {
if (courseCount_ >= kMaxCourses) {
return false; // the Student's rule
}
if (!course.reserveSeat(name_)) {
return false; // the Course's rule
}
courses_[courseCount_] = course.title();
++courseCount_;
return true;
}
Predict: Dave is the fourth student to try C++ Programming, which holds three.
What does the program print for Dave, what does dave.describe() say, and what
happens to Physics I at the end?
Output (run it first)
Two courses, four students, no enrolments yet:
C++ Programming: 0/3 (empty)
Calculus I: 0/3 (empty)
Alice enrols in both courses:
C++ Programming: ok
Calculus I: ok
Alice is enrolled in 2 course(s): C++ Programming Calculus I
Three more students try the same course:
Bob: ok
Carol: ok
Dave: refused <- the Course refused; Dave never saw the number 3
C++ Programming: 3/3 Alice Bob Carol
Dave is enrolled in 0 course(s): none
The Student has a rule of its own, enforced somewhere else:
Bob is enrolled in 2 course(s): C++ Programming Calculus I
Alice's third course: ok
Alice's fourth course: refused <- refused by Alice's limit of 3, not by Physics I, which is empty
Alice is enrolled in 3 course(s): C++ Programming Calculus I Computer Networks
A by-value parameter is a separate object:
the copy arrives with 0 enrolled
the copy leaves with 3 enrolled
the original is untouched:
Physics I: 0/3 (empty)
(that copy filled up and was then destroyed on return;
Physics I itself never held a single student)
Three things this output proves, and one it warns about.
Proves: the refusal is total. Dave's Student is unchanged (0 courses) and the
Course never recorded him. Two objects, no possibility of disagreeing, because the
Student's record is written only after the Course says yes.
Proves: two rules, two owners. Alice's fourth enrolment is refused by her limit
of three courses — while Physics I is completely empty and willing. Neither class
knows the other's number.
Proves: a by-value parameter is a separate object. fillWithoutAsking(Course copy)
takes a copy, fills all three of its seats, and returns. The copy is destroyed, and
Physics I still reports 0/3. The function did real work on an object nobody can
observe afterwards.
Warns: that function is a plain bug in almost any real program. It compiles cleanly, does something plausible, and has no effect on the caller. How much of the program was copied? When exactly was the copy destroyed? Module 3 answers both, precisely, and Module 10 shows how to prevent it.
Example 5 — one type, two files, one command¶
examples/module-02/header-source/{Book.h,Book.cpp,main.cpp}
Three files. Book.h declares what a Book is; Book.cpp defines what its functions
do; main.cpp uses it and has never seen Book.cpp.
Predict — and then actually try it: delete Book.cpp from that command. What
happens?
Answer (try it first)
It still **compiles** — and then fails to **link**: `main.cpp` compiled perfectly, because it saw a declaration: a promise that `Book::describe` exists somewhere. The linker then went looking and found nobody who had defined it. This is the most important distinction in the module's mechanics, and why Module 1 spent a whole section on it. The fix is to add `Book.cpp` back to the command. In Module 16 you will stop typing file lists by hand and let a build tool do it — but you cannot debug a build system you do not understand, which is why you are typing it now.What to look at in these three files. Book.cpp includes Book.h, and its
functions are written Book::describe() — the scope qualifier that says which class a
function belongs to and brings Book's members into scope inside the body. Book.h
includes only <string>, because that is all its declarations mention, even though
describe() prints; the <iostream> include lives in Book.cpp, where printing
happens. That is the rule from §8 of Concepts applied.
Build all three lines of a header/source pair by hand once
It is the last time in the course you will need to. From Lab 2 onwards your programs have more than one file, and knowing which file was missing from the command — rather than guessing — is what the next page (Debugging) teaches.
Example 6 — designing a type from nothing¶
examples/module-02/05-bank-account/main.cpp
The first five examples read. This one is designed, using the order from §3 of Concepts, and it is the shape Lab 2 asks for.
Step 1 — the invariant, in one sentence.
The balance is never negative, and every operation that changes it states its own precondition; a refused operation changes nothing.
Step 2 — the operations a bank program actually needs.
| Operation | Why the caller needs it |
|---|---|
| open an account with a balance | you cannot create one any other way |
deposit(paisa) |
money in |
withdraw(paisa) |
money out |
balanceText() |
display it |
holder() |
identify it |
Ask the question that matters: is there a setBalance in that table? No — and that
is a design decision, not an omission. A caller who could set the balance could break
the invariant in one line.
Step 3 — the representation those operations need.
Two members, and one of them carries a decision worth noticing: money is stored as an
integer number of paisa, not as a double. A double cannot represent most decimal
fractions exactly, so it accumulates small errors — acceptable in a physics
simulation, not in a bank balance.
Predict: the program opens an account with Rs 2500.00, deposits Rs 1250.50, then tries to withdraw Rs 4000.00. What is the balance afterwards, and what does the withdrawal return?
Output (run it first)
Account opened:
Ayesha: Rs 2500.00
Deposits:
Rs 1250.50 -> ok, balance Rs 3750.50
Rs 0.00 -> refused, balance Rs 3750.50 <- not a deposit
Rs -50.00 -> refused, balance Rs 3750.50 <- nor is this
Withdrawals:
Rs 4000.00 -> refused, balance Rs 3750.50 <- the balance would have gone negative
Rs 3750.50 -> ok, balance Rs 0.00
The invariant still holds, and the object is still usable:
Ayesha: Rs 1.00
A second account is a second object:
Ayesha: Rs 1.00
Bilal: Rs 5.00
The refused withdrawal changed nothing — that is what "a refused operation changes nothing" means in practice, and it is the property every well-designed type has and almost no badly-designed one does.
Now the uncomfortable question, which is Module 4's whole subject. This class has no setter and still the invariant is only mostly safe. Find the gap:
BankAccount(std::string holder, long long openingPaisa)
: holder_(std::move(holder)), balancePaisa_(openingPaisa) {
if (openingPaisa < 0) {
balancePaisa_ = 0; // silently "fixes" a negative opening balance
}
}
The constructor pretends a negative opening balance was zero. A caller who passed
-50000 has been told nothing, and the object that exists is not the object they asked
for. The honest answers are refuse to construct (Module 13, exceptions) or make such
a type impossible (Module 4, invariants). Nobody who has ever debugged a silent
correction will prefer it to an error.
What to do with this page¶
- Type out each example yourself — do not copy files. Typing is where the syntax sticks, and it is how you will meet the small errors.
- Predict the output of each one in writing before running it.
- Break each program once, in a way this page suggests, and keep the error message.
- Then take the three steps of Example 6 and design a
Vehicleor anEmployeeRecord— invariant, operations, representation — on paper, before opening a text editor. Lab 2 starts exactly there.
Next: Misconceptions and mistakes — the six wrong mental models, and the errors everyone makes. · All modules