Pointers & Memory Management
This is the chapter that separates C++ from most other languages. In languages like Python or Java, memory is managed for you. In C++, you are in control. Understanding memory is the key to writing high-performance, crash-free applications.
1. The Memory Model: Stack vs. Heap
Every C++ program uses two main areas of memory. Understanding the difference is critical.
The Stack (Automatic Memory)
Think of the stack like a stack of plates. You put things on top and take them off the top.
- Fast: Allocation is just moving a pointer.
- Automatic: Variables are destroyed when they go out of scope (e.g., function ends).
- Small: Limited size (usually a few MBs).
- Usage: Local variables, function parameters.
void func() {
int x = 10; // Allocated on the stack
// x is destroyed automatically here when func() returns
}
The Heap (Free Store)
Think of the heap like a large warehouse. You can put things anywhere, but you have to remember where you put them.
- Slower: Requires OS allocation logic.
- Manual: You control lifetime (or use smart pointers).
- Large: Limited only by system RAM.
- Usage: Large objects, dynamic arrays, objects that must outlive their scope.
void func() {
int* ptr = new int(10); // Allocated on the heap
delete ptr; // Must be manually freed!
}
2. Pointers vs. References
Both refer to memory addresses, but they have different rules and use cases.
Pointers (*)
A pointer is a variable that stores a memory address.
- Can be null: It might point to nothing (
nullptr).
- Reassignable: It can point to object A, then later to object B.
- Explicit: You must dereference (
*ptr) to get the value.
int x = 10;
int* ptr = &x; // ptr holds address of x
std::cout << ptr; // Prints address (e.g., 0x7ffee...)
std::cout << *ptr; // Prints value (10)
ptr = nullptr; // Can be null
References (&)
A reference is an alias for an existing variable. It’s like a nickname.
- Cannot be null: Must always refer to a valid object.
- Immutable binding: Once initialized, it cannot refer to something else.
- Syntactic sugar: No dereferencing needed; use it like the original variable.
int x = 10;
int& ref = x; // ref is an alias for x
ref = 20; // x is now 20
Rule of Thumb: Use references by default (safer, cleaner). Use pointers only when you need to handle nullptr or reassign the address.
3. Raw Pointers (The Old Way)
In “Legacy C++” (pre-C++11), we used new and delete to manage heap memory.
// Allocate
int* arr = new int[100];
// Use
arr[0] = 5;
// Deallocate (CRITICAL!)
delete[] arr;
The Problem:
- Memory Leaks: If you forget
delete, memory is never freed.
- Dangling Pointers: If you delete but keep using the pointer, you crash.
- Exception Safety: If an exception is thrown before
delete, you leak.
Modern C++ Solution: Never use new/delete directly. Use Smart Pointers.
4. Smart Pointers (The Modern Way)
Smart pointers (in <memory>) wrap raw pointers and automatically manage memory using the RAII pattern. They are as fast as raw pointers but safe.
std::unique_ptr
- Exclusive ownership: Only one pointer owns the object.
- Zero overhead: Same size as a raw pointer.
- Automatically deletes when it goes out of scope.
- Cannot be copied, only moved.
#include <memory>
void process() {
// Create unique_ptr (C++14 make_unique is preferred)
std::unique_ptr<int> ptr = std::make_unique<int>(10);
// Use it like a raw pointer
*ptr = 20;
// No delete needed! Automatically freed here.
}
// Transfer ownership
std::unique_ptr<int> ptr2 = std::move(ptr); // ptr is now nullptr, ptr2 owns the int
std::shared_ptr
- Shared ownership: Multiple pointers can point to the same object.
- Reference counting: Object is deleted only when the last pointer is destroyed.
- Overhead: Slightly slower due to reference counting logic.
std::shared_ptr<int> p1 = std::make_shared<int>(10);
{
std::shared_ptr<int> p2 = p1; // Ref count = 2
} // p2 destroyed, Ref count = 1
// p1 destroyed, Ref count = 0 -> Memory freed
5. RAII (Resource Acquisition Is Initialization)
RAII is the most important idiom in C++. It binds resource lifecycle (memory, files, locks) to object lifetime.
Principle:
- Acquire resource in the constructor.
- Release resource in the destructor.
Since stack objects are automatically destroyed when they go out of scope, RAII guarantees cleanup (even if exceptions are thrown).
class FileHandler {
FILE* file;
public:
FileHandler(const char* name) {
file = fopen(name, "r");
std::cout << "File opened\n";
}
~FileHandler() {
if (file) {
fclose(file);
std::cout << "File closed\n";
}
}
};
void func() {
FileHandler fh("data.txt");
// ... do work ...
// fh destructor called automatically here! File closed.
}
Summary
- Stack is fast and automatic; Heap is manual and large.
- Prefer References (
&) over Pointers (*) when possible.
- Avoid
new/delete. Use std::make_unique or std::make_shared.
- RAII ensures resources are always cleaned up.
Next, we’ll apply these concepts to build robust structures using Object-Oriented Programming.