new, delete, shared_ptr, unique_ptr, weak_ptr. The heap is powerful but dangerous — smart pointers make it safe.
Every C++ program has three distinct memory regions, each with different lifetime rules:
| Memory | What lives here | Lifetime |
|---|---|---|
| Static | Global variables, static locals, class static members | Entire program |
| Stack | Local (automatic) variables | Until 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.
Click to allocate and free objects. Watch them appear in different memory regions based on how they're created.
new an object but never delete it?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
| Bug | What happens | Result |
|---|---|---|
| Forget to delete | Memory is never freed | Memory leak |
| Use after delete | Pointer points to freed memory | Dangling pointer (UB) |
| Delete twice | Same memory freed twice | Heap corruption (UB) |
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.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
| Operation | Effect |
|---|---|
make_shared<T>(args) | Allocate and return shared_ptr (preferred) |
shared_ptr<T> p(q) | Copy; increments ref count |
p = q | Decrement 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) |
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.Create shared_ptrs, copy them, and reset them. Watch the reference count change and see when the object gets freed.
shared_ptr freed?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 | unique_ptr | |
|---|---|---|
| Ownership | Shared (ref counted) | Exclusive |
| Copyable? | Yes | No (move only) |
| Overhead | Control block + ref count | Zero (same as raw ptr) |
| Use when | Multiple owners | Single, clear owner |
unique_ptr to another?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 }
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.
| Operation | Effect |
|---|---|
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) |
weak_ptr directly?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. }
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 }
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
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.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 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
delete (without []) on a dynamically allocated array?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.
Create shared_ptrs, copy them, and destroy them. The object (center) is freed when the last shared_ptr disappears.
Dynamic memory and smart pointers are fundamental building blocks for all advanced C++ programming. Nearly every non-trivial class uses them internally.
| This Chapter | Builds Toward |
|---|---|
| Smart pointer ownership | Move semantics and ownership transfer (Ch 13) |
| RAII pattern | Resource management in all class design |
| Reference counting | Classes that act like pointers (Ch 13) |
| Dynamic arrays / allocator | How vector and string work internally |
| weak_ptr cycles | Observer pattern, caches, tree structures (Ch 15) |
Next up: Chapter 13: Copy Control — what happens when objects are copied, moved, assigned, and destroyed.