Pointers, Dynamic Memory, and Inheritance
Has the faith been continuously believed — and what kinds of arguments are there?
Chapter 6 — Pointers, Dynamic Memory, and Inheritance
“Therefore, since we are surrounded by so great a cloud of witnesses, let us also lay aside every weight, and sin which clings so closely, and let us run with endurance the race that is set before us.” — Hebrews 12:1
“Every scribe who has been trained for the kingdom of heaven is like a master of a house, who brings out of his treasure what is new and what is old.” — Matthew 13:52
This week merges Coding 1 chapters 11 and 12. If you have the sixteen-week book open beside you, this is “Pointers, this, and Dynamic Memory” and “Inheritance in C++” taught as one idea instead of two — because in C++ they are one idea, and §6.9 will show you exactly where they meet.
Your Week at a Glance
Four sessions, about three hours each. Twelve hours. Every session ends with a checkpoint you can verify by yourself, right then, without asking anyone.
| # | Session | ~Hrs | What you do | Checkpoint at the end |
|---|---|---|---|---|
| 1 | Pointer mechanics | 3 | Read §6.1–§6.5. Type and run code/pointer_basics.cpp, code/null_pointer_guard.cpp, code/object_pointers.cpp, code/new_delete.cpp. Do the pointer-mechanics reps in the exercises. | You can say out loud what &x, *p, p->field, and nullptr each mean, and your new_delete run prints the five squares and exits with no crash. |
| 2 | Linked lists and the destructor | 3 | Read §6.6–§6.8. Build code/linked_list_by_hand.cpp from scratch (don’t copy-paste — retype it). Then code/chain_class.cpp. Do the linked-list reps. | You can build a 3-node chain, walk it, and free it — and rerunning it in OnlineGDB with -fsanitize=address prints no leak report. |
| 3 | Inheritance and dispatch | 3 | Read §6.9–§6.16. Run code/vehicle_hierarchy.cpp, code/ctor_dtor_order.cpp, code/slicing_demo.cpp, code/pure_virtual.cpp. Do the inheritance and polymorphism reps. | One for loop over a Base* array prints three different lines, and you can explain why deleting through that array needs virtual ~Base(). |
| 4 | The merge, then start P5 | 3 | Read §6.17–§6.18. Run code/case_file_chain.cpp. Take the Checkpoint in §6.21 cold. Then open Project 5 and get the Normal tier skeleton compiling. | You pass §6.21 at 6 of 8 or better, and your project file compiles clean with -Wall -Wextra even if it does almost nothing yet. |
Now the honest part. Twelve hours covers the reading, the reps, and starting the project. Week 6 is the heaviest week of this course, and P5 is the biggest single build in it — it merges two projects from the sixteen-week edition. If you are going for Normal tier only, twelve to fourteen hours is realistic. If you are going for Medium or Hard, budget more, and start the project on day three, not day six. Pointer bugs do not respond well to panic.
Coach’s Note — Everybody hits a wall in this chapter. It is the week that separates students who understand C++ from students who are surviving it, and you are getting it at double speed. So here is the strategy: do not try to hold the whole chapter in your head at once. Hold one pointer at a time. Draw it. Name what it points at. Ask who is responsible for freeing it. That’s the entire skill.
Why This Matters
For five chapters you have used variables that the compiler put somewhere in memory without telling you where. The compiler picked the spot, the size, and the timing. You just used the variable by name.
This week takes the hood off.
A pointer is a variable whose value is the memory address of another variable. That one idea unlocks four things you cannot do otherwise:
- Refer to a thing without copying it.
- Hand a “handle” to data through a function, and have the function change the original.
- Build data structures whose size is decided while the program is running, not when it is compiled.
- Reach the same data from several places without duplicating it.
That third one is the big one. Last week your Library had Book books[MAX_BOOKS] with MAX_BOOKS = 20 — a fixed twenty-slot array, decided at compile time, most of it usually empty. What if you wanted any number of books? You would have to ask the operating system for memory while the program runs (new) and give it back when you’re finished (delete). That is a pointer story, and it ends in a linked list: a chain of little objects, each one holding the address of the next, exactly as long as the data demands.
For the apologetics theme, that chain is not an accident. The historical chain of Christian witness — Clement of Rome writing to Corinth around AD 96, Ignatius writing on the road to his execution around 107, Polycarp writing to the Philippians, Irenaeus writing Against Heresies around 180, Athanasius, Augustine, Anselm, Aquinas, Luther — is a chain in which each generation points back to the one before, by name and by quotation. The data structure is the metaphor. You will build it.
One direct word before we start, in the LCMS spirit. We are not modeling apostolic succession in the Roman Catholic sense — a sacramental doctrine about authority transmitted by unbroken physical contact, which confessional Lutherans reject. We are modeling the historical continuity of public testimony: the documented record that Christians in every generation professed the same faith and pointed back to the same sources. That’s something Lutherans, Roman Catholics, the Reformed, and the Eastern Orthodox each affirm in their own way, and it’s checkable in any decent library. Use real people. Use real citations. Never invent a quote.
Then, in the second half of the week, inheritance — the feature that lets one class build on another so that shared structure is written once. And the payoff of inheritance, polymorphism: a pointer to a base class that is actually pointing at a derived object, dispatching to the right method automatically. The apologetics frame for that half is the argument case file: cosmological, moral, ontological, teleological arguments all share a shape (premises, a conclusion, a characteristic defensive move) and differ in what they actually argue. The classical apologetic case is several arguments working together — which is precisely what a polymorphic collection is.
Why these two halves are one week is §6.9. Read straight through to it; the answer is not “the calendar made us.”
6.1 — Addresses and Pointers
Every variable lives at some address in memory. You can ask for the address with the address-of operator, &:
int x = 42;
cout << &x << endl; // prints a hex number like 0xffffdd128444
That hex number is the actual location where x lives. The value of x is 42. The address of x is some hex number. Two completely different things.
A pointer is a variable that holds an address. The declaration uses a *:
int x = 42;
int* p = &x; // p holds the address of x
cout << x << endl; // 42
cout << p << endl; // the address
cout << *p << endl; // 42 — "dereference": follow the address back to the value
Three operations to know cold:
&x— “address of x.” Produces a value of typeint*(pointer to int).*p— “the int thatppoints to.” This is the dereference operator, the inverse of&.p->field— whenppoints at a struct or object, this reaches a member. It is shorthand for(*p).field.
A confusion worth clearing up immediately: the * in int* p is part of the declaration. The * in *p = 5 is the dereference operator. Same character, unrelated jobs:
int* p = &x; // declaring p as a pointer to int — the * belongs to the TYPE
*p = 5; // dereferencing p, then assigning 5 to what it points to
Some authors write int *p with the asterisk next to the name; this book writes int* p so the asterisk visually belongs to the type. Both are legal, and mixing them in one file makes your code harder to read, so pick one.
Here is the whole idea in one runnable program — code/pointer_basics.cpp:
#include <iostream>
using namespace std;
int main() {
int x = 42;
int* p = &x; // p holds the address of x
cout << "x = " << x << endl; // the value
cout << "&x = " << &x << endl; // the address
cout << "p = " << p << endl; // same address, stored in p
cout << "*p = " << *p << endl; // follow p back to the value
*p = 100; // write through the pointer
cout << endl;
cout << "After *p = 100:" << endl;
cout << "x = " << x << endl;
cout << "*p = " << *p << endl;
int y = 7;
p = &y; // a pointer can be aimed somewhere else later
cout << endl;
cout << "After p = &y:" << endl;
cout << "*p = " << *p << endl;
cout << "x = " << x << " (unchanged)" << endl;
return 0;
}
Actual output from one run:
x = 42
&x = 0xffffdd128444
p = 0xffffdd128444
*p = 42
After *p = 100:
x = 100
*p = 100
After p = &y:
*p = 7
x = 100 (unchanged)
Your two hex numbers will not be 0xffffdd128444. They are real memory addresses; they change every run and on every machine. What must match is the pattern: &x and p print the same thing as each other, *p prints the same thing as x, and writing through *p changes x itself.
Coach’s Note — The asterisk is doing two unrelated jobs depending on context. In a declaration it is part of the type; in an expression it dereferences. Once your eye sorts those two apart automatically, most of the remaining pointer syntax is bookkeeping.
6.2 — nullptr, and the Pointer That Points Nowhere
A pointer that deliberately points at nothing is a null pointer. In modern C++ you spell it nullptr:
int* p = nullptr;
Dereferencing a null pointer (*p when p == nullptr) is undefined behavior — in practice, an immediate crash. So the defensive pattern, which you will write dozens of times this week, is:
if (p != nullptr) {
*p = 42;
}
nullptr is also how a function says “I didn’t find it.” That’s the shape of code/null_pointer_guard.cpp:
// Returns a pointer to the first element equal to target, or nullptr.
// Caller does NOT delete the result — the array still owns that memory.
int* find_first(int arr[], int n, int target) {
for (int i = 0; i < n; i++) {
if (arr[i] == target) {
return &arr[i];
}
}
return nullptr;
}
Its actual output:
A properly initialized pointer:
p == nullptr? yes
Searching for 4:
found, value = 4
Searching for 9:
not found
data[0] after writing through the found pointer: 30
That last line is the important one. find_first didn’t return a copy of the element. It returned a handle to the real element, and writing through the handle changed the array.
A null pointer is NOT the same as an uninitialized pointer
This is the single most important distinction in the chapter. Read it twice.
int* a = nullptr; // points at nothing — null, but defined.
int* b; // holds whatever garbage was in that memory — uninitialized.
a holds a known value you can test with if (a == nullptr). b holds whatever bytes happened to be sitting there — possibly a valid-looking address pointing into the stack, the heap, or nowhere at all.
Dereferencing b is worse than dereferencing nullptr. When you dereference null, the operating system notices the bad address and kills your program immediately, at the right line, with a clear signal. When you dereference garbage, the value might look like a real address; you write through it; you corrupt some other variable; and your program crashes somewhere else, much later, with no visible connection to the cause — or it never crashes and just produces wrong answers forever.
The rule: initialize every pointer. Even when you have no real address to give it yet:
int* p = nullptr; // ✅ good
int* q; // ❌ wild value
If you take one habit away from this chapter, take that one.
NULL (legacy)
You will see an older spelling — NULL, all caps — in C code and in pre-2011 C++. In modern C++ use nullptr. It is type-safe; NULL is just a macro that expands to 0, which misbehaves in some overload situations.
6.3 — Pointers vs. References
You have been using references (& in parameter lists) since Chapter 3. References and pointers are cousins, not twins.
| Feature | Reference (int&) | Pointer (int*) |
|---|---|---|
| Can be null | No — must refer to something | Yes (nullptr) |
| Can be re-aimed later | No — bound once, at creation | Yes |
| Read the value | Direct: r | Dereference: *p |
| Reach a member | Direct: r.field | Arrow: p->field |
| Declaration | int& r = x; | int* p = &x; |
References are stricter, therefore safer. Pointers are more flexible, therefore more dangerous. Rule of thumb: use a reference if you can, a pointer if you must. You must use a pointer when:
- You need to represent “no value” (
nullptr). - You need to point at a different thing later.
- You need to manage memory yourself with
newanddelete.
Project 5 needs all three. Reach for pointers.
6.4 — Pointers to Structs and Objects: -> and this
Pointers to structs and class objects work like pointers to anything else. The only new syntax is the arrow:
struct Manuscript {
string name;
int century;
};
Manuscript m;
m.name = "Codex Sinaiticus";
m.century = 4;
Manuscript* mp = &m;
cout << (*mp).name << endl; // works, but nobody writes this
cout << mp->name << endl; // the arrow: dereference and reach a member, in one move
mp->name is exactly shorthand for (*mp).name. It works for methods too: wp->print() calls print() on the object wp points at.
Using . where you need -> is the most common typo of the week, and the compiler is unusually kind about it. This is real GCC output:
witnesses.cpp: In function 'int main()':
witnesses.cpp:12:15: error: request for member 'name' in 'p', which is of pointer type 'Witness*' (maybe you meant to use '->' ?)
12 | cout << p.name << endl;
| ^~~~
Yes, it really does ask you that.
this, formally
You met this briefly in Chapter 5. Now it has a type. Inside any non-static method, this is a pointer to the object the method was called on. Inside Witness’s methods, this is a Witness*.
Two places it earns its keep:
1. Disambiguating a shadowed name. When a parameter has the same name as a field:
Witness& set_name(string name) {
this->name = name; // field = parameter
return *this;
}
2. Returning yourself, so calls can chain. return *this; hands back a reference to the object, so the next call can be tacked onto the end:
wp->set_name("Irenaeus of Lyons").set_century(2);
An array of pointers is not an array of objects
Witness* roster[3]; // three slots, each holding a Witness* (or nullptr)
That is not three witnesses. It is three handles, each of which may point at a witness living somewhere else, or at nothing. It is the single most useful shape in the second half of this chapter, because those three slots can point at three different kinds of thing.
All of that is in code/object_pointers.cpp, whose actual output is:
Reaching a struct through a pointer:
(*mp).name = Codex Sinaiticus
mp->name = Codex Sinaiticus
mp->century = 4
Calling a method through a pointer:
Polycarp of Smyrna (century 2)
After chained setters:
Irenaeus of Lyons (century 2)
Walking an array of pointers:
Clement of Rome (century 1)
Ignatius of Antioch (century 2)
Irenaeus of Lyons (century 2)
6.5 — new, delete, and Who Owns What
Every object you have made so far has been stack-allocated: declared inside a function, destroyed automatically when the function ended. Stack allocation is fast and convenient, and it has one hard limit — the size must be known when the program is compiled. Book books[MAX_BOOKS] requires MAX_BOOKS to be a compile-time constant, fixed before the program ever runs.
For memory whose size or lifetime is decided while the program runs, you use the heap:
Book* b = new Book("Mere Christianity", "C.S. Lewis");
// ... use b ...
delete b;
new Book(...) does three things: it grabs enough heap memory for one Book, runs Book’s constructor on that memory, and hands you back a pointer to it. delete b does two: it runs Book’s destructor, then releases the memory.
new and delete come in pairs. Every new you write needs a matching delete on every path out of the code. Miss one and you have leaked — the heap keeps that memory reserved forever, and nobody knows the address anymore, so it can never be reclaimed.
Dynamic arrays, and the brackets that matter
int n;
cin >> n; // size known only at run time
int* arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = i * i;
}
delete[] arr; // note: delete[], not delete
new T[n] must be released with delete[] p. new T must be released with delete p. Mismatching them is undefined behavior. Recent GCC catches it at compile time — this is GCC 14’s wording:
array_delete.cpp:8:12: warning: 'void operator delete(void*, long unsigned int)' called on pointer returned from a mismatched allocation function [-Wmismatched-new-delete]
8 | delete arr;
| ^~~
array_delete.cpp:5:25: note: returned from 'void* operator new [](long unsigned int)'
Older compilers say nothing at all and you find out at run time, if you’re lucky. Match the brackets.
code/new_delete.cpp exercises both, and its actual output is:
Heap int: 42
Heap int after write: 43
squares[0] = 0
squares[1] = 1
squares[2] = 4
squares[3] = 9
squares[4] = 16
delete on a nullptr is a no-op. Program still standing.
That last line is a genuinely useful fact: delete on a null pointer is legal and does nothing. Which is why the habit delete p; p = nullptr; is worth building — after it, a stray second delete p is harmless.
Ownership — the most important word in this chapter
Every heap allocation has exactly one owner: the code responsible for eventually calling delete. Other code may hold pointers to the same memory, but only the owner frees it. Everyone else just looks.
C++ has no keyword for this. It is a contract you keep in your head and document in your comments and your function names. Pointers come in two flavors:
- Owning pointer — the one handle responsible for
delete. The local variable that received anew; a class field storing allocations the class manages. - Non-owning pointer (also called borrowed, or an observer) — a pointer that just looks at memory someone else owns. You never
deleteit.
Book* mine = new Book("Confessions", "Augustine"); // owning — I must delete
Book* found = library.find_book("Confessions"); // non-owning — the library owns it
Deleting a non-owning pointer creates a double-free when the real owner frees it later. That is worse than a leak: a leak wastes memory, a double-free corrupts the heap.
How to document ownership without a keyword:
- Name functions honestly.
find_bookreturns a borrowed pointer;create_bookreturns one you own. - Put a one-line comment above the function:
// Caller does NOT delete the result.or// Caller takes ownership. - Do it every time. You will not remember in three days.
Coach’s Note — Professional C++ automates all of this with
std::unique_ptr,std::shared_ptr, and the RAII pattern — writeunique_ptr<Book> b = make_unique<Book>(...)and thedeletehappens for you whenbgoes out of scope. Java’s garbage collector, which arrives in Chapter 7, does the same thing for all memory by default. We are not using either, because the point of this chapter is to feel the mechanics. By the time you ship Project 5 you should never want to manage memory by hand again. That is not a failure of the lesson. That is the lesson.
6.6 — Self-Referencing Structures: The Linked List
Here is the move pointers unlock. A struct that contains a pointer to its own type:
struct Witness {
string name;
int century;
string testimony;
Witness* next; // a Witness that points at another Witness
};
Each witness knows who comes next. Wire several together and you have a linked list: nodes connected by pointers, with the last node’s next set to nullptr to mark the end. The list is named by its first node, which everyone calls the head.
Building three by hand:
Witness* irenaeus = new Witness{"Irenaeus of Lyons", 2, "Against Heresies", nullptr};
Witness* polycarp = new Witness{"Polycarp of Smyrna", 2, "Letter to the Philippians", irenaeus};
Witness* clement = new Witness{"Clement of Rome", 1, "1 Clement", polycarp};
Witness* head = clement;
Notice the order: build from the back, so each new node has something real to point at. Notice also that every field is initialized, including next. An uninitialized next is a wild pointer and the walk below will drive off a cliff.
The traversal loop. Memorize this. It is the canonical shape and you will write it twenty times this week:
Witness* current = head;
while (current != nullptr) {
cout << current->name << " (century " << current->century << ")" << endl;
current = current->next;
}
Read it as: start at the head; while you are standing on a real node, do something with it, then step to the next one; when you step onto nullptr, you have run off the end and the loop stops.
The same walk written as a for loop, which some people find clearer:
for (Witness* w = head; w != nullptr; w = w->next) {
length++;
}
Freeing the chain. This is where beginners get hurt:
current = head;
while (current != nullptr) {
Witness* next = current->next; // save next BEFORE the delete
delete current;
current = next;
}
head = nullptr;
Save next first. If you write delete current; current = current->next; you are reading a field out of memory you just handed back to the system. Sometimes it happens to work. That is the worst possible outcome, because it means you’ll ship it.
code/linked_list_by_hand.cpp is the complete program. Its actual output:
Walking the chain:
Clement of Rome (century 1): 1 Clement
Polycarp of Smyrna (century 2): Letter to the Philippians
Irenaeus of Lyons (century 2): Against Heresies
Chain length: 3
freeing Clement of Rome
freeing Polycarp of Smyrna
freeing Irenaeus of Lyons
Chain freed. 3 new, 3 delete.
Why linked lists? They grow without a ceiling. Adding a node is new, then rewire one pointer. There is no fixed size, no pre-allocated empty slots, no “array is full” case. The chain is exactly as long as the data demands.
The cost: reaching the k-th element means walking k pointers — O(n) instead of an array’s instant arr[k]. Different tradeoffs for different jobs. A chain of historical witnesses that you mostly walk front-to-back is a very good fit.
A note on the data. In this book,
centurymeans the century of the testimony — when the person wrote the thing you are citing — not the century they were born in. Several of these people lived across a century boundary. Ignatius was born around AD 35 but wrote his letters around 107, so hiscenturyis 2. Fixing the convention keeps everyone’s data comparable.
6.7 — Wrapping the List in a Class
Raw nodes in main are fine for learning and terrible for building. The next step is to hide the pointer work inside a class, exactly as Chapter 5 taught you: private data, public methods, a destructor that cleans up.
Here is the heart of code/chain_class.cpp — an excerpt, with print’s body left out because you already wrote it in §6.6 and the seed data and main left out because they are ordinary:
class Chain {
private:
struct Node {
string name;
int century;
string testimony;
Node* next;
};
Node* head;
int count;
// Returns the first node with this name, or nullptr. NON-OWNING:
// the Chain still owns the node. Nobody else ever deletes it.
Node* find_node(string name) const {
Node* current = head;
while (current != nullptr) {
if (current->name == name) {
return current;
}
current = current->next;
}
return nullptr;
}
public:
Chain() : head(nullptr), count(0) {}
Chain(const Chain&) = delete;
Chain& operator=(const Chain&) = delete;
~Chain() {
Node* current = head;
while (current != nullptr) {
Node* next = current->next; // save next BEFORE the delete
delete current;
current = next;
}
head = nullptr;
count = 0;
}
void add(string name, int century, string testimony) {
head = new Node{name, century, testimony, head};
count++;
}
bool contains(string name) const {
return find_node(name) != nullptr;
}
void remove(string name) {
Node* current = head;
Node* previous = nullptr;
while (current != nullptr && current->name != name) {
previous = current;
current = current->next;
}
if (current == nullptr) { // not-found case
cout << " (nobody named \"" << name << "\" in the chain)" << endl;
return;
}
if (previous == nullptr) { // head case
head = current->next;
} else { // middle or tail case
previous->next = current->next;
}
delete current;
count--;
}
void print() const { /* the traversal loop from §6.6 */ }
int size() const { return count; }
};
Five things to notice.
1. Node is a private nested type. It is an implementation detail of Chain; code outside doesn’t need to know the class stores nodes at all.
2. add inserts at the front, in one line. new Node{name, century, testimony, head} builds a node whose next is the old head, and then head = n makes it the new head. Constant time, no traversal. Because it pushes onto the front, adding witnesses in reverse-chronological order makes print walk them earliest-first.
3. remove has three cases and you must handle all three. Head (there is no previous node, so head itself moves), middle-or-tail (the previous node’s next skips over the doomed node), and not-found (do nothing, gracefully). The previous == nullptr test is how you know you’re on the head.
4. The destructor finally does real work. In Chapter 5 your destructors mostly printed a message. This one walks the whole chain and frees every node. Without it, every Chain that goes out of scope leaks all of its nodes. With it, cleanup is automatic and total — which is exactly the trade C++ offers you: manage it by hand, but manage it in one place.
5. Copying is forbidden, on purpose. See below.
Its actual output:
Chain of 8 witnesses:
Clement of Rome (century 1): 1 Clement, c. 96
Polycarp of Smyrna (century 2): Letter to the Philippians
Irenaeus of Lyons (century 2): Against Heresies, c. 180
Athanasius of Alexandria (century 4): On the Incarnation, c. 318
Augustine of Hippo (century 5): Confessions, c. 400
Anselm of Canterbury (century 11): Proslogion, 1078
Thomas Aquinas (century 13): Summa Theologiae, c. 1265-1274
Martin Luther (century 16): 95 Theses, 1517
contains("Augustine of Hippo") -> true
contains("Napoleon") -> false
Removing the head, a middle node, and a name that isn't there:
(nobody named "Napoleon" in the chain)
Chain is now 6 witnesses:
Polycarp of Smyrna (century 2): Letter to the Philippians
Irenaeus of Lyons (century 2): Against Heresies, c. 180
Athanasius of Alexandria (century 4): On the Incarnation, c. 318
Anselm of Canterbury (century 11): Proslogion, 1078
Thomas Aquinas (century 13): Summa Theologiae, c. 1265-1274
Martin Luther (century 16): 95 Theses, 1517
Stress test:
after 100 adds: size = 106
after 50 removes: size = 56
Coach’s Note — Do NOT copy a Chain
Try this:
Chain c1;
c1.add("Augustine of Hippo", 5, "Confessions, c. 400");
Chain c2 = c1; // looks innocent...
Without protection, that line compiles and plants a time bomb. C++‘s default copy constructor does a shallow copy — it copies c1.head byte-for-byte into c2.head. Now two Chains hold pointers to the same nodes. When c2 dies, its destructor frees all of them. When c1 dies, its destructor walks the same, already-freed nodes and frees them again. That’s a double-free, and it will crash your program far away from the actual mistake.
The general principle: when a class owns heap memory, writing a destructor is not enough — you must also decide what happens when the class is copied. Professional C++ calls this the Rule of Three (destructor, copy constructor, copy assignment) or, in modern C++, the Rule of Five. We are not teaching the full ceremony in this course, but you need to know the rule exists.
For Project 5, the cleanest answer is to forbid copying outright:
Chain(const Chain&) = delete; // no copying
Chain& operator=(const Chain&) = delete; // no assigning either
Now Chain c2 = c1; is a compile error, and a wonderfully clear one:
chain.cpp: In function 'int main()':
chain.cpp:17:16: error: use of deleted function 'Chain::Chain(const Chain&)'
17 | Chain c2 = c1;
| ^~
chain.cpp:10:5: note: declared here
10 | Chain(const Chain&) = delete;
| ^~~~~
Loud, immediate, at the right line, before the bomb ever ships. That is the right kind of error.
6.8 — Leaks, Dangling Pointers, and How to Find Them
Three failure modes, all of them yours to prevent.
A memory leak is heap memory you never freed. The classic shape:
void leaky_function() {
int* p = new int[1000];
// ... use p ...
return; // ❌ forgot delete[] — and now nobody knows the address
}
A dangling pointer is a pointer to memory that has already been freed. Using one is a use-after-free:
Book* b = new Book("Confessions", "Augustine");
Book* alias = b; // both point at the same heap object
delete b;
cout << alias->title; // ❌ dangling — undefined behavior
Its close relative is returning the address of a local variable, which dangles the instant the function returns. GCC catches that one for free:
bad_return.cpp: In function 'int* make_number()':
bad_return.cpp:6:12: warning: address of local variable 'x' returned [-Wreturn-local-addr]
6 | return &x;
| ^~
A double-free is calling delete twice on the same address, usually because two pointers thought they owned it.
How to find them
1. AddressSanitizer in OnlineGDB — the default for this course. Add -fsanitize=address to your compiler flags (the gear icon, then “Extra Compiler Flags” — the same box where you already put -Wall -Wextra, space-separated):
g++ -std=c++17 -Wall -Wextra -fsanitize=address witnesses.cpp -o witnesses
OnlineGDB runs Linux, so AddressSanitizer there includes LeakSanitizer. Your program then checks itself as it runs and prints a report on exit naming the file and line of the new that was never freed. Here is a real one, from a two-node chain where only the head was deleted:
Clement of Rome -> Polycarp of Smyrna
=================================================================
==31==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 40 byte(s) in 1 object(s) allocated from:
#0 ... in operator new(unsigned long)
#1 ... in main /home/student/leaky_chain.cpp:12
Line 12 is the new for the second node. The report is doing your debugging for you: it tells you where the orphaned allocation was born. (The Direct leak of 40 byte(s) is the node itself; a report like this usually also lists an Indirect leak for the string inside it.)
2. valgrind, on Linux, if you ever build locally:
valgrind --leak-check=full ./witnesses
Slower than ASan, extremely thorough.
3. Pair every new with a delete by inspection. Slowest, most reliable, works everywhere, and it is the discipline that eventually makes the tools unnecessary.
Warning — if you’re working locally on a Mac, read this
Neither tool above will find leaks on a modern Mac.
valgrinddoes not support Apple-Silicon (M-series) Macs at all.- AddressSanitizer does run on macOS and will still catch use-after-free, double-free, and buffer overruns — but its leak detection is disabled on Darwin. Ask for it explicitly and ASan tells you so, in those words:
AddressSanitizer: detect_leaks is not supported on this platform.So a clean run on your Mac is not evidence that your program is leak-free — the tool never looked. Do the leak check in OnlineGDB, which runs Linux. That is this course’s official environment and the one your work is graded in.
6.9 — The Bridge: Why Inheritance Belongs in the Same Week as Pointers
Stop here for a minute. This section is the reason these two halves are one week rather than two.
You could teach inheritance without pointers. Plenty of books do, and the result is a student who can write class Car : public Vehicle and cannot explain why any of it matters. Here is the thing those books have to skip:
In C++, polymorphism is a pointer feature.
Not a class feature. Not a keyword feature. A pointer feature. The whole payoff of inheritance — write one loop, get correct behavior for a dozen different types — only happens when you are holding the object through a pointer or a reference to its base class. Hold it by value and the polymorphism evaporates (§6.15, object slicing, is exactly that failure). Which means the second half of this week is unreachable without the first half.
Look at how tightly the two halves interlock:
| First half (pointers) | Second half (inheritance) | Where they meet |
|---|---|---|
Base* p can point at anything | A Car is a Vehicle | A Vehicle* can hold a Car |
| An array of pointers, §6.4 | Several classes sharing an interface | Vehicle* fleet[3] — one loop, three behaviors |
delete runs a destructor, §6.5 | Derived classes have their own destructors | virtual ~Base() — §6.14, the keystone |
| Someone owns each allocation, §6.5 | A container holds Base* it created | The container’s destructor deletes every element |
| A destructor that frees a chain, §6.7 | Each chained item is polymorphic | §6.17 — one program, both halves |
Read the third row again, because it is the sharpest point. When you write delete v; where v is a Vehicle* that actually points at a Car, C++ has to decide at run time which destructor to run. That decision is dynamic dispatch — an inheritance feature — being triggered by delete — a pointer feature. Neither half of the week makes sense without the other. A student who learned inheritance in isolation writes a base class with a non-virtual destructor, and every derived object they ever delete through a base pointer half-destructs, silently, forever.
So: same week. On purpose. Keep going.
6.10 — Base Classes and Derived Classes
Consider three related types: Car, Truck, Motorcycle. They all have wheels, a weight, a top speed. They all describe themselves and compute a cost per mile. Written as three independent classes, the shared parts get duplicated three times — and when the shared logic changes, you update three places and miss one.
Inheritance lets one class build on another:
class Vehicle {
public:
int wheels;
double weight;
void describe() const {
cout << "A vehicle with " << wheels << " wheels weighing "
<< weight << " kg." << endl;
}
};
class Car : public Vehicle {
public:
int passengers;
};
Read the second declaration out loud: “Car is a Vehicle, publicly.” Car automatically has everything Vehicle has — wheels, weight, describe() — plus the passengers field it added:
Car c;
c.wheels = 4; // inherited
c.weight = 1200; // inherited
c.passengers = 5; // Car's own
c.describe(); // inherited
Vocabulary you’ll see everywhere:
- Base class (also parent, superclass): the class being inherited from.
Vehicle. - Derived class (also child, subclass): the class doing the inheriting.
Car.
The public in : public Vehicle means “what was public in Vehicle stays public in Car, and a Car is recognized as a Vehicle.” The alternative, : private Vehicle, is rare and this course does not use it.
6.11 — protected: The Middle Access Level
You know public (visible everywhere) and private (visible only inside the class). Inheritance introduces a third: protected — visible inside the class and inside any class derived from it, but not to outside code.
class Vehicle {
protected:
int wheels;
double weight;
public:
Vehicle(int w, double m) : wheels(w), weight(m) {}
};
class Car : public Vehicle {
public:
Car(int w, double m) : Vehicle(w, m) {}
void show() const { cout << wheels << endl; } // ✅ legal — derived sees protected
};
int main() {
Car c(4, 1200);
cout << c.wheels << endl; // ❌ not legal from out here
}
That last line produces:
protected_access.cpp: In function 'int main()':
protected_access.cpp:18:15: error: 'int Vehicle::wheels' is protected within this context
18 | cout << c.wheels << endl;
| ^~~~~~
protected_access.cpp:6:9: note: declared protected here
Reach for protected when subclasses need to read or write the data directly and outside code has no business touching it. Project 5’s Argument base declares its shared fields protected so each subclass’s defend() can read them directly — the right call there, because the hierarchy is shallow, the subclasses only read the fields, and there are no invariants to protect.
Coach’s Note — Outside small focused hierarchies like this one, the modern engineering instinct is to keep data
privateand give subclasses protected getter methods instead. That hides the representation while still letting subclasses do specific things with it. Worth knowing as a habit; not what Project 5 asks for.
6.12 — Constructor Chaining (and the Mirror Image on the Way Out)
When a derived object is built, the base class constructor runs first. Always. The Vehicle parts of a Car have to exist before the Car-specific parts can be set up.
If the base has only a parameterized constructor, the derived class must call it explicitly, in the initializer list:
class Vehicle {
public:
int wheels;
Vehicle(int w) : wheels(w) {}
};
class Car : public Vehicle {
public:
int passengers;
Car(int w, int p) : Vehicle(w), passengers(p) {}
// ^^^^^^^^^^ call the base constructor, first
};
Forget that call and the compiler goes looking for a Vehicle() that doesn’t exist:
vehicles.cpp: In constructor 'Car::Car(int)':
vehicles.cpp:13:30: error: no matching function for call to 'Vehicle::Vehicle()'
13 | Car(int p) : passengers(p) {}
| ^
vehicles.cpp:7:5: note: candidate: 'Vehicle::Vehicle(int)'
vehicles.cpp:7:5: note: candidate expects 1 argument, 0 provided
Read that as: “you didn’t say how to build the Vehicle part, and I can’t guess.”
Destruction is the exact mirror. Derived destructor body first, then base. You never chain destructors by hand; the compiler does it.
code/ctor_dtor_order.cpp makes all of this visible with a three-level hierarchy — FineTuning derives from Teleological derives from Argument — and traces every constructor and destructor as it fires. Its actual output:
1. Building a FineTuning on the stack:
[ctor] Argument(Fine-Tuning Argument)
[ctor] Teleological()
[ctor] FineTuning()
Fine-tuning: the physical constants sit in a narrow life-permitting band.
(leaving the block — destructors fire now)
[dtor] ~FineTuning()
[dtor] ~Teleological()
[dtor] ~Argument(Fine-Tuning Argument)
2. Building one on the heap, held by a BASE pointer:
[ctor] Argument(Fine-Tuning Argument)
[ctor] Teleological()
[ctor] FineTuning()
Fine-tuning: the physical constants sit in a narrow life-permitting band.
(calling delete through Argument*)
[dtor] ~FineTuning()
[dtor] ~Teleological()
[dtor] ~Argument(Fine-Tuning Argument)
Construction runs outside-in: Argument, Teleological, FineTuning. Destruction runs inside-out: FineTuning, Teleological, Argument. And section 2 shows the whole sequence still firing correctly when the object is destroyed through an Argument* — which only works because ~Argument is virtual. That’s §6.14.
6.13 — virtual and override: Dynamic Dispatch
Inheritance by itself shares structure. Virtual methods are what give you dispatch: the right version of a method runs based on what the object actually is, not what type of pointer you happen to be holding it with.
Mark the method virtual in the base:
class Vehicle {
protected:
int wheels;
double weight;
public:
Vehicle(int w, double m) : wheels(w), weight(m) {}
virtual ~Vehicle() {} // see §6.14 — required as soon as anything is virtual
virtual void describe() const {
cout << "A vehicle with " << wheels << " wheels, " << weight << " kg." << endl;
}
virtual double cost_per_mile() const {
return weight / 1000.0;
}
};
Then override it in the derived classes:
class Car : public Vehicle {
private:
int passengers;
public:
Car(int w, double m, int p) : Vehicle(w, m), passengers(p) {}
void describe() const override {
cout << "A car carrying " << passengers << " passengers." << endl;
}
double cost_per_mile() const override {
return Vehicle::cost_per_mile() * 1.1; // 10% over the base rate
}
};
Two details in that snippet are worth their own sentence.
Base::method() calls the base version explicitly. Vehicle::cost_per_mile() inside Car::cost_per_mile() is not infinite recursion — it reaches past the override to the version it replaced. Extremely useful when the derived behavior is “whatever the base does, plus something.”
override is optional and you should always write it. It tells the compiler “I intend to be overriding a virtual method from the base — please check.” Get the name or the signature wrong and you hear about it immediately:
override_typo.cpp:12:10: error: 'void Car::describe() const' marked 'override', but does not override
12 | void describe() const override { cout << "A car." << endl; }
| ^~~~~~~~
Without override, that same mistake compiles perfectly and creates a brand-new unrelated method that nothing ever calls. Your polymorphism then “doesn’t work” and there is no error message anywhere to explain why. This is the single most common silent bug in the second half of this chapter, and one keyword prevents all of it.
The payoff: a polymorphic collection
const int N = 3;
Vehicle* fleet[N] = {
new Car(4, 1200, 5),
new Truck(6, 3500, 2000),
new Vehicle(2, 200)
};
for (int i = 0; i < N; i++) {
fleet[i]->describe();
cout << " Cost per mile: $" << fleet[i]->cost_per_mile() << endl;
}
for (int i = 0; i < N; i++) {
delete fleet[i];
}
The static type of every element is Vehicle*. The dynamic types are Car, Truck, and Vehicle. Calling ->describe() looks at the real object and runs its version. That is dynamic dispatch, and it is the whole point.
code/vehicle_hierarchy.cpp is that program. Actual 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 different outputs. The loop does not know or care what kinds of vehicle are in the array — and, crucially, you can add a fourth kind without touching the loop at all.
Coach’s Note — Without
virtual,fleet[i]->describe()always runsVehicle’s version, because the compiler dispatches on the static type. Withvirtual, the dispatch happens at run time on the actual type. The overwhelming majority of “polymorphism doesn’t work” bugs are a missingvirtualin the base or a missingoverridein the derived. Write both, every time.
6.14 — Virtual Destructors: Where the Two Halves Meet
This is the keystone section of the week. It is a pointer rule and an inheritance rule at the same time.
If a class has any virtual methods, give it a virtual destructor.
Here is why. When you write delete v; where v is a Vehicle* that actually points at a Car, C++ has to pick a destructor. If ~Vehicle is not virtual, the compiler dispatches statically on the pointer’s type: only ~Vehicle runs. ~Car never fires. Anything the Car allocated — a heap buffer, a chain of nodes, a file handle — leaks. Every time. Silently.
With virtual ~Vehicle() {}, the destructor dispatches dynamically like any other virtual method: ~Car runs, then ~Vehicle runs, exactly as §6.12’s trace showed.
GCC, with -Wall, warns you about the missing keyword:
fleet.cpp: In function 'int main()':
fleet.cpp:18:5: warning: deleting object of polymorphic class type 'Vehicle' which has non-virtual destructor might cause undefined behavior [-Wdelete-non-virtual-dtor]
18 | delete v;
| ^~~~~~~~
Note that it is a warning, not an error. Your program builds, runs, and looks fine. Worse: compile that same file without -Wall and GCC says nothing whatsoever. This is precisely why the course insists on -Wall -Wextra and a zero-warning build. The compiler is willing to tell you — but only if you asked it to talk, and only if you read what it says.
The body of a virtual destructor is usually empty — virtual ~Vehicle() {}. Declaring it virtual is the entire job.
6.15 — Object Slicing
Pass a derived object by value into a parameter of the base type, and the derived parts get sliced off:
void inspect(Teleological a) { // by VALUE
a.defend();
}
FineTuning ft;
inspect(ft); // ❌ a is a freshly-built Teleological, copied from ft's base part only
The parameter a is not the object you passed. It is a brand-new Teleological, copy-constructed from only the Teleological portion of ft. The FineTuning parts — including its override of defend() — are gone. This is object slicing, and there is no warning and no error. It just quietly does the wrong thing.
The fix is to stop copying:
void inspect(const Teleological& a) { a.defend(); } // by reference — ✅
void inspect(const Teleological* a) { a->defend(); } // by pointer — ✅
Both keep the real object, so both dispatch correctly. code/slicing_demo.cpp hands the same object to all three inspectors. Actual output:
Calling ft.defend() directly:
direct: Fine-tuning: the physical constants sit in a narrow life-permitting band.
Handing the same object to three inspectors:
by value: Design: ordered means toward ends suggest a designer (Paley).
by reference: Fine-tuning: the physical constants sit in a narrow life-permitting band.
by pointer: Fine-tuning: the physical constants sit in a narrow life-permitting band.
One object, three parameter styles, and the by-value one is provably wrong. This is also why you cannot store polymorphic objects in a Vehicle array — Vehicle fleet[3] would slice every one of them. Polymorphic collections hold pointers. Back to §6.9: polymorphism is a pointer feature.
6.16 — Pure Virtual Methods: A Preview of Abstraction
Sometimes a base class has no sensible default. What would a generic Argument::defend() even print? The base exists to declare that every argument defends itself, not how.
For that, declare the method pure virtual — = 0 instead of a body:
class Argument {
protected:
string label;
string source;
public:
Argument(string l, string s) : label(l), source(s) {}
virtual ~Argument() {}
virtual void defend() const = 0; // pure virtual — no body
virtual void respond_to_skeptic() const = 0;
void header() const { cout << "[" << label << "] source: " << source << endl; }
};
A class with at least one pure virtual method is an abstract class. You cannot create one:
Argument a; // ❌
case_file.cpp: In function 'int main()':
case_file.cpp:11:14: error: cannot declare variable 'a' to be of abstract type 'Argument'
11 | Argument a;
| ^
case_file.cpp:4:7: note: because the following virtual functions are pure within 'Argument':
4 | class Argument {
| ^~~~~~~~
case_file.cpp:7:18: note: 'virtual void Argument::defend() const'
Notice how helpful that error is: it tells you the class is abstract and lists exactly which methods you still owe it. You can still declare Argument* pointers and Argument& references — which is all a polymorphic collection needs. Any subclass that implements every pure virtual becomes concrete and instantiable; any subclass that misses one stays abstract.
code/pure_virtual.cpp builds two concrete arguments on that abstract base. Actual output:
[Cosmological Argument] source: Aquinas, Summa Theologiae I, Q.2, A.3 (Third Way)
1. Contingent things need a sufficient cause.
2. The universe is contingent.
3. An infinite regress of contingent causes explains nothing.
4. Therefore a necessary being exists.
Skeptic: "Then what caused God?"
Response: a necessary being is precisely the kind of thing that does not require a cause.
[Moral Argument] source: C.S. Lewis, Mere Christianity, Book 1
1. Objective moral obligations exist.
2. Obligations need a source beyond preference.
3. Therefore a transcendent moral standard exists.
Skeptic: "Morality is just evolved preference."
Response: explaining how we came to feel an obligation is not the same as explaining why it binds.
This is your first look at abstraction, which Chapter 8 takes up properly in Java, where the same idea wears the keyword abstract and gains a cousin called an interface. Same concept. Different clothes.
Coach’s Note (confessional Lutheran framing) — Luther was famously wary of natural theology — reasoning up to God from creation alone, what he called a theology of glory — and centered his own work on the theology of the cross: God revealed in Christ crucified, received by faith through Word and Sacrament, not deduced. The arguments in this chapter and in Project 5 belong to the broader Christian tradition, and Lutheran apologetics has generally treated them as secondary witnesses to general revelation rather than the foundation of faith. Use them as conversation tools. Don’t preach with them. And notice what the code is teaching you theologically almost by accident: no single argument is “the” argument. A
CaseFileholding oneArgument*would be a bad case file and a bad use of polymorphism.
6.17 — Putting Both Halves in One Program
Here is the week in a single build: a hand-rolled linked list whose every node owns a polymorphic object.
The Argument base below is §6.16’s, with two small changes: defend() has an ordinary default body instead of being pure (so the base is concrete and the code compiles even before you add a subclass), and it gains one accessor, string get_label() const { return label; }, so the container can name what it is releasing. Everything else is the same. The whole file, base classes included, is code/case_file_chain.cpp.
class CaseFile {
private:
struct Node {
Argument* arg; // OWNED by this node
Node* next;
};
Node* head;
Node* tail;
int count;
public:
CaseFile() : head(nullptr), tail(nullptr), count(0) {}
CaseFile(const CaseFile&) = delete;
CaseFile& operator=(const CaseFile&) = delete;
~CaseFile() {
Node* current = head;
while (current != nullptr) {
Node* next = current->next; // save next BEFORE deleting
cout << " releasing " << current->arg->get_label() << endl;
delete current->arg; // the argument the node owns
delete current; // then the node itself
current = next;
}
head = nullptr;
tail = nullptr;
count = 0;
}
// The CaseFile TAKES OWNERSHIP of a. Do not delete it yourself.
void add(Argument* a) {
Node* n = new Node{a, nullptr};
if (head == nullptr) { head = n; } else { tail->next = n; }
tail = n;
count++;
}
// NON-OWNING: returns a borrowed pointer, or nullptr. Do not delete it.
Argument* find(string label) const {
for (Node* current = head; current != nullptr; current = current->next) {
if (current->arg->get_label() == label) {
return current->arg;
}
}
return nullptr;
}
void present_all() const {
for (Node* current = head; current != nullptr; current = current->next) {
current->arg->header();
current->arg->defend(); // one call, three behaviors
cout << endl;
}
}
int size() const { return count; }
};
Count how many ideas from this week are load-bearing in that one class:
- A nested
Nodewith a self-referencingnextpointer (§6.6). - A
tailpointer soaddappends in order without walking the list (§6.6). - Deleted copy operations, because it owns heap memory twice over (§6.7).
- A destructor that walks the chain and frees two things per node — the argument, then the node (§6.7, §6.5).
- A
findthat returns a documented non-owning pointer, ornullptr(§6.2, §6.5). present_allcalling a virtual method through a base pointer, so one loop produces three different cases (§6.13).- And, invisibly holding the whole thing together,
virtual ~Argument()— without it,delete current->argwould run only the base destructor (§6.14).
code/case_file_chain.cpp is the complete program. Actual output:
Case file holds 3 arguments.
[Cosmological] Aquinas, Summa Theologiae I, Q.2, A.3 (Third Way)
The universe is contingent; contingent things need a sufficient cause;
an infinite regress of them explains nothing; therefore a necessary being exists.
[Moral] C.S. Lewis, Mere Christianity, Book 1
Objective obligations exist; they are not settled by preference or majority;
therefore they point past us to a transcendent standard.
[Ontological] Anselm, Proslogion 2-3; modal form in Plantinga
A maximally great being is possible; possible necessary existence, on the modal analysis,
entails actual existence; therefore such a being exists.
Pulling one argument back out of the chain:
[Moral] C.S. Lewis, Mere Christianity, Book 1
find("Transcendental") returned nullptr, as it should.
Leaving main. The CaseFile destructor runs now:
releasing Cosmological
releasing Moral
releasing Ontological
Compile that with -fsanitize=address in OnlineGDB and you get no leak report at all. That is the standard for Project 5.
6.18 — When Not to Inherit
Inheritance is powerful and badly overused. Four warnings.
1. Deep hierarchies are usually a mistake. Three or four levels is rare in good code and normally a smell. Project 5’s argument hierarchy goes two levels — three at Hard tier. That is the right shape for the problem.
2. “Is-a” versus “has-a.” Use inheritance only when the derived class genuinely is a kind of the base. A Car is a Vehicle. A FineTuning argument is a Teleological argument. If you are reaching for inheritance just to share a method or two, you want composition instead — the class has a helper object.
3. Composition over inheritance is the modern engineering watchword. When one class can simply contain another, do that. Reach for inheritance when there is a real is-a relationship and you need polymorphism.
4. Multiple inheritance — we don’t teach it. C++ technically lets a class inherit from more than one base (class Hybrid : public Car, public Boat). It opens the door to the diamond problem (two bases sharing a grandparent produce duplicated or ambiguous members) and has a well-earned reputation as a footgun. Single inheritance plus interfaces covers everything you need for clean OO design, and that is the road Java takes — one parent via extends, as many interfaces as you like via implements, which you will meet in Chapter 8. If you ever meet a real codebase using multiple inheritance, learn it then.
Coach’s Note — Java is somewhat infamously a language where inheritance gets overused; its standard library has four- and five-level hierarchies that a modern engineer would refactor. When you shift to Java next week, watch for it. Just because a language makes inheritance easy doesn’t mean every problem wants it.
6.19 — Common Bugs (Week 6 Edition)
Every message below is real output from g++ -std=c++17 -Wall -Wextra on Linux — the same compiler family OnlineGDB runs. Line numbers and hex addresses will be yours, not these, and long sanitizer stack traces are trimmed here with .... Where a crash line comes from the shell rather than your program, wording varies slightly between environments; the diagnostic phrase is what matters.
Bug: Using . on a pointer.
witnesses.cpp:12:15: error: request for member 'name' in 'p', which is of pointer type 'Witness*' (maybe you meant to use '->' ?)
12 | cout << p.name << endl;
What it means: p is a pointer, so you must dereference before reaching a member.
Fix: p->name, or the long form (*p).name. If instead you meant p to be an object rather than a pointer, drop the * from its declaration.
Bug: The program dies partway through with no message from your code.
About to walk the chain...
Segmentation fault
(The shell reports exit status 139.)
What it means: You dereferenced a null pointer, a freed pointer, or a wild uninitialized one. Nine times out of ten this week it is walking a chain whose next was never set, or calling head->something when head is nullptr.
Fix: Print the pointer right before you use it — cout << "head = " << head << endl;. A 0 means null; a garbage-looking value means uninitialized. Then guard: if (p != nullptr). Rebuild with -fsanitize=address and the sanitizer will name the exact line.
Bug: Double free.
first delete ok
free(): double free detected in tcache 2
Aborted
Under -fsanitize=address the same program says it more clearly:
==39==ERROR: AddressSanitizer: attempting double-free on 0x502000000010 in thread T0:
#1 ... in main /home/student/double_free.cpp:9
What it means: delete ran twice on one address — usually because two pointers both believed they owned it, or a chain got shallow-copied.
Fix: Decide who owns it (§6.5). Set the owning pointer to nullptr right after delete, since deleting null is a legal no-op. If a class owns heap memory, = delete its copy constructor and copy assignment.
Bug: Use-after-free.
==24==ERROR: AddressSanitizer: heap-use-after-free on address 0x502000000010 ...
READ of size 4 at 0x502000000010 thread T0
#0 ... in main /home/student/use_after_free.cpp:8
...
freed by thread T0 here:
#1 ... in main /home/student/use_after_free.cpp:7
What it means: You deleted the memory and then read through a surviving copy of the pointer. Read those two line numbers together — line 7 freed it, line 8 used it.
Fix: In a chain destructor, always Node* next = current->next; before delete current;. Everywhere else, treat every alias of a deleted pointer as radioactive.
Bug: Memory leak — nothing crashes, but ASan reports on exit.
==31==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 40 byte(s) in 1 object(s) allocated from:
#1 ... in main /home/student/leaky_chain.cpp:12
What it means: Some new has no matching delete. The report names the allocation site, not the missing delete.
Fix: Go to that line, find what it allocated, and trace who was supposed to free it. In a chain, the usual cause is a destructor that frees the head and forgets to walk. Remember: run this check in OnlineGDB, not on a Mac (§6.8).
Bug: delete / delete[] mismatch.
array_delete.cpp:8:12: warning: 'void operator delete(void*, long unsigned int)' called on pointer returned from a mismatched allocation function [-Wmismatched-new-delete]
8 | delete arr;
and at run time, under ASan:
==46==ERROR: AddressSanitizer: alloc-dealloc-mismatch (operator new [] vs operator delete) on 0x503000000040
What it means: new int[5] was released with plain delete.
Fix: new T pairs with delete p. new T[n] pairs with delete[] p. Match the brackets.
Bug: Returning a pointer to a local variable.
bad_return.cpp: In function 'int* make_number()':
bad_return.cpp:6:12: warning: address of local variable 'x' returned [-Wreturn-local-addr]
6 | return &x;
What it means: x is destroyed when the function returns, so the caller receives an address to nothing. This is a warning — the program compiles and may even appear to work.
Fix: Return by value, or allocate with new and document who owns it, or take an output parameter by reference.
Bug: Copying a class that owns heap memory.
chain.cpp:17:16: error: use of deleted function 'Chain::Chain(const Chain&)'
17 | Chain c2 = c1;
chain.cpp:10:5: note: declared here
10 | Chain(const Chain&) = delete;
What it means: Good news — this error means you did protect the class, and something in your code tried to copy it anyway. Common culprits: passing a Chain to a function by value, or returning one by value.
Fix: Pass by reference: void report(const Chain& c). If you had not written the = delete lines, there would be no error here at all — just a double-free crash later (§6.7).
Bug: Forgot to call the base constructor.
vehicles.cpp: In constructor 'Car::Car(int)':
vehicles.cpp:13:30: error: no matching function for call to 'Vehicle::Vehicle()'
13 | Car(int p) : passengers(p) {}
vehicles.cpp:7:5: note: candidate: 'Vehicle::Vehicle(int)'
vehicles.cpp:7:5: note: candidate expects 1 argument, 0 provided
What it means: The base has no default constructor, and the derived constructor never said how to build the base part.
Fix: Put the base call first in the initializer list: Car(int w, int p) : Vehicle(w), passengers(p) {}.
Bug: override won’t compile.
override_typo.cpp:12:10: error: 'void Car::describe() const' marked 'override', but does not override
12 | void describe() const override { cout << "A car." << endl; }
What it means: There is no matching virtual method in the base. Either you forgot virtual there, or the signatures don’t match — a missing const and a misspelled name are the usual suspects.
Fix: That is override doing its job. Compare the two declarations character by character, including const.
Bug: Accessing a protected member from outside.
protected_access.cpp:18:15: error: 'int Vehicle::wheels' is protected within this context
18 | cout << c.wheels << endl;
protected_access.cpp:6:9: note: declared protected here
What it means: protected means “this class and its descendants,” not “anyone who has an object.”
Fix: Add a public getter, or move the code that needs it into a method of the class.
Bug: Can’t create the base class.
case_file.cpp:11:14: error: cannot declare variable 'a' to be of abstract type 'Argument'
case_file.cpp:4:7: note: because the following virtual functions are pure within 'Argument':
case_file.cpp:7:18: note: 'virtual void Argument::defend() const'
What it means: The base has a pure virtual method (= 0), so it is abstract by design.
Fix: Instantiate a concrete subclass instead. If a subclass triggers this same error, it is because it hasn’t implemented every pure virtual yet — the note lists the ones you still owe.
Bug: Destructors half-fire when deleting through a base pointer.
fleet.cpp:18:5: warning: deleting object of polymorphic class type 'Vehicle' which has non-virtual destructor might cause undefined behavior [-Wdelete-non-virtual-dtor]
18 | delete v;
What it means: The base destructor isn’t virtual, so delete on a Vehicle* only ever runs ~Vehicle. Everything the derived class owned leaks.
Fix: virtual ~Vehicle() {} in the base. And treat this warning as an error — it is the loudest whisper in the chapter.
Bug: No error at all — the override just doesn’t run.
What it means: One of two silent failures. Either the base method isn’t virtual (so dispatch happens on the static type), or you passed the object by value into a base-typed parameter and sliced it (§6.15). Neither produces any diagnostic.
Fix: Add virtual to the base and override to every derived version — override converts the first failure into a compile error. For the second, change the parameter to const Base& or Base*. And never store polymorphic objects in a Base array; store Base*.
6.20 — Reps
Full set in the exercises. Three to start you off:
Rep 1. Declare an int. Print its value, its address, a pointer holding that address, and the dereferenced pointer. Then assign through the pointer and print the original variable to prove it changed.
Rep 2. Allocate one int on the heap with new, use it, delete it, and set the pointer to nullptr. Then allocate an array of 10 ints with new[], fill it with cubes, print them, and delete[] it. Confirm the program runs to completion with no crash.
Rep 3. Write a Node struct with an int value and a Node* next. Build a 3-node chain by hand, traverse it with the while (current != nullptr) loop, print each value, then free every node — saving next before each delete.
Then do the rest of them in the exercises. Every single one tells you the exact output to expect, so you can grade yourself the moment you run it.
6.21 — Checkpoint: Can You Do This Yet?
Close the book. Close every tab. Open a blank OnlineGDB file and write each of these from memory. No copy-paste, no scrolling back.
- Declare an
int, take its address into a pointer, and print the value through the pointer. Three lines. - Write the four-line loop that walks a linked list from
headto the end, printing each node’sname. - Write the destructor loop that frees every node in a chain — including the line that saves
nextbefore thedelete. - Write a
remove(string name)that handles the head case correctly. State out loud how you know you’re on the head. - Write a base class with one
virtualmethod and onevirtualdestructor, and a derived class thatoverrides the method. Six lines each, no bodies longer than onecout. - Write a derived constructor that passes two arguments up to a base constructor via the initializer list.
- Write, from memory, the three-line polymorphic loop: an array of
Base*, aforthat calls the virtual method, and aforthat deletes each element. - Explain in one sentence why
void f(Base b)breaks polymorphism andvoid f(Base& b)does not.
The pass bar: 6 of 8, written correctly on the first try, compiling clean with -Wall -Wextra.
If you got fewer than 6 — and specifically if you missed #2, #3, or #7 — do not start the project yet. Go back and re-drill: #1–#4 live in §6.1 through §6.7, and #5–#8 live in §6.10 through §6.15. Retype the two programs those sections point at, by hand, without looking at them for more than a line at a time. Then take this checkpoint again tomorrow. An hour of re-drilling now saves five hours of pointer debugging later. That is not encouragement; that is arithmetic.
6.22 — When You’re Stuck (and Nobody’s in the Room)
Pointer bugs feel personal. They are not. They are mechanical, and they yield to a mechanical process. Work this ladder in order. Do not skip to rung 8.
1. Read the actual error text, all of it, top line first. GCC reports the first real error first, and everything after it may be noise caused by that one. Fix the top error, recompile, look again. This week the messages are unusually literal: “maybe you meant to use ->?” means you meant to use ->.
2. Find your error in §6.19. Fourteen entries, every one carrying real message text. Match the phrase, not the line number.
3. Print the pointer before you use it. This is the week’s highest-value debugging move, and it costs one line:
cout << "DEBUG head=" << head << " current=" << current << endl;
0 means null. A plausible hex value means it points somewhere. Absurd garbage means it was never initialized. You now know which of the three problems you have.
4. Draw it. Get paper. Draw three boxes, label them head, current, next, and draw an arrow for every pointer. Now single-step your loop on the paper, erasing and redrawing the arrows. Almost every linked-list bug is visible in four boxes and dies within ten minutes of drawing.
5. Shrink it to a minimal reproduction. Copy your file into a new OnlineGDB tab and delete everything not involved in the crash — other methods, the seed data, the stress test — until you have under twenty lines that still misbehave. Two things happen: usually you find the bug while cutting, and if you don’t, you now have something small enough to reason about (and to paste into a question).
6. Let the sanitizer answer it. Add -fsanitize=address to your Extra Compiler Flags in OnlineGDB and run again. For use-after-free and double-free it prints the line that freed the memory and the line that touched it afterward. For leaks it prints the line that allocated. That is not a hint; that is the answer.
7. Rubber-duck it with the week’s three questions. Out loud, to a wall or a pet or your phone’s voice recorder, for each new in your program: Who owns this? Who deletes it? On every path? Then for each virtual call: Am I holding this by pointer, by reference, or by value? Saying it out loud is the point — silent reading skips the contradiction your ears would catch.
8. Post to the discussion board. Include, in this order: (a) what you expected, (b) the verbatim error or output — copy-paste, not a paraphrase, (c) the minimal reproduction from rung 5, (d) what you already tried from rungs 3, 4, and 6. Title it with the error phrase, not “help!” — someone hitting the same message next week will find your thread.
9. Email the instructor. Use this template exactly; it gets a fast, specific answer instead of a request for more information:
Subject: Ch 6 / P5 — [the error phrase, e.g. "heap-use-after-free in Chain::remove"]
Section or tier: §6.7, Project 5 Normal tier, remove() method
OnlineGDB link: <your link, flags already set to -Wall -Wextra -fsanitize=address>
What I expected: size() to print 7 after removing the head.
What happened: <paste the exact error or output, unedited>
Smallest version that still fails: <paste the 20-line reproduction>
What I already tried:
1. Printed head and current before the delete — head was 0 after the first removal.
2. Drew the three-node case on paper; the head case looks right to me.
3. Ran with -fsanitize=address; it points at chain.cpp line 41.
My best guess: I think I'm deleting the node before I rewire head, but I can't see where.
Nine rungs. In practice, rungs 3, 4, and 6 solve the overwhelming majority of week-6 problems, and they take about fifteen minutes combined. Work them before you wait on anyone.
6.23 — This Week’s Project
You’re ready for Project 5 — Chain of Witnesses & Argument Case File, in Project 5. It is due at the end of Week 6, and it is the largest single build in this course.
It has two halves, matching this chapter’s two halves:
The Chain of Witnesses is a hand-built singly linked list of real historical witnesses to the Christian faith — a Chain class with add, remove, print, size, deleted copy operations, and a destructor that frees every node with zero leaks. remove must handle the head case, the middle case, and the not-found case. Your seed data must be real people with real, citable testimony, and century follows this chapter’s convention: the century of the testimony, not of the birth.
The Argument Case File is a small class hierarchy — a base Argument with a virtual defend() and a virtual destructor, plus concrete subclasses (cosmological, moral, ontological, and more at the higher tiers) — held polymorphically in a container that owns them and cleans them up. One loop, several characteristic defenses, real attributions to real sources.
The higher tiers push on exactly the muscles this chapter built: deeper pointer surgery on one side, a deeper hierarchy and pure virtuals on the other. Project 5 has the full specification, the tier list, the rubric, and the submission workflow. Submit an OnlineGDB link; see Appendix A for the exact steps and where to set your compiler flags.
Two guardrails, restated because they matter more than the code:
Do not invent quotes or attributions. Every testimony must be a real quotation or a faithful one-sentence summary of something the person actually wrote, and every argument’s source must be a real text where that form of the argument was actually made. Plausible-sounding fiction is worse than an empty field. If you can’t source a witness, use a different witness.
Do not build a case file with one argument in it. The design point and the apologetic point are the same: the classical case is several arguments working together, each strong where the others are weak. Polymorphism is what that looks like in code.
6.24 — Coach’s Final Word for Week 6
This is the technically hardest week of the course, and this is the last week of C++ in it.
Pointers, manual memory, and linked structures are the bedrock of the language, and everything built on top of them — polymorphism, generics, smart pointers, the entire architecture of every serious C++ program — assumes you can think clearly about what owns what memory, who points at what, and when things get freed. Inheritance and virtual dispatch, which used to feel like a separate topic, turn out to be that same question asked at a higher altitude: which destructor runs, which override fires, and what exactly is on the other end of this pointer.
If this week took you twice as long as Week 5, that’s normal. If it took four times as long, that is still normal. Slow down. Draw the boxes. Print the pointer. Turn on the sanitizer. The tools are not a crutch; using them well is the skill.
Next week you shift to Java, where the garbage collector does §6.5 through §6.8 for you, automatically, forever. It is going to feel like a gift. It will only feel like a gift because you did this week by hand.
And take one look back at what you actually built. Not a linked list — a chain of witnesses, each one a real person who wrote a real document that still exists, each one pointing back to the one before, held in your computer’s memory in the same shape history left it in. Then a case file where several different arguments each answer in their own voice from a single loop. That’s a good week’s work.
See you next week. We shift gears.
Up next: Read the exercises and run every rep, checking your output against the expected output instead of skimming past it. Then take the Checkpoint in §6.21 cold. Then open Project 5 and build the Chain of Witnesses and the Argument Case File. After that, Chapter 7 — Java arrives, and so does the automatic transmission.
Week 6 Knowledge Check
class Base { public: void speak() { cout << "Base"; } };
class Derived : public Base { public: void speak() { cout << "Derived"; } };
Derived d;
Base* p = &d;
p->speak(); class Base { public: virtual void who() { cout << "Base "; } };
class Derived : public Base { public: void who() override { cout << "Derived "; } };
void byValue(Base b) { b.who(); }
void byRef(Base& b) { b.who(); }
Derived d;
byValue(d); byRef(d); class Animal { public: Animal() { cout << "Animal "; } virtual ~Animal() { cout << "~Animal "; } };
class Dog : public Animal { public: Dog() { cout << "Dog "; } ~Dog() { cout << "~Dog "; } };
Animal* a = new Dog();
delete a;