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