Skip to content

Dynamic Memory: new, delete & Their Dangers

Learn how dynamic allocation works with new and delete, understand the dangers of memory leaks, double-free, and use-after-free, and discover why modern C++ should contain zero raw new/delete.

Dynamic Allocation with new and delete

The stack is fast but limited in size and scope. When you need objects that outlive their creating scope, or whose size is unknown at compile time, you allocate on the heap (free store).

new T(args) allocates memory for one T, calls its constructor, and returns a T*. delete p calls the destructor and frees the memory. For arrays: new T[n] and delete[] p. Mismatching new with delete[] or new[] with delete is undefined behavior — the compiler will not catch this.

new, delete, and Their Array Variants

Here is the raw allocation API. Study it so you recognize it in legacy code, but never write this in new code — use smart pointers instead.

new_delete.cpp
#include <iostream>
#include <cstring>

struct Widget {
    int id;
    Widget(int i) : id(i) { std::cout << "Widget " << id << " created\n"; }
    ~Widget() { std::cout << "Widget " << id << " destroyed\n"; }
};

int main() {
    // Single object
    Widget* w = new Widget(1);
    std::cout << "Using widget " << w->id << '\n';
    delete w;  // destructor called, memory freed

    // Array
    Widget* arr = new Widget[3]{{10}, {20}, {30}};
    for (int i = 0; i < 3; ++i)
        std::cout << arr[i].id << ' ';
    std::cout << '\n';
    delete[] arr;  // MUST use delete[] for new[]

    // UNDEFINED BEHAVIOR — don't do these:
    // delete arr;      // wrong: used delete instead of delete[]
    // delete[] w;      // wrong: used delete[] instead of delete
}

Placement new

Placement new constructs an object at a specific memory address without allocating. It is used by allocators, memory pools, and containers like std::vector internally. You must call the destructor explicitly — delete must NOT be used on placement-new'd objects.

placement_new.cpp
#include <iostream>
#include <new>       // required for placement new
#include <cstddef>   // std::byte
#include <memory>    // std::align

struct Sensor {
    int id;
    double reading;
    Sensor(int i, double r) : id(i), reading(r) {
        std::cout << "Sensor " << id << " init\n";
    }
    ~Sensor() { std::cout << "Sensor " << id << " shutdown\n"; }
};

int main() {
    // Pre-allocate raw storage (aligned)
    alignas(Sensor) std::byte buffer[sizeof(Sensor)];

    // Construct in pre-allocated memory
    Sensor* s = new (buffer) Sensor(42, 3.14);
    std::cout << "Reading: " << s->reading << '\n';

    // Must destroy explicitly — do NOT use delete
    s->~Sensor();  // explicit destructor call
    // delete s;    // WRONG — memory was not allocated by new
}

The Three Deadly Memory Bugs

Raw new/delete exposes you to three categories of bugs:

1. Memory Leak — allocating with new but never calling delete. The memory is never returned to the OS (until process exit). In a long-running server, this eventually consumes all available RAM.

2. Double Free — calling delete on the same pointer twice. The second delete corrupts the heap allocator's internal data structures, often leading to crashes much later in unrelated code. This is extremely hard to debug.

3. Use-After-Free — accessing memory after delete. The memory may have been reused for a different allocation, so you could be reading or writing another object's data. This is a security vulnerability (CVE-class bug).

Pitfall

Consider: f(new A(), new B()). The compiler may evaluate arguments in any order and may interleave them:

1. Allocate memory for A
2. Allocate memory for B
3. Construct A
4. Construct B

If step 4 throws, A is leaked — nobody calls delete on it. Even f(std::shared_ptr(new A()), std::shared_ptr(new B())) had this problem before C++17 (which guarantees that each argument is fully evaluated before the next). The fix: always use std::make_unique or std::make_shared — they perform allocation and construction as a single, exception-safe operation.