Memory Management and Smart Pointers

This section documents C++23 (ISO/IEC 14882:2024), as published by ISO/IEC JTC1/SC22/WG21 (wg21), verified against the freely available working draft N5046 (eel.is/c++draft) and cppreference.com.

This content was generated with the assistance of AI and should be verified against the working draft and cppreference.com before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Stack vs. Free Store

Objects with automatic storage duration (local variables) live on the stack — created and destroyed in strict LIFO order, at essentially zero cost. Objects with dynamic storage duration are allocated on the free store (colloquially "the heap") with new, live until explicitly `delete`d, and can outlive the scope that created them:

void stackExample() {
    int local = 5;              // destroyed automatically when stackExample() returns
}

int* heapExample() {
    int* p = new int(5);         // lives until delete -- can be returned, stored, passed around
    return p;                     // caller now owns *p and must eventually delete it
}

new/delete and Why to Avoid Them Directly

int* single = new int(42);
delete single;

int* array = new int[10];
delete[] array;                 // must match new[] with delete[] -- mismatching is undefined behavior

Manual new/delete is error-prone: forgetting to delete leaks; delete`ing twice or after an exception between `new and delete is undefined behavior. Modern C++ almost never calls new/delete directly outside of smart-pointer/container implementations themselves — see RAII and smart pointers below.

RAII

Resource Acquisition Is Initialization: tie a resource’s lifetime to an object’s lifetime, so the destructor releases it automatically — on every exit path, including exceptions — with no try/finally needed:

class FileHandle {
public:
    explicit FileHandle(const char* path) : file_(std::fopen(path, "r")) {}
    ~FileHandle() { if (file_) std::fclose(file_); }    // always runs, even if an exception propagates through

    FileHandle(const FileHandle&) = delete;              // a raw FILE* isn't safely copyable -- delete copy
    FileHandle& operator=(const FileHandle&) = delete;

    // Declaring a destructor suppresses the implicit move members, so write them out to keep the type movable
    // (see the special-member decision chart in Classes and Objects). Without these two, the deleted copy
    // operations above would be the only candidates and `FileHandle b = std::move(a);` would not compile.
    FileHandle(FileHandle&& other) noexcept : file_(std::exchange(other.file_, nullptr)) {}
    FileHandle& operator=(FileHandle&& other) noexcept {
        if (this != &other) {
            if (file_) std::fclose(file_);
            file_ = std::exchange(other.file_, nullptr);
        }
        return *this;
    }

private:
    std::FILE* file_;
};

std::unique_ptr/std::shared_ptr, std::lock_guard (see Threads and Synchronization), and std::fstream are all RAII types built into the standard library — reach for one of them before writing a custom RAII wrapper like FileHandle above.

std::unique_ptr

Exclusive ownership, zero overhead over a raw pointer (no reference count), move-only:

#include <memory>

std::unique_ptr<int> make() {
    return std::make_unique<int>(42);     // prefer make_unique over "new" directly -- exception-safe,
}                                           // and avoids repeating the type name

int main() {
    auto p = make();
    // auto p2 = p;                      // error: unique_ptr has no copy constructor
    auto p2 = std::move(p);               // fine: ownership transfers, p is now empty
    return *p2;
}

std::shared_ptr and std::weak_ptr

Shared ownership via reference counting — the object is destroyed when the last shared_ptr to it is destroyed or reset:

#include <memory>
#include <iostream>

int main() {
    auto shared1 = std::make_shared<int>(42);   // prefer make_shared: one allocation for object + control block
    auto shared2 = shared1;                       // reference count now 2
    std::cout << shared1.use_count() << '\n';      // 2

    std::weak_ptr<int> weak = shared1;             // observes without extending the object's lifetime
    if (auto locked = weak.lock()) {                // lock() returns a shared_ptr, or empty if already destroyed
        std::cout << *locked << '\n';
    }
}

weak_ptr exists specifically to break reference cycles: two objects holding `shared_ptr`s to each other would otherwise never reach a reference count of zero — see the ownership graph below.

Custom Deleters

#include <memory>
#include <cstdio>

struct FileCloser {
    void operator()(std::FILE* f) const { if (f) std::fclose(f); }
};

int main() {
    std::unique_ptr<std::FILE, FileCloser> file(std::fopen("data.txt", "r"), FileCloser{});
    // file automatically fclose()d when it goes out of scope, even on an early return/exception
}

A shared_ptr can also take a deleter, and additionally supports the aliasing constructor — a shared_ptr that shares a control block (and thus lifetime) with another, but points at a different address (e.g. a member of the owned object).

Alignment

#include <new>

struct alignas(16) Aligned16 {   // force 16-byte alignment, e.g. for SIMD types
    float data[4];
};

static_assert(alignof(Aligned16) == 16);

void* raw = ::operator new(64, std::align_val_t(32));   // C++17: over-aligned allocation
::operator delete(raw, std::align_val_t(32));

Allocators Overview

Every standard container is a template over an allocator (std::allocator<T> by default), which controls how memory is obtained — a custom allocator (arena/pool-based, tracked, or pmr-based) can replace the default without changing the container’s interface:

#include <array>
#include <cstddef>          // std::byte
#include <memory_resource>
#include <vector>

int main() {
    std::array<std::byte, 1024> buffer{};
    std::pmr::monotonic_buffer_resource pool(buffer.data(), buffer.size());
    std::pmr::vector<int> v(&pool);      // allocates from "buffer" instead of the global heap, no per-element
    v.push_back(1);                       // free-store allocation until the pool itself is exhausted
    v.push_back(2);
}

std::pmr ("polymorphic memory resource", C++17) containers share one concrete type (std::pmr::vector<int>) regardless of which memory resource backs them, unlike a plain std::vector<int, MyAllocator>, which is a different type per allocator.

Detecting Leaks with Valgrind

Valgrind still catches every raw new/delete mistake shown earlier on this page — a new with no matching delete, or a new[]/delete mismatch — exactly as it would in C.

Smart pointers eliminate most of those, but they do not prevent a shared_ptr reference cycle: two objects each holding a shared_ptr to the other (see the std::shared_ptr and std::weak_ptr section above) never reach a reference count of zero, so neither object is ever destroyed.

#include <memory>

struct B;

struct A {
    std::shared_ptr<B> b;
    ~A() { }
};

struct B {
    std::shared_ptr<A> a;   // should be a weak_ptr to break the cycle -- see above
    ~B() { }
};

int main() {
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();
    a->b = b;
    b->a = a;   // cycle: a and b now keep each other alive forever
}

Valgrind reports the cycle’s memory as leaked: once main returns, no root pointer (stack, global, or register) reaches either object any more, so the pair is unreachable garbage despite each object’s reference count still being 1:

$ valgrind --leak-check=full --show-leak-kinds=all ./app
==12346== HEAP SUMMARY:
==12346==     definitely lost: 32 bytes in 1 blocks
==12346==     indirectly lost: 32 bytes in 1 blocks
==12346==       possibly lost: 0 bytes in 0 blocks
==12346==     still reachable: 0 bytes in 0 blocks

By contrast, a function-local static or namespace-scope global smart pointer is a genuine root, so a block it still points to at exit is reported as "still reachable" instead — expected, not a bug. Either way, --show-leak-kinds=all is needed to see anything beyond Memcheck’s default --show-leak-kinds, which is definite,possible.

Ownership Graph: unique_ptr / shared_ptr / weak_ptr

unique_ptr exclusively owns its object; shared_ptr instances share ownership via a reference count; weak_ptr observes without owning or extending the object’s lifetime

See Also