Memory Management

Understanding heap, stack, allocators, and dynamic memory

🧠 What is Memory Management?

Memory management controls how programs allocate, use, and deallocate memory. C++ provides stack allocation, heap allocation with new/delete, and custom allocators for efficient memory usage.


// Stack allocation (automatic)
int x = 42;

// Heap allocation (manual)
int* ptr = new int(42);
delete ptr;
                                    

Memory Types

📚

Stack Memory

Fast, automatic, limited size

int arr[100];  // Stack
char buffer[1024];
🏗️

Heap Memory

Large, manual, flexible size

int* ptr = new int[1000];
delete[] ptr;
⚙️

Custom Allocators

Specialized memory strategies

std::vector<int, 
  MyAllocator<int>> vec;
🔒

Smart Pointers

Smart pointers automate memory management, preventing leaks and simplifying ownership. std::unique_ptr enforces exclusive ownership and is lightweight; std::shared_ptr allows shared ownership via reference counting; std::weak_ptr breaks reference cycles. They call the appropriate destructor automatically when the pointer goes out of scope. Using smart pointers eliminates most manual delete calls, making code exception-safe and easier to reason about, which is foundational for robust modern C++ applications.

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

🔹 Stack vs Heap

Understanding stack and heap allocation is crucial for performance and correctness. Stack memory is fast, automatically managed (LIFO), and used for local variables with known, small sizes and lifetimes. Heap memory is slower, manually managed (via new/delete or smart pointers), and used for dynamic, large, or long-lived data. Misuse can lead to stack overflow, memory leaks, or fragmentation. Modern C++ encourages using stack where possible and employing RAII with smart pointers for heap allocations.

#include <iostream>

void stackExample() {
    // Stack allocation - automatic cleanup
    int stackVar = 100;
    int stackArray[10];
    
    std::cout << "Stack variable: " << stackVar << std::endl;
    // Automatically destroyed when function ends
}

void heapExample() {
    // Heap allocation - manual cleanup required
    int* heapVar = new int(200);
    int* heapArray = new int[10];
    
    std::cout << "Heap variable: " << *heapVar << std::endl;
    
    // Must manually delete
    delete heapVar;
    delete[] heapArray;
}

int main() {
    stackExample();
    heapExample();
    return 0;
}

Output:

Stack variable: 100
Heap variable: 200

🔹 Smart Pointers

Smart pointers automate memory management, preventing leaks and simplifying ownership. std::unique_ptr enforces exclusive ownership and is lightweight; std::shared_ptr allows shared ownership via reference counting; std::weak_ptr breaks reference cycles. They call the appropriate destructor automatically when the pointer goes out of scope. Using smart pointers eliminates most manual delete calls, making code exception-safe and easier to reason about, which is foundational for robust modern C++ applications.

#include <memory>
#include <iostream>

class Resource {
public:
    Resource(int id) : id_(id) {
        std::cout << "Resource " << id_ << " created\n";
    }
    
    ~Resource() {
        std::cout << "Resource " << id_ << " destroyed\n";
    }
    
    void use() {
        std::cout << "Using resource " << id_ << std::endl;
    }
    
private:
    int id_;
};

int main() {
    // unique_ptr - exclusive ownership
    std::unique_ptr<Resource> ptr1 = std::make_unique<Resource>(1);
    ptr1->use();
    
    // shared_ptr - shared ownership
    std::shared_ptr<Resource> ptr2 = std::make_shared<Resource>(2);
    {
        std::shared_ptr<Resource> ptr3 = ptr2;  // Shared
        ptr3->use();
    }  // ptr3 destroyed, but resource still alive
    
    ptr2->use();  // Still valid
    
    return 0;  // All resources automatically cleaned up
}

Output:

Resource 1 created
Using resource 1
Resource 2 created
Using resource 2
Using resource 2
Resource 1 destroyed
Resource 2 destroyed

🔹 Custom Allocator Example

Custom allocators optimize memory allocation for specific patterns or constraints. By defining an allocator that conforms to the std::allocator interface, you can control where and how containers like std::vector acquire memory. Use cases include pooling (reusing fixed-size blocks), arena/stack allocators (fast, sequential allocation), and allocating from shared memory or a specific hardware region. This advanced technique is key in high-performance computing, embedded systems, and game engines where default new is unsuitable.

#include <vector>
#include <iostream>

template<typename T>
class DebugAllocator {
public:
    using value_type = T;
    
    DebugAllocator() = default;
    
    template<typename U>
    DebugAllocator(const DebugAllocator<U>&) {}
    
    T* allocate(std::size_t n) {
        std::cout << "Allocating " << n << " objects\n";
        return static_cast<T*>(std::malloc(n * sizeof(T)));
    }
    
    void deallocate(T* p, std::size_t n) {
        std::cout << "Deallocating " << n << " objects\n";
        std::free(p);
    }
};

template<typename T, typename U>
bool operator==(const DebugAllocator<T>&, const DebugAllocator<U>&) {
    return true;
}

int main() {
    std::vector<int, DebugAllocator<int>> vec;
    vec.push_back(1);
    vec.push_back(2);
    vec.push_back(3);
    
    return 0;
}

Output:

Allocating 1 objects
Allocating 2 objects
Deallocating 1 objects
Allocating 4 objects
Deallocating 2 objects
Deallocating 4 objects

🧠 Test Your Knowledge

Which memory type is automatically managed?