20 topics
← Back to Quick Reference/
Topic 20
DDyynnaammiicc MMeemmoorryy
new/delete · unique_ptr · shared_ptr · weak_ptr · Ownership · Pools
C++17 · Advanced ReferenceDynamic Memory in C++
01Stack vs Heap
Local variables live on the stack — fast, automatically managed, limited in size (~1–8 MB). Objects that need to outlive their scope, be shared, or are too large for the stack go on the heap — managed with new /delete or (preferably) smart pointers.
The ownership model
- 1.Every heap object must have exactly one owner responsible for its deletion.
- 2.
unique_ptr— single owner. Zero overhead. Deleted when it goes out of scope. - 3.
shared_ptr— multiple owners. Reference-counted. Deleted when count hits zero. - 4.
weak_ptr— non-owner observer. Must lock before use. Breaks cycles.
Rules
- 1.Never use raw
newin application code — wrap immediately in a smart pointer. - 2.
new[]must be paired withdelete[], never plaindelete. - 3.Prefer
make_unique/make_sharedovernew— they are exception-safe and (for shared_ptr) more efficient. - 4.Use
std::vectorinstead ofnew T[]— it manages its own memory.
If you find yourself writing delete, you are almost certainly doing it wrong. Let RAII and smart pointers handle cleanup.
new & delete
02// ── new / delete — single object ───────────────────────────── int* p = new int(42); // allocate + initialize *p; // 42 delete p; // release memory p = nullptr; // good practice: null after delete // ── new[] / delete[] — arrays ──────────────────────────────── int* arr = new int[10]{}; // 10 zero-initialized ints arr[0] = 5; delete[] arr; // must use delete[], not delete // ── Placement new — construct into existing memory ──────────── alignas(int) char buf[sizeof(int)]; int* p2 = new (buf) int(99); // constructs in buf — no heap alloc p2->~int(); // must call destructor manually // ── operator new / operator delete — raw allocation ────────── void* raw = ::operator new(sizeof(MyClass)); MyClass* obj = new (raw) MyClass(args); obj->~MyClass(); ::operator delete(raw); // ⚠ new throws std::bad_alloc if allocation fails // Use new (std::nothrow) T to get nullptr instead of exception int* safe = new (std::nothrow) int(5); if (!safe) { /* handle OOM */ }
| new T(args) | Allocates heap memory and constructs T. Throws std::bad_alloc on failure. |
| delete p | Calls destructor then frees memory. Always null p afterward to prevent double-delete. |
| new T[n] | Allocates array. Must be freed with delete[], not delete. |
| placement new | Constructs an object in pre-allocated memory. No heap allocation. Must call destructor manually. |
| new (nothrow) | Returns nullptr instead of throwing on allocation failure. Use in memory-constrained environments. |
Always use
make_unique instead of new. Raw new in application code is a code smell — it requires manual delete and is not exception-safe.unique_ptr
03#include <memory> // ── unique_ptr — sole ownership, zero overhead ─────────────── auto p = std::make_unique<int>(42); // preferred — exception safe *p; // 42 p.get(); // raw int* (non-owning) p.reset(); // destroy object; p becomes null p = nullptr; // same as reset() // Transfer ownership — source becomes null auto q = std::move(p); // p=null, q owns the int // unique_ptr for arrays auto arr = std::make_unique<int[]>(10); arr[5] = 99; // operator[] defined for array form // Custom deleter auto file = std::unique_ptr<FILE, decltype(&fclose)>( fopen("data.txt", "r"), fclose); // fclose called on destruction // ── Returning from a factory ────────────────────────────────── std::unique_ptr<Shape> makeShape(std::string_view type) { if (type == "circle") return std::make_unique<Circle>(5.0); if (type == "square") return std::make_unique<Square>(3.0); return nullptr; } auto shape = makeShape("circle"); shape->area(); // virtual dispatch still works through unique_ptr
| make_unique<T> | Preferred construction. Exception-safe. Equivalent to unique_ptr<T>(new T(args)) but cleaner. |
| std::move(p) | Transfers ownership. Source becomes null. unique_ptr cannot be copied — only moved. |
| p.get() | Raw non-owning pointer. Use for C APIs. Don't store — may dangle after unique_ptr is reset. |
| custom deleter | Second template param specifies the deleter type. Use for FILE*, handles, C resources. |
shared_ptr
04#include <memory> // ── shared_ptr — shared ownership via reference count ──────── auto s1 = std::make_shared<int>(10); // count=1 auto s2 = s1; // count=2 (copy) auto s3 = s1; // count=3 s1.use_count(); // 3 s1.reset(); // count=2; object still alive s2.reset(); // count=1 s3.reset(); // count=0 → object destroyed // shared_ptr is copyable — unique_ptr is not void register(std::shared_ptr<Widget> w) { cache.push_back(w); } // ── make_shared vs new ──────────────────────────────────────── // make_shared: one allocation for object + control block (faster) auto good = std::make_shared<int>(5); // ✅ single alloc // shared_ptr<int> bad(new int(5)); // ⚠ two allocs // ── Circular reference — memory leak! ──────────────────────── struct Node { std::shared_ptr<Node> next; // ❌ if two nodes point to each other, }; // neither ref count reaches zero
| use_count() | Current reference count. Useful for debugging — don't base logic on it in production. |
| make_shared | Single allocation for object + control block. Faster than shared_ptr<T>(new T). Preferred. |
| circular refs | If A holds shared_ptr<B> and B holds shared_ptr<A>, neither count reaches zero — memory leak. Use weak_ptr for back-references. |
| thread safety | The control block (ref count) is thread-safe. The pointed-to object is NOT — protect with a mutex. |
shared_ptr is not free. Every copy increments an atomic counter. Prefer
unique_ptr and pass by raw pointer/reference for non-owning access.weak_ptr
05#include <memory> // weak_ptr — non-owning observer of a shared_ptr // Doesn't increment the reference count // Must be "locked" to use — returns null if object was destroyed struct Cache { std::weak_ptr<Resource> cached; std::shared_ptr<Resource> get() { if (auto r = cached.lock()) { // lock() → shared_ptr or null return r; // object still alive } return nullptr; // object was destroyed } }; // ── Breaking circular references ────────────────────────────── struct Node { std::shared_ptr<Node> next; std::weak_ptr<Node> prev; // ✅ weak breaks the cycle }; // ── Observer pattern ────────────────────────────────────────── class EventSource { std::vector<std::weak_ptr<Listener>> listeners_; public: void notify() { listeners_.erase( std::remove_if(listeners_.begin(), listeners_.end(), [](auto& w){ return w.expired(); }), listeners_.end()); for (auto& w : listeners_) if (auto l = w.lock()) l->onEvent(); } };
| w.lock() | Returns shared_ptr if the object exists, nullptr if it was destroyed. Always check the result. |
| w.expired() | Returns true if the managed object has been destroyed. Faster than lock() when you only need to check. |
| cycle breaking | In a parent-child graph, parent owns children (shared_ptr), children reference parent (weak_ptr) — no cycle. |
| observer lists | Store weak_ptr in observer/listener lists — dead observers are automatically skippable without crashing. |
Ownership Decision Guide
06// ── The ownership decision tree ────────────────────────────── // Is the object shared between multiple owners? // Yes → shared_ptr<T> // No → unique_ptr<T> (or stack allocation if small + short-lived) // Do you need a non-owning reference to a shared object? // → weak_ptr<T> (safe, checks if alive) // → raw T* (unsafe, only if lifetime is well-known) // Is the object large or needs to outlive the current scope? // → heap allocation (unique_ptr or shared_ptr) // Otherwise → stack or member variable // ── When raw new/delete is acceptable ──────────────────────── // 1. Inside a custom smart pointer or container (you're building RAII) // 2. Placement new for custom memory management // 3. Rare performance-critical code after profiling // ⚠ Never: raw new in application code where a smart pointer works // ── Summary ─────────────────────────────────────────────────── // unique_ptr → 95% of heap allocation needs // shared_ptr → shared ownership between components // weak_ptr → caches, observers, cycle-breaking // raw ptr → non-owning views only
| unique_ptr | Default choice for heap allocation. Zero overhead over raw pointer. 95% of cases. |
| shared_ptr | Use when multiple components genuinely need to co-own an object's lifetime. |
| weak_ptr | Caches, observer lists, parent back-references — anywhere you need to observe without owning. |
| raw T* | Non-owning view only. Acceptable when lifetime is guaranteed by the caller (e.g. passing to a function). |
Advanced: Pools & PMR
07// ── Stack allocation (no heap at all) ──────────────────────── int arr[1000]; // 4 KB on the stack — instant, no overhead std::array<int,1000> a; // same, with bounds checking // ── Custom allocator (advanced) ─────────────────────────────── // Standard containers accept an allocator as the second template param // Useful for: arenas, pools, logging allocation, embedded systems // ── Object pool — reuse fixed-size blocks ───────────────────── template<typename T, std::size_t N> class Pool { alignas(T) char storage_[N * sizeof(T)]; std::stack<T*> free_; public: Pool() { for (std::size_t i = 0; i < N; i++) free_.push(reinterpret_cast<T*>(storage_) + i); } T* alloc() { auto p = free_.top(); free_.pop(); return p; } void free(T* p) { p->~T(); free_.push(p); } }; // ── std::pmr (C++17) — polymorphic memory resources ────────── #include <memory_resource> std::pmr::monotonic_buffer_resource pool(1024); std::pmr::vector<int> v{&pool}; // v allocates from pool, no heap
| stack alloc | Fastest — no heap, no overhead. Use for small, short-lived objects. Limited to ~1–8 MB total. |
| object pool | Pre-allocate N slots, reuse without calling the system allocator. Eliminates heap fragmentation. |
| std::pmr (C++17) | Polymorphic memory resources — swap the allocator behind a container at runtime. Great for arenas and pools. |
| monotonic_buffer | Bump allocator — allocations are O(1), no individual frees. Free everything at once by destroying the resource. |
Profile before optimizing allocation. For most programs,
make_unique and vector are fast enough. Custom allocators give large gains only in allocation-heavy hot paths.