What Is Dynamic Memory Allocation?

Dynamic memory allocation asks for memory while the program is running, rather than having the compiler set it aside in advance. You request it with new and hand it back with delete.

C++ gives you three ways to obtain storage, and the third exists because the first two share a limitation.

Kind Where it lives Size decided Lifetime
Static Static storage Compile time The whole program
Automatic The call stack Compile time Its enclosing block
Dynamic The heap Run time Until you delete it

Static and automatic allocation are convenient precisely because they are automatic: you declare a variable and its size and lifetime are settled. That is also the constraint. The size has to be known when the program is compiled, and the lifetime is tied to a scope you did not choose.

Dynamic allocation drops both restrictions. You can allocate an amount decided by input the program has just read, and you can keep it alive across function boundaries. In exchange, releasing it becomes your responsibility.

Allocating a Single Variable

new takes a type, obtains storage for one object of it, and returns a pointer:

int* sample{ new int };

You can initialize at the same time, which is almost always what you want:

int* reading{ new int{ 42 } }; // brace initialization
int* score{ new int(87) };     // direct initialization

Putting it together, with the cleanup that has to follow:

#include <iostream>

int main()
{
    int* sample{ new int };
    *sample = 87;

    int* reading{ new int{ 42 } };

    std::cout << "Sample: " << *sample << '\n';
    std::cout << "Reading: " << *reading << '\n';

    delete sample;
    sample = nullptr;

    delete reading;
    reading = nullptr;

    std::cout << "Sample pointer is null: " << (sample == nullptr) << '\n';

    return 0;
}

Output:

Sample: 87
Reading: 42
Sample pointer is null: 1

What new and delete Actually Do

At run time the operating system has given your program a pool of memory, the heap. new asks the heap for a block of the right size, marks it as in use, and returns its address. Nothing about that block is special: it is ordinary memory, reachable only through the pointer you were handed.

delete returns the block to the pool so a later new can reuse it. It does not erase the contents, and it does not change your pointer. Both of those facts matter more than they sound.

Key Concept
delete releases the memory, not the pointer. The pointer variable still exists and still holds the same address; that address simply no longer belongs to you.

Dangling Pointers

A pointer holding the address of memory that has been freed is a dangling pointer. Reading or writing through one is undefined behavior, and the old contents are often still sitting there, so it can appear to work for a while before it fails somewhere unrelated.

Assigning nullptr after delete is what makes the danger testable, because a null pointer can be checked and a dangling one cannot be distinguished from a valid one.

The trap is that only the pointer you assign is fixed:

#include <iostream>

int main()
{
    int* sample{ new int{ 12 } };
    int* alias{ sample };

    delete sample;
    sample = nullptr;

    std::cout << "alias is not null: " << (alias != nullptr) << '\n';

    return 0;
}

Output:

alias is not null: 1

One allocation, two pointers to it. Deleting through sample freed the memory both of them referred to, and nulling sample says nothing about alias, which is now dangling while looking perfectly healthy. Dereferencing it would be undefined behavior.

Best Practice
Set a pointer to nullptr immediately after deleting through it, unless it is going out of scope anyway. Be aware this only helps the pointer you assign, so avoid keeping several pointers to one allocation.

Deleting Null Is Safe

delete on a null pointer does nothing at all. It is explicitly allowed, so guarding a delete with an if that checks for null is unnecessary.

When Allocation Fails

The heap is finite, and new can fail. By default it throws std::bad_alloc, and an unhandled exception terminates the program.

When you would rather test for failure than handle an exception, std::nothrow changes the failure mode to a null return:

#include <iostream>
#include <new>

int main()
{
    int* buffer{ new (std::nothrow) int{ 5 } };

    if (buffer == nullptr)
    {
        std::cerr << "Allocation refused\n";
        return 1;
    }

    std::cout << "Allocated and holding " << *buffer << '\n';

    delete buffer;
    buffer = nullptr;

    return 0;
}

Output:

Allocated and holding 5

The check is not decoration. A failed new (std::nothrow) returns nullptr, and dereferencing that is undefined behavior, so this form is only safer than the throwing one if you actually test the result.

In practice, a failing single-object allocation usually means the process is in serious trouble, so many programs let the exception terminate them rather than trying to continue.

Memory Leaks

A memory leak is allocated memory the program can no longer reach and therefore can no longer free. It stays reserved until the process exits.

Three ways to cause one, all of them variations on losing the only pointer you had:

The pointer goes out of scope.

void collectSample()
{
    int* sample{ new int{} };
} // sample is destroyed here; the int it pointed at is not

The pointer is reassigned before the delete.

int* sample{ new int{} };
sample = new int{};       // the first allocation is now unreachable

The pointer is overwritten with another address.

int* sample{ new int{} };
int stackValue{ 5 };
sample = &stackValue;     // same result, the heap block is orphaned

Each one leaves a block allocated with nothing referring to it. The fix in every case is to delete before the last pointer to the memory changes or disappears.

Conclusion

new and delete are the mechanism underneath every dynamic allocation in C++, and worth understanding for exactly that reason. Writing them by hand is another matter: every path out of a function, including the ones taken by early returns and thrown exceptions, has to reach the matching delete.

Later lessons cover the tools that make this manageable, std::vector for dynamic arrays and smart pointers for single objects, both of which handle the release for you.

Summary

Why it exists: static and automatic allocation need the size at compile time and tie the lifetime to a scope. Dynamic allocation decides both at run time.

Allocating: new int gives storage for one int and returns a pointer; new int{ 42 } initializes it at the same time.

Releasing: delete returns the memory to the heap. It does not clear the contents and does not modify the pointer.

Dangling pointers: a pointer to freed memory. Assign nullptr after deleting, remembering that other pointers to the same block are left dangling.

Deleting null is safe and needs no guard.

Failure: new throws std::bad_alloc by default; new (std::nothrow) returns nullptr instead, which is only safer if you check it.

Memory leaks: allocated memory with no pointer left to reach it, caused by the pointer going out of scope, being reassigned, or being overwritten. It stays reserved until the program ends.