Chapter 6 — Reps
Conditioning, not grading. Type it, compile it, run it. AI off.
Seventeen reps — the heaviest set in the course, for the heaviest week. They climb: pointer mechanics, the heap, the chain, then inheritance and dispatch. Later reps reuse earlier ones, so work in order and keep every file.
Every rep ships with the exact output it produces. Nobody is in the room with you at midnight; the output block is your grader. Compare it line by line, and if one character is off, you have found something worth understanding before moving on.
Build them all the same way, and read the warnings:
g++ -std=c++17 -Wall -Wextra -o rep rep.cpp
Three reps are bug drills — break working code on purpose, then read what the compiler says. The error text below is real GCC 14 output, the compiler family OnlineGDB runs. Your line numbers will be yours and wording shifts between versions, so match the phrase, not the punctuation.
Reps 1–5: Pointer Mechanics
Rep 1 — Three Values
§6.20’s teaser rep, in full. Declare int manuscripts = 12; and int* p = &manuscripts;. Print, on labeled lines: the value, the address, the pointer, and the dereference. Print whether p == &manuscripts. Then write *p = 20; and reprint. Then aim p at int copies = 3; and reprint both.
Expected output:
manuscripts = 12
&manuscripts = 0x16f6d2e24
p = 0x16f6d2e24
*p = 12
p == &manuscripts? yes
--- after *p = 20 ---
manuscripts = 20
*p = 20
--- after p = &copies ---
*p = 3
manuscripts = 20 (unchanged)
Your two hex numbers will not be 0x16f6d2e24 — real addresses change every run. What must match: those two lines print the same thing as each other, p == &manuscripts? says yes, and every other line matches character for character.
Rep 2 — The Null Guard
Write void print_int(int* p) that prints (null) when p == nullptr and *p otherwise. In main, declare int* p = nullptr;, report whether it is null, and call print_int(p). Point it at int year = 96;, report and call again. Finally call print_int(nullptr) directly.
The defensive pattern from §6.2. You will write it dozens of times before Sunday.
Expected output:
p starts as nullptr:
is p null? yes
print_int(p) -> (null)
After p = &year:
is p null? no
print_int(p) -> 96
print_int(nullptr) -> (null)
Rep 3 — Value, Reference, Pointer
Three functions, all trying to add 10 to their argument: void by_value(int n), void by_reference(int& n), void by_pointer(int* n). by_value also prints its local copy so you can watch the change happen and then evaporate. by_pointer guards with if (n != nullptr) before writing. In main, start int count = 100;, call all three in turn printing count after each, then call by_pointer(nullptr) to prove the guard holds.
§6.3’s table, turned into a program.
Expected output:
start: 100
inside by_value, the copy is now 110
after by_value: 100
after by_reference: 110
after by_pointer: 120
after by_pointer(nullptr): 120 (no crash)
Rep 4 — The Arrow
Write a Witness struct with string name, int century, and a void print() const printing name (century N). Build Witness clement (“Clement of Rome”, century 1), take Witness* wp = &clement;, and print (*wp).name, wp->name, and wp->century — proving the two forms are one thing (§6.4). Call wp->print() through the pointer.
Then build Witness ignatius{"Ignatius of Antioch", 2} and Witness irenaeus{"Irenaeus of Lyons", 2}, put all three addresses in Witness* roster[3], and loop calling roster[i]->print(). That array holds three handles, not three witnesses.
Expected output:
(*wp).name = Clement of Rome
wp->name = Clement of Rome
wp->century = 1
wp->print() -> Clement of Rome (century 1)
Walking an array of pointers:
Clement of Rome (century 1)
Ignatius of Antioch (century 2)
Irenaeus of Lyons (century 2)
Rep 5 — Bug Drill: A Dot on a Pointer
Take Rep 4. Change one line — wp->name to wp.name — and compile. This is the most common typo of the week, and the compiler is unusually generous about it.
Expected output: it does not compile. GCC 14 says:
rep05_bug.cpp: In function 'int main()':
rep05_bug.cpp:14:16: error: request for member 'name' in 'wp', which is of pointer type 'Witness*' (maybe you meant to use '->' ?)
14 | cout << wp.name << endl;
It really does ask you that. Learn the phrase “which is of pointer type” — in Project 5 you want to fix this in four seconds, not four minutes. Put the arrow back and confirm Rep 4 builds clean.
Reps 6–8: The Heap
Rep 6 — new, delete, delete[]
Allocate one heap int with new int(96), print it, write 107 through the pointer, print again, delete it, set the pointer to nullptr, and delete it a second time — legal, and does nothing (§6.5).
Then read a size n from cin, allocate int* cubes = new int[n];, fill it with i * i * i, print each element on its own line, release it with delete[]. The teaser used a fixed 10; deciding the size at run time is the whole reason the heap exists.
Expected output, for the stated input 6:
heap int: 96
after writing through the pointer: 107
delete on a nullptr is a no-op. Program still standing.
How many cubes? cubes[0] = 0
cubes[1] = 1
cubes[2] = 8
cubes[3] = 27
cubes[4] = 64
cubes[5] = 125
Freed with delete[]. 2 new, 2 delete.
That fourth line looks wrong and isn’t: this transcript was piped in, so the 6 was never echoed. Type it in OnlineGDB and the 6 appears right after the prompt.
Rep 7 — Owning vs. Borrowed
The most important word in the chapter is ownership (§6.5). Write two functions that differ only in who owes the delete, and comment each one honestly:
int* find_first_over(int arr[], int n, int threshold)— a pointer to the first element greater thanthreshold, ornullptr. Non-owning: the array still owns that memory.int* clone_value(int v)— returnsnew int(v). Owning: the caller mustdeleteit.
In main, print int years[4] = {96, 107, 180, 318};. Find the first value over 100, print it, write 999 through the borrowed pointer, and reprint the array — proving you got a handle to the real element, not a copy. Search for something over 5000 and handle the nullptr. Then clone_value(42), print, and delete.
Expected output:
years: 96 107 180 318
first over 100 -> 107 (borrowed)
after writing 999 through the borrowed pointer:
years: 96 999 180 318
first over 5000 -> not found (nullptr)
owned copy -> 42
owned copy deleted. 1 new, 1 delete.
delete the pointer find_first_over handed you and you have a double-free. Rep 8 shows what that sounds like.
Rep 8 — Bug Drill: Break the Heap Three Ways
Three broken programs, one bug each. Write, compile, read the message, fix.
(a) Mismatched brackets. Allocate new int[5], use it, release it with plain delete cubes;.
rep08a_bug.cpp: In function 'int main()':
rep08a_bug.cpp:10:12: warning: 'void operator delete(void*, std::size_t)' called on pointer returned from a mismatched allocation function [-Wmismatched-new-delete]
10 | delete cubes;
rep08a_bug.cpp:5:27: note: returned from 'void* operator new [](std::size_t)'
A warning, not an error — it still built, and older compilers say nothing at all. Match the brackets.
(b) A dangling pointer. Write int* make_number() that declares int x = 42; and returns &x; dereference the result in main.
rep08b_bug.cpp: In function 'int* make_number()':
rep08b_bug.cpp:6:12: warning: address of local variable 'x' returned [-Wreturn-local-addr]
6 | return &x;
Also a warning. It runs, and may even print 42 — the worst outcome, because then you would ship it.
(c) Double free. Allocate new int(42), print value = 42, delete it, print first delete ok, then delete the same pointer again. This one compiles perfectly clean — no warning at all.
value = 42
first delete ok
Then it dies. Nothing further prints and the shell reports exit status 134 — an aborted process. Now rebuild it with -fsanitize=address -g added, as §6.8 instructs. Observed report, trimmed at the ... marks where the stack frames get long and machine-specific:
value = 42
first delete ok
=================================================================
==21029==ERROR: AddressSanitizer: attempting double-free on 0x6020000000f0 in thread T0:
#1 ... in main rep08c_bug.cpp:9
...
freed by thread T0 here:
#1 ... in main rep08c_bug.cpp:7
...
previously allocated by thread T0 here:
#1 ... in main rep08c_bug.cpp:5
SUMMARY: AddressSanitizer: double-free rep08c_bug.cpp:9 in main
Read those three line numbers together: line 5 allocated it, line 7 freed it, line 9 freed it again. That is not a hint; that is the answer. Reach for -fsanitize=address reflexively — it is the highest-value habit in this chapter.
Reps 9–12: The Chain
Rep 9 — Three Nodes by Hand
Give Witness a Witness* next field. Build a three-node chain from the back, so each new node has something real to point at: Anselm of Canterbury (century 11, next is nullptr), then Augustine of Hippo (5, pointing at Anselm), then Athanasius of Alexandria (4, pointing at Augustine). Name the first node head.
Then three things, each in the canonical shape from §6.6:
- Walk it with
while (current != nullptr), printing each node. - Count it with
for (Witness* w = head; w != nullptr; w = w->next). - Free every node — saving
nextbefore eachdelete— then sethead = nullptr.
Century means the century of the testimony, not of the birth — §6.6’s convention, and Project 5 uses it.
Expected output:
Walking the chain:
Athanasius of Alexandria (century 4)
Augustine of Hippo (century 5)
Anselm of Canterbury (century 11)
Chain length: 3
freeing Athanasius of Alexandria
freeing Augustine of Hippo
freeing Anselm of Canterbury
head is null now? yes
Rep 10 — Trace It on Paper First
Do not type this in yet. Get paper. Read the program, draw the boxes and arrows, and write down — in ink, before you touch a keyboard — exactly what you think all four output lines will be.
#include <iostream>
using namespace std;
struct Node {
int value;
Node* next;
};
int main() {
Node* head = nullptr;
for (int i = 1; i <= 4; i++) {
head = new Node{i * i, head}; // add at the FRONT
}
int sum = 0;
for (Node* c = head; c != nullptr; c = c->next) {
cout << c->value << " ";
sum += c->value;
}
cout << endl;
cout << "sum = " << sum << endl;
Node* second = head->next;
second->value = 100;
for (Node* c = head; c != nullptr; c = c->next) {
cout << c->value << " ";
}
cout << endl;
while (head != nullptr) {
Node* next = head->next;
delete head;
head = next;
}
cout << "head == nullptr? " << (head == nullptr ? "yes" : "no") << endl;
return 0;
}
Now type it, run it, and compare against your paper.
Expected output:
16 9 4 1
sum = 30
16 100 4 1
head == nullptr? yes
(Lines one and three end in a trailing space — the loop prints a space after every value.)
Three places people go wrong. Insertion is at the front, so the list comes out backwards from the order it was built. second is not a copy — it is a second handle on the same node, so writing through it changes what the first loop printed. And the free loop saves next before the delete, the only reason the last line ever runs.
This rep maps straight onto the Part A code-reading exam. If your paper matched the screen, you can read pointer code. If not, redo it with different starting values before moving on.
Rep 11 — Wrap It in a Class
Now hide the pointer work, as §6.7 does. Write a Chain class with a private nested Node struct (name, century, next), a Node* head, an int count, and:
Chain() : head(nullptr), count(0) {}Chain(const Chain&) = delete;andChain& operator=(const Chain&) = delete;add(string name, int century)— one line, inserting at the frontprint() const,size() const- a destructor that walks the chain, prints
releasing <name>per node, frees it
In main, add five witnesses latest-first — Luther (16), Aquinas (13), Anselm (11), Augustine (5), Clement (1) — so front-insertion makes print() walk them earliest-first. Print the chain, the size, and a line announcing the destructor.
Expected output:
Chain of 5 witnesses:
Clement of Rome (century 1)
Augustine of Hippo (century 5)
Anselm of Canterbury (century 11)
Thomas Aquinas (century 13)
Martin Luther (century 16)
size() = 5
Leaving main. The destructor runs now:
releasing Clement of Rome
releasing Augustine of Hippo
releasing Anselm of Canterbury
releasing Thomas Aquinas
releasing Martin Luther
Then break it on purpose. Add Chain c2 = c1; and recompile.
rep11_bug.cpp: In function 'int main()':
rep11_bug.cpp:34:16: error: use of deleted function 'Chain::Chain(const Chain&)'
34 | Chain c2 = c1;
rep11_bug.cpp:13:5: note: declared here
13 | Chain(const Chain&) = delete;
That is the good outcome. Delete the two = delete lines and it compiles silently — then both Chains free the same nodes and you get Rep 8(c) at a distance. Put them back.
Rep 12 — remove(), All Three Cases
Extend Rep 11’s Chain with a private Node* find_node(string) const helper (non-owning — say so in a comment), a public bool contains(string) const built on it, and void remove(string name) covering §6.7’s three cases: head, middle-or-tail, not-found.
The head case is the one people get wrong. You know you are on the head because previous == nullptr.
Seed the same five witnesses. Test contains on a name that is there and one that isn’t; remove the head (Clement), a middle node (Anselm), the tail (Luther), and somebody absent (Napoleon); then print the survivors and the size.
Expected output:
Start: 5 witnesses
Clement of Rome (century 1)
Augustine of Hippo (century 5)
Anselm of Canterbury (century 11)
Thomas Aquinas (century 13)
Martin Luther (century 16)
contains("Augustine of Hippo") -> true
contains("Napoleon") -> false
Removing the head:
Removing a middle node:
Removing the tail:
Removing somebody who isn't there:
(nobody named "Napoleon" in the chain)
End: 2 witnesses
Augustine of Hippo (century 5)
Thomas Aquinas (century 13)
Then rebuild with -fsanitize=address in OnlineGDB: zero leaks, zero errors. This is the rep most likely to leak, and it is Project 5’s Normal tier almost verbatim. Get it clean here and half the project is built.
Reps 13–17: Inheritance and Dispatch
Rep 13 — Base, Derived, protected, Chained Constructors
Write class Vehicle with protected int wheels and double weight, a constructor Vehicle(int w, double m) printing [ctor] Vehicle(N wheels, M kg), a virtual ~Vehicle() printing [dtor] ~Vehicle(), and a virtual void describe() const.
Then class Car : public Vehicle with a private int passengers, a constructor chaining up — Car(int w, double m, int p) : Vehicle(w, m), passengers(p) — printing [ctor] Car(N passengers), a ~Car() override that prints, and a describe() marked override that reads the inherited wheels directly, which is exactly what protected makes legal (§6.11).
Build Car c(4, 1200, 5) inside a block, call describe(), and let the block end so you can watch the destructors fire.
Expected output:
Building a Car:
[ctor] Vehicle(4 wheels, 1200 kg)
[ctor] Car(5 passengers)
A car carrying 5 passengers, riding on 4 wheels.
Leaving the block:
[dtor] ~Car()
[dtor] ~Vehicle()
Construction outside-in, destruction inside-out, every time (§6.12). Now two one-line experiments.
Reach into protected from outside. Add cout << c.wheels << endl; in main:
rep13_bug.cpp: In function 'int main()':
rep13_bug.cpp:20:15: error: 'int Vehicle::wheels' is protected within this context
20 | cout << c.wheels << endl;
rep13_bug.cpp:6:9: note: declared protected here
Skip the base constructor call. Change Car’s constructor to Car(int p) : passengers(p) with no Vehicle(...) in the initializer list:
rep13_bug2.cpp: In constructor 'Car::Car(int)':
rep13_bug2.cpp:28:30: error: no matching function for call to 'Vehicle::Vehicle()'
28 | Car(int p) : passengers(p) {
rep13_bug2.cpp:10:5: note: candidate: 'Vehicle::Vehicle(int, double)'
rep13_bug2.cpp:10:5: note: candidate expects 2 arguments, 0 provided
(Source-echo lines and the generated copy constructor are trimmed here.) Read it as: you didn’t say how to build the Vehicle part, and I can’t guess.
Rep 14 — Bug Drill: virtual and the Silent Failure
Vehicle gets virtual ~Vehicle(), virtual void describe() const, and virtual double cost_per_mile() const returning weight / 1000.0. Car overrides both, its cost_per_mile returning Vehicle::cost_per_mile() * 1.1; Truck overrides both, adding cargo_weight / 500.0 to the base rate. Build Vehicle* fleet[3] holding new Car(4, 1200, 5), new Truck(6, 3500, 2000), and new Vehicle(2, 200), loop once calling both methods, loop again deleting each.
Expected output:
A car carrying 5 passengers.
Cost per mile: $1.32
A truck hauling 2000 kg of cargo.
Cost per mile: $7.5
A vehicle with 2 wheels, 200 kg.
Cost per mile: $0.2
One loop, three behaviors. Now break it in two stages.
Stage 1: delete virtual from Vehicle::describe() only, leaving override on the derived versions.
rep14_bugA.cpp:25:10: error: 'void Car::describe() const' marked 'override', but does not override
25 | void describe() const override {
rep14_bugA.cpp:38:10: error: 'void Truck::describe() const' marked 'override', but does not override
That is override doing the one job it exists for.
Stage 2: delete override from the two derived describe()s as well. Now it compiles clean — no error, no warning:
A vehicle with 4 wheels, 1200 kg.
Cost per mile: $1.32
A vehicle with 6 wheels, 3500 kg.
Cost per mile: $7.5
A vehicle with 2 wheels, 200 kg.
Cost per mile: $0.2
Stare at that. describe() collapsed to the base version for all three objects, while cost_per_mile() — still virtual — dispatched correctly on the same objects in the same loop. One method polymorphic, one not, no diagnostic anywhere. This is the silent bug that closes §6.19, and virtual in the base plus override in every derived class is the whole cure. Restore both.
Rep 15 — Object Slicing
Keep Vehicle and Car with a virtual describe(). Three inspectors, all calling describe(), differing only in the parameter:
void inspect_by_value(Vehicle v);
void inspect_by_reference(const Vehicle& v);
void inspect_by_pointer(const Vehicle* v);
Build one Car c(4, 1200, 5), call c.describe() directly, then hand the same object to all three.
Expected output:
direct: A car carrying 5 passengers.
by value: A vehicle with 4 wheels, 1200 kg.
by reference: A car carrying 5 passengers.
by pointer: A car carrying 5 passengers.
One object, four calls, and the by-value one is provably wrong — no warning, no error (§6.15). The parameter v was never your Car; it was a fresh Vehicle, copy-constructed from the Vehicle-shaped part of it. Which is why a polymorphic collection holds Vehicle* and never Vehicle: an array of objects would slice every element on the way in.
Rep 16 — The Virtual Destructor
The keystone (§6.14) — and this rep makes it visible without a sanitizer.
Give Vehicle a constructor and a virtual ~Vehicle() that both print. Give Car a private int* odometer allocated in its initializer list with new int(0), a constructor printing [ctor] Car — odometer allocated, and a ~Car() override that runs delete odometer; and prints [dtor] ~Car — odometer freed. In main: Vehicle* v = new Car(4, 5);, call v->describe(), then delete v;.
Expected output:
[ctor] Vehicle
[ctor] Car — odometer allocated
A car carrying 5 passengers, odometer 0.
Deleting through a Vehicle*:
[dtor] ~Car — odometer freed
[dtor] ~Vehicle
Done.
Now drop the virtual from ~Vehicle() (and the override from ~Car(), which will no longer compile without it). GCC warns — and only because you asked for -Wall:
rep16_bug.cpp: In function 'int main()':
rep16_bug.cpp:41:5: warning: deleting object of polymorphic class type 'Vehicle' which has non-virtual destructor might cause undefined behavior [-Wdelete-non-virtual-dtor]
41 | delete v;
And it runs anyway. The first three lines are unchanged; the end is not:
Deleting through a Vehicle*:
[dtor] ~Vehicle
Done.
~Car never fired. The odometer is still on the heap and nobody knows its address. The program exits with status 0 and reports nothing — that one missing line is the entire evidence. Compile the same file without -Wall and even the warning disappears. Put virtual back; you now know why this course refuses to build with warnings off.
Rep 17 — Pure Virtual and the Abstract Base
Write class Argument with protected string label and string source, a constructor taking both, a virtual ~Argument() {}, a non-virtual void header() const printing [label] source: source, and one pure virtual:
virtual void defend() const = 0;
Then three concrete subclasses, each with a two-line defend() and a real attribution — Cosmological (Aquinas, Summa Theologiae I, Q.2, A.3, the Third Way), Moral (C.S. Lewis, Mere Christianity, Book 1), Ontological (Anselm, Proslogion 2–3). Hold all three in Argument* case_file[3], loop once calling header() then defend(), loop again deleting each.
Never invent an attribution. Swap in an argument of your own only with a text where that form of it was actually made.
Expected output:
[Cosmological] source: Aquinas, Summa Theologiae I, Q.2, A.3 (Third Way)
The universe is contingent; contingent things need a sufficient cause;
therefore a necessary being exists.
[Moral] source: C.S. Lewis, Mere Christianity, Book 1
Objective moral obligations exist; they need a source beyond preference;
therefore a transcendent moral standard exists.
[Ontological] source: Anselm, Proslogion 2-3
A maximally great being is possible; on the modal analysis possible necessary
existence entails actual existence; therefore such a being exists.
Case file closed. 3 new, 3 delete.
Then try to build the base itself — add Argument a("Cosmological"); to main:
rep17_bug.cpp: In function 'int main()':
rep17_bug.cpp:15:14: error: cannot declare variable 'a' to be of abstract type 'Argument'
15 | Argument a("Cosmological");
rep17_bug.cpp:5:7: note: because the following virtual functions are pure within 'Argument':
rep17_bug.cpp:11:18: note: 'virtual void Argument::defend() const'
One of the most helpful errors in C++: it says the class is abstract and itemizes what you still owe it. You will see it again the moment a Project 5 subclass forgets one.
Done? One Last Thing.
Open a fresh file, from_memory_6.cpp. Close the book, close every tab, write it without looking. Both halves of the week in one program — and the shape of Project 5.
- An abstract
Witnessbase:protected string nameandint century, a constructor taking both, avirtual ~Witness(), avirtual void testify() const = 0;, and a publicstring get_name() const. - Two concrete subclasses,
LetterandTreatise, each chaining to the base constructor and overridingtestify()to print<name> wrote a letter in century <N>./... wrote a treatise in century <N>. - A
Chainwith a private nestedstruct Node { Witness* w; Node* next; };, aNode* head, deleted copy operations, anadd(Witness*)that inserts at the front and takes ownership, apresent() constwalking the chain callingtestify(), and a destructor that — savingnextfirst — printsreleasing <name>, deletes the witness, then the node. - A
mainthat adds aTreatisefor Irenaeus of Lyons (century 2), aLetterfor Polycarp of Smyrna (2), aLetterfor Clement of Rome (1), presents them, and returns.
Match the output exactly — it is the spec.
Expected output:
Chain of witnesses:
Clement of Rome wrote a letter in century 1.
Polycarp of Smyrna wrote a letter in century 2.
Irenaeus of Lyons wrote a treatise in century 2.
Leaving main:
releasing Clement of Rome
releasing Polycarp of Smyrna
releasing Irenaeus of Lyons
Then rebuild in OnlineGDB with -fsanitize=address and confirm zero leaks — two allocations freed per node, every one accounted for. Compare against code/case_file_chain.cpp only after yours runs.
If it works first try, you have the move. If not, the failure names the muscle to re-drill: no dispatch is §6.13, a missing releasing line is §6.14, a crash in the destructor is §6.6, a leak is §6.5. Then take the Checkpoint in §6.21 cold.
Up next: Project 5 — Project 5: Chain of Witnesses & Argument Case File.