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::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
- Avoid raw new/delete - Use smart pointers or containers
- Prefer make_unique/make_shared - Exception-safe and efficient
- Use unique_ptr by default - Shared ownership only when needed
- Pass smart pointers by reference when not transferring ownership
- Use weak_ptr for observing - Break cycles, caches
- 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
- Implement a simple RAII wrapper for a file handle.
- Create a class that correctly implements Rule of Five.
- Build a simple object pool using smart pointers.
- Implement a doubly-linked list with weak_ptr for prev pointers.
- Create a resource cache using weak_ptr.
- Write a factory that returns unique_ptr and demonstrate conversion to shared_ptr.
- Implement a custom deleter that logs destructions.
- Find and fix memory leaks in a given code sample.
- Create a tree structure with parent weak_ptr and children shared_ptr.
- Implement enable_shared_from_this for a class.
- Build a thread-safe singleton using smart pointers.
- Create a scope guard that runs cleanup on exception.