Chapter 05 · Reps

From Struct to Class: Objects and Encapsulation — Reps

← Back to Chapter 5

Chapter 5 — Reps

Conditioning, not grading. Nobody sees these. Do all eighteen, in order, before you open P4.

Ground rules, unchanged: type every line — do not paste. Compile with -Wall -Wextra and fix every warning. Run it. AI off. Stuck? Work the §5.22 ladder before you look anything up.

Every rep ends with the output the program actually produced. That is your grader: if yours prints something else you have a bug, even if the code “looks right.” Nobody is walking past your screen this week, so the output is the whole feedback loop.

The compiler output below came from GNU g++ 14.2.0 with -std=c++17 -Wall -Wextra, the compiler family OnlineGDB runs. File-name prefixes are stripped and line numbers belong to the file that produced them, so yours will differ. Wording drifts between versions; §5.19 gives clang’s too.


Reps 1–4: Data With Rules Attached

Rep 1 — The struct that cannot say no

Type struct_vs_class.cpp from §5.1 from scratch — both types and the main that abuses them. Run it. Then uncomment tight.balance = -999999.0; and compile again: confirm the compiler refuses, and comment it back out.

Expected output:

[Maya] balance: $-999999
Rejected: insufficient funds.
[Maya] balance: $100

Same two fields, opposite behavior. That difference is the whole chapter.


Rep 2 — The account that enforces itself

Type class_basics.cpp from §5.5 from scratch: private balance and owner; public init, deposit, withdraw, get_balance, print_statement. main calls init("Maya", 100.0), deposit(50.0), deposit(-25.0), withdraw(200.0), withdraw(80.0), print_statement(). Trace it on paper and write down the final balance before you run it.

Expected output:

Rejected: deposit must be positive.
Rejected: insufficient funds.
[Maya] balance: $70

Two of those five calls were rejected by guards inside the class. main never had the option.


Rep 3 — Bug drill: reaching past private

Keep Rep 2’s Account. Make each change one at a time, compile, read the error, undo it. Do not skip one because you think you know what it says — you are training your eye for Part A.

(a) Add a.balance = 9999; to main. Expected output:

error: 'double Account::balance' is private within this context
   21 |     a.balance = 9999;
      |       ^~~~~~~
note: declared private here
    7 |     double balance;
      |            ^~~~~~~
note: field 'double Account::balance' can be accessed via 'double Account::get_balance() const'

That last line: g++ found your accessor and suggested it. It is right.

(b) Add cout << a.owner << endl; — reading is blocked too. Expected output:

error: 'std::string Account::owner' is private within this context

(c) Add a private bool is_valid(double amount) { return amount > 0; }, use it inside init, then call a.is_valid(50.0) from main. Expected output:

error: 'bool Account::is_valid(double)' is private within this context

private applies to methods too. A private method is a helper the class uses on itself.

(d) Delete the semicolon after the class’s closing }. Expected output:

error: expected ';' after class definition
   16 | }
      |  ^
      |  ;

§5.19 bug 2 — the error you will meet most often all week.


Rep 4 — The setter that isn’t worth having

§5.4, made concrete. Write two classes, each with a private double balance and a double get_balance() const:

  • SloppyAccountvoid init(double starting) and void set_balance(double b), which just assigns.
  • TightAccountvoid init(double starting) (a negative start clamps to 0) and void withdraw(double amount), printing Rejected: withdrawal must be positive. for a non-positive amount and Rejected: insufficient funds. when it exceeds the balance.

In main: init both to 100.0; print sloppy balance: and tight balance: ; print --- the attack ---; call sloppy.set_balance(-999999.0); and tight.withdraw(999999.0);; print both again.

Expected output:

sloppy balance: 100
tight balance: 100
--- the attack ---
Rejected: insufficient funds.
sloppy balance: -999999
tight balance: 100

SloppyAccount has a private field, a getter, a setter, and zero protection — a public field with extra typing. Name methods for what they do.


Reps 5–7: One Class, Many Objects

Rep 5 — Three counters

The Counter class from §5.6: private int count; public init(), increment(), reset(), int get() const.

In main, make counters a, b, c; init all three; increment a once, b twice, c three times; print each as a: , b: , c: . Then a.reset() and print one summary line, after a.reset(): a=0 b=2 c=3, built from the three get() calls — do not hardcode it.

Expected output:

a: 1
b: 2
c: 3
after a.reset(): a=0 b=2 c=3

One class, three independent instances. If all three print the same number you made count static, or you are sharing an object by accident.


Rep 6 — BoundedTimer

Private int seconds_remaining and int max_seconds. Public init(int max) (sets max_seconds, a negative clamping to 0, then matches seconds_remaining to it), tick() (subtracts 1, never below 0), reset() (restores seconds_remaining from max_seconds), bool is_done() const, int get_remaining() const.

In main: init to 5; tick() ten times in a loop; print remaining after 10 ticks: and done? (yes/no); then reset() and print remaining after reset: and done? again.

Expected output:

remaining after 10 ticks: 0
done? yes
remaining after reset: 5
done? no

-5 means your clamp is missing. max_seconds exists so reset() has something to restore — a private field nothing ever reads is a field you did not need. (§5.20’s teaser Rep C builds this class with a constructor, which is Rep 10’s syntax. Both ways is the point.)


Rep 7 — Declare inside, define outside

Restructure Rep 2’s Account the §5.7 way: declarations only inside the class, all five bodies below it with the Account:: prefix. While you are in there, make withdraw return booltrue if the money moved — and mark get_balance and print_statement const.

In main: init("Maya", 100.0); deposit(50.0); if (a.withdraw(500.0)) printing Withdrawal of $500 succeeded. / Withdrawal of $500 did not happen.; if (a.withdraw(80.0)) printing Withdrawal of $80 succeeded.; print_statement(); then get_balance() reports: and the value.

Expected output:

Rejected: insufficient funds.
Withdrawal of $500 did not happen.
Withdrawal of $80 succeeded.
[Maya] balance: $70
get_balance() reports: 70

Now break it: delete Account:: from the front of the deposit definition and compile. Expected output:

error: 'balance' was not declared in this scope
   26 |         balance += amount;
      |         ^~~~~~~

The compiler names the field, not the missing prefix — it cannot know what you meant. Without Account:: you wrote a free function, and free functions have no balance (§5.19 bug 9).


Reps 8–9: this

Rep 8 — Greeter

Type this_keyword.cpp from §5.8 from scratch: private string name; void set_name(string name) whose body is this->name = name;; greet(); greet_twice() calling greet(); then this->greet();. In main: set_name("Maya"), greet(), set_name("Marcus"), greet_twice().

Expected output:

Hello, Maya!
Hello, Marcus!
Hello, Marcus!

Both spellings in greet_twice produce the same call. this-> does nothing there — which is why the next rep matters.


Rep 9 — Bug drill: the silent one

Delete Rep 8’s this-> so the line reads name = name;. Compile and run. Expected output — GNU g++ compiles this without a word of complaint:

Hello, !
Hello, !
Hello, !

No error. No warning. Three wrong answers. The parameter shadows the field, so name = name; assigns the parameter to itself and the field stays the empty string a default-constructed string starts as (§5.19 bug 13).

Compile on a Mac and clang does catch it — but do not build a habit on a warning your grading compiler does not emit:

warning: explicitly assigning value of variable of type 'string' (aka 'basic_string<char>')
to itself; did you mean to assign to member 'name'? [-Wself-assign-overloaded]
   12 |         name = name;
      |         ~~~~ ^ ~~~~
      |         this->

Now fix it twice: put this-> back and confirm Rep 8’s output returns; then rename the parameter to new_name, delete the this-> again, and confirm it is still correct. Two fixes, one bug. Reach for the rename by default — no shadow, nothing to remember.


Reps 10–12: Constructors

Rep 10 — The constructor replaces init

Take Rep 7’s Account and delete init entirely. Declare double balance; before string owner;, then add:

  • Account() — initializer list setting balance to 0.0 and owner to "unset"; body prints [Account default-constructed].
  • Account(string n, double starting) — initializer list (declaration order!), a negative start clamping to 0; body prints [Account constructed for <owner>].
  • ~Account() — prints [Account for <owner> closed].

In main: Account fresh;; Account maya("Maya", 100.0);; maya.deposit(50.0); if (!maya.withdraw(500.0)) printing Withdrawal of $500 did not happen.; maya.withdraw(80.0); maya.print_statement(); then --- end of main ---.

Expected output:

[Account default-constructed]
[Account constructed for Maya]
Rejected: insufficient funds.
Withdrawal of $500 did not happen.
[Maya] balance: $70
--- end of main ---
[Account for Maya closed]
[Account for unset closed]

Read the last two lines. maya was built second and destroyed first — destruction is always the reverse of construction. And there is no longer any window in which an Account exists but is not valid.


Rep 11 — Three constructors, one class

The warm-up for half of P4. Private, in this order: string owner, double balance, double given_away, double spent. Three initializer-list constructors: () → owner "unnamed" and zeros; (string n) → that name and zeros; (string n, double starting) → that name, that balance (negatives clamp to 0), and zeros.

deposit(double) rejects non-positive with Rejected: deposit must be positive. bool give(double) and bool spend(double) each reject non-positive (Rejected: gift must be positive. / Rejected: expense must be positive.) and over-drawing (Rejected: cannot give what is not there. / Rejected: cannot spend what is not there.); otherwise they subtract from balance and add to their own total. const getters for all three numbers. print_ledger() const prints [owner] balance: $B | given: $G | spent: $S.

In main: build anonymous, fresh("Marcus"), funded("Maya", 1000.0); print all three ledgers; print ---; on funded call deposit(200.0), give(150.0), spend(75.0), give(-5.0), spend(99999.0), print_ledger(); finally fresh.give(10.0) and fresh.print_ledger().

Expected output:

[unnamed] balance: $0 | given: $0 | spent: $0
[Marcus] balance: $0 | given: $0 | spent: $0
[Maya] balance: $1000 | given: $0 | spent: $0
---
Rejected: gift must be positive.
Rejected: cannot spend what is not there.
[Maya] balance: $975 | given: $150 | spent: $75
Rejected: cannot give what is not there.
[Marcus] balance: $0 | given: $0 | spent: $0

give and spend do identical arithmetic and are still two methods, because the ledger has to tell them apart. That distinction is the stewardship half of P4.


Rep 12 — Bug drill: the default constructor you deleted

Take Rep 11 and delete the zero-argument constructor. Change nothing else. Compile.

Expected output:

error: no matching function for call to 'StewardshipAccount::StewardshipAccount()'
   69 |     StewardshipAccount anonymous;
      |                        ^~~~~~~~~
note: candidate: 'StewardshipAccount::StewardshipAccount(std::string, double)'
note:   candidate expects 2 arguments, 0 provided
note: candidate: 'StewardshipAccount::StewardshipAccount(std::string)'
note:   candidate expects 1 argument, 0 provided

§5.19 bug 4. Define any constructor and C++ stops generating the free default one — and the compiler lists every constructor it does have, with why each fails. Read those candidate: notes; they answer “what could I have called instead?” Restore the default constructor, confirm Rep 11’s output returns. This error comes back in Rep 18, somewhere much stranger.


Reps 13–14: Initializer Lists and const

Rep 13 — Book, and a function that promises not to touch it

Book is the other half of P4. Private, in this order: string title, string author, bool available. Two initializer-list constructors: Book() → two empty strings and true; Book(string t, string a) → those, and true. Then check_out(), return_book(), and const is_available(), get_title(), get_author(), print()print() outputs two leading spaces, 'TITLE' by AUTHOR, then (available) or (checked out). Plus a free function void show(const Book& b) printing show() says: , the title, by, the author.

In main: Book empty;, Book mc("Mere Christianity", "C.S. Lewis");, Book rfg("The Reason for God", "Timothy Keller");. Check mc out, print all three, call show(mc), then mc.return_book() and print mc again.

Expected output:

  '' by  (available)
  'Mere Christianity' by C.S. Lewis (checked out)
  'The Reason for God' by Timothy Keller (available)
show() says: Mere Christianity by C.S. Lewis
  'Mere Christianity' by C.S. Lewis (available)

Look at line 1: the default-constructed Book prints empty quotes, a double space, and (available) — a valid, useless book. That is every empty slot in Rep 18’s array.


Rep 14 — Bug drill: order, and two ways to get const wrong

Three changes to Rep 13, one at a time. Undo each before the next.

(a) Reorder the parameterized constructor’s list to : available(true), title(t), author(a). Expected output:

warning: 'Book::available' will be initialized after [-Wreorder]
   10 |     bool available;
      |          ^~~~~~~~~
warning:   'std::string Book::title' [-Wreorder]
    8 |     string title;
      |            ^~~~~
warning:   when initialized here [-Wreorder]
   14 |     Book(string t, string a) : available(true), title(t), author(a) {}
      |     ^~~~

A warning, not an error — this still builds and runs correctly. The compiler is telling you your code lies: fields initialize in declaration order no matter what you wrote (§5.12).

(b) Delete const from get_title(). Leave show(const Book& b) alone. Expected output:

error: passing 'const Book' as 'this' argument discards qualifiers [-fpermissive]
   30 |     cout << "show() says: " << b.get_title()
      |                                ~~~~~~~~~~~^~
note:   in call to 'std::string Book::get_title()'

“Discards qualifiers” means this call would throw away the const you promised. Mark the getter const; do not un-const the parameter (§5.19 bug 7).

(c) Assign inside a const method: bool is_available() const { available = true; return available; }. Expected output:

error: assignment of member 'Book::available' in read-only object
   19 |     bool is_available() const { available = true; return available; }
      |                                 ~~~~~~~~~~^~~~~~

The compiler is holding you to your own promise. One of the two — the const or the assignment — is wrong, and only you know which one.


Reps 15–16: Object Lifetime

Rep 15 — Trace it by hand FIRST

The highest-value rep in the chapter, and it only works in the stated order. Type the program. Do not run it yet. Write the full output on paper, line by line. Then run it and compare. If paper and program disagree, you do not own destructor ordering — reread §5.13 before going on. Type exactly this:

#include <iostream>
#include <string>
using namespace std;

class Marker {
private:
    string label;

public:
    Marker(string l) : label(l) { cout << "  + " << label << endl; }
    ~Marker() { cout << "  - " << label << endl; }
};

class Shelf {
private:
    Marker top;
    Marker bottom;

public:
    Shelf(string name) : top(name + " top"), bottom(name + " bottom") {
        cout << "  Shelf " << name << " ready" << endl;
    }
    ~Shelf() { cout << "  Shelf closing" << endl; }
};

void visit() {
    cout << "entering visit()" << endl;
    Marker guest("guest");
    cout << "leaving visit()" << endl;
}

int main() {
    cout << "main starts" << endl;
    Marker first("first");
    {
        Shelf s("A");
        cout << "inside the block" << endl;
    }
    cout << "after the block" << endl;
    visit();
    cout << "main ends" << endl;
    return 0;
}

Expected output:

main starts
  + first
  + A top
  + A bottom
  Shelf A ready
inside the block
  Shelf closing
  - A bottom
  - A top
after the block
entering visit()
  + guest
leaving visit()
  - guest
main ends
  - first

Five facts, each of them Part A material. A composed object’s members are built before the enclosing constructor’s body — that is why + A top precedes Shelf A ready. Members are destroyed after the enclosing destructor’s body — Shelf closing precedes - A bottom. Members die in reverse declaration order. A block-scope object dies at its closing brace, not at the end of the function. A function’s local dies when the function returns.

Missing the Shelf closing / - A bottom order is the normal miss. Reread until it is obvious.


Rep 16 — Bug drill: the destructor that never fires

(a) In Rep 15, change ~Marker() to Marker() — drop the tilde, nothing else. It compiles cleanly under -Wall -Wextra. Run it.

Expected output:

main starts
  + first
  + A top
  + A bottom
  Shelf A ready
inside the block
  Shelf closing
after the block
entering visit()
  + guest
leaving visit()
main ends

Every - line is gone and nothing complained, because you did not write a destructor — you wrote a second, unused default constructor (§5.19 bug 10, silent form). When a lifecycle message is not printing, check for a missing tilde first.

(b) Restore the tilde, then try ~Marker(string reason). Expected output:

error: destructors may not have parameters
   15 |     ~Marker(string reason) {
      |     ^
error: 'reason' was not declared in this scope
   16 |         cout << "  - " << label << " (" << reason << ")" << endl;
      |                                            ^~~~~~

One mistake, two errors — the second is the compiler recovering from the first. Fix the first, recompile, and the second vanishes. Rung 1 of §5.22, and the habit that saves you the most time this week.


Reps 17–18: Composition

Rep 17 — A Member borrows a Book

Keep Rep 13’s Book. Add Member: private string name, string borrowed_titles[5], int borrowed_count; Member() and Member(string n), both initializer lists setting the count to 0; bool borrow(Book& book) — note the & — returning false if the book is unavailable or the shelf of 5 is full, otherwise calling book.check_out(), recording book.get_title(), bumping the count, returning true; plus void list_borrowed() const and string get_name() const. Add a helper void try_borrow(Member& m, Book& b) that calls m.borrow(b) and prints NAME borrowed 'TITLE'. or NAME could not borrow 'TITLE'.

In main: three books (Mere Christianity / C.S. Lewis, The Reason for God / Timothy Keller, Confessions / Augustine of Hippo) and one Member maya("Maya"). try_borrow on mc, then rfg, then mc again. Then maya.list_borrowed() — printing Maya has N book(s): and one - TITLE per line — then Shelf: and print() on all three.

Expected output:

Maya borrowed 'Mere Christianity'.
Maya borrowed 'The Reason for God'.
Maya could not borrow 'Mere Christianity'.
Maya has 2 book(s):
  - Mere Christianity
  - The Reason for God
Shelf:
  'Mere Christianity' by C.S. Lewis (checked out)
  'The Reason for God' by Timothy Keller (checked out)
  'Confessions' by Augustine of Hippo (available)

Now the experiment that matters. Change one character — bool borrow(Book book), no &. It compiles with zero warnings. Expected output with the & removed:

Maya borrowed 'Mere Christianity'.
Maya borrowed 'The Reason for God'.
Maya borrowed 'Mere Christianity'.
Maya has 3 book(s):
  - Mere Christianity
  - The Reason for God
  - Mere Christianity
Shelf:
  'Mere Christianity' by C.S. Lewis (available)
  'The Reason for God' by Timothy Keller (available)
  'Confessions' by Augustine of Hippo (available)

Maya borrowed the same book twice and the shelf never noticed: borrow checked out a copy that evaporated when the call ended. No error, no warning, wrong answer — symptom three in §5.22’s rung 3. Put the & back.


Rep 18 — A container that owns its books

The thing P4 asks for. Above main, const int MAX_BOOKS = 5;. Keep Rep 13’s Book above TinyLibrary (§5.19 bug 14 shows what happens otherwise).

TinyLibrary: private Book books[MAX_BOOKS] and int book_count; a default constructor setting the count to 0; void add_book(const Book& b), printing Shelf full; 'TITLE' not added. and returning if full, otherwise storing and bumping the count; void list_catalog() const, printing Catalog (N books): then print() for each real slot — loop to book_count, never MAX_BOOKS; Book* find_book(string title), returning &books[i] on a match and nullptr otherwise; ~TinyLibrary(), printing [Library closed: N books.]

In main: add six books as temporaries, e.g. lib.add_book(Book("Orthodoxy", "G.K. Chesterton"));Mere Christianity / C.S. Lewis, The Reason for God / Timothy Keller, Confessions / Augustine of Hippo, Pensees / Blaise Pascal, Orthodoxy / G.K. Chesterton, The Everlasting Man / G.K. Chesterton. List the catalog. Find "Confessions"; if the pointer is not null, print Found: TITLE by AUTHOR and call check_out() through the pointer. Find "The Everlasting Man" and print Not in this library: The Everlasting Man on null. List the catalog again.

Expected output:

Shelf full; 'The Everlasting Man' not added.
Catalog (5 books):
  'Mere Christianity' by C.S. Lewis (available)
  'The Reason for God' by Timothy Keller (available)
  'Confessions' by Augustine of Hippo (available)
  'Pensees' by Blaise Pascal (available)
  'Orthodoxy' by G.K. Chesterton (available)
Found: Confessions by Augustine of Hippo
Not in this library: The Everlasting Man
Catalog (5 books):
  'Mere Christianity' by C.S. Lewis (available)
  'The Reason for God' by Timothy Keller (available)
  'Confessions' by Augustine of Hippo (checked out)
  'Pensees' by Blaise Pascal (available)
  'Orthodoxy' by G.K. Chesterton (available)

Confessions is (available) in the first catalog and (checked out) in the second. That is the payoff of returning a Book*: the pointer aims at the real array slot inside the library, so hunted->check_out() changed the shelf. A finder returning a copy prints (available) twice and costs you an hour.

One last drill. Delete Book’s default constructor and compile. Expected output:

error: no matching function for call to 'Book::Book()'
   36 |     TinyLibrary() : book_count(0) {}
      |                                 ^
note: candidate: 'Book::Book(std::string, std::string)'
note:   candidate expects 2 arguments, 0 provided

Read where it points: TinyLibrary’s constructor, in a class you did not touch. Book books[5]; default-constructs five Books the instant a Library exists, and there is no default constructor to run. The cause is in a different class from the message (§5.19 bug 5). That is why Book() exists. Restore it.


Done? One Last Thing.

Open a fresh file, from_memory_5.cpp. Book closed. Nothing open but the editor. No searching, no AI. Write:

  1. Player: private string name, int hp, int max_hp; Player() and Player(string n, int starting_hp) (initializer lists; hp and max_hp both start at starting_hp); a destructor printing [Player NAME gone]; take_damage(int) clamping at 0; heal(int) clamping at max_hp; bool is_alive() const; string get_name() const; print() const printing NAME: HP/MAX_HP plus (alive) or (down).
  2. Team: private Player roster[4], int player_count; a default constructor; bool add(const Player& p) printing Roster full; NAME not added. and returning false on the fifth; Player* find(string wanted) returning an address or nullptr; print_all() const printing Team (N players): then each player; a destructor printing [Team disbanded: N players].
  3. A main declaring Team team; first, then five players — Maya 30, Marcus 25, Ada 20, Blaise 15, Grace 10 — adding all five; finding "Marcus" and, guarding the pointer, hitting him for 40 then healing 10; finding "Augustine" and printing No such player: Augustine; then print_all() and --- end of main ---.

Expected output:

Roster full; Grace not added.
No such player: Augustine
Team (4 players):
  Maya: 30/30 (alive)
  Marcus: 10/25 (alive)
  Ada: 20/20 (alive)
  Blaise: 15/15 (alive)
--- end of main ---
[Player Grace gone]
[Player Blaise gone]
[Player Ada gone]
[Player Marcus gone]
[Player Maya gone]
[Team disbanded: 4 players]
[Player Blaise gone]
[Player Ada gone]
[Player Marcus gone]
[Player Maya gone]

Check two things in that tail; they are the whole chapter in one block. Marcus is at 10/25 — 25 minus 40 clamped to 0, then healed 10, capped at max_hp. And there are nine [Player ...] lines from five locals plus four roster entries: the five in main die first in reverse declaration order, then team dies and its body prints, and only then do its roster members die, also in reverse.

Clean compile, zero warnings, this output on the first attempt: you have the move. Take the §5.21 checkpoint, then open P4.

If not — and for most people it is not, the first time — the failure is data. Which item broke? Go to its section (§5.10–§5.12 constructors, §5.13 destructors, §5.15 the array, §5.16 the finder and const), redo the two reps that drill it, write it from memory again. That loop is the skill.


Up next: Project 4P4: Stewardship Account & Apologetics Library. Every class you just built is half of a piece of it.