C++ / MEMORY OWNERSHIP AND SMART POINTERS
Ownership and who deletes what
Decide and document, for every heap object, which single piece of code deletes it, and tell owning pointers apart from borrowed ones in an API.
What you will learn
- Name the one owner and the exact delete point for every heap object you create
- Read a signature and tell owning returns and sink parameters from borrowed ones
- Transfer ownership by handing over the pointer and nulling the old holder
- Diagnose a memory bug as zero owners (leak) or two owners (double delete)
Understanding Ownership and who deletes what
Every new creates an obligation, not just an object: exactly one delete has to run for that address, exactly once. A raw Widget* carries none of that information, because it is an address and nothing else, so two pointers to the same object look identical whether one of them is responsible for freeing it or not. Ownership therefore lives outside the type, in a comment, a naming convention, or a class invariant. If you cannot answer "which single piece of code deletes this, and when?" in one sentence, the bug is already in the design.
At any moment there are only two roles. The owner is the one holder that will free the object and must do it once; everyone else is a borrower, allowed to read and even modify the object but never to free it, and never to outlive the owner. Ownership bugs are miscounts of owners: zero owners means the memory is never returned, and two owners means the same block is freed twice, which hands the allocator a block it may have already reused and typically corrupts unrelated data instead of crashing at the delete. Ownership can move between holders, but a move only stays honest if the old holder forgets the address, which is why a transfer is a hand-over plus a null assignment.
Since raw pointers cannot express any of this, C++ code states it in the signature instead. Returning a bare pointer from a factory conventionally means the caller now owns the object; taking a bare pointer that the function deletes is a sink parameter; taking a const reference or const pointer means the function is only looking. Passing borrowed objects by reference is the strongest form of that, because a caller cannot accidentally delete a reference. Smart pointers, which come next, exist to move these sentences out of comments and into types the compiler checks, but the rule they enforce is exactly the one above.
<iostream>
<string>
<utility>
struct Widget {
std::string name;
explicit Widget(std::string n) : name(std::move(n)) {
std::cout << "make " << name << '\n';
}
~Widget() { std::cout << "destroy " << name << '\n'; }
};
// Returns ownership: the caller must delete the result.
Widget* createWidget(const std::string& name) {
return new Widget(name);
}
// Borrows: reads the object, never deletes it, never stores the pointer.
void printWidget(const Widget* w) {
std::cout << "borrowed " << w->name << '\n';
}
// Takes ownership: this function is responsible for the delete.
void consumeWidget(Widget* w) {
std::cout << "consuming " << w->name << '\n';
delete w;
}
int main() {
Widget* a = createWidget("A"); // main owns A
printWidget(a); // main still owns A
delete a; // main's obligation, discharged here
Widget* b = createWidget("B"); // main owns B
consumeWidget(b); // ownership handed over
b = nullptr; // main holds no owning pointer now
std::cout << "end of main\n";
}
A heap object must have exactly one owner at every moment, and because a raw pointer cannot say who that is, the ownership contract has to be stated by the API and honored by hand.
Worked examples
One owning container, many observers
Two vectors hold the same addresses, but only one of them is the owner that deletes.
<iostream>
<vector>
struct Node {
int id;
~Node() { std::cout << "delete node " << id << '\n'; }
};
int main() {
std::vector<Node*> owner; // owns every Node it holds
std::vector<Node*> ready; // observes only, deletes nothing
for (int i = 1; i <= 3; ++i) owner.push_back(new Node{i});
ready.push_back(owner[0]);
ready.push_back(owner[2]);
for (Node* n : ready) std::cout << "ready: " << n->id << '\n';
ready.clear(); // drops addresses, frees nothing
for (Node* n : owner) delete n; // exactly one delete per Node
owner.clear();
std::cout << "done\n";
}
Example explained
Line 1owner is declared as the single owning container, so the loop over owner is the only place a Node is freed.
Line 2ready.push_back(owner[0]) copies an address, not a Node, so both vectors point at the same objects while only one owns them.
Line 3ready.clear() prints nothing, because clearing a vector of raw pointers destroys pointers, and a pointer has no destructor.
Line 4ready is cleared before the delete loop, since after the loop every address it held would refer to freed memory.
Handing ownership in and back out
A class takes ownership in its constructor, blocks copying, and gives ownership back through release().
<iostream>
struct Connection {
int id;
explicit Connection(int i) : id(i) { std::cout << "open " << id << '\n'; }
~Connection() { std::cout << "close " << id << '\n'; }
};
class Holder {
public:
explicit Holder(Connection* c) : c_(c) {} // takes ownership
~Holder() { delete c_; } // and discharges it here
Holder(const Holder&) = delete; // two Holders would delete once each
Holder& operator=(const Holder&) = delete;
Connection* get() const { return c_; } // lends, ownership unchanged
Connection* release() { // hands ownership back out
Connection* out = c_;
c_ = nullptr;
return out;
}
private:
Connection* c_;
};
int main() {
{
Holder h(new Connection(1));
std::cout << "using " << h.get()->id << '\n';
} // h still owns 1, so its destructor closes it
Connection* raw = nullptr;
{
Holder h(new Connection(2));
raw = h.release(); // main is the owner from here on
} // h owns nothing, its destructor deletes a null pointer
std::cout << "still open " << raw->id << '\n';
delete raw;
}
Example explained
Line 1The constructor taking Connection* is the transfer point: after it runs, only the Holder is allowed to delete.
Line 2The deleted copy constructor removes the easiest way to end up with two owners, which would delete the same Connection twice.
Line 3release() nulls c_ before returning, so the object cannot be closed by both the Holder and main.
Line 4Leaving the second block prints nothing because delete on a null pointer is defined to do nothing.
Important notes
delete on a null pointer is well defined and does nothing, which is precisely why nulling the source after a transfer keeps the number of real deletes at one.
Ownership is about who frees, not who may modify: a borrower can legitimately hold a non-const pointer and change the object, as long as it never deletes it and never outlives the owner.
Common mistakes
Calling delete on a pointer that was only lent to you, such as a function parameter the caller still owns, which produces two deletes for one allocation and corrupts the allocator's bookkeeping.
Letting a class that holds an owning raw pointer be copied by the compiler's default copy constructor: both objects hold the same address and both destructors delete it, so pushing one copy into a vector is enough to double-free.
Reading "ownership transferred" as "we both have it" and continuing to dereference or delete the local pointer after passing it to a sink function, which is a use-after-free or a second delete of the same block.
Try it yourself
Change, predict, then run
Extend the Holder class with reset(Connection* c) that deletes the connection it currently owns before storing the new one. Call it with a second connection and then with nullptr, and confirm from the open and close lines that each connection is closed exactly once.
Open the C++ workspaceCheck your understanding
A Logger stores every Message* it is handed in a vector and deletes them all in its destructor. One caller allocates a Message with new, hands it to the Logger, and also deletes its own copy of the pointer afterwards. What is the defect, in ownership terms?
- The Logger's vector copies each Message, so memory use doubles
- The Message never gets an owner, so it leaks
- Two parties consider themselves the owner, so the Message is deleted twice
- It is safe as long as the caller's delete runs before the Logger is destroyed
Show answer
The Logger's contract is that it owns and deletes every Message it receives, so a caller that also deletes makes two owners for one allocation, and the second delete operates on a block the allocator has already taken back. Option 4 is tempting because sequencing the deletes feels like it fixes the ordering, but the block is gone after whichever delete runs first, so the remaining one is still invalid; the fix is to remove one of the two deletes. Option 1 is wrong because the vector stores addresses, not Message objects.