πŸ”’ Private Site

This site is password-protected.

Memory Management in C++

Memory management is what separates C++ from most other languages. Understanding it deeply is non-negotiable for systems programming, quant development, and writing bug-free code.


Table of Contents


Glossary β€” Key Terms at a Glance

Term Meaning
Stack Fast, automatic memory; local variables, function calls; grows downward
Heap Dynamic memory; new/malloc; slower; programmer-managed lifetime
BSS Memory segment for uninitialized global/static variables (zeroed)
Pointer A variable that holds a memory address
Reference An alias for an existing variable; cannot be null or reseated
Dangling Pointer A pointer to memory that has been freed β€” undefined behavior if used
Memory Leak Allocated memory that is never freed β€” program’s memory grows indefinitely
RAII Resource Acquisition Is Initialization β€” tie resource lifetime to object scope
unique_ptr Smart pointer with exclusive ownership β€” no overhead, no sharing
shared_ptr Smart pointer with reference counting β€” multiple owners, freed when count hits 0
weak_ptr Non-owning observer of a shared_ptr β€” breaks circular references
Pool Allocator Pre-allocated memory block for O(1) allocation β€” used in HFT

1 β€” Memory Layout

1.1 Program Memory Regions

Definition: Every C++ program’s memory is organized into distinct regions, each with different allocation rules, speed, and lifetime.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  High addresses
β”‚       Stack         β”‚  ← Local variables, function calls (grows DOWN)
β”‚         ↓           β”‚
β”‚                     β”‚
β”‚         ↑           β”‚
β”‚        Heap         β”‚  ← Dynamic allocation (grows UP)
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚   BSS (uninitializedβ”‚  ← Global/static vars initialized to 0
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚   Data (initialized)β”‚  ← Global/static vars with initial values
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚       Text          β”‚  ← Program code (read-only)
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  Low addresses

1.2 Stack vs Heap Comparison

Feature Stack Heap
Allocation Automatic (compiler manages) Manual (new/malloc)
Speed Very fast (pointer bump) Slow (system call, fragmentation)
Lifetime Until scope ends } Until delete/free
Size Limited (~1-8 MB typical) Limited by system RAM
Thread Safety Each thread has its own stack Shared across threads

Key Insight: Stack allocation is ~100x faster than heap allocation. In performance-critical code (HFT, game engines), minimizing heap allocations on the hot path is essential.


2 β€” Pointers

2.1 Pointer Basics

Definition: A pointer is a variable that holds the memory address of another variable. It is C++’s mechanism for indirect access.

int x = 42;
int* ptr = &x;       // ptr holds the address of x

std::cout << ptr;     // prints address, e.g. 0x7ffd5e8c
std::cout << *ptr;    // 42 β€” dereference: access the value at that address

*ptr = 100;           // modifies x through the pointer
std::cout << x;       // 100

πŸ” Why Pointers Matter: Pointers enable dynamic memory allocation, polymorphism (virtual dispatch via base pointers), data structures (linked lists, trees), and interacting with hardware addresses. They are the foundation of C++ systems programming.


2.2 Pointer Arithmetic

int arr[] = {10, 20, 30, 40, 50};
int* p = arr;         // points to arr[0]

*(p + 2);             // arr[2] = 30  (moves 2 * sizeof(int) bytes forward)
p[3];                 // arr[3] = 40  (equivalent to *(p + 3))
p++;                  // now points to arr[1]

⚠️ Pointer arithmetic only makes sense within an array. Incrementing a pointer past the end of an array or into unrelated memory is undefined behavior.


2.3 Null Pointers & void*

// Always use nullptr (C++11) β€” type-safe null pointer
int* p1 = nullptr;     // PREFERRED βœ“
int* p2 = NULL;        // C-style (avoid)
int* p3 = 0;           // old-style (avoid)

if (p1 == nullptr) { /* safe check */ }
if (!p1) { /* also works */ }

// void* β€” generic pointer (can point to any type)
void* vp = &x;
// *vp;                 // ERROR: can't dereference void*
int* ip = static_cast<int*>(vp);  // must cast to a typed pointer first

3 β€” References

3.1 Reference Basics

Definition: A reference is an alias β€” another name for an existing variable. It shares the same memory location.

int x = 42;
int& ref = x;    // ref IS x (same memory address)
ref = 100;        // x is now 100

// References MUST be initialized and CANNOT be reseated
// int& bad;      // ERROR: uninitialized reference
// ref = y;       // this doesn't reseat ref β€” it assigns y's VALUE to x

3.2 Pointers vs References

Feature Pointer Reference
Can be null βœ… ❌
Can be reassigned βœ… ❌
Syntax *ptr, ptr->member Direct use like original
Arithmetic βœ… can increment/decrement ❌
Memory overhead sizeof(ptr) bytes Usually none (compiler optimization)

Rule of Thumb: Use references unless you need null or reassignment. References are safer and clearer. Use pointers for optional parameters (nullable), dynamic polymorphism, and data structures (linked nodes).


4 β€” Dynamic Memory

4.1 new and delete

// Single object
int* p = new int(42);       // allocate on heap, initialize to 42
delete p;                    // free the memory

// Array
int* arr = new int[100];    // allocate array of 100 ints
delete[] arr;                // MUST use delete[] for arrays

⚠️ Critical Rule: new ↔ delete, new[] ↔ delete[]. Mixing them (e.g., delete on new[]) is undefined behavior β€” heap corruption, crashes, silent data loss.


4.2 Common Memory Bugs

Memory Leak β€” forgetting to free:

void leak() {
    int* p = new int(42);
    return;  // p's memory is NEVER freed β€” leaked!
}

πŸ” Why This Is Dangerous: Call leak() in a loop for hours and the program slowly consumes all available RAM until it crashes or the OS kills it.

Double Free:

int* p = new int(42);
delete p;
delete p;  // UNDEFINED BEHAVIOR β€” corrupts the heap allocator

Dangling Pointer:

int* p = new int(42);
delete p;
*p = 10;   // UNDEFINED BEHAVIOR β€” p points to freed memory

Returning Address of Local:

int* getVal() {
    int local = 42;
    return &local;  // BAD: local is destroyed when function returns
}
// Caller gets a dangling pointer!

5 β€” RAII

5.1 The Most Important C++ Idiom

Definition: RAII (Resource Acquisition Is Initialization) β€” Bind resource lifetime to object lifetime. The constructor acquires the resource; the destructor releases it. When the object goes out of scope, cleanup is automatic and guaranteed β€” even if an exception is thrown.

class FileHandle {
    FILE* file;
public:
    FileHandle(const char* path) : file(fopen(path, "r")) {
        if (!file) throw std::runtime_error("Cannot open file");
    }
    ~FileHandle() {
        if (file) fclose(file);  // guaranteed cleanup
    }
    
    // Delete copy (file handles shouldn't be duplicated)
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;
    
    // Allow move (transfer ownership)
    FileHandle(FileHandle&& other) noexcept : file(other.file) {
        other.file = nullptr;
    }
    
    FILE* get() const { return file; }
};

void process() {
    FileHandle fh("data.txt");    // resource acquired in constructor
    // ... use fh.get() ...
}   // fh destroyed here β†’ file automatically closed
    // Even if an exception is thrown between construction and here!

πŸ” Why RAII is fundamental: It eliminates memory leaks, file handle leaks, mutex deadlocks, and every other resource management bug β€” by construction. If you hold resources in RAII wrappers, you literally cannot forget to release them.


6 β€” Smart Pointers

Smart pointers implement RAII for heap memory. They automatically delete the managed object when ownership ends. Always use smart pointers instead of raw new/delete.

6.1 unique_ptr β€” Exclusive Ownership

#include <memory>

// Creating (prefer make_unique)
auto p = std::make_unique<int>(42);       // PREFERRED βœ“
std::unique_ptr<int> p2(new int(42));     // also works

// Usage β€” same as a raw pointer
std::cout << *p;     // 42
*p = 100;

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

// Cannot copy β€” ownership is EXCLUSIVE
// auto p3 = p;  // ERROR: deleted copy constructor

// Can move β€” TRANSFER ownership
auto p3 = std::move(p);  // p is now nullptr, p3 owns the memory

// Custom deleter (e.g., for C APIs)
auto file_ptr = std::unique_ptr<FILE, decltype(&fclose)>(
    fopen("data.txt", "r"), &fclose);

πŸ” Why unique_ptr is the default choice: It has zero overhead β€” same size as a raw pointer, no reference counting, no extra allocations.

static_assert(sizeof(std::unique_ptr<int>) == sizeof(int*));

6.2 shared_ptr β€” Shared Ownership

auto sp1 = std::make_shared<int>(42);  // reference count = 1
auto sp2 = sp1;                         // reference count = 2
auto sp3 = sp1;                         // reference count = 3

sp2.reset();                            // reference count = 2
sp3 = nullptr;                          // reference count = 1
// when sp1 goes out of scope β†’ count = 0 β†’ memory freed automatically

πŸ” Cost of shared_ptr:

  • Extra allocation for control block (reference counts + deleter)
  • make_shared is smarter β€” allocates object + control block in one allocation
  • Atomic reference counting β€” thread-safe but ~2x slower than unique_ptr
  • Use only when you genuinely need multiple owners

6.3 weak_ptr β€” Breaking Cycles

Definition: weak_ptr observes a shared_ptr without affecting its reference count. Used to break circular references.

std::shared_ptr<int> sp = std::make_shared<int>(42);
std::weak_ptr<int> wp = sp;  // does NOT increase ref count

// Must lock() before using β€” converts to shared_ptr if still alive
if (auto locked = wp.lock()) {
    std::cout << *locked;    // safe to use
} else {
    // the object has been destroyed
}

wp.expired();  // true if the managed object is gone

Breaking Circular References:

struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;  // weak breaks the cycle!
};
// Without weak_ptr: A→B→A→B→... ref count never hits 0 → MEMORY LEAK

6.4 Smart Pointer Decision Tree

Need heap allocation?
β”œβ”€β”€ Single owner?        β†’ std::unique_ptr  (DEFAULT β€” 90% of the time)
β”‚   β”œβ”€β”€ Transfer later?  β†’ std::move(ptr)
β”‚   └── Polymorphism?    β†’ unique_ptr<Base>
β”œβ”€β”€ Multiple owners?     β†’ std::shared_ptr
β”‚   └── Observer/cache?  β†’ std::weak_ptr
└── No heap needed?      β†’ just use stack allocation!

7 β€” Debugging & Tools

7.1 Memory Pitfalls Checklist

Bug Symptom Prevention
Memory leak Growing memory usage over time Smart pointers, RAII
Double free Crash, heap corruption Smart pointers (automatic)
Dangling pointer Crash, wrong values, security exploits Smart pointers, references
Buffer overflow Crash, security vulnerability std::vector, .at() bounds checking
Stack overflow Crash in deep recursion Increase stack size, use iteration
Uninitialized memory Random/garbage values Brace initialization {}
Mismatched new/delete Heap corruption new[] ↔ delete[], or use smart pointers

7.2 Debugging Tools

Tool What It Detects Command
Address Sanitizer Buffer overflow, use-after-free, double free, leaks g++ -fsanitize=address -g
Memory Sanitizer Uninitialized reads g++ -fsanitize=memory -g
Valgrind Leaks, invalid reads/writes, uninitialized use valgrind --leak-check=full ./program
UB Sanitizer Undefined behavior (signed overflow, null deref) g++ -fsanitize=undefined -g

Use Address Sanitizer during all development. It catches most memory bugs with ~2x slowdown.


8 β€” Custom Allocators (Advanced)

8.1 Pool Allocator for HFT

Definition: A pool allocator pre-allocates a large block of memory and hands out chunks in $O(1)$. No system calls, no locking, no fragmentation.

class PoolAllocator {
    char* pool;
    size_t offset;
    size_t capacity;
public:
    PoolAllocator(size_t size)
        : pool(new char[size]), offset(0), capacity(size) {}
    ~PoolAllocator() { delete[] pool; }
    
    void* allocate(size_t bytes) {
        if (offset + bytes > capacity) throw std::bad_alloc();
        void* ptr = pool + offset;
        offset += bytes;
        return ptr;
    }
    
    void reset() { offset = 0; }  // "free" everything at once β€” O(1)
};

πŸ” Why in HFT? new/malloc take ~100ns (involves system calls and lock contention). A pool allocator takes ~1-5ns. At millions of operations per second, this difference is enormous.


Practice Questions

Q1. Draw the memory layout of a C++ program. Explain each region (Text, Data, BSS, Heap, Stack) and what is stored there.

Q2. What is the difference between a pointer and a reference? When would you use each? Give concrete examples.

Q3. Explain what happens with int* p = new int[10]; delete p; β€” why is this undefined behavior?

Q4. What is RAII? Write a class that manages a dynamically allocated array using RAII. Show that it is exception-safe.

Q5. Compare unique_ptr, shared_ptr, and weak_ptr. For each, give a scenario where it is the correct choice.

Q6. Explain the circular reference problem with shared_ptr. Show code that leaks and fix it with weak_ptr.

Q7. What is a dangling pointer? Write three different examples of code that create dangling pointers.

Q8. Why is std::make_shared preferred over std::shared_ptr<T>(new T)? What optimization does it enable?

Q9. A trading system processes 10 million messages/second. Each message is a 64-byte object. Why would new/delete be too slow? Design a pool allocator for this use case.

Q10. Explain Address Sanitizer. What types of bugs does it catch? What is its runtime overhead?


Key Takeaways

  1. Use smart pointers, not raw new/delete β€” unique_ptr 90% of the time
  2. RAII is the single most important C++ idiom β€” tie resource lifetime to scope
  3. Use make_unique and make_shared β€” safer and more efficient
  4. Stack allocation is always fastest β€” prefer it over heap when possible
  5. Use Address Sanitizer during development β€” catches bugs before production
  6. Never return pointers/references to local variables β€” instant dangling pointer
  7. weak_ptr breaks circular shared_ptr references β€” prevents hidden leaks

← Back to C++ Notes