An object can own a resource — a chunk of memory it borrowed from the operating system and must give back. The whole Rule of Three is about one honest question: when this object is born-by-copy, and when it dies, does the resource get its own copy, or do two objects fight over one?
This page assumes you have seen nothing . Before you touch the Rule of Three itself, we will define — from absolute zero — every piece of notation and every idea the parent note leans on: what a variable in memory is, what an object is, what an address is, what a pointer is, what the heap is, what new and delete do, and what an object owning a resource even means. Each idea is anchored to a picture and earns its keep.
Definition Memory, address & variable
Your computer's memory (RAM) is one long row of tiny boxes , each holding one byte. Every box has a permanent house-number called its address . A variable is simply a name you give to one or more of those boxes so you can talk about them without quoting the raw address — when you write int x;, the name x is glued to a chosen box, and reading/writing x reads/writes that box's contents.
The picture: a row of boxes. Box number 1000 holds the character 'h', box 1001 holds 'i', box 1002 holds a special "end of text" marker written '\0' (a byte whose value is zero — called the null terminator ). The number under a box (1000) is its address; the letter inside is its value . A variable is just a label stuck onto one of these boxes.
Why the topic needs this: the entire double-free bug is about two variables holding the same address . You cannot understand "same address" until you can see variables as labels on numbered boxes.
Intuition Value vs. address
A box has two facts: where it lives (address, a number) and what's inside (value, the data). Copying the where is cheap and dangerous; copying the what is the safe thing we usually want. Hold onto this — it is the whole story of shallow vs deep copy in Deep vs shallow copy .
An object is a bundle of boxes that belong together , given one name and one type. A MyString object, for instance, is two boxes side by side: one holding a char* (an arrow — see §4) and one holding a len (a count). The object is those boxes; its name is a variable that labels the whole bundle. When we say "an object owns a resource", we mean: one of the boxes inside this bundle holds an arrow to boxes borrowed elsewhere, and the bundle is responsible for them.
Why the topic needs this: the parent note keeps saying "an object owns a resource", "two objects fight over one buffer", "the object dies". Every one of those sentences is really about a bundle of boxes — one of which is an arrow — and whether two such bundles point at the same borrowed run. Keep the picture: an object is a labelled group of boxes.
char and char*
A char is one box holding one letter (technically one byte).
Text like "hi" is a run of boxes — one char per letter, ended by the '\0' marker so the machine knows where the word stops.
char* (read "char star" ) is a variable that holds the address of the first box of such a run. The * means "this is a pointer to". We meet the full idea of pointers next.
Why +1 everywhere? The word "hi" is 2 letters but needs 3 boxes — the extra one for '\0'. That is exactly why the parent writes new char[len + 1]: len letters plus one terminator box. Miss the +1 and you write off the end of your street.
A pointer is a variable whose value is an address . Instead of holding data itself, it holds "the data is over there ". We draw a pointer as an arrow from the pointer's own box to the box it names.
The picture: a small box labelled data (the pointer) with an arrow reaching across to the "hi\0" run. The pointer's value is 1000 — the address of the first letter. To "follow the arrow" and reach the letters is called dereferencing .
Definition The empty arrow:
nullptr
Sometimes a pointer points at nothing on purpose . That special "points nowhere" value is written ==nullptr== (older code writes NULL or 0). Picture it as an arrow lying flat with no target — a box holding the address 0, which is guaranteed never to be a real box you own. Checking if (p == nullptr) asks "is this arrow empty?" You must never dereference nullptr (never follow the empty arrow) — doing so crashes.
dangling pointer: an arrow to boxes already returned.
After delete[] data;, the boxes are handed back — but data still holds their old address. Now data is a dangling pointer : an arrow pointing at a plot of land you no longer own. Why it's a trap: following it, or delete[]-ing it again, touches memory that isn't yours. Fix: either stop using it, or set data = nullptr; right after freeing so the empty arrow makes the mistake visible. This dangling-after-free danger is exactly the double-free the Rule of Three prevents.
Common mistake "A pointer
is the text."
It feels that way because printing a char* shows you the text. Why it's wrong: the pointer is just the arrow — a house-number. The text lives elsewhere. Two arrows can point at the same text; that is precisely the danger. Fix: always ask "am I copying the arrow, or the thing the arrow points at?"
Why the topic needs this: the compiler's automatic copy duplicates the arrow (the address), not the run of boxes. Two objects, two arrows, one buffer. Everything the parent calls "shallow copy" is this picture with two arrows .
Definition Where variables live: stack, static, and heap
A running program stores boxes in three places. Static storage holds boxes that live the whole program (global variables). The stack holds boxes for local variables — created when a function starts, automatically thrown away when it ends, in strict last-in-first-out order (you do not manage them). The heap (also called free store ) is a big pool of boxes you can borrow at any moment and for any lifetime — but you are responsible for returning them. The heap is the only place where you decide when the boxes are born and die; that freedom is why it needs new and delete.
new, new[], delete, delete[]
new char[n] asks the heap: "lend me a run of n boxes." It returns the address of the first one — an arrow you store in a char*.
delete[] p says: "I'm done, take those boxes back." You must hand back the same arrow new[] gave you.
Single new / delete are for one box; new[] / delete[] are for a run . They must be matched: array-new with array-delete.
The picture: a "borrow counter" onto the heap. new[] slides a run of boxes across the counter to you (arrow returned). delete[] slides them back. Because heap boxes outlive the function that made them (unlike stack boxes), nobody frees them for you — the whole point of the Rule of Three is making sure every borrow is returned exactly once — never zero times (a memory leak , boxes never returned) and never twice (a double free , returning already-returned boxes, which crashes).
Definition Undefined behaviour (UB)
Undefined behaviour means the C++ language rules place no requirement at all on what happens next. The program might crash, might silently corrupt other data, might appear to work today and fail tomorrow, or fail only on a different machine. It is not a friendly error message — it is the compiler being allowed to assume you never did the forbidden thing . That is why UB is so dangerous: the bug can hide for months. Double-free, dereferencing a dangling or null pointer, and mismatched new/delete are all UB.
delete where you used new[].
They look almost identical. Why it's wrong: array-new records how many boxes it lent; plain delete doesn't ask, so bookkeeping breaks — this is undefined behaviour (see the box above — anything may happen). Fix: the bracket must match — new[] ⟷ delete[]. See Dynamic memory new and delete .
Why the topic needs this: "the resource an object owns" is a run of heap boxes from new[]. The destructor is just the promise "I will delete[] my boxes when I die."
Definition Owning a resource
An object owns a resource when it is responsible for returning it . Our MyString object (a bundle of boxes) holds a char* pointing at a new[]-ed run of heap boxes; that run is its property. Ownership means: nobody else should return my boxes, and I must return them exactly once.
Why "own" is the key word: if two objects both believe they own the same run, both will try to delete[] it — double free (UB). The Rule of Three exists to guarantee one buffer, one owner by giving each copied object its own run (a deep copy). This idea of "resource freed automatically when the owner dies" is the seed of Destructors and RAII .
T, &, const, this
T — a stand-in name for "some class type" (like MyString). The parent writes T(const T& other) meaning "works for whatever class we're building."
& after a type — "reference to" : const T& other means "look at the original other directly, don't make a copy of it." An alias, not a duplicate.
const — a promise "I will not change this." const T& = "I'll look but not touch."
this — inside a member function, this is the address of the object the method was called on — an arrow to "myself" . *this means "myself" (follow the arrow).
&other and this == &other
The & in front of a variable means "give me its address." So &other is the arrow to other , and this is the arrow to me . The self-assignment check this == &other literally asks: "is the object I'm copying from the very same object I am?" If the two arrows point to the same box, it's a = a. That is why Self-assignment and exception safety matters — you'd otherwise free your own source.
& the "reference" with & the "address-of".
Same symbol, two jobs. Rule: & after a type (const T& x) = reference (an alias). & before a value (&other) = "the address of." Position tells you which.
Address is a house number
Variable is a label on boxes
Object is a bundle of boxes
Pointer is an arrow to a box
nullptr is an empty arrow
char star points at a run of letters
Dangling pointer after free
Shallow copy copies the arrow
Deep copy copies the boxes
Two owners one buffer means double free
Double free is undefined behaviour
One owner one buffer is safe
this and address-of and reference
Test yourself — you are ready for the parent note when every line reveals no surprise.
What is an address ? The permanent house-number of a box (byte) in memory.
What is a variable ? A name (label) glued to one or more memory boxes so you can read/write them by name.
What is an object ? A bundle of boxes that belong together under one name and type.
What two facts does every memory box carry? Where it lives (address) and what's inside (value).
What is a pointer ? A variable whose value is an address — drawn as an arrow to another box.
What is nullptr? The special "points nowhere" pointer value (address 0) — an empty arrow you must never dereference.
What is a dangling pointer ? An arrow still holding the address of boxes that have already been returned (freed).
What does char* hold? The address of the first box of a run of characters.
Why does a 2-letter word need 3 boxes? One extra box for the null terminator '\0' that marks the end.
What is the heap ? A pool of boxes you can borrow at any time for any lifetime, and must return yourself (unlike the automatic stack).
What does new char[n] give you? An arrow (address) to a freshly borrowed run of n heap boxes.
What must new[] be paired with, and why? delete[] — mismatched forms are undefined behaviour.
What is undefined behaviour ? A situation the language leaves totally unconstrained — crash, corruption, or silent "works for now"; the compiler assumes it never happens.
What is a memory leak ? Borrowed boxes never returned (delete never called).
What is a double free ? Returning the same boxes twice — undefined behaviour (a crash).
What does it mean for an object to own a resource? It is responsible for returning that resource exactly once.
Shallow vs deep copy in one line each? Shallow copies the arrow (shared buffer); deep copies the boxes (own buffer).
What does &other mean (value form)? The address of other — an arrow to it.
What does const T& other mean? A reference (alias) to other that you promise not to change.
What is this? The address of the object the current method was called on.
What does this == &other test? Whether I am the very same object I'm being asked to copy from (self-assignment).
Why is len a std::size_t? It's a count/size, which can never be negative.