From Struct to Class: Objects and Encapsulation
What does Christianity say about money — and about the life of the mind?
Chapter 5 — From Struct to Class: Objects and Encapsulation
“Everyone to whom much was given, of him much will be required.” — Luke 12:48
“A class is a struct with rules.” — every C++ teacher who has ever gotten tired of explaining classes
This week merges Coding 1 chapters 9 and 10. If you are coming from the sixteen-week book, everything in “From Struct to Class” and everything in “Constructors, Destructors, and Encapsulation” is here — re-sequenced into one arc, because they are one idea told in two halves.
Your Week at a Glance
Twelve honest hours, split into four working sessions of about three. Do not try to do this in one sitting; classes are the concept students most often “understand” on Tuesday and cannot write on Thursday. Spacing is doing real work for you here.
| Session | ~Time | What you do | Checkpoint before you stop |
|---|---|---|---|
| 1 — Objects exist | 3 hrs | Read §5.1–§5.8. Type and run struct_vs_class.cpp, class_basics.cpp, two_objects.cpp, this_keyword.cpp, outside_definitions.cpp. Do Reps 1–6 in the exercises. | Without looking, write a class with one private field and one public method that refuses bad input. Then explain, out loud, why a.balance = 9999; will not compile. |
| 2 — Objects build and clean up after themselves | 3 hrs | Read §5.9–§5.13. Type and run constructor_demo.cpp, three_constructors.cpp, lifecycle_trace.cpp. Do Reps 7–12. | Predict lifecycle_trace.cpp’s output on paper first, then run it. If your prediction and the program disagree, you do not yet own destructor ordering — reread §5.13. |
| 3 — Objects own objects | 3 hrs | Read §5.14–§5.18, then skim §5.19. Type and run book_member.cpp and library_container.cpp. Finish the remaining reps. Then take the self-test in §5.21. | Score at least 6 of 7 on the §5.21 checkpoint. Below that, do not open the project — go back and re-drill. |
| 4 — Build P4 | 3 hrs | Build the Stewardship Account half, then the Apologetics Library half, of P4 (Project 4). Add tiers if time allows. Submit. | Your program compiles with zero warnings under -Wall -Wextra, and every rule your program enforces lives inside a method — none of it in main. |
If you fall behind, cut the Medium/Hard tiers of P4 before you cut the reps. The reps are what make Chapter 6 survivable.
Why This Matters
In Chapter 4 you learned to bundle related data into a struct. That was a genuine win: parallel arrays became one array of structs, and the compiler started catching a whole family of bugs that used to be yours to catch.
But a struct is passive. It holds data and nothing else. Any code, anywhere in your program, can reach into it and write anything it likes — including nonsense:
struct LooseAccount {
string owner;
double balance;
};
LooseAccount a;
a.owner = "Maya";
a.balance = 100.0;
a.balance = -999999.0; // legal. Maya now owes a million dollars for no reason.
There is a rule here — a balance should never go negative — but the struct does not know about it. The rule lives scattered through your code, in every function that touches a balance, and you have to remember to enforce it every single time. Forget once and the data is garbage. And nothing in the program will tell you.
A class is the fix. A class is a struct with the rules attached. It hides some of its data (private) so that outside code cannot reach in. It exposes a small set of methods — functions that belong to the class — that are the only legitimate way to change the data. The rule about negative balances lives in exactly one place, and the compiler makes it impossible for any other code to route around it.
For the theme: stewardship. Christianity has long held that what you have is not fundamentally yours. It is entrusted, and the entrustment carries rules. A “bank account” that lets anyone set the balance to whatever they want is not an account at all — it is a sticky note. Your project this week is a stewardship account and an apologetics library, and in both of them the class structure is not decoration. The class is the rules.
5.1 — The Struct That Cannot Say No
Start by seeing the problem with your own eyes. Type this in and run it — this is code/struct_vs_class.cpp:
#include <iostream>
#include <string>
using namespace std;
// A struct: pure data, no rules. Anyone can write anything into it.
struct LooseAccount {
string owner;
double balance;
};
// A class: same data, but the data is private and the rules are methods.
class TightAccount {
private:
string owner;
double balance;
public:
void init(string n, double starting) {
owner = n;
balance = (starting >= 0) ? starting : 0;
}
void withdraw(double amount) {
if (amount <= 0) {
cout << "Rejected: withdrawal must be positive." << endl;
return;
}
if (amount > balance) {
cout << "Rejected: insufficient funds." << endl;
return;
}
balance -= amount;
}
void print_statement() {
cout << "[" << owner << "] balance: $" << balance << endl;
}
};
int main() {
LooseAccount loose;
loose.owner = "Maya";
loose.balance = 100.0;
loose.balance = -999999.0; // Legal. Nothing in the program stopped it.
cout << "[" << loose.owner << "] balance: $" << loose.balance << endl;
TightAccount tight;
tight.init("Maya", 100.0);
tight.withdraw(999999.0); // The class says no.
// tight.balance = -999999.0; // ❌ Will not compile: balance is private.
tight.print_statement();
return 0;
}
Actual output:
[Maya] balance: $-999999
Rejected: insufficient funds.
[Maya] balance: $100
Two data models, identical fields, opposite behavior. The struct did what it was told. The class did what it was for.
Now uncomment the line marked ❌ and compile again. The compiler refuses. That refusal is the entire point of this chapter — not a limitation you work around, but a guarantee you bought.
5.2 — The class Keyword: public and private
A minimal class looks a lot like a struct with one new keyword:
class Account {
public:
double balance;
string owner;
};
Two things changed from the struct version: struct became class, and a public: label appeared. Without that label, the fields would be private by default — that is the rule for class. (For struct, the default is public.) So far this is a struct wearing a costume. The interesting part starts when you make some fields private:
class Account {
private:
double balance;
string owner;
public:
void deposit(double amount) {
balance += amount;
}
};
Now:
balanceandownerare private. They exist, they take up memory, they are perfectly real — but code outside the class cannot read or write them.depositis a public method: a function that belongs to the class and can be called from outside.
From main:
Account a;
a.deposit(100); // legal — deposit is public
a.balance = 9999; // ❌ ERROR — balance is private
There are two access labels that matter to you this week (a third, protected, arrives with inheritance in Chapter 6):
public:— reachable from anywhere. This is the class’s external interface: the list of things the outside world is allowed to ask for.private:— reachable only from inside the class’s own methods. This is the class’s internal state.
The rule of thumb that will carry you through the rest of this course, and honestly through the rest of your career:
Data is private. Methods are (mostly) public. The methods are the rules over the data.
Why that split? Because the data is the part with rules. If the data is private, no outside code can violate a rule, because no outside code can touch the data at all without going through a method — and the method is where the rule lives.
The formal name for this is encapsulation: hiding internal state behind a controlled interface. It is one of the four classical pillars of object-oriented programming. The other three — inheritance, polymorphism, abstraction — arrive in Chapters 6, 7, and 8. The mantra for all of them starts here: the class is in charge of its own data. Outside code asks the class to do things; outside code never reaches inside.
For the record, the complete language-level difference between struct and class in C++ is this: in a class, members and base classes default to private; in a struct, they default to public. That is all. Everything else — that structs are “just data” and classes “have behavior” — is convention, not law. It is a good convention, this book follows it, and §5.17 says when to pick which.
5.3 — Methods: The Rules, Written Down
A method is a function defined inside a class. Its body can use the class’s fields directly, by name, with no . in sight:
class Account {
private:
double balance;
string owner;
public:
void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
double get_balance() {
return balance;
}
};
Look at deposit. The line if (amount > 0) is the rule: no negative deposits, ever, from anywhere. Outside code that calls account.deposit(-50) gets nothing — the guard clause you learned in Chapter 3 rejected it. There is no path around that guard, because there is no path to balance except through a method.
Methods can call other methods
Inside a method, you can call another method of the same class directly, with no dot:
class Account {
private:
double balance;
public:
void deposit(double amount) { balance += amount; }
void double_deposit(double amount) {
deposit(amount); // calls this object's own deposit
deposit(amount);
}
};
The class implicitly knows which object is calling. double_deposit on checking calls checking’s deposit; on savings it calls savings’s. You never say which — the object you called the method on is carried along automatically. §5.8 shows you the mechanism that makes that work.
5.4 — Accessors and Mutators
Two words you will see in every OOP course and every job posting:
- An accessor (informally, a getter) is a public method whose only job is to hand out a read-only view of a private field.
double get_balance() { return balance; }. - A mutator (informally, a setter) is a public method whose job is to change a private field, ideally with validation.
void deposit(double amount)is a mutator. So is a hypotheticalvoid set_balance(double b).
Accessors are almost always fine. Mutators need thought.
Coach’s Note — You almost never want a generic
set_balance(double). The entire reasonbalanceis private is to stop outside code from setting it to anything it likes. A method nameddeposit, which validatesamount > 0, is good. A method namedset_balance, which lets any caller overwrite the field with any number, is a public field with extra typing — you have done the ceremony of encapsulation and kept none of the benefit. Name your methods for what they do, not for what they touch.deposit,withdraw,give,spend,check_out,return_book,take_damage,heal. Verbs. Rules.
There is one honest exception: a field with genuinely no invariant — a nickname on a Member, say, where any string is legal — can have a plain setter. If there is truly no rule, a setter is not hiding one.
5.5 — Creating Objects, and the Uninitialized-Field Problem
You declare an object exactly the way you declare a variable:
Account a;
That creates an Account object named a. Its private fields exist; you just cannot reach them from outside. But what are they set to? The answer depends on the field’s type, and it is the most important gotcha in this section:
- Built-in scalar types —
int,double,bool,char, raw pointers — are not initialized. They hold whatever bits happened to be sitting in that memory. Reading them before you write them is undefined behavior: the program may print0, may print something like6.95333e-310, may print a plausible-looking number that is simply wrong, and may do something different tomorrow for no reason you can see. - Class-type fields —
string, and any class you write that has a default constructor — are initialized, because their own default constructor runs. Astringbecomes"".
So an Account with a string owner and a double balance, freshly declared, has a perfectly good empty owner and a garbage balance. That is a nasty split: half the object is safe and half is a landmine.
The stopgap fix is to initialize by hand through a method named init. Here is the full worked example — code/class_basics.cpp:
#include <iostream>
#include <string>
using namespace std;
class Account {
private:
double balance;
string owner;
public:
void init(string n, double starting) {
owner = n;
balance = (starting >= 0) ? starting : 0;
}
void deposit(double amount) {
if (amount > 0) {
balance += amount;
} else {
cout << "Rejected: deposit must be positive." << endl;
}
}
void withdraw(double amount) {
if (amount <= 0) {
cout << "Rejected: withdrawal must be positive." << endl;
return;
}
if (amount > balance) {
cout << "Rejected: insufficient funds." << endl;
return;
}
balance -= amount;
}
double get_balance() {
return balance;
}
void print_statement() {
cout << "[" << owner << "] balance: $" << balance << endl;
}
};
int main() {
Account a;
a.init("Maya", 100.0);
a.deposit(50.0);
a.deposit(-25.0); // rejected by the guard clause
a.withdraw(200.0); // rejected: insufficient funds
a.withdraw(80.0);
a.print_statement();
// a.balance = 9999; // ❌ Will not compile: balance is private.
return 0;
}
Trace it on paper before you run it. init sets balance to 100. deposit(50) makes it 150. deposit(-25) is rejected and prints. withdraw(200) is rejected and prints. withdraw(80) leaves 70. Then the statement prints.
Actual output:
Rejected: deposit must be positive.
Rejected: insufficient funds.
[Maya] balance: $70
Every rule about this account lives inside the class. main is incapable of breaking any of them. That is encapsulation doing its job.
It is also, frankly, clunky. You have to remember to call init, and nothing forces you to. Hold that thought — §5.9 is where it gets fixed for good.
5.6 — Many Objects, Independent State
Write the class once; make as many objects as you want. Each one carries its own copy of every field:
Account checking;
Account savings;
checking.init("Maya checking", 500);
savings.init("Maya savings", 2000);
checking.deposit(100); // checking is now 600; savings is untouched
This is the second big win, and it is worth proving to yourself rather than taking on faith. Run code/two_objects.cpp:
#include <iostream>
using namespace std;
class Counter {
private:
int count;
public:
void init() { count = 0; }
void increment() { count++; }
void reset() { count = 0; }
int get() { return count; }
};
int main() {
Counter a;
Counter b;
a.init();
b.init();
a.increment();
a.increment();
a.increment();
b.increment();
b.increment();
cout << "a: " << a.get() << " (expected 3)" << endl;
cout << "b: " << b.get() << " (expected 2)" << endl;
a.reset();
cout << "a after reset: " << a.get() << " (expected 0)" << endl;
cout << "b unchanged: " << b.get() << " (expected 2)" << endl;
return 0;
}
Actual output:
a: 3 (expected 3)
b: 2 (expected 2)
a after reset: 0 (expected 0)
b unchanged: 2 (expected 2)
One class definition. Two completely independent objects, governed by identical rules. This is why OOP scales: you write the rules once and stamp out as many rule-following objects as the problem needs.
The vocabulary, since you will be graded on it: the class is the type; each object is an instance of that type. “Instantiating a class” means making an object from it.
5.7 — Where Method Bodies Live
Everything so far has defined method bodies right inside the class. That is the simplest style and it is fine. There is a second style: declare the method inside the class, define it outside.
Here is code/outside_definitions.cpp, which also fixes a real weakness in the earlier withdraw — it now returns bool, so the caller can find out whether the rule blocked the operation. This listing is abbreviated on purpose: three of the five method bodies are unchanged from §5.5, so they are noted rather than reprinted. That means the excerpt below compiles but will not link — the linker, not the compiler, will say undefined reference to 'Account::deposit(double)' and two more like it. That distinction is worth filing away: “undefined reference” always means the compiler found a declaration and the linker could not find a body. The copy in code/ has all five bodies and builds cleanly.
#include <iostream>
#include <string>
using namespace std;
// ---- This block is the "header" part: the interface. ----
class Account {
private:
double balance;
string owner;
public:
void init(string n, double starting); // declaration only
void deposit(double amount); // declaration only
bool withdraw(double amount); // declaration only
double get_balance(); // declaration only
void print_statement(); // declaration only
};
// ---- This block is the "implementation" part: the bodies. ----
void Account::init(string n, double starting) {
owner = n;
balance = (starting >= 0) ? starting : 0;
}
// Returning bool lets the CALLER find out whether the rule blocked the move.
bool Account::withdraw(double amount) {
if (amount <= 0) {
cout << "Rejected: withdrawal must be positive." << endl;
return false;
}
if (amount > balance) {
cout << "Rejected: insufficient funds." << endl;
return false;
}
balance -= amount;
return true;
}
// deposit, get_balance and print_statement are unchanged from §5.5, just
// written out here with the Account:: prefix. See code/outside_definitions.cpp.
int main() {
Account a;
a.init("Maya", 100.0);
a.deposit(50.0);
if (a.withdraw(500.0)) {
cout << "Withdrawal of $500 succeeded." << endl;
} else {
cout << "Withdrawal of $500 did not happen." << endl;
}
if (a.withdraw(80.0)) {
cout << "Withdrawal of $80 succeeded." << endl;
}
a.print_statement();
cout << "get_balance() reports: " << a.get_balance() << endl;
return 0;
}
Actual output:
Rejected: insufficient funds.
Withdrawal of $500 did not happen.
Withdrawal of $80 succeeded.
[Maya] balance: $70
get_balance() reports: 70
The Account:: prefix is the scope resolution operator. It tells the compiler “the withdraw I am defining here is the Account::withdraw, the one you already saw declared.” Leave it off and you have written a plain free function that knows nothing about balance — see bug 9 in §5.19 for exactly what the compiler says when you do that.
Inside the body you still use balance and owner directly, with no prefix, because the method belongs to the class and the class’s fields are in scope.
The header / implementation split — and why we are not doing it
Look at the two commented blocks in that file. In a professional C++ project, they would be two different files:
account.h <-- the class declaration: what an Account has and offers
account.cpp <-- the method definitions: what each method actually does
main.cpp <-- code that #includes "account.h" and uses Accounts
account.h is the header: the interface, the part you hand to other programmers so they know what they can call. account.cpp is the implementation: the bodies, which nobody else needs to read. Other files write #include "account.h" and get the declarations; the compiler links the bodies in at the end. This is how essentially all real C++ is organized, and it is why C++ codebases have that .h/.cpp pairing you have probably noticed on GitHub.
This book keeps everything in one .cpp file, on purpose. OnlineGDB compiles a single file by default, and juggling three files in a browser tab buys you nothing this week and costs you an hour of setup friction. So you now know what the split is and why it exists — and when you meet it in a real codebase you will recognize it — but every program you write in this course stays single-file. The “declare inside, define outside” style you just used is the exact same separation, done within one file. If you can read outside_definitions.cpp, you can read a header/implementation pair.
When to use which style:
- Body inside the class — fine for short methods, one to three lines. Getters especially. Keeps everything in one place.
- Body outside the class — better for longer methods. Keeps the class declaration short enough to read at a glance, which is the whole point of a declaration.
Use both. Nobody will grade you on the choice; they will grade you on whether the class is readable.
5.8 — this
Inside a method, the keyword this refers to the object the method was called on. Specifically, this is a pointer to that object. Pointers get their full treatment in Chapter 6; for now you need one operator and one rule.
The operator is ->, which means “follow the pointer, then access the member.” So this->balance is “the balance belonging to the object I am a method of.”
The rule is: these two lines mean exactly the same thing.
balance += amount;
this->balance += amount;
You almost never need this, because fields are already accessible by bare name. There are two situations where it matters.
1. A parameter shadows a field. This one you will hit this week. Run code/this_keyword.cpp:
#include <iostream>
#include <string>
using namespace std;
class Greeter {
private:
string name;
public:
// The parameter `name` shadows the field `name`.
void set_name(string name) {
this->name = name; // this->name is the FIELD; name is the PARAMETER
}
void greet() {
cout << "Hello, " << name << "!" << endl;
}
// Methods can call this object's other methods. Both spellings work.
void greet_twice() {
greet(); // implicit: this object's greet()
this->greet(); // explicit: same call, spelled out
}
};
int main() {
Greeter g;
g.set_name("Maya");
g.greet();
g.set_name("Marcus");
g.greet_twice();
return 0;
}
Actual output:
Hello, Maya!
Hello, Marcus!
Hello, Marcus!
Now do the experiment that makes it stick: delete the this-> so the line reads name = name;. It still compiles — GNU g++ says nothing at all — and the program prints Hello, ! three times, because the parameter was assigned to itself and the field was never touched. A silent wrong answer is worse than a compile error, and this is one of the very few places in C++ where you can produce one this easily. Put the this-> back.
2. You need to hand the current object to something else. log_account(this); passes a pointer to the current object out to a free function. You will do this in Chapter 6; you do not need it in Project 4.
One trap worth naming now: this is a pointer, so it takes ->, not .. Writing this.deposit(50) is a compile error, and the compiler is unusually helpful about it — see bug 3 in §5.19.
5.9 — The Bridge: Why Constructors Belong in This Same Week
Stop and notice what the first half of this chapter left broken.
You built a type that enforces its own rules — after somebody remembers to call init(). Between the moment Account a; runs and the moment a.init(...) runs, the object is a live object with a garbage balance, and every guarantee the class makes is a lie. The class can enforce “no negative deposits.” It cannot enforce “you must initialize me before you use me,” because that rule lives in your head, and heads forget.
That is not a small gap. It is the same gap the struct had, just narrower. A class that can be caught in an invalid state at any point in its life has not actually bought you an invariant; it has bought you an invariant most of the time, which in software means an invariant until the day it costs you.
There is a matching gap at the other end. When the object dies, nothing happens. Today that costs you nothing — Account owns no resources. In Chapter 6, when your objects hold memory you asked the operating system for, “nothing happens when the object dies” is called a memory leak.
So the second half of this week closes both ends of the object’s life:
- A constructor makes initialization automatic and unskippable. You cannot forget it, because the language runs it for you.
- A destructor makes cleanup automatic and unskippable, for the same reason.
That is why these two halves are one week and not two. “Private data plus public methods” is a half-finished idea: it protects the object in the middle of its life and leaves both ends exposed. Constructors and destructors are not a separate topic that happens to come next — they are the part that makes encapsulation true. Everything from §5.10 forward is finishing the sentence you started in §5.2.
And once objects reliably construct themselves, a new thing becomes practical: objects that contain other objects. A Library full of Books is only safe if every Book in it is guaranteed to be valid the instant the Library exists — which is exactly what constructors guarantee. That is §5.14, and it is why the second half of the week ends in composition rather than starting there.
5.10 — The Default Constructor
A constructor is a special method that runs automatically when an object is created. It has the same name as the class and no return type at all — not even void:
class Account {
private:
double balance;
string owner;
public:
Account() { // ← the constructor
balance = 0.0;
owner = "unset";
}
void deposit(double amount) {
if (amount > 0) balance += amount;
}
double get_balance() { return balance; }
};
Now when you write:
Account a;
…the compiler calls Account::Account() on the new object for you. You cannot forget it. You cannot skip it. By the time the next line of your program runs, the object is in a known, valid state.
A constructor that takes no parameters is called a default constructor. Here is the part that explains your garbage balance from §5.5: if you write no constructor at all, C++ quietly generates a default one for you — and the generated one does not initialize scalar fields. It default-constructs class-type fields (which is why string owner came out as "") and leaves double and int holding whatever was already in that memory. Writing your own default constructor is how you take that decision back.
5.11 — Parameterized and Overloaded Constructors
A constructor can take parameters, exactly like any other function:
class Account {
private:
double balance;
string owner;
public:
Account(string n, double starting) {
owner = n;
balance = (starting >= 0) ? starting : 0;
}
};
And then:
Account a("Maya", 100.0);
One line instead of two, and — much more importantly — there is now no window between “object exists” and “object is valid.” The init() pattern is gone.
Here is the rule that bites everyone once: the moment you define any constructor, C++ stops generating the default one for you. If your only constructor is Account(string, double), then Account a; no longer compiles. That is not a bug; it is the language assuming that if you cared enough to specify how an Account gets built, you meant it. If you want both, write both.
Constructors overload just like functions do: same name, different parameter lists, and the compiler picks the one that matches your arguments. Here is code/three_constructors.cpp, with three:
#include <iostream>
#include <string>
using namespace std;
class StewardshipAccount {
private:
string owner;
double balance;
double given_away;
public:
// 0 arguments
StewardshipAccount()
: owner("unnamed"), balance(0.0), given_away(0.0) {}
// 1 argument
StewardshipAccount(string n)
: owner(n), balance(0.0), given_away(0.0) {}
// 2 arguments
StewardshipAccount(string n, double starting)
: owner(n), balance(starting >= 0 ? starting : 0), given_away(0.0) {}
void deposit(double amount) {
if (amount <= 0) {
cout << "Rejected: deposit must be positive." << endl;
return;
}
balance += amount;
}
// Giving is a withdrawal that the account also remembers.
bool give(double amount) {
if (amount <= 0) {
cout << "Rejected: gift must be positive." << endl;
return false;
}
if (amount > balance) {
cout << "Rejected: cannot give what is not there." << endl;
return false;
}
balance -= amount;
given_away += amount;
return true;
}
double get_balance() const { return balance; }
double get_given_away() const { return given_away; }
void print_ledger() const {
cout << "[" << owner << "] balance: $" << balance
<< " | given away: $" << given_away << endl;
}
};
With this main:
int main() {
StewardshipAccount anonymous; // 0 args
StewardshipAccount fresh("Marcus"); // 1 arg
StewardshipAccount funded("Maya", 1000.0); // 2 args
anonymous.print_ledger();
fresh.print_ledger();
funded.print_ledger();
cout << "---" << endl;
funded.deposit(200.0);
funded.give(150.0);
funded.give(-5.0); // rejected
funded.give(99999.0); // rejected
funded.print_ledger();
fresh.give(10.0); // rejected: empty account
fresh.print_ledger();
return 0;
}
Actual output:
[unnamed] balance: $0 | given away: $0
[Marcus] balance: $0 | given away: $0
[Maya] balance: $1000 | given away: $0
---
Rejected: gift must be positive.
Rejected: cannot give what is not there.
[Maya] balance: $1050 | given away: $150
Rejected: cannot give what is not there.
[Marcus] balance: $0 | given away: $0
Read that output next to the code until every line is obvious. Note especially that give returns bool — the caller is told whether the gift happened, instead of having to guess from a printed message. When a rule can reject an operation, say so in the return type.
One thing to watch: a one-argument constructor like StewardshipAccount(string n) lets C++ silently convert a string into a StewardshipAccount in some contexts. It will not bite you in this project. It is the reason you will eventually meet the keyword explicit in professional code.
5.12 — Initializer Lists
You already saw the syntax in §5.11. Here it is named. C++ has a dedicated way to initialize fields, which goes between the constructor’s parameter list and its body, introduced by a colon:
class Account {
private:
double balance;
string owner;
public:
Account(string n, double starting)
: balance(starting >= 0 ? starting : 0), owner(n)
{
// body — runs AFTER the fields already have their values
}
};
: balance(...), owner(n) initializes the fields directly. The alternative — assigning inside the body — first default-constructs each field and then overwrites it. For a double the difference is nothing. For a string, or for a field that is itself a class you wrote, you have built the thing twice. And for a few kinds of field (references, and const fields) the initializer list is not an optimization, it is the only way: they cannot be assigned after the fact at all.
List the fields in the order they were declared. Above, balance is declared before owner, so it comes first in the list. This is not stylistic fussiness. C++ always initializes members in declaration order and ignores the order you wrote in the list. If you write them out of order, the compiler warns you (this is g++‘s wording, which is what OnlineGDB shows; clang phrases it differently — see bug 6 in §5.19):
warning: 'Account::owner' will be initialized after [-Wreorder]
warning: 'double Account::balance' [-Wreorder]
warning: when initialized here [-Wreorder]
Take that warning seriously rather than silencing it. If one field’s initializer ever reads another field, the real initialization order decides whether you read a live value or garbage — and the real order is the declaration order, not the order your eyes see. Keep the two in sync and the question never comes up. (This book compiles with -Wall -Wextra for exactly this reason; a warning you ignore is a bug you scheduled.)
Style guidance: use an initializer list for plain “just set these fields” work, which is most of the time. Use the body when you need conditional logic, a loop, a method call, or a cout.
Coach’s Note — Initializer lists look strange for about a day and then become invisible. The mental model: the initializer list is where the fields come into existence; the constructor body is where you do additional work on fields that already exist. Both styles are acceptable in this course. Initializer lists are the professional default, so build the habit now while the classes are small.
5.13 — Destructors
A destructor is a special method that runs automatically when an object is about to be destroyed. Its name is the class name with a ~ (tilde) in front. It takes no parameters — ever — and has no return type:
class Account {
private:
string owner;
public:
~Account() {
cout << "[Account for " << owner << " closed]" << endl;
}
};
When an Account goes out of scope — its function returns, its block ends, the program finishes — ~Account() runs. You do not call it. You cannot forget to call it.
Here is code/constructor_demo.cpp, which puts a constructor, an overload, and a destructor together:
#include <iostream>
#include <string>
using namespace std;
class Account {
private:
double balance; // declared first...
string owner; // ...so it comes first in every initializer list
public:
// Default constructor: no parameters. Runs for `Account a;`
Account() : balance(0.0), owner("unset") {
cout << "[Account default-constructed]" << endl;
}
// Parameterized constructor. Runs for `Account b("Maya", 100.0);`
Account(string n, double starting)
: balance(starting >= 0 ? starting : 0), owner(n) {
cout << "[Account constructed for " << owner << "]" << endl;
}
// Destructor: runs automatically when the object dies.
~Account() {
cout << "[Account for " << owner << " closed]" << endl;
}
void deposit(double amount) {
if (amount > 0) balance += amount;
}
// const = "this method promises not to modify the object."
double get_balance() const { return balance; }
string get_owner() const { return owner; }
};
int main() {
Account a;
Account b("Maya", 100.0);
b.deposit(50.0);
cout << b.get_owner() << " balance: " << b.get_balance() << endl;
cout << "--- end of main ---" << endl;
return 0;
// Destructors fire here, in reverse order of construction: b, then a.
}
Actual output:
[Account default-constructed]
[Account constructed for Maya]
Maya balance: 150
--- end of main ---
[Account for Maya closed]
[Account for unset closed]
Notice the last two lines: b was constructed second and destroyed first. Destruction is the reverse of construction, always. Objects come off the stack in the opposite order they went on.
For most classes this week you will not have much work to do in a destructor — the class owns no external resources, and the compiler cleans up ordinary fields for you. Destructors still matter for two reasons.
1. They make object lifetime visible. A cout in the destructor turns an invisible language rule into printed evidence. Run code/lifecycle_trace.cpp and read the output next to the source:
#include <iostream>
#include <string>
using namespace std;
class Marker {
private:
string label;
public:
Marker(string l) : label(l) {
cout << " + constructed: " << label << endl;
}
~Marker() {
cout << " - destroyed: " << label << endl;
}
string get_label() const { return label; }
};
void borrow_a_marker() {
cout << "entering borrow_a_marker()" << endl;
Marker temp("temp (inside a function)");
cout << "leaving borrow_a_marker()" << endl;
} // temp dies here
int main() {
cout << "main starts" << endl;
Marker first("first (main scope)");
Marker second("second (main scope)");
cout << "before the inner block" << endl;
{
Marker inner("inner (block scope)");
cout << "inside the inner block" << endl;
} // inner dies here
cout << "after the inner block" << endl;
borrow_a_marker();
cout << "main ends" << endl;
return 0;
}
Actual output:
main starts
+ constructed: first (main scope)
+ constructed: second (main scope)
before the inner block
+ constructed: inner (block scope)
inside the inner block
- destroyed: inner (block scope)
after the inner block
entering borrow_a_marker()
+ constructed: temp (inside a function)
leaving borrow_a_marker()
- destroyed: temp (inside a function)
main ends
- destroyed: second (main scope)
- destroyed: first (main scope)
Three facts, proved: constructors run in declaration order; an object declared inside { } dies at the closing brace, not at the end of the function; destructors run in reverse order of construction.
2. They become load-bearing in Chapter 6. When you allocate memory with new, something has to delete it. The destructor is where that goes. This week’s destructors are practice reps for a move that becomes mandatory next week.
Coach’s Note — Forward reference: the Rule of Three. Once a class owns a heap-allocated resource, a destructor alone is not enough — you also have to think about what happens when the object is copied or assigned. C++ calls this the Rule of Three: if you need a custom destructor, you almost certainly need a custom copy constructor and copy assignment operator too. (Modern C++ adds two more for moves; that is the Rule of Five.) You do not need the ceremony this week — P4 stores books and accounts by value, so the compiler-generated copies are correct. The calculus changes the minute you store a
Book*that came fromnew. Chapter 6 walks into it deliberately.
5.14 — Composition: Objects That Hold Objects
A class can have fields that are themselves objects of other classes. This is composition, and it is how OOP systems are actually built.
Here is code/book_member.cpp. A Member borrows a Book:
#include <iostream>
#include <string>
using namespace std;
class Book {
private:
string title;
string author;
bool available;
public:
Book() : title(""), author(""), available(true) {}
Book(string t, string a) : title(t), author(a), available(true) {}
void check_out() { available = false; }
void return_book() { available = true; }
bool is_available() const { return available; }
string get_title() const { return title; }
string get_author() const { return author; }
void print() const {
cout << " '" << title << "' by " << author
<< (available ? " (available)" : " (checked out)") << endl;
}
};
class Member {
private:
string name;
string borrowed_titles[10]; // up to 10 at a time
int borrowed_count;
public:
Member() : name(""), borrowed_count(0) {}
Member(string n) : name(n), borrowed_count(0) {}
// Takes the Book by reference so the real book gets checked out,
// not a throwaway copy.
bool borrow(Book& book) {
if (!book.is_available()) return false;
if (borrowed_count >= 10) return false;
book.check_out();
borrowed_titles[borrowed_count] = book.get_title();
borrowed_count++;
return true;
}
void list_borrowed() const {
cout << name << " has " << borrowed_count << " book(s):" << endl;
for (int i = 0; i < borrowed_count; i++) {
cout << " - " << borrowed_titles[i] << endl;
}
}
string get_name() const { return name; }
};
Its main (in the file) creates three books and one member, borrows two of them, tries to borrow one of those twice, then prints the catalog and Maya’s shelf. Actual output:
Maya borrowed Mere Christianity.
Maya borrowed The Reason for God.
Can't double-borrow Mere Christianity.
'Mere Christianity' by C.S. Lewis (checked out)
'The Reason for God' by Timothy Keller (checked out)
'Confessions' by Augustine of Hippo (available)
Maya has 2 book(s):
- Mere Christianity
- The Reason for God
Three things to see here.
1. borrow takes Book& — a reference. Pass-by-reference, from Chapter 3, doing real work. If the parameter were Book book (by value), borrow would receive a copy, check out the copy, and the copy would evaporate at the end of the call. The real book on the shelf would still be available and you would have a mystifying bug with no error message. Whenever a method must change an object it was handed, take it by reference.
2. Neither class reaches into the other’s private fields. Member::borrow asks book.is_available() and then tells book.check_out(). It never touches book.available, and could not if it wanted to. Each class stays in charge of its own state; they cooperate through public methods only. That is the discipline that keeps a hundred-class program comprehensible.
3. Member does not own the books. It stores titles — copies of strings — and the books themselves live in main. Ownership matters, and Chapter 6 will make you decide it deliberately.
Coach’s Note — “Composition over inheritance” is the modern engineering watchword. When you can solve a problem by making one class contain another (or hold a reference to another), prefer that to making one class inherit from another. Composition is more flexible, harder to misuse, and far easier to reason about. Chapter 6 teaches inheritance properly and you will see exactly why this guidance exists.
5.15 — A Container Class: The Apologetics Library
Now the move that makes a system out of parts: a class that owns a collection of other objects and coordinates them. Here are the pieces of code/library_container.cpp that are new.
const int MAX_BOOKS = 20;
const int MAX_MEMBERS = 10;
class Library {
private:
Book books[MAX_BOOKS];
int book_count;
Member members[MAX_MEMBERS];
int member_count;
public:
Library() : book_count(0), member_count(0) {}
// const reference: no copy is made of the Book you hand in.
void add_book(const Book& b) {
if (book_count >= MAX_BOOKS) {
cout << "Shelf full; '" << b.get_title() << "' not added." << endl;
return;
}
books[book_count] = b;
book_count++;
}
// ... add_member is the same shape ...
void list_catalog() const {
cout << "Catalog (" << book_count << " books):" << endl;
for (int i = 0; i < book_count; i++) {
books[i].print();
}
}
~Library() {
cout << "[Library closed: " << book_count << " books, "
<< member_count << " members.]" << endl;
}
};
Four design points, each of which will come up in your project.
Arrays of objects need a default constructor. The line Book books[MAX_BOOKS]; declares twenty Books the instant a Library is created, and every one of them is default-constructed. If Book had only the two-argument constructor, this would not compile — see bug 5 in §5.19 for the exact error. That is the real reason Book() exists in §5.14. It is not decoration; it is what makes Book storable in an array.
Take parameters by const reference. add_book(const Book& b) avoids copying the whole Book on every call. const says “I will not modify what you handed me,” which lets callers pass temporaries and lets the compiler check your promise. If your Book had a chatty destructor, passing by value would also print a spurious “book destroyed” message every time the copy died at the end of the call — a genuinely confusing thing to debug.
Count, do not guess. book_count tracks how many of the twenty slots are real. Every loop runs to book_count, never to MAX_BOOKS. This is exactly the array-plus-count discipline from Chapter 3, now living inside a class where nobody outside can get the count out of sync with the data. That is a real improvement over a loose array and a loose int sitting next to each other in main.
The destructor reports. ~Library printing a summary is not busywork; it is proof that you know when your container dies and what state it was in.
Coach’s Note — Real C++ engineers write
vector<Book>for “a growable collection of Books,” andvectormakes the array-plus-count plumbing and the default-constructor requirement both disappear. This course uses fixed-size arrays because they keep memory visible, and because you cannot appreciate whatvectordoes for you until you have done it by hand. The pain is the point. Chapter 6 and the Java chapters will both feel like relief.
5.16 — Finders, const Methods, and Returning Objects
Returning a copy vs. returning something you can modify
Suppose Library needs a find method. The obvious version returns a Book:
Book Library::find_copy(string title) {
for (int i = 0; i < book_count; i++) {
if (books[i].get_title() == title) {
return books[i]; // returns a COPY
}
}
return Book("", ""); // sentinel: an empty Book
}
That works for “find and inspect.” It is useless for “find and check out,” because the caller gets a copy — check it out and the shelf copy stays available. You have two ways to hand back the real thing:
// Option A — return a reference. Convenient, but what does "not found" look like?
Book& Library::find_ref(string title) {
for (int i = 0; i < book_count; i++) {
if (books[i].get_title() == title) {
return books[i];
}
}
// A reference cannot be null. We would have to crash, throw, or return
// a reference to some static stand-in Book. All three are ugly.
static Book not_found("", "");
return not_found;
}
// Option B — return a pointer. nullptr is an honest "not found."
Book* Library::find_book(string title) {
for (int i = 0; i < book_count; i++) {
if (books[i].get_title() == title) {
return &books[i]; // the ADDRESS of the stored object
}
}
return nullptr;
}
Use Option B for finders. &books[i] means “the address of that array slot,” and nullptr is the standard way to say “there is nothing here.” At the call site it reads naturally — and notice ->, the same operator you met on this:
Book* hunted = lib.find_book("Confessions");
if (hunted != nullptr) {
cout << "Found: " << hunted->get_title()
<< " by " << hunted->get_author() << endl;
}
That is the exact call site at the bottom of library_container.cpp. Note what it could also do: because hunted points at the array slot inside the Library and not at a copy, adding hunted->check_out(); inside that if would check out the real book on the shelf. That is the whole reason a finder returns a pointer instead of a Book.
Always check for nullptr before you follow a pointer. Always. Chapter 6 spends a week on why.
Here is the whole file working. Actual output:
Maya checked out 'Mere Christianity'.
Marcus could not borrow 'Mere Christianity'.
Marcus checked out 'Pensees'.
No such book: The Screwtape Letters
No such member: Jordan
Catalog (4 books):
'Mere Christianity' by C.S. Lewis (checked out)
'The Reason for God' by Timothy Keller (available)
'Confessions' by Augustine of Hippo (available)
'Pensees' by Blaise Pascal (checked out)
Members (2):
Maya (1):
- Mere Christianity
Marcus (1):
- Pensees
Found: Confessions by Augustine of Hippo
Not in this library: The Everlasting Man
[Library closed: 4 books, 2 members.]
Read the checkout method in the source file. It finds a member, finds a book, checks both against nullptr, and only then calls m->borrow(*b). Three separate things can go wrong — no such member, no such book, the book is already out — and each one gets its own message and its own return false. That is what a coordinating method looks like: it does not do the work itself, it asks the objects that own the data and reports honestly on what they said.
const methods
A method declared const promises not to modify the object:
double get_balance() const { return balance; }
The const goes after the parameter list and before the {. Inside a const method you can read fields but cannot write them — trying is a compile error (bug 8 in §5.19).
Why bother? Because of const references. A function that takes const Account& may only call const methods on it, and the compiler enforces that. If your getter is not marked const, it cannot be called on a const object at all, and you get a genuinely baffling error message (bug 7). Since you are now passing objects around as const Book& and const Member&, this stops being theoretical immediately.
Practical rule: accessors and queries are const. Mutators are not.
get_title, get_author, is_available, print, list_borrowed, get_balance — all const. check_out, return_book, borrow, deposit, give — not const, because they change the object.
Go back and look at class_basics.cpp in §5.5: its get_balance() is not marked const. That file is the “before” picture on purpose. Every getter written from §5.11 onward is const. Fix it in your own copy as a rep.
5.17 — Class or Struct?
The honest answer is that in C++ the line is fuzzy, because both can have methods, constructors, and private members. The convention nearly every codebase follows:
structwhen the type is just data with no invariants. APointwithxandy. ADatewithyear,month,day. Outside code is expected to read and write the fields directly, and no rule is being broken when it does.classwhen the type has rules. AnAccountwhose balance must not go negative. ALibrarythat manages its own catalog. APlayerwhose HP must not exceed max HP.
In other languages the question is easier or does not exist — Java, which you meet in Chapter 7, has no struct at all. For now: data → struct; data plus rules → class.
Coach’s Note — In a year you will default to
classfor nearly everything except trivial containers, and that is fine. The point was never picking the right keyword. The point is knowing whether your type has rules, and writing accordingly.
5.18 — What You Now Have
Take stock before the bug list. In one week you built: data that cannot be corrupted from outside (private); one place where each rule lives (methods); guaranteed valid state at birth (constructors) and cleanup at death (destructors); objects that contain and coordinate other objects (composition, containers, finders); and a compiler that enforces read-only intent (const).
That is most of object-oriented programming. Chapters 6, 7, and 8 extend it — none of them replace it.
5.19 — Common Bugs (Week 5 Edition)
OnlineGDB compiles with GNU g++, so the messages below are g++‘s exact wording. If you compile somewhere else — a Mac with clang, for instance — the words differ but the meaning is identical; where the phrasing diverges a lot, both are given. Every one of these was produced by actually compiling the broken code, not from memory.
Bug 1 — Reaching into a private field from outside.
error: 'double Account::balance' is private within this context
13 | a.balance = 9999;
| ^~~~~~~
note: declared private here
(clang: error: 'balance' is a private member of 'Account')
What it means: main, or some other outside code, is touching a private field directly.
Fix: This is usually the compiler being right. Add a public method that does the operation with its rule attached — deposit, withdraw, check_out — and call that. Only add a getter if the outside code genuinely just needs to read.
Bug 2 — Missing semicolon after the class.
error: expected ';' after class definition
6 | }
| ^
| ;
(clang: error: expected ';' after class)
What it means: A class declaration ends with };, not }. You wrote }.
Fix: Add the semicolon. Be grateful: when this happens in a bigger file the error often lands on the next thing in the file instead, and you get a nonsense message about main. If an error points at a line that looks perfect, check the semicolon on the class above it.
Bug 3 — this. instead of this->.
error: request for member 'deposit' in '(Account*)this', which is of pointer
type 'Account*' (maybe you meant to use '->' ?)
9 | this.deposit(a);
| ^~~~~~~
(clang: error: member reference type 'Account *' is a pointer; did you mean to use '->'?)
What it means: this is a pointer. Pointers use ->.
Fix: Write this->deposit(a); — or, simpler, just deposit(a);. You rarely need this for a method call at all.
Bug 4 — Account a; after you wrote a parameterized constructor.
error: no matching function for call to 'Account::Account()'
11 | Account a;
| ^
note: candidate: 'Account::Account(std::string)'
note: candidate expects 1 argument, 0 provided
(clang: error: no matching constructor for initialization of 'Account')
What it means: Defining any constructor stops C++ from generating the default one. You have Account(string); you no longer have Account().
Fix: Either construct with arguments — Account a("Maya"); — or add an explicit default constructor: Account() : owner("unset") {}. If your class will ever live in an array, you need the default constructor. Add it.
Bug 5 — An array of objects whose class has no default constructor.
error: no matching function for call to 'Book::Book()'
15 | Library() : count(0) {}
| ^
note: candidate: 'Book::Book(std::string)'
note: candidate expects 1 argument, 0 provided
(clang points at the field instead: error: constructor for 'Library' must explicitly initialize the member 'books' which does not have a default constructor)
What it means: Book books[20]; default-constructs twenty Books. There is no default constructor to run.
Fix: Give Book a default constructor. Note where g++ points the error — at Library’s constructor, not at the array declaration. The cause is in a different class from the message.
Bug 6 — Initializer list order does not match declaration order.
warning: 'Account::owner' will be initialized after [-Wreorder]
warning: 'double Account::balance' [-Wreorder]
warning: when initialized here [-Wreorder]
(clang: warning: field 'owner' will be initialized after field 'balance' [-Wreorder-ctor])
What it means: Fields initialize in declaration order regardless of the order you wrote in the list. The compiler is telling you the code lies about what happens. Fix: Reorder the initializer list to match the field declarations. Do not silence the warning.
Bug 7 — Calling a non-const method through a const reference.
error: passing 'const Account' as 'this' argument discards qualifiers [-fpermissive]
12 | cout << a.get_balance() << endl;
| ~~~~~~~~~~~~~^~
note: in call to 'double Account::get_balance()'
(clang: error: 'this' argument to member function 'get_balance' has type 'const Account', but function is not marked const)
What it means: You have a const Account& and you called a method that is not marked const. The compiler will not assume your getter is harmless; you have to say so.
Fix: Add const after the parameter list: double get_balance() const { return balance; }. “Discards qualifiers” is g++‘s way of saying “this call would throw away the const you promised.”
Bug 8 — Assigning to a field inside a const method.
error: assignment of member 'Account::balance' in read-only object
8 | double get_balance() const { balance = 0; return balance; }
| ~~~~~~~~^~~
(clang: error: cannot assign to non-static data member within const member function 'get_balance')
What it means: You marked a method const and then modified the object. One of those two is wrong.
Fix: If the method is supposed to be a query, remove the assignment. If it is supposed to change the object, remove the const.
Bug 9 — Defining a method outside the class and forgetting ClassName::.
error: 'balance' was not declared in this scope
11 | if (amount > 0) balance += amount;
| ^~~~~~~
(clang: error: use of undeclared identifier 'balance')
What it means: You wrote void deposit(double amount) { ... } instead of void Account::deposit(double amount) { ... }. Without the prefix that is a plain free function, and free functions do not have a balance.
Fix: Add Account:: before the method name. Note that the error is about the field, not about the missing prefix — the compiler cannot know what you meant.
Bug 10 — A destructor with parameters (or a mistyped one).
error: destructors may not have parameters
9 | ~Account(string reason) { ... }
| ^
error: 'reason' was not declared in this scope
(clang: error: destructor cannot have any parameters)
What it means: Destructors take nothing, return nothing, and there is exactly one per class. The related silent version of this bug: writing Account() when you meant ~Account(). That compiles fine and gives you a second constructor that never runs as a destructor, so your “closed” message simply never prints.
Fix: ~ClassName() { }. If a lifecycle message is not printing, check for a missing tilde first.
Bug 11 — cout << myObject;
error: no match for 'operator<<' (operand types are 'std::ostream'
{aka 'std::basic_ostream<char>'} and 'Account')
11 | ... cout << a << endl; ...
(clang: error: invalid operands to binary expression ('ostream' ... and 'Account'). Both compilers then print a wall of “candidate” notes listing every overload of << that exists. Ignore the wall; read the first line.)
What it means: cout knows how to print int, double, string, and friends. It does not know how to print your class, because you never told it how.
Fix: Give the class a void print() const method and call a.print();. (Teaching cout to print your type directly means overloading operator<<, which is not in this course.)
Bug 12 — Returning a reference to a local object.
warning: reference to local variable 'b' returned [-Wreturn-local-addr]
14 | return b;
| ^
note: declared here
(clang: warning: reference to stack memory associated with local variable 'b' returned [-Wreturn-stack-address])
What it means: The local object is destroyed when the function returns, so the caller is handed a reference to memory that no longer holds anything valid. This is a warning, not an error — the program compiles, runs, and then misbehaves. What it does next is genuinely undefined: reading through that reference printed an empty title when this was tested, but it could equally print garbage, print a stale-but-plausible value, or crash, and it can differ between two runs of the same binary. Do not go looking for the pattern. There isn’t one.
Fix: Return by value (Book find(...)) or return a pointer to something that outlives the call (&books[i], which lives in the Library, not in the function). Never return a reference or pointer to a local.
Bug 13 — The silent one: name = name;
There is no error message. There is no warning from GNU g++. The program compiles and runs and prints the wrong answer.
void set_name(string name) { name = name; } // ❌ assigns the parameter to itself
This is the Greeter class from §5.8 with the this-> deleted. Its name field was never assigned, so it is still the empty string a default-constructed string starts as. Running the same main — set_name("Maya"), greet(), set_name("Marcus"), greet_twice() — printed:
Hello, !
Hello, !
Hello, !
(clang does warn here: warning: explicitly assigning value of variable of type 'string' to itself; did you mean to assign to member 'name'? — but do not count on your compiler catching it.)
What it means: The parameter shadows the field. Inside the method, the bare name name refers to the parameter, so the assignment does nothing at all.
Fix: this->name = name; — or rename the parameter (new_name) so no shadow exists. When a setter appears to do nothing, this is the first thing to check.
Bug 14 — Using a class before the compiler has seen it.
error: 'Book' has not been declared
9 | bool borrow(Book& book) { ... }
| ^~~~
error: request for member 'check_out' in 'book', which is of non-class type 'int'
error: cannot convert 'Book' to 'int&'
(clang: error: unknown type name 'Book')
What it means: In a single file, the compiler reads top to bottom. If Member mentions Book and Book is defined below Member, Book does not exist yet. Look at the second g++ message: it decided Book must be an int and then produced two more errors downstream of its own guess. One real mistake, three messages.
Fix: Define Book above Member. General rule for single-file OOP: define the classes that get used before the classes that use them.
Bug 15 — A path through a method that returns nothing.
warning: control reaches end of non-void function [-Wreturn-type]
12 | balance -= amount;
| ~~~~~~~~^~~~~~~~~
(clang: warning: non-void function does not return a value in all control paths [-Wreturn-type])
What it means: bool withdraw(...) returns false on the two rejection paths and then falls off the end on the success path. The caller receives garbage. Because it is a warning, the program still builds.
Fix: Add the missing return true;. This is exactly why the book compiles with -Wall -Wextra and why “zero warnings” is a grading criterion — this bug produces an if (a.withdraw(x)) that behaves randomly.
5.20 — Reps
The full set is in the exercises. Three teasers so you can start right now:
Rep A. Type class_basics.cpp from scratch — do not copy and paste. Then add the line a.balance = 9999; to main and compile. Confirm you get bug 1’s message verbatim. Delete the line.
Rep B. Write a Counter class with a private int count, a default constructor that sets it to 0, and public increment(), reset(), and int get() const. In main, make three counters, increment them 1, 2, and 3 times, and print all three. Expected output: 1 2 3 in whatever format you choose — the point is that they differ.
Rep C. Write a BoundedTimer class with private int seconds_remaining and int max_seconds. Constructor BoundedTimer(int max) sets both, using an initializer list. tick() subtracts 1 but never goes below 0. bool is_done() const returns true at 0. reset() puts seconds_remaining back to max_seconds — that is what the second field is for, and a private field nothing ever reads is a field you did not need. In main, build a 5-second timer, call tick() ten times, print the remaining seconds, then reset() and print again. It must print 0 and then 5 — never -5.
Then do the rest in the exercises. All of them. This is the week where skipping reps shows up on the final.
5.21 — Checkpoint: Can You Do This Yet?
Close the book. Open a blank file. No copying, no searching, no AI. Seven items. Give yourself about forty minutes.
- Write a class
Playerwith privatestring nameandint hp, a constructorPlayer(string n, int starting_hp)using an initializer list, and a destructor that prints"[Player <name> gone]". - Add
void take_damage(int amount)that subtracts but clamps at 0, andbool is_alive() const. - Write, from memory, the line that makes
hpunreachable frommain, and then the exact compiler error you would get ifmaintried anyway. - Write a
Teamclass holdingPlayer roster[4]and anint player_count, with a constructor that sets the count to 0 and anadd(const Player& p)method that refuses to add a fifth player. - Explain in one sentence why
Playermust have a default constructor for item 4 to compile. - Write a method
Player* find(string wanted)onTeamthat returns the address of the matching player ornullptr, and the three-line call site inmainthat uses it safely. - State which of your methods should be
constand why.
Pass bar: 6 of 7, written correctly on the first attempt, compiling with zero warnings under -Wall -Wextra.
If you scored 5 or below, do not start P4 yet. Identify which items failed and go back:
- Items 1–3 failed → re-read §5.2, §5.3, §5.10–§5.12 and redo Reps 1–6.
- Item 4 or 5 failed → re-read §5.15 and bug 5 in §5.19.
- Item 6 failed → re-read §5.16.
- Item 7 failed → re-read the
consthalf of §5.16.
Re-drilling costs you two hours. Starting the project on a shaky foundation costs you the week.
5.22 — When You’re Stuck (and Nobody’s in the Room)
Nobody is going to walk by and glance at your screen. That is a real disadvantage and this section is how you make up for it. Work the ladder in order — do not skip to rung 5 because rungs 1–4 feel slow. They are faster.
Rung 1 — Read the error. The first one only.
C++ error messages cascade: one real mistake becomes eight messages, and messages 2 through 8 are the compiler recovering from the first. Scroll up to the very first error: line and fix only that. Then recompile. Bug 14 in §5.19 is a worked example — one missing class definition, three errors.
Then decode it in this order: (a) which file and line? (b) which identifier is it naming? (c) is that identifier a field, a method, or a class? Nearly every Week 5 error is one of the fifteen in §5.19. Read that list before you do anything else — it was built from the errors this exact material produces.
Rung 2 — Minimal reproduction.
Open a new OnlineGDB tab. Copy in only the class that is misbehaving, plus a main that does three lines with it. Nothing else. Ninety percent of the time the bug either disappears — which tells you the problem is in the interaction, not the class — or it reproduces in twenty lines you can actually read. This week the reproduction is easy because a class is a natural unit to isolate. Do it.
Rung 3 — Print the lifecycle.
Week 5 has a specific failure mode: the object is not in the state you think it is in. Add a cout to the constructor, the destructor, and the top of the misbehaving method printing the object’s fields. Run it. You are looking for one of four things:
- The constructor you expected did not run (you got the default one, or a different overload).
- A field is garbage → an uninitialized scalar; see §5.5 and §5.10.
- A field changes and then changes back → you are modifying a copy; check for a missing
&on a parameter (§5.14). - A setter appears to do nothing → shadowed parameter; see bug 13.
lifecycle_trace.cpp exists to give you a known-good output to compare against.
Rung 4 — Rubber duck, out loud, in this exact script. Point at your class and say the following sentences aloud. Not in your head — aloud. “This class is responsible for ____. Its private data is ____. The rules over that data are ____. The method I am debugging is supposed to ____. It is actually ____. The last line that behaved as I expected was ____.” The gap almost always appears at the third or fourth sentence, because that is where you discover the rule you believe your class enforces is not written anywhere in it.
Rung 5 — Go back to the book, at the specific place. Not “reread the chapter.” Use the map:
| Symptom | Section |
|---|---|
| ”private within this context” | §5.2, §5.3 |
”no matching function for call to X::X()” | §5.11, §5.15 |
| Field is garbage / random number | §5.5, §5.10 |
| Change doesn’t stick | §5.8 (bug 13), §5.14 (missing &) |
Something about const / “discards qualifiers” | §5.16 |
| Destructor never prints | §5.13, bug 10 |
| Reorder warning | §5.12 |
Also: Appendix C is the C++/Java syntax reference, Appendix D is the glossary (look up encapsulation, instance, composition), and Appendix A covers OnlineGDB itself if the tool rather than the code is fighting you.
Rung 6 — Post to the discussion board. Use this shape; it gets answered, and vague posts do not:
Week 5 — [one-line symptom] What I’m trying to do: [one sentence] Minimal code that shows it (from Rung 2):
[15–25 lines, complete and compilable]Exact compiler output (first error only):
[paste it]What I expected: [one sentence] What happened instead: [one sentence] What I already tried: [Rungs 1–5, specifically]
Then keep working on something else. Do not sit and refresh.
Rung 7 — Email the instructor. Same content as the post, plus your OnlineGDB share link (Appendix A shows how to make one) and this subject line:
Accelerated Coding 1 — Week 5 — [your name] — [class name]: [symptom]
Say what you have already tried. “It doesn’t work” gets a slower answer than “my Library constructor won’t compile, I think it’s the Book default constructor, here’s the error and my minimal repro.”
And the meta-rung: the twenty-minute rule. If you have been stuck on the same error for twenty minutes with no new information, stop. Get up. Come back in an hour, or tomorrow. This week in particular, the bug is usually something you will see in four seconds with fresh eyes and never see while tired. Budget for that: it is why the week is four sessions and not one.
5.23 — This Week’s Project
P4 — Stewardship Account & Apologetics Library, due at the end of Week 5. The spec is Project 4; read the whole thing before you type anything.
It has two halves, matching the two halves of this chapter.
The Stewardship Account is the payoff of §5.1–§5.13. A class that tracks money entrusted to someone: deposits, gifts, spending — each one its own method, each with its own rule, none of them bypassable from main. The Christian concept of stewardship — what you have is not fundamentally yours; it is entrusted, and the entrustment carries rules — maps onto the encapsulation move almost exactly. That is not a decorative analogy. The class is the rules.
A direct word about the framing: this project does not endorse a prosperity-gospel reading of generosity. Nothing in your program rewards the account for giving. It records, accurately, what was given. That is the whole design.
The Apologetics Library is the payoff of §5.14–§5.16. A small system of cooperating objects: Book, Member, and a Library that owns and coordinates them. Real books — Lewis’s Mere Christianity, Keller’s The Reason for God, Augustine’s Confessions, Pascal’s Pensées. Members check them out; the library tracks who has what; the destructor prints a clean closing summary. The library is one of the oldest Christian institutions — the medieval monasteries preserved Western literature precisely because monks copied, catalogued, and lent books — and the cataloging side is what you are modeling.
Two files in code/ are warm-up skeletons for these halves. Both compile and run as-is; both have the interesting methods stubbed as TODOs:
stewardship_starter.cpp— the account, withgive()andspend()left for you.library_starter.cpp—Bookcomplete,Memberpartly stubbed,Libraryleft entirely to you.
They are not the project and they are not a substitute for reading the spec. They exist so your first ten minutes are spent writing a method rather than staring at a blank file.
Design guidance that applies to both halves:
- Every rule lives in a method. If
maincontains anifthat checks a balance or an availability flag, you have put a rule in the wrong place. Move it inside the class. - Methods that can fail return
bool. Avoid withdrawthat prints “insufficient funds” leaves the caller unable to react. Look at §5.7 and §5.11 for the pattern. - Getters are
const. All of them. - Compile with
-Wall -Wextraand fix every warning. Bugs 6, 12, and 15 in §5.19 are all warnings that produce genuinely broken programs.
5.24 — Coach’s Final Word for Week 5
Classes are the single biggest concept in this course, and this week you took them at double speed. Be honest with yourself about whether it landed.
Here is the test that matters, and it is not the checkpoint: can you look at a problem description — “track a person’s borrowed books,” “model a donation ledger” — and decide on your own what the class is, what its private data is, and what its methods are? Not implement one that was handed to you. Design one. That decision is the actual skill. Everything else this week was syntax in service of it.
Half of every technical interview you will ever sit touches this material. Java, which arrives in two weeks, is class-everything — there is no way to write a Java program that avoids it. Chapter 6’s inheritance, Chapter 8’s interfaces, and the entire final exam are built directly on top of what you did in the last twelve hours.
So: if Chapter 5 feels shaky, stop and re-drill before Chapter 6. Half-understood classes are a debt, and this course charges interest weekly. If it feels solid — if you can write a class with a constructor, a destructor, private data, guarded mutators, and const accessors without looking anything up — then you have crossed the line from “person learning to code” to “person who writes object-oriented software.” That is a real line and you just crossed it alone, at whatever hour you are reading this, with nobody in the room. Do not let that go unnoticed.
See you next week. Pointers, and the memory underneath everything you just built.
Up next: Work every rep in the exercises, then build P4 in Project 4. After that, Chapter 6 — pointers, dynamic memory, and inheritance.
Week 5 Knowledge Check
class Counter {
int c;
public:
Counter() { c = 0; }
void bump() { c++; }
int get() { return c; }
};
Counter x, y;
x.bump(); x.bump(); y.bump();
cout << x.get() << " " << y.get(); class Trace {
string n;
public:
Trace(string s) : n(s) { cout << "+" << n << " "; }
~Trace() { cout << "-" << n << " "; }
};
int main() {
Trace a("A");
{ Trace b("B"); }
cout << "| ";
}