Visual walkthrough — C++ as superset of C — key additions
We build strictly from zero. Every keyword is explained the first time it appears. If you have only ever seen basic C (a struct, a function, a pointer), you have enough to follow line one.
Step 1 — The starting atom: a struct with an unlocked door
WHAT. In C, a struct is just a labelled box that glues several values together under one name. Here a bank account is one integer, the balance.
struct Account { int balance; };
void deposit(struct Account* a, int x) { a->balance += x; }Read the pieces:
struct Account { ... }— the box. Inside,int balanceis one slot holding a whole number.Account* a— the star*means "ais a pointer: an address, an arrow pointing at an Account somewhere in memory." (See Pointers vs References.)a->balance— the arrow->means "follow the pointer, then reach inside for thebalanceslot."
WHY start here. This is the smallest program that already has the problem C++ was built to solve: the data (balance) and the code that guards it (deposit) live in two separate places, and nothing forces anyone to use the guard.
PICTURE. The box has a slot, but its door is wide open — any line of code can reach in and scribble on balance.

Step 2 — The crack: anyone can corrupt the invariant
WHAT. An invariant is a promise your program should always keep. Here the promise is: "balance only ever changes through deposit / withdraw." In C, that promise is unenforceable:
struct Account a;
a.balance = -999999; // legal C — nobody stops usThe dot . reaches directly into the slot (used when you have the box itself, not a pointer to it).
WHY it matters. A bug three files away can set balance to garbage and the compiler shrugs. As programs grow to thousands of these boxes, this is the complexity that crushes C projects — not slow code, but unprotected data.
PICTURE. A rogue arrow reaches past the intended deposit door and pokes the slot directly, turning it red.

Step 3 — Bucket 1: the class locks the door (private + methods)
WHAT. A class is a struct whose slots are private by default — sealed — and which carries its own functions, called member functions or methods.
class Account {
int balance = 0; // private: sealed slot
public:
void deposit(int x) { balance += x; } // the one legal door
int get() const { return balance; } // read-only door
};Term by term:
int balance = 0— sealed because it sits beforepublic:. Outside code physically cannot name it.public:— everything after this colon is a door the outside world may use.void deposit(int x)— a method bundled inside the box; it may touchbalancebecause it lives inside.constafterget()— a compiler-checked promise: "this door only reads, never writes."
WHY this exact tool. We needed the box to refuse stray writes. private is the compiler-enforced lock; methods are the labelled doors. The invariant from Step 2 is now impossible to break by accident — the error appears at compile time.
PICTURE. The same box, now with a solid wall around balance; the only openings are two labelled doors, deposit (in) and get (out). The rogue arrow from Step 2 now bounces off the wall.

More at Classes and Objects in C++.
Step 4 — Bucket 2: one shape, any colour (templates)
WHAT. Suppose we want "the bigger of two things." In C you either write it per type or use an unsafe macro. A template writes it once for every type at all:
template<typename T>
T maxv(T a, T b) { return a > b ? a : b; }template<typename T>— "Tis a placeholder for some type, decided later."- Everywhere
Tappears, the compiler will substitute the real type (int,double,std::string...) and stamp out a separate real function for each one used.
WHY not the C macro #define MAX(a,b) ((a)>(b)?(a):(b))? Because a macro is blind text-substitution: MAX(i++, j) pastes i++ twice, incrementing i two times — a silent bug. It also does no type checking. A template is a real function: each argument is evaluated exactly once and the types are verified. Same reach as the macro, none of the traps.
PICTURE. A single stencil labelled T feeds a photocopier; out come three concrete, coloured copies — maxv<int>, maxv<double>, maxv<string> — each identical in shape.

More at Templates and Generic Programming.
Step 5 — Bucket 3: references remove the pointer noise
WHAT. To let a function change the caller's variable, C hands over an address (a pointer). C++ adds a reference: a second name for the same variable.
void inc(int& r) { r++; } // r is another name for the caller's int
int x = 5;
inc(x); // x becomes 6 — no & at the call, no * insideint& r— the ampersand&in a parameter means "ris a reference: an alias for whatever variable you pass."- Inside,
r++changes the original directly — no*needed, because a reference is auto-followed.
WHY a reference and not a pointer? Compare C: void inc(int* p){ (*p)++; } inc(&x); — noisy * and &, and p could be null or later re-pointed somewhere else. A reference cannot be null and cannot be re-seated, so an entire family of pointer bugs simply cannot occur. Same power, fewer sharp edges. (Full contrast: Pointers vs References.)
PICTURE. Left: a pointer drawn as a separate box holding an arrow that points at x. Right: a reference r drawn as a second nametag stuck directly onto the same x box — no extra storage, no arrow to mis-aim.

Step 6 — Bucket 3 cont.: new/delete build and destroy properly
WHAT. To make objects on the heap (memory that outlives the current function), C uses malloc/free. C++ adds new/delete, which are type-aware:
Account* p = new Account; // allocates AND runs the constructor
delete p; // runs the destructor AND freesWHY not just malloc? malloc hands back raw bytes and free throws them away — neither runs the object's setup/teardown code (constructor/destructor). A freshly malloc-ed Account has an uninitialised balance; new guarantees the = 0 from Step 3 actually runs. Mixing the two families (free on a new-ed object) is undefined behaviour. Rule: pair new↔delete, malloc↔free. (Deep dive: new and delete vs malloc and free.)
PICTURE. Two assembly lines. malloc drops a blank, unlabelled block. new drops the same block but a little robot arm (the constructor) stamps balance = 0 onto it before it leaves.

Step 7 — Bucket 4: namespaces stop name collisions
WHAT. C has one global pool of names. If two libraries both define init(), they clash and the program won't link. A namespace is a surname you attach to a group of names:
namespace net { void init(); }
namespace gfx { void init(); }
net::init(); // the networking one
gfx::init(); // the graphics onenamespace net { ... }— everything inside gets the surnamenet.net::init— the::(scope operator) reads "theinitthat belongs tonet."
WHY. Two clashing init()s used to force ugly renames like net_init, gfx_init. Namespaces let both keep the short name and stay distinct. This is exactly why the standard library lives in the std namespace — hence std::vector. (See Namespaces and the std namespace.)
PICTURE. Two envelopes each labelled init, previously piled in one tray and colliding; now sorted into two folders titled net:: and gfx::, no collision.

Step 8 — Bucket 5: the STL + exceptions (reuse and a clean error path)
WHAT. The STL (Standard Template Library) ships ready-made containers built from the templates of Step 4 — vector (a self-growing array), map, string, plus algorithms like sort. Exceptions give a separate channel for errors:
try {
if (amount < 0) throw std::runtime_error("negative deposit");
account.deposit(amount);
} catch (const std::exception& e) {
// the error path, kept out of the happy path
}throw X— "stop normal flow; something went wrong; carryXup the call stack."try { ... } catch (...) { ... }— thetryblock is the happy path; thecatchblock runs only if athrowfires. (See Exception Handling try-catch-throw and The STL — vector, map, string.)
WHY. In C you check a return code after every call, and the error handling drowns the real logic. Exceptions let the happy path read cleanly, with the error path pulled aside — the same reasoning as every step above: let humans manage complexity.
PICTURE. A single rail (the happy path) runs straight through; a throw flips a switch that diverts onto a separate red side-rail (the catch), so the two flows never tangle.

Step 9 — The degenerate case: where the "superset" claim cracks
WHAT. Everything above adds to C. But C++ is only a near-superset — it is stricter, so a few valid C programs are rejected by a C++ compiler:
| C code | In C | In C++ |
|---|---|---|
int* p = malloc(4); |
legal (implicit void*→int*) |
error — needs a cast |
int new = 5; |
legal | error — new is a keyword |
struct S {}; S x; |
needs struct S x; |
fine — tag is a type |
WHY this is not a contradiction. C++ closes doors on purpose — the same instinct as private. The implicit void* conversion that C allows is exactly the kind of silent type hole C++ refuses. So the strictness is not a bug in the superset story; it is the story. Treat C-meant code with a C compiler when in doubt. (See Compilation Model — C vs C++.)
PICTURE. A gate labelled "C++ compiler" lets clean C code through but stops three specific carts — labelled malloc-no-cast, int new, and implicit void* — each bouncing off with a red X.

The one-picture summary
Every step answered one question — "close which door?" — and each answer is one of the five buckets. Here they are stacked back onto the plain C atom we started with.

Term by term: each brace is a pain we watched happen followed by the tool that fixed it — never a feature for its own sake.
Recall Feynman retelling — say the whole walkthrough out loud
We started with a LEGO box holding one number, balance, and a helper that adds to it — but the box had no lid, so any kid could reach in and wreck the number (Steps 1–2). So we gave the box a lid with two labelled slots — put-money-in and read-money — and sealed the rest: that's a class (Step 3). Then we got tired of building the same "which is bigger?" tool for every colour of brick, so we made a photocopier that stamps one shape in any type — templates (Step 4). Passing bricks around was noisy with pointer stars and arrows, so we invented a second nametag for the same brick — a reference (Step 5) — and a smarter builder, new, that actually assembles the brick instead of dropping a blank one like malloc did (Step 6). Two kids kept naming their pieces the same thing, so we gave each kid a surname folder — namespaces (Step 7). Then we handed everyone a box of pre-built kits (STL) and a separate emergency chute for when something breaks (exceptions), so the normal instructions stay readable (Step 8). Finally we noticed the new, stricter inspector rejects a few sloppy old C carts on purpose — that's why C++ is a near-superset, not a perfect one (Step 9). Stack all the helpers back on the plain box and you have C++.
Connections
- C++ as superset of C — key additions (parent)
- Classes and Objects in C++
- Templates and Generic Programming
- Pointers vs References
- new and delete vs malloc and free
- Namespaces and the std namespace
- The STL — vector, map, string
- Exception Handling try-catch-throw
- Compilation Model — C vs C++
In Step 3, what compiler mechanism enforces the "balance only changes via deposit" invariant?
private access — sealed slots plus public methods as the only doors, checked at compile time.Why is the C macro MAX unsafe compared with a template, per Step 4?
MAX(i++, j) evaluates i++ twice and does no type checking; a template is a real function (each argument evaluated once, types verified).Give the one behavioural difference between new and malloc shown in Step 6.
new runs the constructor (so balance = 0 actually executes); malloc returns raw uninitialised bytes.Name the three C constructs Step 9 shows a C++ compiler rejecting.
int* p = malloc(4); without a cast, int new = 5; (reserved keyword), and reliance on implicit void*→T* conversion.