Memory, Smart Pointers and Move · Lesson 13 of 33
Smart Pointers in C++: unique_ptr and shared_ptr
C++ inherits raw pointers and manual memory management from C, with new and delete in place of malloc and free. Forgetting a delete leaks memory; deleting twice crashes. Modern C++ solves this with smart pointers: objects that own heap memory and free it automatically (RAII). In modern code you should almost never write delete.
In this lesson
- Raw pointers, new and delete – and their problems
std::unique_ptr: single ownershipstd::shared_ptr: shared ownership with reference countingstd::weak_ptrand choosing the right pointer
new and delete
#include <iostream>
int main() {
int* p = new int(42); // one int on the heap
int* arr = new int[5]{1, 2, 3, 4, 5}; // an array on the heap
std::cout << *p << ' ' << arr[4] << '\n';
delete p; // free a single object
delete[] arr; // free an array: must use delete[]
p = nullptr; // nullptr, not NULL, in C++
return 0;
}
42 5
Why raw new/delete is risky
- An early
returnor an exception skips thedelete→ memory leak. - Mixing
deleteanddelete[]is undefined behaviour. - Two pointers to the same object → double delete, or a dangling pointer.
- Nothing in the type says who is responsible for deleting.
For arrays, use std::vector. For single heap objects, use smart pointers.
unique_ptr: one owner
#include <iostream>
#include <memory>
#include <string>
#include <utility>
struct Document {
std::string title;
explicit Document(std::string t) : title(std::move(t)) { std::cout << "open " << title << '\n'; }
~Document() { std::cout << "close " << title << '\n'; }
};
std::unique_ptr<Document> create(const std::string& name) {
return std::make_unique<Document>(name); // ownership moves to the caller
}
int main() {
auto report = create("report.pdf");
std::cout << "editing " << report->title << '\n';
// auto copy = report; // error: unique_ptr can't be copied
auto owner = std::move(report); // ownership transferred
std::cout << "report is " << (report ? "set" : "empty") << ", owner has " << owner->title << '\n';
{
auto temp = std::make_unique<Document>("temp.txt");
} // temp closed here automatically
std::cout << "end of main\n";
return 0;
} // owner closed here
open report.pdf editing report.pdf report is empty, owner has report.pdf open temp.txt close temp.txt end of main close report.pdf
unique_ptr has the same size and speed as a raw pointer, but deletes its object when it goes out of scope. It can't be copied – only moved – so there's always exactly one owner. It should be your default choice.
shared_ptr: shared ownership
#include <iostream>
#include <memory>
#include <string>
#include <vector>
struct Image {
std::string file;
explicit Image(std::string f) : file(std::move(f)) {}
~Image() { std::cout << "freed " << file << '\n'; }
};
int main() {
auto logo = std::make_shared<Image>("logo.png");
std::cout << "owners: " << logo.use_count() << '\n';
{
std::vector<std::shared_ptr<Image>> pages{logo, logo, logo}; // three more owners
std::cout << "owners: " << logo.use_count() << '\n';
} // the vector's copies are released
std::cout << "owners: " << logo.use_count() << '\n';
logo.reset(); // last owner gone → object freed
std::cout << "done\n";
return 0;
}
owners: 1 owners: 4 owners: 1 freed logo.png done
Reference counting
- make_sharedcreates the object and a control block with count = 1
- each copycount + 1
- each destroyed or reset copycount − 1
- count reaches 0the object is deleted
weak_ptr: observing without owning
#include <iostream>
#include <memory>
struct Node {
int value;
std::shared_ptr<Node> next; // owns the next node
std::weak_ptr<Node> prev; // doesn't own: avoids a reference cycle
explicit Node(int v) : value(v) {}
~Node() { std::cout << "node " << value << " freed\n"; }
};
int main() {
auto a = std::make_shared<Node>(1);
auto b = std::make_shared<Node>(2);
a->next = b;
b->prev = a;
if (auto p = b->prev.lock()) { // lock() gives a shared_ptr if the object still exists
std::cout << "b's previous node is " << p->value << '\n';
}
return 0;
} // both nodes freed: no cycle of shared_ptrs
b's previous node is 1 node 1 freed node 2 freed
If prev were a shared_ptr, a and b would own each other, their counts would never reach zero, and both would leak. weak_ptr breaks such cycles – common in trees with parent links, caches and observer patterns.
Which pointer?
| Situation | Use |
|---|---|
| A sequence of objects | std::vector<T> (no pointer at all) |
| One owner of a heap object (polymorphic objects, large objects, factories) | std::unique_ptr<T> via make_unique |
| Several owners, lifetime unknown in advance | std::shared_ptr<T> via make_shared |
| Refer to a shared object without keeping it alive | std::weak_ptr<T> |
| A function just uses an object it doesn't own | T&, const T& or a raw T* (non-owning) |
Interview angle
Expect "unique_ptr vs shared_ptr", "how does shared_ptr know when to delete?" (reference count in a control block) and "what is a reference cycle and how does weak_ptr fix it?". Also: prefer make_shared/make_unique over new – they're exception-safe and make_shared does one allocation instead of two.
Practice
Try it yourself
Store polymorphic animals in a
std::vector<std::unique_ptr<Animal>>and print their sounds.Show solution
#include <iostream> #include <memory> #include <vector> struct Animal { virtual ~Animal() = default; virtual const char* sound() const = 0; }; struct Dog : Animal { const char* sound() const override { return "Woof"; } }; struct Cat : Animal { const char* sound() const override { return "Meow"; } }; int main() { std::vector<std::unique_ptr<Animal>> zoo; zoo.push_back(std::make_unique<Dog>()); zoo.push_back(std::make_unique<Cat>()); for (const auto& a : zoo) std::cout << a->sound() << '\n'; return 0; }Woof Meow
Quick check
Can you copy a unique_ptr?
No – only move it with std::move.
When is a shared_ptr's object deleted?
When the last shared_ptr owning it is destroyed or reset (count reaches 0).
What problem does weak_ptr solve?
Reference cycles between shared_ptrs, which would otherwise leak.
Summary
- Raw new/delete is error-prone; use vector for arrays and smart pointers for heap objects.
- unique_ptr = one owner (the default); shared_ptr = reference-counted shared ownership.
- weak_ptr observes without owning and breaks cycles; create with make_unique/make_shared.
Finished this lesson?
Mark it complete to track your progress, then carry on with the next lesson.