C++ Primer, Chapter 12

Dynamic Memory

new, delete, shared_ptr, unique_ptr, weak_ptr. The heap is powerful but dangerous — smart pointers make it safe.

Prerequisites: Chapter 7 (Classes) + Chapter 9 (Containers).
9
Chapters
4+
Simulations

Chapter 0: Three Kinds of Memory

Every C++ program has three distinct memory regions, each with different lifetime rules:

MemoryWhat lives hereLifetime
StaticGlobal variables, static locals, class static membersEntire program
StackLocal (automatic) variablesUntil enclosing block exits
Heap (free store)Dynamically allocated objects (new)Until explicitly freed (delete)

Static and stack memory are managed automatically by the compiler. But the heap is your responsibility. You allocate with new, and you must free with delete. Forget to delete? Memory leak. Delete too early? Dangling pointer. Delete twice? Undefined behavior.

The core problem: Dynamic memory is notoriously hard to manage correctly. Smart pointers exist to solve this problem — they automatically free memory when it's no longer needed, making leaks and dangling pointers nearly impossible.
Memory Regions

Click to allocate and free objects. Watch them appear in different memory regions based on how they're created.

What happens when you new an object but never delete it?

Chapter 1: new & delete — The Raw Way

new allocates memory on the heap and returns a pointer. delete frees it. Simple in theory, treacherous in practice.

c++
int *p = new int(42);      // allocate and initialize to 42
string *ps = new string(10, '9'); // "9999999999"
int *p2 = new int;         // uninitialized — dangerous!
int *p3 = new int();        // value-initialized to 0

delete p;     // free the int
delete ps;    // free the string
p = nullptr; // good practice: reset pointer after delete

The Three Deadly Sins

BugWhat happensResult
Forget to deleteMemory is never freedMemory leak
Use after deletePointer points to freed memoryDangling pointer (UB)
Delete twiceSame memory freed twiceHeap corruption (UB)
This is why raw new/delete is discouraged. It's too easy to get wrong. Every new must be paired with exactly one delete. In the presence of exceptions, early returns, and complex control flow, this is nearly impossible to guarantee manually. That's why smart pointers exist.
What is a dangling pointer?

Chapter 2: shared_ptr — Shared Ownership

A shared_ptr is a smart pointer that keeps a reference count. Multiple shared_ptrs can point to the same object. The object is freed automatically when the last shared_ptr pointing to it is destroyed.

c++
#include <memory>

// make_shared: the safest way to create shared_ptr
auto p = make_shared<int>(42);  // ref count = 1
auto q(p);                       // copy: ref count = 2
auto r = make_shared<int>(99);

r = q;  // r now points to 42; ref count of 42 = 3
        // ref count of 99 drops to 0 → 99 is freed

Key Operations

OperationEffect
make_shared<T>(args)Allocate and return shared_ptr (preferred)
shared_ptr<T> p(q)Copy; increments ref count
p = qDecrement p's count, increment q's count
p.use_count()Number of shared_ptrs sharing ownership
p.unique()True if use_count() == 1
p.reset()Decrement count; if 0, free the object
p.get()Return raw pointer (use carefully)
Prefer make_shared over new. make_shared<int>(42) is safer than shared_ptr<int>(new int(42)) because it does a single allocation (combining the object and the control block) and avoids the possibility of a memory leak if an exception is thrown between the new and the shared_ptr construction.
Reference Counting

Create shared_ptrs, copy them, and reset them. Watch the reference count change and see when the object gets freed.

ref count: 0
When is the object managed by a shared_ptr freed?

Chapter 3: unique_ptr — Exclusive Ownership

A unique_ptr "owns" the object it points to. Only one unique_ptr can point to a given object at a time. When the unique_ptr is destroyed, the object is freed. You cannot copy a unique_ptr — you can only move it.

c++
unique_ptr<int> p1(new int(42));
// unique_ptr<int> p2(p1);  // ERROR: cannot copy
unique_ptr<int> p2(std::move(p1));  // OK: transfer ownership
// p1 is now nullptr; p2 owns the int

p2.reset();     // frees the int, p2 is now nullptr
p2.reset(new int(99));  // frees old (if any), takes ownership of new

// release() surrenders ownership WITHOUT freeing
int *raw = p2.release();  // p2 is nullptr; caller must delete raw
delete raw;

shared_ptr vs unique_ptr

shared_ptrunique_ptr
OwnershipShared (ref counted)Exclusive
Copyable?YesNo (move only)
OverheadControl block + ref countZero (same as raw ptr)
Use whenMultiple ownersSingle, clear owner
Default to unique_ptr. It has zero overhead compared to a raw pointer and makes ownership crystal clear. Only use shared_ptr when you genuinely need multiple owners. You can always convert a unique_ptr to a shared_ptr later, but not the reverse.
How do you transfer ownership from one unique_ptr to another?

Chapter 4: weak_ptr — Breaking Cycles

A weak_ptr points to an object managed by a shared_ptr but does not affect the reference count. It "observes" without owning. Its main purpose is to break circular references that would otherwise cause memory leaks.

c++
auto sp = make_shared<int>(42);  // ref count = 1
weak_ptr<int> wp(sp);             // ref count still 1

// weak_ptr can't be dereferenced directly
// wp->  // ERROR
// *wp   // ERROR

// Must lock() to get a shared_ptr (might be null if object is freed)
if (auto p = wp.lock()) {   // lock returns shared_ptr
    // p is valid; use *p safely
    cout << *p;              // 42
} else {
    // object has been freed
}

The Circular Reference Problem

Imagine two objects that each hold a shared_ptr to the other. Neither can ever reach ref count 0, so neither is ever freed. This is a circular reference — a memory leak that shared_ptr alone cannot prevent.

The fix: make one of the pointers a weak_ptr. It doesn't contribute to the ref count, breaking the cycle.

Classic use case: A tree where parent nodes own children (shared_ptr), but children need a back-pointer to their parent. Make the parent pointer a weak_ptr to avoid a cycle.
OperationEffect
weak_ptr<T> wp(sp)Create from shared_ptr, no ref count change
wp.lock()Returns shared_ptr if object alive, else empty shared_ptr
wp.expired()True if object has been freed
wp.use_count()Number of shared_ptrs (not including weak_ptrs)
Why can't you dereference a weak_ptr directly?

Chapter 5: Smart Pointers & Exceptions

One of the strongest arguments for smart pointers is exception safety. Consider what happens when an exception is thrown between a new and its corresponding delete:

c++
void f() {
    int *p = new int(42);
    // ... code that might throw ...
    delete p;  // NEVER REACHED if exception thrown → LEAK
}

void g() {
    auto p = make_shared<int>(42);
    // ... code that might throw ...
    // p is destroyed when g exits (normally or by exception)
    // → object is freed automatically. No leak.
}
RAII (Resource Acquisition Is Initialization): Tie resource lifetime to object lifetime. When the object goes out of scope — for any reason — the destructor frees the resource. Smart pointers are the canonical example of RAII.

Custom Deleters

Sometimes you need to free a resource that isn't memory (a file handle, a network connection). Smart pointers accept a custom deleter:

c++
// Use shared_ptr to manage a C-style connection
void end_connection(connection *p) { disconnect(*p); }

void f(destination &d) {
    connection c = connect(&d);
    shared_ptr<connection> p(&c, end_connection);
    // use the connection...
    // p goes out of scope → end_connection is called → clean disconnect
}
Rule of thumb: Any time you acquire a resource (memory, file, lock, connection), wrap it in an RAII object immediately. Never let a resource exist in a "naked" state where an exception could cause a leak.
What principle does RAII follow?

Chapter 6: Dynamic Arrays

Sometimes you need to allocate an array on the heap. C++ provides new[] and delete[], plus the safer allocator class.

c++
// new[] allocates an array of n elements
int *pa = new int[10];          // 10 uninitialized ints
int *pb = new int[10]();        // 10 value-initialized ints (all 0)
string *ps = new string[10];   // 10 empty strings

// MUST use delete[] (not delete) for arrays
delete[] pa;
delete[] ps;
// delete pa;  // WRONG: undefined behavior
new[] returns a pointer, not an array. You can't use begin(), end(), or range-for on it. You can't call .size(). It's just a raw pointer with no bounds information. Prefer vector in almost all cases.

unique_ptr with Arrays

c++
// unique_ptr can manage dynamic arrays
unique_ptr<int[]> up(new int[10]);
up[3] = 42;      // subscript works
// automatically calls delete[] when destroyed

The allocator Class

The allocator class separates allocation from construction. This matters when you want to allocate memory for objects but construct them later:

c++
allocator<string> alloc;
auto p = alloc.allocate(10);           // raw memory for 10 strings
alloc.construct(p, "hello");          // construct first string
alloc.construct(p + 1, 10, 'x');    // construct second: "xxxxxxxxxx"

alloc.destroy(p);                      // destroy first string
alloc.destroy(p + 1);                 // destroy second
alloc.deallocate(p, 10);              // free the memory
This is how vector works internally. It allocates a block of raw memory, constructs elements one at a time as you push_back, and separates the concepts of capacity (allocated memory) and size (constructed objects).
What happens if you use delete (without []) on a dynamically allocated array?

Chapter 7: Ownership Simulator

Let's trace the lifecycle of a heap object through shared_ptr ownership. Watch the reference count rise and fall, and see exactly when the object gets freed.

shared_ptr Lifecycle

Create shared_ptrs, copy them, and destroy them. The object (center) is freed when the last shared_ptr disappears.

no object
Observe the pattern: The object exists as long as at least one shared_ptr points to it. The moment the reference count hits zero, the object is freed immediately and automatically. No delete needed, no leak possible.

Chapter 8: Beyond — Connections

Dynamic memory and smart pointers are fundamental building blocks for all advanced C++ programming. Nearly every non-trivial class uses them internally.

This ChapterBuilds Toward
Smart pointer ownershipMove semantics and ownership transfer (Ch 13)
RAII patternResource management in all class design
Reference countingClasses that act like pointers (Ch 13)
Dynamic arrays / allocatorHow vector and string work internally
weak_ptr cyclesObserver pattern, caches, tree structures (Ch 15)
Lippman's advice: "Programs tend to use dynamic memory for one of three purposes: (1) They don't know how many objects they'll need (use a container). (2) They don't know the exact type (use inheritance + smart pointers). (3) They want to share data between objects (use shared_ptr)." For all other cases, prefer stack allocation.

Continue Reading

Next up: Chapter 13: Copy Control — what happens when objects are copied, moved, assigned, and destroyed.