5.2.8 · D2C++ Programming

Visual walkthrough — Copy constructor and copy assignment — Rule of Three

2,099 words10 min readBack to topic

We will use exactly one running example: a tiny class that owns a piece of heap memory holding a word. Everything else — pointers, new, delete — we build from zero, right where we need it.


Step 1 — What "owns a resource" even means (picture the box and the note)

WHAT. An object is just a little bundle of variables sitting somewhere in memory. When one of those variables is a pointer — a variable whose value is an address, a house number telling you where some other memory lives — we say the object points at a resource.

WHY this matters. The pointer variable and the memory it points at are two different things living in two different places. The pointer sits inside the object (cheap, small). The memory it points at was carved out of the heap — a big pool of memory you request at run time with the keyword new and must hand back with delete. See Dynamic memory new and delete.

PICTURE. In the figure, the small blue square is the object. Inside it is a variable data holding an arrow (the address). The orange box far away is the actual heap buffer holding the characters c a t \0. The arrow is the "note with the box's address"; the orange box is the "toy box".

Figure — Copy constructor and copy assignment — Rule of Three

The whole drama that follows is one question: when we copy the object, what happens to that arrow?


Step 2 — What the compiler does for free: it photocopies the arrow

WHAT. If you write no copy code, the compiler generates a copy that goes member by member. For a pointer member, "copy the member" means copy the address value — the arrow — not the box it points to. This is a shallow copy (see Deep vs shallow copy).

WHY it does this. The compiler has no idea data is owned memory. To it, data is just a number. Copying a number is copying a number. It cannot guess that this number is a claim of ownership.

PICTURE. Object a already points at the orange buffer. We write b = a (a copy). The compiler duplicates the arrow: now b.data holds the same address. Two blue squares, two arrows, one orange box. Both arrows land on the same buffer — this is called aliasing.

Figure — Copy constructor and copy assignment — Rule of Three

  • — the pointer inside the new object b.
  • — plain value copy of the address.
  • — the same address a already holds. After this line, both equal the same house number.

Notice: no new ran. No second box was ever built.


Step 3 — Why the shallow copy is a time bomb: the double free

WHAT. Every object that owns a resource has a destructor ~T() that runs delete[] data when the object dies. With two objects sharing one buffer, that destructor runs twice on the same address.

WHY this is fatal. delete[] tells the heap "this box is free again — reuse it." Saying that once is correct. Saying it twice about the same box is undefined behaviour: the second delete[] frees memory that no longer belongs to anyone, corrupting the heap's bookkeeping. In practice: a crash.

PICTURE. Watch the timeline. Object b goes out of scope first → delete[] frees the orange box (now shown gray = returned to the pool). Object a is now dangling: its arrow points at freed memory (red, broken arrow). When a dies, delete[] runs again on the already-freed box → 💥.

Figure — Copy constructor and copy assignment — Rule of Three

Step 4 — The fix, seen: a deep copy builds a second box

WHAT. A deep copy does what the compiler wouldn't: it calls new to allocate a fresh buffer, then copies the characters into it — not the address. That is the whole job of the copy constructor MyString(const MyString& other).

WHY a fresh allocation. So the two objects own independent boxes. Now each destructor frees its own box exactly once — no sharing, no double free, no aliasing.

PICTURE. b is being born from a. First new char[len+1] carves out a second orange box. Then strcpy walks the characters c a t \0 from a's box into b's box. Two objects, two arrows, two boxes — the arrows never cross.

Figure — Copy constructor and copy assignment — Rule of Three

  • — pointer in the new object, now pointing at brand-new memory.
  • — request a fresh block from the heap; returns its address.
  • — room for every character plus one for the \0 terminator that marks the string's end.

Then std::strcpy(b.data, other.data) copies the contents, character by character, until it copies the \0.


Step 5 — Assignment is different: there's an OLD box to throw away first

WHAT. The copy constructor built a fresh object with no prior buffer. The copy assignment operator T& operator=(const T& other) overwrites an object that already owns a box. So before allocating a new one, it must delete[] the old one — otherwise that old box is orphaned forever: a memory leak.

WHY the difference. Birth vs rebirth. A newborn has empty hands. A living object already clutches a box; to hold a new box it must first let go of the old, or that old box floats away unreachable.

PICTURE. Left panel: b owns box "elephant", a owns box "cat". We run b = a. Frame 1: delete[] b.data returns the "elephant" box to the pool (gray). Frame 2: new + strcpy gives b a fresh "cat" box. a is untouched throughout.

Figure — Copy constructor and copy assignment — Rule of Three

The order is sacred: free, then allocate, then copy. See Self-assignment and exception safety.


Step 6 — The degenerate case: a = a (self-assignment)

WHAT. What if the object being assigned to is the same object being copied from? Directly (a = a) or hidden (arr[i] = arr[j] when i == j, or two references to one object). This is the self-assignment edge case, and it destroys the naive operator= from Step 5.

WHY it breaks. Follow the order literally. Step 1 was delete[] data — but if this is &other, then other.data is the box we just freed! Step 3, strcpy(data, other.data), then copies from freed, garbage memory into itself. Corruption.

PICTURE. One object, one box. The naive code runs delete[] on the box (gray, gone). Then it tries to strcpy from that same freed box — the red arrow points into rubble. The output is garbage.

Figure — Copy constructor and copy assignment — Rule of Three

The fix is one guard line at the very top:

  • — the address of the object being assigned to.
  • — the address of the source object.
  • If they are equal, both arrows point at the same object → do nothing, return early, keep the box intact.

Step 7 — The elegant escape: copy-and-swap handles every case at once

WHAT. Instead of hand-writing free/allocate/copy, take the parameter by value (which quietly calls the copy constructor from Step 4), then swap the guts. The overloaded operator= becomes three tiny lines.

WHY it wins. All allocation happens before we touch *this — inside making the by-value copy. If new throws, *this is still intact (this is exception safety). Self-assignment is automatically fine: you copy first, swap second, and the old guts die harmlessly with the parameter.

PICTURE. Three frames: (1) the by-value parameter other is a fresh deep copy of the source; (2) std::swap exchanges the arrows of *this and other — now *this holds the new box, other holds the old box; (3) other goes out of scope and its destructor frees the old box automatically. No manual delete, no self-check needed.

Figure — Copy constructor and copy assignment — Rule of Three
MyString& operator=(MyString other) {  // by value → copy already made
    std::swap(data, other.data);       // *this now owns the new box
    std::swap(len,  other.len);
    return *this;                      // 'other' (old box) dies → freed
}

This foreshadows Move semantics and Rule of Five and Rule of Zero and smart pointers, where the compiler-generated members become safe again because members like std::string already deep-copy themselves.


The one-picture summary

Three life-events, one resource. Birth-by-copy (constructor) builds a fresh box. Rebirth-by-copy (assignment) frees the old box, then builds a fresh one, guarding self-assignment. Death (destructor) frees the box once. All three exist for the same reason: manual ownership of one heap box. Miss any one, and you get a double free, a leak, or corrupted garbage.

Figure — Copy constructor and copy assignment — Rule of Three
Recall Feynman: the whole walkthrough in plain words

Every object is a kid holding a note with a toy box's address. If you let the school photocopy the note (the free shallow copy), two kids point at one box — and at clean-up time both try to throw it away, but you can't bin the same box twice, so everything smashes (Steps 1–3). The fix is: when a kid is born as a copy, give them a brand-new box and copy the toys over (Step 4). When a kid who already has a box is told to copy someone, they must empty their old box in the bin first, then get a new one and copy the toys — otherwise the old box floats away forgotten (Step 5). One sneaky case: a kid told to copy themselves — if they bin their box first, there's nothing left to copy from, so we check "is that me?" and stop early (Step 6). The slick trick is: quietly make the full copy first, then just swap notes and let the leftover box get binned on its own (Step 7). Three jobs — born, reborn, died — one box. That's the Rule of Three.

Recall

In the shallow copy, what exactly gets duplicated? ::: Only the pointer value (the address / the arrow) — not the buffer it points to. Why does the double free happen? ::: Two objects share one address; each destructor calls delete[] on it, freeing the same box twice. What does the copy constructor do that the compiler's version doesn't? ::: Calls new to allocate a fresh buffer, then copies the contents (deep copy). Why must operator= free the old box but the copy ctor must not? ::: The assigned-to object already owns a buffer (leak if unfreed); a newborn owns nothing yet. How does self-assignment break the naive operator=? ::: delete[] frees data, but other.data IS that freed box, so strcpy copies from garbage. Why is copy-and-swap safe for free? ::: The copy (all allocation) happens before *this is touched, so self-assignment and exceptions are handled automatically.


Parent: Copy constructor and copy assignment — Rule of Three