Skip to content
C++ Better Explained
Go back

Memory Management in C++: Heap vs Stack, new/delete, and How to Prevent Memory Leaks

Edit page

Memory Management in C++: Heap vs Stack, new/delete, and How to Prevent Memory Leaks

Introduction: Why Memory Management is C++‘s Superpower AND Its Biggest Trap

When people talk about C++, they often mention two things: incredible performance and the difficulty of memory management. There’s truth to both. Unlike languages like Python or Java that automatically clean up after you, C++ puts you in charge. And that’s actually a feature, not a bug.

Here’s the thing: C++ lets you be as precise as you want about memory. You can allocate exactly what you need, exactly when you need it, and free it the instant you’re done. This is why C++ powers everything from operating systems to game engines to high-frequency trading platforms. The cost? You have to understand what you’re doing.

In this article, I’m going to build your mental model of how memory works in C++. By the end, you’ll understand the difference between stack and heap, how to use new and delete safely, what memory leaks actually are, and—most importantly—how to write code that’s both efficient and leak-free.


The Two Memory Regions: Stack vs Heap (Mental Model: Automatic vs Manual)

Before we talk about new and delete, you need to understand where data actually lives in your program. Your program has access to two distinct regions of memory: the stack and the heap.

Think of it like this:

Here’s the key difference: stack memory is automatic; heap memory is manual.


How Stack Memory Works: Automatic Allocation and the LIFO Principle

When you declare a variable in a function, it’s allocated on the stack:

#include <iostream>
using namespace std;

int main() {
    int x = 42;                    // allocated on the stack
    double y = 3.14;              // allocated on the stack
    int arr[10];                   // array of 10 ints, allocated on the stack

    cout << "x: " << x << endl;
    cout << "y: " << y << endl;

    return 0;  // x, y, arr are automatically freed when main() ends
}

When you declare x, the compiler allocates 4 bytes (on most systems) on the stack and assigns the value 42. When you declare y, the compiler allocates 8 bytes next to x on the stack. You don’t have to do anything.

Here’s what happens automatically:

  1. Allocation is instant—just a pointer increment
  2. Deallocation happens automatically when the variable goes out of scope
  3. Speed is incredible because it’s just moving a pointer

The stack follows a LIFO (Last In, First Out) principle. Variables are freed in reverse order of declaration:

{
    int a = 1;      // stack: [a]
    int b = 2;      // stack: [a, b]
    {
        int c = 3;  // stack: [a, b, c]
    }               // c is freed automatically here
                    // stack: [a, b]
}                   // b, then a are freed automatically
                    // stack: []

The limitation: Stack memory is small and fixed. On most systems, the stack is only a few megabytes. If you try to allocate a huge array on the stack, you’ll get a stack overflow. Also, you can’t allocate stack memory for data whose size you don’t know at compile time.

This is where the heap comes in.


How Heap Memory Works: Dynamic Allocation and Why We Need It

The heap is different. It’s much larger (gigabytes on modern systems), and you request memory dynamically at runtime. This is essential for:

Here’s the key: you request heap memory with new, and you must release it with delete.


Using new and delete: Syntax and Mechanics

Let’s look at how to allocate memory on the heap:

#include <iostream>
using namespace std;

int main() {
    // Allocate a single integer on the heap
    int* ptr = new int;           // ptr points to a heap-allocated int
    *ptr = 42;                    // dereference and set the value

    cout << "Value: " << *ptr << endl;
    cout << "Address: " << ptr << endl;

    // When done, release the memory
    delete ptr;                   // deallocate the memory
    ptr = nullptr;                // good practice: set to nullptr after delete

    return 0;
}

Let’s break this down:

You can also initialize the memory when you allocate it:

int* ptr = new int(42);           // allocate AND initialize to 42
double* d = new double(3.14);     // allocate and initialize to 3.14

Here’s a more realistic example:

#include <iostream>
#include <string>
using namespace std;

int main() {
    // Allocate an integer and a string on the heap
    int* num = new int(100);
    string* name = new string("Alice");

    cout << "Number: " << *num << endl;
    cout << "Name: " << *name << endl;

    // Clean up
    delete num;
    delete name;
    num = nullptr;
    name = nullptr;

    return 0;
}

Using new[] and delete[] for Arrays

When you allocate an array on the heap, you need to use new[] and delete[]:

#include <iostream>
using namespace std;

int main() {
    int n = 100;  // size determined at runtime!

    // Allocate an array of n integers on the heap
    int* arr = new int[n];

    // Use the array like normal
    arr[0] = 10;
    arr[1] = 20;
    arr[n-1] = 99;

    cout << "First element: " << arr[0] << endl;
    cout << "Last element: " << arr[n-1] << endl;

    // IMPORTANT: use delete[], not delete!
    delete[] arr;
    arr = nullptr;

    return 0;
}

Critical: Use delete[] for arrays allocated with new[], and delete for single objects allocated with new. If you mix them up, you’ll corrupt memory.


What is a Memory Leak? (With a Real-World Analogy)

A memory leak happens when you allocate memory with new but forget to delete it. That memory stays allocated forever, even though you’re not using it.

Think of it like borrowing books from a library. If you borrow a book and never return it, the library can’t lend it to anyone else. If you keep borrowing books and never returning them, eventually all the books are gone. That’s a memory leak.

Here’s a simple example:

void badFunction() {
    int* ptr = new int(42);
    // ... do some work ...
    // OOPS! We forgot to delete ptr!
    // The memory is now leaked
}

Every time badFunction() is called, 4 bytes of memory disappear forever. Call this function 1,000,000 times, and you’ve leaked 4 MB. This is why servers crash—they run out of memory.

Here’s a more insidious example:

void leakyFunction() {
    int* ptr = new int(42);

    if (someCondition) {
        cout << "Early exit!" << endl;
        return;  // LEAKED: ptr is never deleted
    }

    delete ptr;
}

Even if you delete at the end, an early return or exception can cause a leak.


How to Detect Memory Leaks: Valgrind and AddressSanitizer

You can’t always see memory leaks by running your program. You need tools. The two most common are Valgrind and AddressSanitizer.

Using Valgrind

Valgrind is a memory debugging tool. Compile your program normally, then run it through Valgrind:

g++ -g -o myprogram myprogram.cpp
valgrind --leak-check=full ./myprogram

The -g flag includes debugging symbols, which helps Valgrind report exactly where leaks occur.

Using AddressSanitizer (Modern Approach)

AddressSanitizer is built into modern compilers. It’s faster than Valgrind:

g++ -fsanitize=address -g -o myprogram myprogram.cpp
./myprogram

When you run the program, AddressSanitizer will report leaks directly.

Here’s a program with a leak:

#include <iostream>
using namespace std;

int main() {
    int* x = new int(42);
    int* y = new int(100);

    delete x;
    // y is leaked!

    return 0;
}

When you run this through AddressSanitizer, it will report:

SUMMARY: AddressSanitizer: 4 byte(s) leaked in 1 allocation(s).

Common Memory Mistakes: Double Delete, Dangling Pointers, and More

Mistake 1: Double Delete

int* ptr = new int(42);
delete ptr;
delete ptr;  // ERROR: double delete!

Deleting the same pointer twice corrupts memory. The heap manager tracks which memory is free and which is in use. Deleting twice confuses it.

Mistake 2: Dangling Pointers

int* ptr = new int(42);
delete ptr;
cout << *ptr << endl;  // ERROR: using a dangling pointer!

After you delete ptr, the pointer still exists, but the memory it points to is no longer allocated. Accessing it is undefined behavior.

Best practice: Always set pointers to nullptr after deleting:

int* ptr = new int(42);
delete ptr;
ptr = nullptr;  // now it's safe to check: if (ptr != nullptr) ...

Mistake 3: Mixing Stack and Heap

int x = 42;        // stack
int* ptr = &x;     // pointer to stack memory
delete ptr;        // ERROR: can't delete stack memory!

Only delete memory that was allocated with new. You can take the address of a stack variable, but you can’t delete it.


The RAII Principle as the Solution

RAII stands for Resource Acquisition Is Initialization. It’s a fundamental C++ principle that solves many memory management problems.

The idea is simple: tie the lifetime of a resource to the lifetime of an object.

Here’s what that means:

Here’s an example:

#include <iostream>
using namespace std;

class IntHolder {
private:
    int* data;

public:
    IntHolder(int value) {
        data = new int(value);  // Acquisition in constructor
        cout << "Allocated memory" << endl;
    }

    ~IntHolder() {
        delete data;             // Release in destructor
        cout << "Freed memory" << endl;
    }

    int getValue() const {
        return *data;
    }
};

int main() {
    {
        IntHolder holder(42);
        cout << "Value: " << holder.getValue() << endl;
    }  // holder's destructor is called automatically here
       // Memory is freed automatically

    return 0;
}

Output:

Allocated memory
Value: 42
Freed memory

This is elegant. The user just creates an IntHolder object and doesn’t have to think about new or delete. The object handles it automatically.


Introduction to Smart Pointers as the Modern Answer

RAII is great, but C++ has made it even easier with smart pointers. A smart pointer is an object that behaves like a pointer but automatically manages the memory it points to.

The two most common are std::unique_ptr and std::shared_ptr:

std::unique_ptr - Exclusive Ownership

#include <iostream>
#include <memory>
using namespace std;

int main() {
    unique_ptr<int> ptr(new int(42));

    cout << "Value: " << *ptr << endl;

    // No need to delete!
    // Memory is freed automatically when ptr goes out of scope

    return 0;
}

std::shared_ptr - Shared Ownership

#include <iostream>
#include <memory>
using namespace std;

int main() {
    shared_ptr<int> ptr1(new int(42));
    {
        shared_ptr<int> ptr2 = ptr1;  // ptr1 and ptr2 point to the same memory
        cout << "Value: " << *ptr2 << endl;
    }  // ptr2 goes out of scope, but memory isn't freed yet
       // because ptr1 still holds a reference

    cout << "Value: " << *ptr1 << endl;

    return 0;
}  // ptr1 goes out of scope, now memory is freed

Smart pointers are the modern C++ way. They follow RAII automatically, so you rarely have to write new or delete manually.


Best Practices for Memory Management in Modern C++

  1. Use smart pointers instead of raw pointers

    auto ptr = make_unique<int>(42);  // preferred
    // instead of: int* ptr = new int(42);
  2. Use RAII for resources

    class FileHandler {
        ifstream file;
    public:
        FileHandler(const string& filename) {
            file.open(filename);  // acquire
        }
        ~FileHandler() {
            file.close();  // release
        }
    };
  3. Avoid raw pointers for ownership

    • Use unique_ptr when one object owns the data
    • Use shared_ptr when multiple objects share ownership
    • Use raw pointers only for non-owning references
  4. Use container classes

    vector<int> v;
    v.push_back(42);  // vector manages memory automatically
    // No need for new or delete
  5. Test with AddressSanitizer

    g++ -fsanitize=address -g -o prog prog.cpp
    ./prog

Conclusion: You’re in Control (And That’s a Good Thing)

Memory management in C++ can seem intimidating, but it’s really just two concepts:

Learn to use new and delete correctly, understand RAII, use smart pointers, and you’ll write code that’s both fast and safe. And remember: C++‘s memory management isn’t a bug; it’s a feature.

The power to control exactly how your program uses memory is why C++ is still the language of choice for performance-critical software. Master this skill, and you’ve mastered a crucial part of C++.


Ready to go deeper? Check out our article on smart pointers for a comprehensive guide to unique_ptr and shared_ptr. Or explore RAII patterns to make your code even more robust.


Take Your C++ Further

If you’re looking to go deeper with C++, the C++ Better Explained Ebook is perfect for you — whether you’re a complete beginner or looking to solidify your understanding. Just $19.

👉 Get the C++ Better Explained Ebook — $19



Edit page
Share this post on:

Previous Post
Top 50 C++ Interview Questions and Answers (Beginner to Advanced)
Next Post
Multithreading in C++: Threads, Mutexes, and Writing Thread-Safe Code