This page is the exhaustive drill for the Rule of Three. The parent note built why three members belong together. Here we hit every case a copy operation can throw at you — construction vs assignment, self-assignment, empty strings, chained assignment, the crash from the compiler-default version, exception safety when new[] fails, and the exam twist about counting how many times memory is allocated and freed.
Before we start, one word we'll lean on constantly:
Every example below runs against this one file. Read it once now so that when an example says "Step 3 — free the old buffer", you can point at the literal line. This is a class (not an aggregate): its members are private, so the only way to touch them is through the constructors, destructor, and operator= shown here.
#include <cstring> // strlen, strcpy#include <cstddef> // size_t#include <utility> // swapclass MyString { char* data; // owned resource: address of a heap buffer std::size_t len; // length, NOT counting the '\0'public: // (0) normal constructor MyString(const char* s = "") { len = std::strlen(s); data = new char[len + 1]; // +1 for the '\0' terminator std::strcpy(data, s); } // (1) destructor — how the resource dies ~MyString() { delete[] data; // [] pairs with new[] } // (2) copy constructor — born as a deep copy MyString(const MyString& other) { len = other.len; data = new char[len + 1]; // OWN, independent buffer std::strcpy(data, other.data); } // (3) copy assignment — overwrite an existing object MyString& operator=(const MyString& other) { if (this == &other) return *this; // guard self-assignment char* tmp = new char[other.len + 1];// allocate FIRST (may throw) std::strcpy(tmp, other.data); delete[] data; // only now free the old buffer data = tmp; len = other.len; return *this; // reference → chaining }};
Now, three terms this page uses as headings — defined before we use them:
Copying an owning object has only a few moving parts, but they combine into distinct cases. Every worked example below is tagged with the cell it covers. The reader should never meet a situation we did not drill.
Figure s01 (deep copy): alt-text — "Left: object a.data (magenta) and b.data (violet). Right: two separate orange buffers each holding cat\0." The magenta arrow is member (2) line data = new char[len+1]for a; the violet arrow is that same line running for b. Because each arrow ends in its own box, a.data != b.data. Trace them — they never meet.
Figure s02 (reassignment): alt-text — "b.data (violet) left; old grey elephant\0 buffer struck out top-right; fresh orange cat\0 buffer bottom-right." The magenta strike-through is Step 4's delete[] data. The violet arrow into the orange box is Step 4's data = tmp (whose contents came from Step 3's new char[len+1] + strcpy). Read top-to-bottom: free the old grey box, then repoint b at the new orange box.
nullptr instead of ""?
strlen(nullptr) is undefined — it is a stricter degenerate case than "". Our default parameter = "" only helps when the caller writes MyString x;; a caller who explicitly writes MyString x(nullptr) still crashes. ::: The empty-string case ("") is safe, but nullptr is a separate, harsher edge that our current constructor does not defend against — a real class would add if (!s) s = ""; as the first line.
Figure s03 (shallow crash): alt-text — "a.data (magenta) and b.data (violet) both point at a single orange hi\0 buffer via arrows labelled 'same address'." Both arrows land in one box — that is Step 1's member-wise pointer copy. The caption delete[] runs TWICE → CRASH is Steps 2 and 3. Compare with s01 (two boxes): here there is only one box with two owners.
What decides copy-constructor vs copy-assignment for x = y?
Whether x is being created on that line (constructor) or already exists (assignment).
In Ex 2, why must operator= free b's old buffer?
b already owned the "elephant" buffer; skipping the free orphans it (memory leak).
Why must the self-assignment guard compare addresses, not values?
Two different objects can hold equal strings in different buffers; only this == &other detects true aliasing.
For MyString(""), why is new char[len+1] still safe?
len == 0 gives new char[1], one byte for the '\0', so strcpy stays in bounds.
Why does operator= return MyString&?
To support chaining a = b = c and to avoid an unnecessary by-value copy of the result.
In Ex 6, what makes the default copy crash?
Shallow pointer copy means two objects share one buffer, so delete[] runs twice on the same address.
In Ex 6b, why allocate into tmp before delete[] data?
If new[] throws, the object is left unchanged (strong exception guarantee) instead of holding a dangling pointer.
In copy-and-swap, where is the old buffer freed?
When the by-value parameter other (now holding the old guts) destructs at the end of the function.
In Ex 8, why does line 5 (a = a) add zero allocations and zero frees?
The self-assignment guard returns early before any delete[] or new[].
Recall Feynman: the ledger, in one sentence
Every time you say new[] you owe the universe one delete[] — no more, no less. A correct copy constructor and copy assignment just make sure that when objects copy each other, nobody double-pays, nobody skips the bill, and even if the shop runs out of memory mid-purchase, you still walk out owning exactly what you walked in with.