5.2.1 · D2C++ Programming

Visual walkthrough — C++ as superset of C — key additions

2,486 words11 min readBack to topic

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 balance is one slot holding a whole number.
  • Account* a — the star * means "a is 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 the balance slot."

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.

Figure — C++ as superset of C — key additions

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 us

The 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.

Figure — C++ as superset of C — key additions

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 before public:. 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 touch balance because it lives inside.
  • const after get() — 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.

Figure — C++ as superset of C — key additions

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> — "T is a placeholder for some type, decided later."
  • Everywhere T appears, 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.

Figure — C++ as superset of C — key additions

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 * inside
  • int& r — the ampersand & in a parameter means "r is 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.

Figure — C++ as superset of C — key additions

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 frees

WHY 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 newdelete, mallocfree. (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.

Figure — C++ as superset of C — key additions

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 one
  • namespace net { ... } — everything inside gets the surname net.
  • net::init — the :: (scope operator) reads "the init that belongs to net."

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.

Figure — C++ as superset of C — key additions

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; carry X up the call stack."
  • try { ... } catch (...) { ... } — the try block is the happy path; the catch block runs only if a throw fires. (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.

Figure — C++ as superset of C — key additions

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 errornew 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.

Figure — C++ as superset of C — key additions

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.

Figure — C++ as superset of C — key additions

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 foldernamespaces (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?
A macro pastes its arguments as text, so 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.