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
1 β Memory Layout
2 β Pointers
3 β References
4 β Dynamic Memory
5 β RAII
6 β Smart Pointers
- 6.1 unique_ptr β Exclusive Ownership
- 6.2 shared_ptr β Shared Ownership
- 6.3 weak_ptr β Breaking Cycles
- 6.4 Smart Pointer Decision Tree
7 β Debugging & Tools
8 β Custom Allocators (Advanced)
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_ptris 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_sharedis 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/malloctake ~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
- Use smart pointers, not raw
new/deleteβunique_ptr90% of the time - RAII is the single most important C++ idiom β tie resource lifetime to scope
- Use
make_uniqueandmake_sharedβ safer and more efficient - Stack allocation is always fastest β prefer it over heap when possible
- Use Address Sanitizer during development β catches bugs before production
- Never return pointers/references to local variables β instant dangling pointer
weak_ptrbreaks circularshared_ptrreferences β prevents hidden leaks