Memory, RAII and Smart Pointers
7 pages · ~5 min✓ Reviewed
Builds on Classes and OOP. Next up: Exceptions and Error Handling.
RAII and the new/delete Problem
1. What RAII Is
- RAII: acquire in the constructor, release in the destructor
- Destructor runs on every scope exit: return, early return, exception
- Constructoracquire
- Usemay throw or return early
- Destructorrelease, always
2. Why Raw new/delete Fails
auto p = new T;
f(); // throws -> delete skipped
delete p;leaks on exception or early return
| Allocate | Release | Wrong (UB) |
|---|---|---|
new T | delete p | delete[] p, free(p) |
new T[n] | delete[] p | delete p |
malloc | free(p) | delete p |
- Double delete and use-after-free are UB
p = nullptrafter delete is a band-aid, not ownership
3. The Rule
- No naked
new/deletein application code - Raw
T*orT&= non-owning observer - Ownership lives in a RAII type
If cleanup is written by hand after a call that can throw, it is a leak waiting to happen.
unique_ptr: Exclusive Ownership
1. What unique_ptr Is
std::unique_ptr<T>owns one object exclusively- Default deleter: zero overhead vs a raw pointer
- Destructor runs delete automatically on scope exit
- Not copyable, only movable
auto b = std::move(a);leavesanullunique_ptr<T[]>usesdelete[]and supportsp[i]
2. Access API
| Call | Effect | Deletes? |
|---|---|---|
get() | borrow the raw pointer | no |
release() | give up ownership, return pointer | no |
reset(p) | delete old object, take p | old one |
auto a = std::make_unique<int>(7); auto b = std::move(a); // a is now null int* raw = b.release(); // you must delete raw delete raw;
release() hands the delete duty back to you
3. Passing and Returning
shared_ptr, weak_ptr and make_shared
1. Control Block
- Holds strong count, weak count, deleter
- Strong count hits 0 → object destroyed
- Copy bumps count atomically
- Control block thread-safe; pointee is not
2. Cycles and weak_ptr
- A owns Bshared_ptr
- B owns Ashared_ptr
- Never 0leak
- Fixweak_ptr back-ref
- weak_ptr does not keep the object alive
lock()gives a shared_ptr, or null if expiredexpired()checks
std::weak_ptr<Node> wp = sp; if (auto s = wp.lock()) { use(*s); // alive here } bool gone = wp.expired();
3. One Control Block
shared_ptr<T> a(p), b(p);→ two control blocks- Both delete
p→ double delete - In a class, use
shared_from_this()
struct W : std::enable_shared_from_this<W> {
std::shared_ptr<W> self() {
return shared_from_this();
}
};object must already be owned by a shared_ptr
make_unique and make_shared
1. Prefer make_* over new
std::make_unique<T>(args)andstd::make_shared<T>(args)replacenew- Type is named once:
auto p = std::make_unique<Foo>(1); - Exception safe in argument lists: no leak if another argument throws
2. make_shared: one allocation
| Form | Allocations | Cache |
|---|---|---|
make_shared<T>() | 1 (object + control block) | friendly |
shared_ptr<T>(new T) | 2 (object, then block) | scattered |
- Trade-off: object and block share one chunk
- Object is destroyed at strong count 0, but its memory stays until the last
weak_ptrdies
3. Limits and the default
- make_* cannot take a custom deleter or a brace-init list
- Fall back to the constructor with
new
Move Semantics, Rvalue References and Copy Elision
1. Lvalue, Rvalue, std::move
- lvalue = has a name and an address; rvalue = temporary
T&&binds to temporaries, so it can steal resourcesstd::move(x)is only a cast toT&&; the move constructor does the transfer- Moved-from object: valid but unspecified; only destroy or assign to it
2. Writing a Move
- Steal the pointer, null the source, mark it noexcept
- Without
noexcept,std::vectorcopies on reallocation
Widget(Widget&& o) noexcept
: p(o.p) { o.p = nullptr; }
Widget b = std::move(a); // a.p is nulla is still valid, just empty
3. Return, Forward, Rules
| Return form | Effect |
|---|---|
return local; | elided or moved |
return std::move(local); | blocks RVO |
return T{}; | elision guaranteed (C++17) |
Smart Pointers and Move Cheat Sheet
1. Who owns it?
| Need | Use |
|---|---|
| Sole owner | unique_ptr |
| Shared owner | shared_ptr |
| Observe shared, no ownership | weak_ptr |
| Non-owning use | T* / T& |
2. Create, transfer, check
auto u = std::make_unique<T>(1); auto s = std::make_shared<T>(1); auto u2 = std::move(u); // u is null std::weak_ptr<T> w = s; if (auto p = w.lock()) { } // null if expired
check a smart pointer with if (s)
| unique_ptr | shared_ptr |
|---|---|
get() borrows | get() borrows |
release() gives up, no delete | use_count() strong count |
reset(p) deletes old, takes p | reset() drops one share |
swap(o) | lock() via weak_ptr |
3. Move rules
Check yourself
What does this print, and why?
nullstd::moveonly casts; the move constructor ofbsteals the pointer and nullsa.
auto a = std::make_unique<int>(5); auto b = std::move(a); std::cout << (a ? "full" : "null");
What is the bug here?
- Two control blocks own one object, so it is deleted twice (UB).
- Copy instead:
b = a;, or usemake_shared.
int* p = new int(1);
std::shared_ptr<int> a(p);
std::shared_ptr<int> b(p);Why is return std::move(w); worse than return w; for a local w?
- It blocks copy elision (RVO), so a move is forced.
return w;lets the compiler elide the object or move it.