Chapter 09

Memory Management

Understanding and mastering C++ memory for safe, efficient programs

What You Will Learn

  • The C++ memory model: stack, heap, static, code segments
  • Stack vs heap allocation and their trade-offs
  • Dynamic memory with new and delete
  • RAII: Resource Acquisition Is Initialization
  • Smart pointers: unique_ptr, shared_ptr, weak_ptr
  • Custom deleters for specialized cleanup
  • Detecting and preventing memory leaks
  • Memory debugging tools
  • Best practices for modern C++ memory management

C++ Memory Model

A C++ program's memory is divided into several regions.

High Memory
┌─────────────────────────┐
│         Stack           │ ← Local variables, function calls
│           ↓             │   (grows downward)
│                         │
│           ↑             │   (grows upward)
│          Heap           │ ← Dynamic allocation (new/malloc)
├─────────────────────────┤
│     BSS (Uninitialized) │ ← Uninitialized global/static
├─────────────────────────┤
│    Data (Initialized)   │ ← Initialized global/static
├─────────────────────────┤
│         Code            │ ← Program instructions
└─────────────────────────┘
Low Memory

Storage Duration

Duration Location Lifetime Example
Automatic Stack Scope-based int x;
Static Data segment Program lifetime static int x;
Dynamic Heap Manual (new/delete) int* p = new int;
Thread TLS Thread lifetime thread_local int x;

Stack vs Heap

Stack Allocation

void function() {
    int a = 10;              // Stack
    double b = 3.14;         // Stack
    int arr[100];            // Stack (100 ints)
    std::string str = "Hi";  // Stack (but string's data is on heap)
}  // All stack variables automatically destroyed here

Heap Allocation

void function() {
    int* ptr = new int(42);       // Heap allocation
    int* arr = new int[1000];     // Heap array
    
    // ... use ptr and arr ...
    
    delete ptr;        // Manual cleanup required
    delete[] arr;      // Use delete[] for arrays
}

Comparison

Aspect Stack Heap
Speed Very fast (pointer bump) Slower (memory search)
Size Limit Limited (1-8 MB typical) Large (available RAM)
Lifetime Automatic (scope-based) Manual (new/delete)
Size Known Must be compile-time Can be runtime
Fragmentation None Possible
Thread Safety Thread-local Shared (needs sync)

When to Use Each

// Stack: Small, fixed-size, local lifetime
void processData() {
    int count = 0;
    double values[10];
    std::array<int, 100> buffer;
}

// Heap: Large, dynamic size, or outlives function
std::vector<int>* createLargeVector() {
    return new std::vector<int>(1000000);  // 1M ints
}

// Heap: Polymorphism
std::unique_ptr<Shape> createShape(const std::string& type) {
    if (type == "circle") return std::make_unique<Circle>();
    return std::make_unique<Rectangle>();
}

new and delete

C++ operators for dynamic memory allocation.

Basic Usage

// Single object
int* ptr = new int;          // Uninitialized
int* ptr2 = new int(42);     // Initialized to 42
int* ptr3 = new int{42};     // Uniform initialization (C++11)

delete ptr;
delete ptr2;
delete ptr3;

// Arrays
int* arr = new int[10];      // Array of 10 ints
int* arr2 = new int[10]{};   // Zero-initialized
int* arr3 = new int[5]{1, 2, 3};  // Partial init

delete[] arr;   // Must use delete[] for arrays!
delete[] arr2;
delete[] arr3;

new vs malloc

// new (C++)
int* p = new int(42);       // Typed, calls constructor
delete p;                    // Calls destructor

// malloc (C - avoid in C++)
int* q = (int*)malloc(sizeof(int));  // Untyped, no constructor
*q = 42;
free(q);                     // No destructor

// For objects, new/delete are essential
MyClass* obj = new MyClass();  // Constructor called
delete obj;                     // Destructor called

Placement new

#include <new>

// Allocate memory without constructing
char buffer[sizeof(MyClass)];

// Construct object at specific location
MyClass* obj = new (buffer) MyClass();

// Must manually call destructor (no delete!)
obj->~MyClass();

Handling Allocation Failure

// Default: throws std::bad_alloc
try {
    int* huge = new int[1000000000000];
} catch (const std::bad_alloc& e) {
    std::cerr << "Allocation failed: " << e.what() << "\n";
}

// nothrow version: returns nullptr
int* ptr = new (std::nothrow) int[1000000000000];
if (ptr == nullptr) {
    std::cerr << "Allocation failed\n";
}

RAII: Resource Acquisition Is Initialization

A C++ idiom where resource lifetime is tied to object lifetime.

The Problem Without RAII

void riskyFunction() {
    int* data = new int[1000];
    
    if (someCondition()) {
        return;  // Memory leak!
    }
    
    if (anotherCondition()) {
        throw std::runtime_error("Error");  // Memory leak!
    }
    
    delete[] data;  // Only reached in happy path
}

RAII Solution

class IntArray {
private:
    int* data;
    size_t size;
    
public:
    IntArray(size_t n) : data(new int[n]), size(n) {}
    
    ~IntArray() {
        delete[] data;  // Always runs, even on exception
    }
    
    int& operator[](size_t i) { return data[i]; }
};

void safeFunction() {
    IntArray arr(1000);  // Resource acquired in constructor
    
    if (someCondition()) {
        return;  // Destructor called, memory freed
    }
    
    if (anotherCondition()) {
        throw std::runtime_error("Error");  // Destructor called
    }
    
}  // Destructor called, memory freed

RAII for Various Resources

// File handle
class File {
    FILE* fp;
public:
    File(const char* name) : fp(fopen(name, "r")) {
        if (!fp) throw std::runtime_error("Can't open file");
    }
    ~File() { if (fp) fclose(fp); }
};

// Mutex lock
class LockGuard {
    std::mutex& mtx;
public:
    LockGuard(std::mutex& m) : mtx(m) { mtx.lock(); }
    ~LockGuard() { mtx.unlock(); }
};

// Standard library provides these!
// std::ifstream, std::lock_guard, std::unique_ptr, etc.

Key Insight

RAII leverages C++'s guarantee that destructors run when objects go out of scope, even during stack unwinding from exceptions.

std::unique_ptr

Exclusive ownership smart pointer. One owner, automatic cleanup.

Creating unique_ptr

#include <memory>

// Preferred: make_unique (C++14)
auto ptr = std::make_unique<int>(42);
auto arr = std::make_unique<int[]>(10);  // Array

// Direct construction
std::unique_ptr<int> ptr2(new int(42));

// Default: nullptr
std::unique_ptr<int> empty;

Using unique_ptr

auto ptr = std::make_unique<int>(42);

// Access like raw pointer
*ptr = 100;              // Dereference
std::cout << *ptr;       // 100

// Get raw pointer (don't delete it!)
int* raw = ptr.get();

// Check if valid
if (ptr) {
    std::cout << "Valid\n";
}

// Release ownership (returns raw pointer)
int* released = ptr.release();
// ptr is now nullptr, you own released
delete released;

// Reset (delete current, optionally set new)
ptr.reset();              // Delete and set to nullptr
ptr.reset(new int(50));   // Delete old, own new

Ownership Transfer

auto ptr1 = std::make_unique<int>(42);

// Can't copy (deleted copy constructor)
// auto ptr2 = ptr1;  // Error!

// Can move
auto ptr2 = std::move(ptr1);
// ptr1 is now nullptr, ptr2 owns the data

// Transfer to function
void consume(std::unique_ptr<int> ptr) {
    // ptr owns the data here
}  // Data deleted when function ends

auto ptr = std::make_unique<int>(42);
consume(std::move(ptr));  // ptr is now nullptr

unique_ptr with Classes

class Widget {
public:
    Widget() { std::cout << "Created\n"; }
    ~Widget() { std::cout << "Destroyed\n"; }
    void doWork() { std::cout << "Working\n"; }
};

void example() {
    auto widget = std::make_unique<Widget>();
    widget->doWork();
    
    // Widget automatically destroyed when widget goes out of scope
}

unique_ptr for Polymorphism

class Shape {
public:
    virtual void draw() = 0;
    virtual ~Shape() = default;
};

class Circle : public Shape {
    void draw() override { std::cout << "Circle\n"; }
};

std::unique_ptr<Shape> createShape() {
    return std::make_unique<Circle>();
}

void useShapes() {
    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(std::make_unique<Circle>());
    
    for (const auto& shape : shapes) {
        shape->draw();
    }
}  // All shapes automatically deleted

std::shared_ptr

Shared ownership smart pointer with reference counting.

Creating shared_ptr

#include <memory>

// Preferred: make_shared (more efficient)
auto ptr = std::make_shared<int>(42);

// Direct construction
std::shared_ptr<int> ptr2(new int(42));

// From unique_ptr
std::unique_ptr<int> unique = std::make_unique<int>(42);
std::shared_ptr<int> shared = std::move(unique);

Shared Ownership

auto ptr1 = std::make_shared<int>(42);
std::cout << ptr1.use_count();  // 1

auto ptr2 = ptr1;  // Copy (not move!)
std::cout << ptr1.use_count();  // 2
std::cout << ptr2.use_count();  // 2

{
    auto ptr3 = ptr1;
    std::cout << ptr1.use_count();  // 3
}  // ptr3 destroyed

std::cout << ptr1.use_count();  // 2

ptr1.reset();
std::cout << ptr2.use_count();  // 1

// Data deleted when last shared_ptr is destroyed

shared_ptr for Shared Resources

class Texture {
public:
    Texture(const std::string& path) {
        std::cout << "Loading: " << path << "\n";
    }
    ~Texture() {
        std::cout << "Unloading texture\n";
    }
};

class Sprite {
    std::shared_ptr<Texture> texture;
public:
    Sprite(std::shared_ptr<Texture> tex) : texture(tex) {}
};

void game() {
    auto texture = std::make_shared<Texture>("player.png");
    
    Sprite player1(texture);
    Sprite player2(texture);  // Same texture, no duplicate load
    
    // Texture stays alive while any sprite uses it
}

make_shared Efficiency

// Two allocations:
std::shared_ptr<int> ptr(new int(42));
// 1. Allocates int
// 2. Allocates control block (ref count, etc.)

// One allocation (more efficient):
auto ptr = std::make_shared<int>(42);
// Single allocation for both int and control block

Thread Safety

// Reference count operations are thread-safe
// But accessing the pointed-to object is NOT automatically safe

std::shared_ptr<int> global;

void thread1() {
    auto local = global;  // Safe: atomic ref count
    *local = 42;          // NOT safe without synchronization
}

void thread2() {
    global = std::make_shared<int>(100);  // Safe
    std::cout << *global;  // Potential race!
}

std::weak_ptr

Non-owning observer of shared_ptr. Doesn't affect reference count.

Breaking Circular References

// Problem: Circular reference causes memory leak
class Node {
public:
    std::shared_ptr<Node> next;
    std::shared_ptr<Node> prev;  // Creates cycle!
};

auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->prev = a;  // Cycle! Neither ever freed

// Solution: Use weak_ptr for back-references
class Node {
public:
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;  // Doesn't own
};

auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->prev = a;  // OK! a can be freed

Using weak_ptr

auto shared = std::make_shared<int>(42);
std::weak_ptr<int> weak = shared;

// Check if still alive
if (weak.expired()) {
    std::cout << "Object is gone\n";
}

// Get shared_ptr (safely)
if (auto locked = weak.lock()) {
    std::cout << *locked << "\n";  // Safe to use
} else {
    std::cout << "Object was deleted\n";
}

// Reset original
shared.reset();

// Now weak is expired
std::cout << weak.expired();  // true
auto locked = weak.lock();     // Returns nullptr

Cache Pattern

class ResourceCache {
    std::map<std::string, std::weak_ptr<Resource>> cache;
    
public:
    std::shared_ptr<Resource> get(const std::string& key) {
        // Try to get from cache
        if (auto it = cache.find(key); it != cache.end()) {
            if (auto resource = it->second.lock()) {
                return resource;  // Still alive, return it
            }
            cache.erase(it);  // Expired, remove from cache
        }
        
        // Create new resource
        auto resource = std::make_shared<Resource>(key);
        cache[key] = resource;  // Store weak reference
        return resource;
    }
};

Custom Deleters

Customize cleanup behavior for smart pointers.

With unique_ptr

// Function pointer deleter
void customDelete(int* p) {
    std::cout << "Custom delete\n";
    delete p;
}

std::unique_ptr<int, void(*)(int*)> ptr(new int(42), customDelete);

// Lambda deleter
auto deleter = [](int* p) {
    std::cout << "Lambda delete\n";
    delete p;
};
std::unique_ptr<int, decltype(deleter)> ptr2(new int(42), deleter);

// For arrays with custom cleanup
std::unique_ptr<FILE, decltype(&fclose)> file(fopen("test.txt", "r"), fclose);

With shared_ptr

// Shared_ptr deleter is type-erased (easier syntax)
auto deleter = [](int* p) {
    std::cout << "Custom shared delete\n";
    delete p;
};

std::shared_ptr<int> ptr(new int(42), deleter);

// No-op deleter for non-owned memory
int stackValue = 42;
std::shared_ptr<int> nonOwning(&stackValue, [](int*){});

// C library resources
std::shared_ptr<FILE> file(fopen("data.txt", "r"), fclose);

Real-World Example

// OpenGL texture wrapper
class GLTexture {
public:
    static std::shared_ptr<GLTexture> create() {
        GLuint id;
        glGenTextures(1, &id);
        return std::shared_ptr<GLTexture>(
            new GLTexture(id),
            [](GLTexture* t) {
                glDeleteTextures(1, &t->id);
                delete t;
            }
        );
    }
    
private:
    GLuint id;
    GLTexture(GLuint id) : id(id) {}
};

Memory Leaks

Memory that is allocated but never freed.

Common Causes

Forgotten delete

void leak() {
    int* p = new int(42);
    // Forgot delete p;
}

Exception Before delete

void leak() {
    int* p = new int(42);
    mayThrow();  // If throws, p leaks
    delete p;
}

Early Return

void leak() {
    int* p = new int(42);
    if (condition) return;  // p leaks
    delete p;
}

Lost Pointer

void leak() {
    int* p = new int(42);
    p = new int(100);  // First allocation leaks!
    delete p;
}

Wrong delete Form

void issue() {
    int* arr = new int[100];
    delete arr;    // Wrong! Should be delete[]
}

Circular shared_ptr

class Node {
    std::shared_ptr<Node> other;
};
// Two Nodes pointing to each other = leak

Prevention

  • Use smart pointers instead of raw new/delete
  • Apply RAII principles
  • Prefer stack allocation when possible
  • Use containers that manage their memory (vector, string)
  • Be careful with circular references

Memory Debugging Tools

Valgrind (Linux/macOS)

# Compile with debug symbols
g++ -g program.cpp -o program

# Run with valgrind
valgrind --leak-check=full ./program

# Output shows:
# - Definitely lost (memory leaks)
# - Invalid reads/writes
# - Use of uninitialized values

AddressSanitizer (ASan)

# Compile with ASan
g++ -fsanitize=address -g program.cpp -o program
clang++ -fsanitize=address -g program.cpp -o program

# Run normally - errors are reported
./program

# Detects:
# - Buffer overflows
# - Use after free
# - Memory leaks
# - Double free

Visual Studio Memory Tools

// Enable CRT debug heap
#define _CRTDBG_MAP_ALLOC
#include <crtdbg.h>

// At program end, report leaks
_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);

// Set breakpoint on specific allocation
_CrtSetBreakAlloc(42);  // Break when allocation #42 happens

Custom Tracking

// Override global new/delete for tracking
void* operator new(size_t size) {
    void* ptr = malloc(size);
    std::cout << "Allocated " << size << " bytes at " << ptr << "\n";
    return ptr;
}

void operator delete(void* ptr) noexcept {
    std::cout << "Freed " << ptr << "\n";
    free(ptr);
}

Best Practices

Modern C++ Memory Guidelines

  1. Avoid raw new/delete - Use smart pointers or containers
  2. Prefer make_unique/make_shared - Exception-safe and efficient
  3. Use unique_ptr by default - Shared ownership only when needed
  4. Pass smart pointers by reference when not transferring ownership
  5. Use weak_ptr for observing - Break cycles, caches
  6. Follow Rule of Zero - Let the compiler generate special members

Parameter Passing Guidelines

// Don't transfer ownership: use reference or raw pointer
void observe(const Widget& w);
void observe(const Widget* w);  // If nullable

// Transfer unique ownership
void consume(std::unique_ptr<Widget> w);

// Share ownership
void share(std::shared_ptr<Widget> w);

// May or may not share (rare)
void mayShare(const std::shared_ptr<Widget>& w);

Factory Functions

// Return unique_ptr from factories
std::unique_ptr<Widget> createWidget() {
    return std::make_unique<Widget>();
}

// Caller can convert to shared_ptr if needed
std::shared_ptr<Widget> shared = createWidget();

Container of Smart Pointers

// Polymorphic container
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>());
shapes.push_back(std::make_unique<Rectangle>());

for (const auto& shape : shapes) {
    shape->draw();
}

// Shared resources
std::vector<std::shared_ptr<Texture>> textures;
// Multiple sprites can reference same texture

Practice Questions

  1. Implement a simple RAII wrapper for a file handle.
  2. Create a class that correctly implements Rule of Five.
  3. Build a simple object pool using smart pointers.
  4. Implement a doubly-linked list with weak_ptr for prev pointers.
  5. Create a resource cache using weak_ptr.
  6. Write a factory that returns unique_ptr and demonstrate conversion to shared_ptr.
  7. Implement a custom deleter that logs destructions.
  8. Find and fix memory leaks in a given code sample.
  9. Create a tree structure with parent weak_ptr and children shared_ptr.
  10. Implement enable_shared_from_this for a class.
  11. Build a thread-safe singleton using smart pointers.
  12. Create a scope guard that runs cleanup on exception.