Chapter 3: Memory Management

Memory management is the defining characteristic of C++. Master this chapter thoroughly: it's what separates C++ developers from developers who write C++ poorly.

Memory Layout

┌─────────────────────────────────────────────────────────┐
│                         Stack                            │
│  - Automatic variables (local variables)                 │
│  - Function parameters                                   │
│  - Return addresses                                      │
│  - LIFO (Last In, First Out)                            │
│  - Fast allocation/deallocation                          │
│  - Limited size (~1-8 MB typically)                      │
├─────────────────────────────────────────────────────────┤
│                          ↓ ↑                             │
├─────────────────────────────────────────────────────────┤
│                         Heap                             │
│  - Dynamic allocation (new/malloc)                       │
│  - Manual lifetime management                            │
│  - Large allocations                                     │
│  - Slower than stack                                     │
│  - Can be fragmented                                     │
├─────────────────────────────────────────────────────────┤
│                   BSS (Uninitialized)                    │
│  - Uninitialized global/static variables                 │
├─────────────────────────────────────────────────────────┤
│                   Data (Initialized)                     │
│  - Initialized global/static variables                   │
│  - String literals                                       │
├─────────────────────────────────────────────────────────┤
│                         Text                             │
│  - Program code                                          │
│  - Read-only                                             │
└─────────────────────────────────────────────────────────┘

Stack vs Heap

#include <iostream>

void demonstrateMemory() {
    // STACK ALLOCATION (automatic)
    int stackVar = 42;              // On stack
    int stackArray[100];            // On stack (400 bytes)
    double coords[3] = {1.0, 2.0, 3.0};  // On stack

    // HEAP ALLOCATION (dynamic)
    int* heapVar = new int(42);     // On heap
    int* heapArray = new int[100];  // On heap

    // Stack variables automatically cleaned up when function returns
    // Heap variables MUST be manually deleted

    delete heapVar;        // Free single object
    delete[] heapArray;    // Free array (note the [])
}

When to Use Stack vs Heap

Use StackUse Heap
Size known at compile timeSize determined at runtime
Small objects (< 1KB rule of thumb)Large objects
Short-lived objectsLong-lived objects (beyond scope)
Automatic cleanup neededShared ownership

Pointers

A pointer stores the memory address of another variable.

int main() {
    int value = 42;
    int* ptr = &value;  // ptr holds the ADDRESS of value

    // Visual representation:
    // value: [42]       <- stored at address 0x1000
    // ptr:   [0x1000]   <- stores the address

    // Dereference: access the value at the address
    std::cout << *ptr << "\n";   // 42
    *ptr = 100;                  // Modify through pointer
    std::cout << value << "\n";  // 100

    // Pointer arithmetic
    int arr[] = {10, 20, 30, 40};
    int* p = arr;  // Points to first element

    std::cout << *p << "\n";     // 10
    std::cout << *(p + 1) << "\n";  // 20
    std::cout << *(p + 2) << "\n";  // 30

    p++;  // Move to next element
    std::cout << *p << "\n";  // 20

    return 0;
}

Null Pointers

// Modern C++ (C++11 and later)
int* ptr = nullptr;  // Null pointer literal

// Check before dereferencing
if (ptr != nullptr) {
    *ptr = 42;  // Safe
}

// Or use implicit conversion to bool
if (ptr) {
    *ptr = 42;
}

// Old C-style (avoid)
int* old_ptr = NULL;  // Macro, can cause issues
int* very_old = 0;    // Works but unclear

Common Pointer Mistakes

// MISTAKE 1: Uninitialized pointer
int* p;        // Contains garbage address!
*p = 42;       // CRASH or undefined behavior

// MISTAKE 2: Dangling pointer
int* dangling() {
    int local = 42;
    return &local;  // BAD: returning address of local variable
}  // local is destroyed here

// MISTAKE 3: Memory leak
void leak() {
    int* p = new int(42);
    // Forgot to delete p
}  // Memory leaked forever

// MISTAKE 4: Double delete
int* p = new int(42);
delete p;
delete p;  // CRASH: double delete

// MISTAKE 5: Wrong delete form
int* arr = new int[100];
delete arr;    // WRONG: should be delete[]
delete[] arr;  // CORRECT

References

A reference is an alias for another variable. It cannot be null or reseated.

int main() {
    int value = 42;
    int& ref = value;  // ref IS value (same memory)

    ref = 100;          // Modifies value
    std::cout << value; // 100

    // References must be initialized
    // int& bad_ref;    // ERROR: must be initialized

    // References cannot be reseated
    int other = 200;
    ref = other;        // This assigns other's value to value, NOT rebinding
    std::cout << value; // 200

    return 0;
}

Pointers vs References

AspectPointerReference
Can be nullYesNo
Can be reseatedYesNo
Syntax*ptr to accessDirect access
ArithmeticSupportedNot supported
Use whenNullability needed, arrays, ownershipAlways valid alias
// When to use what
void usePointer(int* p) {
    if (p) {  // Must check for null
        *p = 42;
    }
}

void useReference(int& r) {
    r = 42;  // Always valid (by contract)
}

// Prefer references when:
// - Parameter will always be valid
// - You don't need to reseat
// - Cleaner syntax desired

// Use pointers when:
// - Value might not exist (nullable)
// - Working with arrays
// - Interfacing with C code
// - Need to reseat to different objects

Dynamic Memory: new and delete

// Single objects
int* p = new int;        // Uninitialized
int* q = new int(42);    // Initialized to 42
int* r = new int{42};    // Brace initialization

delete p;
delete q;
delete r;

// Arrays
int* arr = new int[100];       // Uninitialized
int* arr2 = new int[100]();    // Zero-initialized
int* arr3 = new int[5]{1,2,3}; // Partial init (rest are 0)

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

// Object allocation
class MyClass {
public:
    MyClass(int x) : value(x) {}
    int value;
};

MyClass* obj = new MyClass(42);
delete obj;

MyClass* objects = new MyClass[3]{
    MyClass(1), MyClass(2), MyClass(3)
};
delete[] objects;

new vs malloc

// C++ new (preferred)
int* p = new int(42);   // Calls constructor, type-safe
delete p;               // Calls destructor

// C malloc (avoid in C++)
int* q = (int*)malloc(sizeof(int));  // No initialization
*q = 42;
free(q);                // No destructor called

// Never mix!
// int* p = new int(42);
// free(p);   // WRONG: undefined behavior

// int* q = (int*)malloc(sizeof(int));
// delete q;  // WRONG: undefined behavior

RAII: Resource Acquisition Is Initialization

RAII is the most important C++ idiom. The principle: tie resource lifetime to object lifetime.

// Without RAII - manual resource management (error-prone)
void riskyFunction() {
    int* data = new int[1000];

    if (someCondition) {
        delete[] data;  // Must remember to delete
        return;
    }

    doSomething(data);

    if (otherCondition) {
        delete[] data;  // Must remember again
        throw std::runtime_error("Error");  // What if we forget?
    }

    delete[] data;  // And again
}

// With RAII - automatic resource management
void safeFunction() {
    std::vector<int> data(1000);  // RAII container

    if (someCondition) {
        return;  // Automatically cleaned up
    }

    doSomething(data.data());

    if (otherCondition) {
        throw std::runtime_error("Error");  // Still cleaned up!
    }
}  // data destroyed automatically

RAII Pattern

class FileHandle {
private:
    FILE* file;

public:
    // Acquire resource in constructor
    FileHandle(const char* filename) {
        file = fopen(filename, "r");
        if (!file) {
            throw std::runtime_error("Failed to open file");
        }
    }

    // Release resource in destructor
    ~FileHandle() {
        if (file) {
            fclose(file);
        }
    }

    // Delete copy (prevent double-close)
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;

    // Allow move
    FileHandle(FileHandle&& other) noexcept : file(other.file) {
        other.file = nullptr;
    }

    FILE* get() { return file; }
};

void useFile() {
    FileHandle fh("data.txt");
    // Use fh...
}  // File automatically closed

Smart Pointers

Smart pointers implement RAII for dynamic memory. Always prefer smart pointers over raw new/delete.

std::unique_ptr (Exclusive Ownership)

#include <memory>

// unique_ptr: exactly one owner
std::unique_ptr<int> createInt() {
    return std::make_unique<int>(42);  // Preferred way to create
}

int main() {
    // Creation
    std::unique_ptr<int> p1 = std::make_unique<int>(42);
    auto p2 = std::make_unique<int>(100);  // Cleaner

    // Access
    std::cout << *p1 << "\n";      // Dereference
    int* raw = p1.get();           // Get raw pointer (don't delete it!)

    // Cannot copy (exclusive ownership)
    // std::unique_ptr<int> p3 = p1;  // ERROR

    // Can move
    std::unique_ptr<int> p3 = std::move(p1);
    // p1 is now nullptr

    // Check if valid
    if (p3) {
        std::cout << "p3 is valid\n";
    }
    if (!p1) {
        std::cout << "p1 was moved from\n";
    }

    // Arrays
    auto arr = std::make_unique<int[]>(100);
    arr[0] = 42;

    return 0;
}  // Automatically deleted

std::shared_ptr (Shared Ownership)

#include <memory>

int main() {
    // shared_ptr: reference counted
    auto p1 = std::make_shared<int>(42);
    std::cout << "Use count: " << p1.use_count() << "\n";  // 1

    {
        std::shared_ptr<int> p2 = p1;  // Copy is OK
        std::cout << "Use count: " << p1.use_count() << "\n";  // 2
    }  // p2 destroyed, count decremented

    std::cout << "Use count: " << p1.use_count() << "\n";  // 1

    return 0;
}  // p1 destroyed, count is 0, memory freed

std::weak_ptr (Non-owning Observer)

#include <memory>

// weak_ptr breaks circular references
struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;  // Use weak_ptr to avoid cycle
    int value;
};

int main() {
    auto p1 = std::make_shared<int>(42);
    std::weak_ptr<int> weak = p1;  // Observes but doesn't own

    // Must lock to access
    if (auto shared = weak.lock()) {
        std::cout << *shared << "\n";  // 42
    }

    p1.reset();  // Destroys the int

    if (weak.expired()) {
        std::cout << "Object no longer exists\n";
    }

    return 0;
}

When to Use Which Smart Pointer

┌─────────────────────────────────────────────────────────────┐
│           Smart Pointer Decision Guide                       │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│    Need exclusive ownership?                                 │
│           │                                                  │
│           ├── Yes ──▶  unique_ptr                           │
│           │                                                  │
│           └── No ──▶  Need shared ownership?                │
│                              │                               │
│                              ├── Yes ──▶  shared_ptr        │
│                              │                               │
│                              └── No (observer only)          │
│                                       │                      │
│                                       └──▶  weak_ptr        │
│                                                              │
├─────────────────────────────────────────────────────────────┤
│  Default choice: unique_ptr (lowest overhead)               │
│  Use shared_ptr only when you truly need shared ownership   │
│  Use weak_ptr to observe shared_ptr without extending life  │
└─────────────────────────────────────────────────────────────┘

Move Semantics

Move semantics enable efficient transfer of resources without copying.

#include <iostream>
#include <vector>
#include <string>

class Buffer {
    int* data;
    size_t size;

public:
    // Constructor
    Buffer(size_t s) : data(new int[s]), size(s) {
        std::cout << "Construct\n";
    }

    // Destructor
    ~Buffer() {
        delete[] data;
        std::cout << "Destruct\n";
    }

    // Copy constructor (expensive)
    Buffer(const Buffer& other) : data(new int[other.size]), size(other.size) {
        std::copy(other.data, other.data + size, data);
        std::cout << "Copy\n";
    }

    // Move constructor (cheap)
    Buffer(Buffer&& other) noexcept : data(other.data), size(other.size) {
        other.data = nullptr;  // Leave source in valid state
        other.size = 0;
        std::cout << "Move\n";
    }

    // Copy assignment
    Buffer& operator=(const Buffer& other) {
        if (this != &other) {
            delete[] data;
            size = other.size;
            data = new int[size];
            std::copy(other.data, other.data + size, data);
        }
        std::cout << "Copy assign\n";
        return *this;
    }

    // Move assignment
    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            size = other.size;
            other.data = nullptr;
            other.size = 0;
        }
        std::cout << "Move assign\n";
        return *this;
    }
};

int main() {
    Buffer b1(1000);              // Construct
    Buffer b2 = b1;               // Copy
    Buffer b3 = std::move(b1);    // Move (b1 is now empty)

    std::vector<Buffer> vec;
    vec.push_back(Buffer(500));   // Move (temporary)

    return 0;
}

std::move

std::move doesn't actually move: it casts to an rvalue reference, enabling move semantics.

std::string s1 = "Hello";
std::string s2 = std::move(s1);  // s1's content moved to s2
// s1 is now in a valid but unspecified state (likely empty)

// Use std::move when you're done with an object
void processAndConsume(std::vector<int>&& data) {
    // ... use data ...
}

std::vector<int> myData = {1, 2, 3};
processAndConsume(std::move(myData));
// Don't use myData after this

Common Memory Patterns

Factory Functions

// Return unique_ptr from factory
std::unique_ptr<Base> createObject(const std::string& type) {
    if (type == "A") {
        return std::make_unique<DerivedA>();
    } else {
        return std::make_unique<DerivedB>();
    }
}

// Usage
auto obj = createObject("A");

Passing Smart Pointers

// Pass by value: transfers ownership
void takeOwnership(std::unique_ptr<Widget> widget);

// Pass by reference: shares access
void useWidget(const Widget& widget);  // Preferred for read
void modifyWidget(Widget& widget);     // Preferred for write

// Pass by raw pointer: non-owning access (rarely needed)
void observeWidget(const Widget* widget);

// Pass shared_ptr by value: shares ownership
void shareOwnership(std::shared_ptr<Widget> widget);

// Pass shared_ptr by const ref: uses but doesn't share ownership
void useShared(const std::shared_ptr<Widget>& widget);

Output Parameters

// Prefer return values over output parameters
std::unique_ptr<Result> compute();  // Good

// If you need multiple outputs, use struct or tuple
struct ComputeResult {
    int value;
    std::string message;
};
ComputeResult compute();  // Good

// Or tuple
std::tuple<int, std::string> compute();
auto [value, message] = compute();  // Structured bindings (C++17)

Exercises

  1. Pointer Practice

    • Write a function that swaps two integers using pointers
    • Write it again using references
    • Compare the calling syntax
  2. Memory Leak Detective

    • Write a program that intentionally leaks memory
    • Run it with Valgrind or AddressSanitizer
    • Fix the leaks
  3. Smart Pointer Conversion

    • Take code with raw new/delete
    • Convert to use unique_ptr
    • Ensure no memory leaks with Valgrind
  4. RAII Wrapper

    • Write a RAII class for a resource (e.g., mutex lock, file, network socket)
    • Implement proper move semantics
    • Delete copy operations

Previous: 02 - Basic Syntax Next: 04 - Object-Oriented Programming