20 topics
/
← Back to Quick Reference
Topic 20

Dynamic Memory

new/delete · unique_ptr · shared_ptr · weak_ptr · Ownership · Pools

C++17 · Advanced Reference

Dynamic Memory in C++

01

Stack 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. 1.Every heap object must have exactly one owner responsible for its deletion.
  2. 2.unique_ptr — single owner. Zero overhead. Deleted when it goes out of scope.
  3. 3.shared_ptr — multiple owners. Reference-counted. Deleted when count hits zero.
  4. 4.weak_ptr — non-owner observer. Must lock before use. Breaks cycles.

Rules

  1. 1.Never use raw new in application code — wrap immediately in a smart pointer.
  2. 2.new[] must be paired with delete[], never plain delete.
  3. 3.Prefer make_unique / make_shared over new — they are exception-safe and (for shared_ptr) more efficient.
  4. 4.Use std::vector instead of new 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 pCalls 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 newConstructs 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 deleterSecond 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_sharedSingle allocation for object + control block. Faster than shared_ptr<T>(new T). Preferred.
circular refsIf A holds shared_ptr<B> and B holds shared_ptr<A>, neither count reaches zero — memory leak. Use weak_ptr for back-references.
thread safetyThe 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 breakingIn a parent-child graph, parent owns children (shared_ptr), children reference parent (weak_ptr) — no cycle.
observer listsStore 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_ptrDefault choice for heap allocation. Zero overhead over raw pointer. 95% of cases.
shared_ptrUse when multiple components genuinely need to co-own an object's lifetime.
weak_ptrCaches, 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 allocFastest — no heap, no overhead. Use for small, short-lived objects. Limited to ~1–8 MB total.
object poolPre-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_bufferBump 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.