Skip to main content
Stack vs Heap Memory

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:
  1. Memory Leaks: If you forget delete, memory is never freed.
  2. Dangling Pointers: If you delete but keep using the pointer, you crash.
  3. 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

  1. Stack is fast and automatic; Heap is manual and large.
  2. Prefer References (&) over Pointers (*) when possible.
  3. Avoid new/delete. Use std::make_unique or std::make_shared.
  4. RAII ensures resources are always cleaned up.
Next, we’ll apply these concepts to build robust structures using Object-Oriented Programming.