why smart pointers

A smart pointer is an object that behaves like a pointer but also owns what it points to — it calls delete for you, automatically, the moment ownership ends. That eliminates the two failure modes of raw new/delete: forgetting to delete (a leak) and deleting more than once or deleting the wrong thing (a crash). Both live in <memory>.
unique_ptrshared_ptrweak_ptr
Ownershipexclusive — exactly one ownershared — a reference count tracks how many owners existnone — observes without owning
Copyableno (movable only)yes — copying bumps the ref countyes
Overheadnone — same size/speed as a raw pointera control block with two atomic countersshares the shared_ptr's control block
Use it whenthis is the default — reach for unique_ptr unless you specifically need shared ownershipmultiple parts of the code genuinely need to co-own an object's lifetimebreaking a cycle between shared_ptrs, or observing without extending lifetime

unique_ptr

std::unique_ptr<T> p = std::make_unique<T>(args...); | https://en.cppreference.com/w/cpp/memory/unique_ptr/make_unique | construct T on the heap, owned by p; prefer this over std::unique_ptr<T>(new T(...)) |'sp1'
make_unique is preferred over a bare new mainly for exception safety: with f(unique_ptr<T>(new T), other_arg()), C++ doesn't guarantee argument evaluation order, so if other_arg() throws after new T already ran but before the unique_ptr constructor captures it, that memory leaks. make_unique does construction and ownership capture in one step, so there's no window for that to happen.
Because a unique_ptr can't be copied, passing it to a function by value won't compile — the compiler would need a copy constructor that doesn't exist. Two options: pass by reference (the function borrows it, ownership doesn't change), or pass by value with an explicit std::move (ownership actually transfers to the function, and the caller's pointer becomes null).

void borrow(std::unique_ptr<Widget> &p) { /* p is still owned by the caller after this returns */ }
void take(std::unique_ptr<Widget> p)     { /* p is destroyed when this function returns */ }

auto w = std::make_unique<Widget>();
borrow(w);              // fine — w is unaffected
take(std::move(w));     // ownership transferred — w is now null in the caller
            
p.reset(); | https://en.cppreference.com/w/cpp/memory/unique_ptr/reset | destroy the owned object now and set p to null; reset(newPtr) replaces it with a new owned object |'sp2'
T *raw = p.get(); | https://en.cppreference.com/w/cpp/memory/unique_ptr/get | borrow the raw address without giving up ownership — p still owns and will still delete it |'sp3'
T *raw = p.release(); | https://en.cppreference.com/w/cpp/memory/unique_ptr/release | give up ownership entirely — p becomes null, and raw is now your responsibility to delete |'sp4'
Trace an actual sequence of unique_ptr operations — move, reset, get vs. release — step by step: TopNotchNote/cpp/smart_pointers_unique_ownership.cpp

shared_ptr and the reference count

Every shared_ptr to the same object shares one control block holding two counts: how many shared_ptrs currently own the object, and how many weak_ptrs are observing it. Copying a shared_ptr increments the owner count; destroying or resetting one decrements it. The object is deleted the instant the owner count hits zero — not before, regardless of how many weak observers remain.
auto p = std::make_shared<T>(args...); | https://en.cppreference.com/w/cpp/memory/shared_ptr/make_shared | construct T and its control block in a single heap allocation |'sp5'
p.use_count() | https://en.cppreference.com/w/cpp/memory/shared_ptr/use_count | current number of shared_ptr owners (debugging/diagnostics only — don't branch production logic on it) |'sp6'
Copying a shared_ptr is comparatively expensive — it touches an atomic counter, because the count has to stay correct even if two threads copy the same shared_ptr concurrently. Pass by const& when a function only needs to use the object, and reserve pass-by-value (which does copy it, deliberately) for a function that needs to become a co-owner.
make_shared vs. plain new: make_shared allocates the object and its control block together in one block, which is faster and more cache-friendly — use it by default. The one time to prefer new instead is when a weak_ptr will outlive the object: because the control block and the object share one allocation, that whole block can't be freed until both the owner count and the weak count reach zero. A long-lived weak_ptr can therefore keep a large object's memory pinned after the object itself is already destructed. Allocating with new instead keeps the object and control block as two separate allocations, so the object's memory is freed as soon as the owner count hits zero.

weak_ptr

A weak_ptr observes an object owned by a shared_ptr without being an owner itself — it doesn't keep the object alive, and it knows when the object is gone. You can't dereference a weak_ptr directly; call .lock() first, which returns a shared_ptr that's either valid (and briefly extends the object's lifetime while you use it) or null (if the object is already gone).
auto w = std::weak_ptr<T>(sharedPtr); | https://en.cppreference.com/w/cpp/memory/weak_ptr | create a non-owning observer of an object owned by a shared_ptr |'sp7'
if (auto sp = w.lock()) { ... } | https://en.cppreference.com/w/cpp/memory/weak_ptr/lock | the only safe way to use what a weak_ptr observes — null if the object is already destroyed |'sp8'
The main use case is breaking reference cycles. Two objects holding shared_ptrs to each other never reach a zero owner count — each keeps the other's count above zero forever, leaking both. Making one side of the relationship a weak_ptr breaks the cycle without changing which object logically owns which. TopNotchNote/cpp/smart_pointers_weak_cycle.cpp

iterating containers of unique_ptr

Since a unique_ptr can't be copied, a range-based loop over std::vector<std::unique_ptr<T>> has to bind by reference — the default copy-binding a range-for otherwise uses would try to copy each element and fail to compile.

std::vector<std::unique_ptr<Widget>> widgets;
widgets.push_back(std::make_unique<Widget>());

for (const auto &w : widgets) { w->draw(); }   // by reference — required
// for (auto w : widgets) { ... }                  // error: unique_ptr copy constructor is deleted
            

where to go from here

Move Semantics — the mechanism (std::move) that makes unique_ptr ownership transfer possible.
Pointers & References — the raw-pointer mechanics smart pointers wrap.
Classes & Constructors — giving your own classes correct copy/move behavior.
Threading & Concurrency — shared_ptr's reference count is itself atomic, safe to copy across threads.

reference

cppreference — std::unique_ptr
cppreference — std::shared_ptr
cppreference — std::weak_ptr