Skip to content
C++

Memory Management Quick Reference

C++ memory management cheat sheet — smart pointers, allocators, placement new, RAII, memory model, and common pitfalls.

Ownership hierarchy

cpp
No ownership    →  T*  /  T&  /  std::span<T>  /  std::string_view
Sole ownership  →  std::unique_ptr<T>
Shared          →  std::shared_ptr<T>  +  std::weak_ptr<T>  (cycle-breaking)
Stack/inline    →  T value  /  std::array<T,N>  /  std::inplace_vector

Smart pointer quick reference

cpp
// --- unique_ptr ---
auto p = std::make_unique<T>(args...);  // preferred
auto arr = std::make_unique<T[]>(n);    // array form
p.get();          // raw pointer (non-owning)
p.release();      // relinquish ownership → caller must delete
p.reset();        // delete & null
p.reset(ptr);     // delete old, take new
std::move(p);     // transfer ownership

// --- shared_ptr ---
auto s = std::make_shared<T>(args...);  // single allocation (object + control block)
s.use_count();    // reference count
s.unique();       // use_count() == 1
s.reset();        // decrement count

// --- weak_ptr ---
std::weak_ptr<T> w = s;
w.expired();      // true if no shared_ptr remains
auto locked = w.lock();  // returns shared_ptr or nullptr
w.use_count();    // strong count (from shared_ptr owners)

Dynamic allocation

cpp
// New / delete — avoid for owning code, use smart ptrs
T* p = new T(args...);
delete p;

T* arr = new T[n];
delete[] arr;    // MUST use delete[] for arrays

// Placement new — construct in pre-allocated memory
alignas(T) char buf[sizeof(T)];
T* p = new (buf) T(args...);    // no allocation — construct in buf
p->~T();                         // must call destructor explicitly

// Operator new — raw allocation only, no construction
void* mem = ::operator new(sizeof(T));
T* p = new (mem) T(args...);
p->~T();
::operator delete(mem);

Stack vs heap

cpp
// Stack — fast, automatic lifetime, limited size (~1-8 MB typically)
T obj(args...);               // destroyed at scope exit
T arr[N];                     // N must be compile-time constant

// Heap — dynamic size, manual or RAII lifetime
auto p = std::make_unique<T>(args...);   // freed when p goes out of scope
auto v = std::vector<T>(n);              // resizable heap buffer

// Stack-allocated containers (no heap)
std::array<T, N> a;                     // fixed-size, stack
std::inplace_vector<T, N> iv;           // C++26 — dynamic size, stack storage

RAII pattern

cpp
// Resources acquired in constructor, released in destructor
class FileHandle {
    FILE* f_;
public:
    explicit FileHandle(const char* path) : f_(fopen(path, "r")) {
        if (!f_) throw std::runtime_error("cannot open");
    }
    ~FileHandle() { if (f_) fclose(f_); }
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;
    FILE* get() const { return f_; }
};
// f_ is always closed — even on exception

Memory order (atomics)

cpp
std::atomic<int> x;

// Weakest → strongest:
x.load(std::memory_order_relaxed);    // no ordering — only atomicity
x.load(std::memory_order_acquire);    // "read barrier" — sees all stores before release
x.store(std::memory_order_release);   // "write barrier" — visible to acquire loads
x.load(std::memory_order_seq_cst);    // total order — safe default, most expensive

// Common patterns:
// - Relaxed: counters, flags you only test at sync points
// - Acquire/Release: producer-consumer (producer uses release, consumer uses acquire)
// - SeqCst: when you need global ordering across 3+ threads

Alignment

cpp
alignas(64) float cache_line[16];   // aligned to 64 bytes (cache line)
std::align_val_t{64};               // pass alignment to new

// Check alignment
alignof(T);                         // required alignment of T
reinterpret_cast<uintptr_t>(ptr) % alignof(T) == 0;  // is ptr aligned?

// Aligned allocation (C++17)
void* p = ::operator new(size, std::align_val_t{64});
::operator delete(p, std::align_val_t{64});

// Aligned unique_ptr
auto p = std::make_unique_for_overwrite<alignas(64) float[]>(n);
// Or: custom allocator

Common pitfalls

cpp
// Double delete
T* p = new T;
delete p;
delete p;   // UB — p points to freed memory

// Fix: use smart pointers, or set p = nullptr after delete

// Use after free
T* p = new T;
delete p;
p->method();  // UB

// Dangling reference
T& get_ref() {
    T local;
    return local;  // UB — local destroyed at return
}

// Array-pointer decay
T arr[5];
T* p = arr;       // OK — pointer to first element
delete p;         // UB — must use delete[], and should be new[]

// Mismatched new/delete
T* p = new T[10];
delete p;         // UB — must be delete[]

// Stack overflow
void foo() {
    T big[1'000'000];  // ~4 MB on stack — may overflow
    // Fix: use std::vector<T>(1'000'000);
}

Memory layout

cpp
// Padding — compiler adds bytes to satisfy alignment
struct Padded {
    char a;   // 1 byte
    // 3 bytes padding
    int b;    // 4 bytes
    char c;   // 1 byte
    // 3 bytes padding
};
// sizeof(Padded) = 12, not 6

// Reorder to minimize padding
struct Packed {
    int b;    // 4 bytes
    char a;   // 1 byte
    char c;   // 1 byte
    // 2 bytes padding
};
// sizeof(Packed) = 8

// Force no padding (careful with alignment requirements)
#pragma pack(push, 1)
struct Wire { uint8_t type; uint16_t length; uint32_t crc; };
#pragma pack(pop)

Custom allocator (C++17 PMR)

cpp
#include <memory_resource>

// Stack buffer — no heap allocation for small vectors
std::byte buf[4096];
std::pmr::monotonic_buffer_resource arena{buf, sizeof(buf)};

std::pmr::vector<int> v{&arena};
v.reserve(100);  // uses buf, not heap

// Pool allocator — fast fixed-size allocations
std::pmr::synchronized_pool_resource pool;
std::pmr::vector<std::pmr::string> strings{&pool};
Edit on GitHub