C++ Memory: Stack vs Heap, Smart Pointers, unique_ptr, shared_ptr & Move Semantics

C++ Memory: Stack vs Heap, Smart Pointers, unique_ptr, shared_ptr & Move Semantics
Table of Contents
- Stack vs Heap: The Performance Anatomy
- RAII: The Foundation of Modern C++ Memory
- unique_ptr: Exclusive Ownership
- shared_ptr: Shared Reference-Counted Ownership
- weak_ptr: Breaking Reference Cycles
- Move Semantics: Eliminating Unnecessary Copies
- std::move and std::forward
- Perfect Forwarding
- std::span: Safe Non-Owning Memory Views (C++20)
- The Rule of Zero, Three, and Five
- Frequently Asked Questions
- Key Takeaway
Stack vs Heap: The Performance Anatomy
Stack allocation:
Heap allocation cost:
- Mutex acquisition (thread-safe heap)
- Free block search in allocator's tree/list
- Page fault on first access (OS maps physical page)
- Cache miss (newly allocated memory is cold in cache)
Rule: Default to stack. Use heap only when data must outlive its scope, or when the size exceeds the stack limit (~8MB total).
RAII: The Foundation of Modern C++ Memory
RAII (Resource Acquisition Is Initialization) is the core C++ memory safety idiom: resources (memory, file handles, locks, network connections) are acquired in constructors and released in destructors. Since destructors always run when an object leaves scope — even when exceptions are thrown — RAII guarantees no leaks:
unique_ptr: Exclusive Ownership
std::unique_ptr<T> is a zero-overhead smart pointer providing exclusive ownership. Exactly one unique_ptr owns an object at any time. When the unique_ptr is destroyed, the owned object is automatically deleted:
Zero overhead: At compile time, unique_ptr with a default deleter is exactly as efficient as a raw pointer. It generates no additional machine code compared to manual new/delete — verified on Compiler Explorer.
shared_ptr: Shared Reference-Counted Ownership
std::shared_ptr<T> allows multiple owners. A reference count tracks how many shared_ptr instances own the object; when the last one is destroyed, the object is deleted:
shared_ptr overhead: Each make_shared<T> allocates one control block (reference count + deleter + optional allocator). Copying a shared_ptr is atomic increment — thread-safe but not free (~10ns per copy on modern hardware due to the atomic operation). Avoid copying shared_ptr in hot paths; use const shared_ptr& for read-only access.
weak_ptr: Breaking Reference Cycles
shared_ptr reference cycles (A holds B, B holds A) prevent both from ever being deleted — a memory leak. weak_ptr is a non-owning observer of a shared_ptr-managed object:
Move Semantics: Eliminating Unnecessary Copies
Before C++11, passing or returning a std::vector<int> of 1M elements made a complete deep copy — expensive. Move semantics transfer ownership rather than copying:
std::move and std::forward
std::span: Safe Non-Owning Memory Views (C++20)
std::span<T> is a lightweight non-owning view over a contiguous sequence of T — like a pointer + size, but with bounds checking and range-for support:
Frequently Asked Questions
Should I always prefer unique_ptr over raw pointers?
Yes, for owning pointers. Raw pointers are still appropriate for non-owning "observer" pointers (where the ownership is clear and lifetime is managed elsewhere). In C++, if a pointer owns its target, it should be unique_ptr or shared_ptr. If it doesn't own, it's fine to use T* or T& — just document the ownership model.
What is the difference between make_shared and shared_ptr constructor?
std::make_shared<T>(args) allocates the control block and the T object in a single allocation — better cache locality, one fewer allocation. std::shared_ptr<T>(new T(args)) does two separate allocations. Always prefer make_shared unless you have a specific reason (e.g., custom deleters or aliasing constructors).
When does std::move NOT actually move?
std::move is a cast — it doesn't move anything itself. It enables move semantics by casting to T&&. If the type has no move constructor or move assignment, std::move silently falls back to copy. Also, after return local_var; the compiler applies Named Return Value Optimization (NRVO) or implicit move, so explicitly writing return std::move(local_var) can actually prevent optimization.
Key Takeaway
Modern C++ Memory = RAII + Smart Pointers + Move Semantics. These eliminate manual delete, prevent double-free and use-after-free bugs, and eliminate unnecessary copies of large objects. Once you understand that unique_ptr has zero overhead, shared_ptr has minimal overhead (one atomic increment per copy), and std::move is free (just a cast), you can write memory-safe C++ that matches the performance of bare-metal C.
Read next: Modern C++ Functions: Lambdas, References & Parameter Passing →
Part of the C++ Mastery Course — 30 modules from modern C++ basics to expert systems engineering.
