Dynamic Memory Allocation

Hi everyone ✋

In the previous post we learned the fascinating relationship between pointers and functions ─ how a function has an address, and how we can pass behavior around like data.

If you haven’t read the earlier posts in this series, please go through them first ─ today everything we’ve learned about pointers finally comes together for its most important job.

Today’s topic is where pointers truly become essentialdynamic memory allocation. This is how we ask the computer for memory while our program is running, using two special keywords ─ new and delete.

Up until now, every variable and array we made had its size fixed the moment we wrote the code. Today we break free from that limit 🥳

Let’s take a deep dive 🦴

The problem ─ why do we even need this? 🤔

Let’s start with a real problem, because that’s the best way to understand why dynamic memory exists.

Imagine we’re writing a program that asks the user “how many students are in your class?” and then stores a mark for each one. We might try this ─

#include <iostream>
using namespace std;

int main()
{
    int count;
    cout << "how many students? ";
    cin >> count;

    int marks[count];   // can we really do this? 🤔

    return 0;
}

Here’s the trouble ─ a normal array needs its size known at compile time, before the program even runs.

But count isn’t known until the user types it, which happens at run time. In standard C++, int marks[count]; is not actually legal ─ the size in [] must be a fixed number the compiler knows in advance.

So we’re stuck. We can’t know the size ahead of time, but the array demands we do. This is exactly the problem dynamic memory solves 🥳

Stack vs Heap ─ two kinds of memory

Before we fix this, we need to understand a key idea ─ our program has two different regions of memory to work with.

Everything we’ve made so far ─ our ints, our arrays, our char strings ─ has lived on something called the stack. The stack is fast and automatic ─ variables are created when we declare them and destroyed automatically when they go out of scope. But the stack needs to know sizes in advance, and it’s limited in space.

The other region is called the heap (sometimes called the free store). The heap is a big pool of memory we can request from whenever we want, however much we want, while the program runs. But there’s a catch ─ the heap does not clean up after us. Whatever we take, we must give back ourselves.

Here’s a simple way to remember the difference ─

Stack Heap
Size known when? at compile time at run time
Who cleans up? automatic we must do it
Speed very fast a little slower
Accessed through the variable name a pointer

Notice that last row 👀 ─ heap memory has no name of its own. The only way to reach it is through a pointer. This is why we needed the whole series before this post ─ dynamic memory is pointers in action 🥳

Meet new ─ asking for memory on demand

The new keyword asks the heap for a chunk of memory, and hands us back its address. We store that address in a pointer, and now we own that memory.

Let’s allocate a single integer.

#include <iostream>
using namespace std;

int main()
{
    // ask the heap for one integer, store its address in ptr
    int *ptr = new int;

    *ptr = 42;   // put a value into that memory

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

    return 0;
}

The output will be ─

value: 42

🥳 Let’s read what happened.

new int went to the heap, reserved enough space for one integer, and returned its address.

We caught that address in ptr.

Then *ptr = 42; used dereferencing (from our very first post!) to store 42 in that heap memory.

We can even give it a starting value right away, like this ─

int *ptr = new int(42);   // allocate AND initialize in one step

Notice the beautiful thing here ─ this memory has no variable name. There’s no int x anywhere. The only way we can reach that 42 is through ptr. If we lose ptr, we lose the memory forever 😱 (more on that danger soon).

Meet delete ─ giving the memory back

Remember the heap’s rule ─ whatever we take, we must give back. When we’re done with dynamic memory, we return it using delete.

#include <iostream>
using namespace std;

int main()
{
    int *ptr = new int(42);

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

    delete ptr;   // give the memory back to the heap
    ptr = nullptr; // good habit ─ point it at nothing now

    return 0;
}

The output will be ─

value: 42

🥳 The delete ptr;

line released the heap memory so it can be reused. Notice we did not delete the pointer itself ─ ptr is still a variable on the stack. What we deleted was the heap memory it was pointing to.

And see that ptr = nullptr; line?

After deleting, ptr still holds the old address, but that memory is no longer ours ─ it’s now a dangling pointer, pointing to something we don’t own. Setting it to nullptr makes it safe, clearly saying “this points to nothing now.” This is a very good habit 🤔

Finally solving our problem ─ a dynamic array

Now let’s return to our original problem ─ an array whose size the user chooses at run time. With new, we can finally do it 🥳

To allocate an array on the heap, we use new type[size]

#include <iostream>
using namespace std;

int main()
{
    int count;
    cout << "how many students? ";
    cin >> count;

    // allocate an array of 'count' integers on the heap
    int *marks = new int[count];

    // use it just like a normal array!
    for (int i = 0; i < count; i++)
    {
        marks[i] = (i + 1) * 10;
    }

    cout << "marks: ";
    for (int i = 0; i < count; i++)
    {
        cout << marks[i] << " ";
    }
    cout << endl;

    delete[] marks;   // note the square brackets!
    marks = nullptr;

    return 0;
}

A sample run ─

how many students? 4
marks: 10 20 30 40

🥳🥳 It works! We asked the user for a size, allocated exactly that many integers, and used the memory just like a regular array. This was impossible with a stack array.

Two things deserve a close look here.

First, remember the arrays post?

new int[count] gives us the address of the first element ─ so marks is a pointer, and we can use marks[i] on it exactly like a normal array. Everything we learned about array-pointer decay applies perfectly 🥳

Second, and very important ─ notice we wrote delete[] marks; with square brackets, not just delete marks;. When we allocate an array with new[], we must free it with delete[]. The brackets tell C++ “this was a whole array, clean up all of it.” Forgetting the brackets on an array is a serious bug ⚠️

The golden rule ─ every new needs a delete

Let’s make this crystal clear, because it’s the single most important rule of dynamic memory ─

  • Every new must be matched by exactly one delete.
  • Every new[] must be matched by exactly one delete[].

Think of it like borrowing books from a library 📚 ─ every book you check out, you must return. If you keep borrowing and never return, eventually the library runs empty.

When we forget to delete, the memory stays reserved even though we’re not using it anymore. Our program holds onto memory it can never reach again. This is called a memory leak ─ and let’s understand it properly, because it’s the classic dynamic-memory mistake.

The memory leak trap ⚠️

A memory leak happens when we lose the only pointer to some heap memory without freeing it first. Since the heap memory has no name, once the pointer is gone, that memory is lost forever ─ we can never reach it or free it again.

Let’s see the trap in action.

#include <iostream>
using namespace std;

void leaky()
{
    int *ptr = new int(100);   // allocate on the heap

    // ... we use ptr ...

    // OOPS ─ function ends without delete!
    // ptr is destroyed, but the heap memory is NOT freed 😱
}

int main()
{
    for (int i = 0; i < 1000000; i++)
    {
        leaky();   // leaking a little more each time!
    }

    return 0;
}

Every single time leaky() runs, it grabs a fresh integer from the heap and then throws away the only pointer to it (because ptr is a local variable that dies when the function ends). The heap memory itself never gets freed. Call this a million times, and we’ve leaked a million integers 😱

The memory is still reserved by our program, but completely unreachable ─ there’s no pointer left to find it. Over time, a leaky program eats more and more memory until the system slows to a crawl or crashes.

The fix is simple ─ always delete before we lose the pointer ─

void notLeaky()
{
    int *ptr = new int(100);

    // ... we use ptr ...

    delete ptr;   // give it back before the function ends! 🥳
}

One delete in the right place, and the leak is gone. This is why the “every new needs a delete rule matters so much 🤔

Another danger ─ the dangling pointer

We met this one briefly earlier, but it’s worth its own moment because it’s the opposite mistake to a leak.

A dangling pointer is a pointer that still holds an address to memory that has already been deleted. The pointer looks fine, but the memory it points to is no longer ours ─ using it is dangerous.

#include <iostream>
using namespace std;

int main()
{
    int *ptr = new int(42);

    delete ptr;   // memory is freed...

    cout << *ptr << endl;   // DANGER ─ ptr is now dangling! 😱

    return 0;
}

After delete ptr;, the pointer ptr still holds the old address, but that memory has been returned to the heap. Reading *ptr now is undefined behavior ─ it might print garbage, it might crash, it might appear to work and then break later. Never trust a pointer after deleting what it points to.

The fix is the habit we already saw ─ set it to nullptr right after deleting ─

delete ptr;
ptr = nullptr;   // now ptr clearly points to nothing

If we accidentally use a nullptr, at least the program will fail loudly and immediately, instead of silently corrupting things. A loud crash is far easier to fix than a silent bug 🥳

A quick summary of the two big dangers

Let’s put the two classic mistakes side by side so they’re easy to keep apart.

Memory Leak Dangling Pointer
What happens memory is never freed memory is freed but still used
The mistake forgot to delete used pointer after delete
The symptom program eats memory over time garbage values or crashes
The fix always delete when done set pointer to nullptr after delete

Both come down to the same core truth ─ on the heap, we are responsible for the memory ourselves. The stack cleaned up for us; the heap does not 🤔

The modern safety net ─ smart pointers 🥳

After reading about leaks and dangling pointers, you might be feeling nervous ─ “how am I supposed to never make these mistakes?” 😅

Good news ─ modern C++ heard you. It gives us smart pointers, which handle the delete for us automatically. The most common one is std::unique_ptr.

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

int main()
{
    // a smart pointer that cleans up after itself
    unique_ptr<int> ptr = make_unique<int>(42);

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

    // no delete needed ─ it frees itself automatically! 🥳

    return 0;
}

The output will be ─

value: 42

The magic here is that unique_ptr owns the memory, and the moment it goes out of scope, it automatically calls delete for us. No leaks, no dangling pointers, no forgetting ─ the cleanup is guaranteed 🥳

We still use *ptr to reach the value, exactly like a raw pointer, so everything we learned still applies. The smart pointer just adds an automatic safety net on top.

Smart pointers are a rich topic and deserve their own post ─ so we’ll explore unique_ptr, shared_ptr, and friends in detail later 😉 For now, just know they exist, and that they’re built directly on top of the new/delete foundation we learned today. Understanding raw new and delete first is exactly what makes smart pointers make sense.

So what should we remember? 🤔

Let’s wrap up the key takeaways ─

  • Normal (stack) variables need their size known at compile time; dynamic memory lets us decide sizes at run time.
  • The heap is a pool of memory we request with new and reach only through a pointer.
  • new gives us heap memory and returns its address; new type[size] gives us a dynamic array.
  • delete returns single-object memory; delete[] returns array memory ─ match them correctly.
  • Every new needs exactly one delete (and every new[] needs one delete[]).
  • A memory leak is forgetting to delete ─ the memory becomes unreachable and wasted.
  • A dangling pointer is using a pointer after delete ─ set it to nullptr right after deleting.
  • Modern C++ offers smart pointers (unique_ptr, etc.) that call delete automatically ─ built on top of everything we learned today.

Once these click, you understand why pointers exist in the first place ─ they’re the key to using memory flexibly and powerfully 👀✨

Congratulations 🥳🥳🥳
We now understand dynamic memory allocation, the most important job pointers do.

In the next post we will talk about smart pointers ─ unique_ptr, shared_ptr, and how modern C++ lets us enjoy the power of dynamic memory without the fear of leaks and dangling pointers 😉

Happy Coding 💻 🎵

Leave a Comment

Your email address will not be published. Required fields are marked *