AlgoMaster Logo

new & delete Operators

High Priority14 min readUpdated September 21, 2026
Listen to this chapter
Unlock Audio

The previous lesson introduced the new/delete mechanics: int* p = new int(5); delete p; allocates and frees one int on the heap. This chapter goes a layer deeper. It splits the new expression into its two real steps (allocation and construction), shows the corresponding split for delete, covers what happens when allocation fails, explains why mismatching new[] with delete is a serious bug, and looks at the exception-safety guarantees that new provides automatically.

The Two-Step new Expression

new Product("Mouse", 24.99) is not one function call. The compiler turns that single expression into two distinct steps that happen in order:

  1. Call operator new(size) to obtain enough raw, uninitialized memory to hold a Product.
  2. Construct a Product in that memory by invoking its constructor with "Mouse" and 24.99.

The pointer the expression evaluates to is the address returned by step 2, which for ordinary types is the same address step 1 returned. Two steps, one expression.

The diagram tracks one new expression through both steps. operator new is a real function in the standard library, declared in <new>. It can be called directly for raw memory without construction, though that's rare outside of library code.

A small program that calls operator new and operator delete explicitly, without any construction in between:

operator new(16) returned 16 contiguous bytes of uninitialized heap memory. No constructor ran because there was no type to construct. operator delete returned those bytes to the runtime without running any destructor.

The full new T(args) expression is built on top of these primitives. The compiler emits something equivalent to:

That second line is placement new. The takeaway: new T(args) is one expression but two phases, and that split matters when allocation succeeds but construction fails.

The Two-Step delete Expression

delete mirrors new. delete p compiles to two steps in the opposite order:

  1. Run p->~T(), the destructor of the object p points at.
  2. Call operator delete(p) to return the raw memory to the runtime.

If T is a built-in like int, step 1 is a no-op (built-ins have no destructor) and only the deallocation runs. For class types with non-trivial destructors, step 1 is where any resources the object owned get cleaned up.

The order matters. If operator delete ran first, the destructor would try to clean up resources inside memory the runtime had already taken back. The destructor always runs while the object is still considered "alive" in its own memory.

operator delete can be called directly too, but only on memory from operator new (not from new T(args), because that memory has a constructed object in it that needs destroying first).

The two-step split is hidden behind the delete p; line, but the destructor message proves step 1 happened before step 2. Tracing compiler-generated code would show the destructor call followed by an operator delete call.

Overloading operator new and operator delete

Because operator new and operator delete are real functions, classes can replace them with their own versions. This is how libraries implement object pools, memory tracking, alignment-aware allocators, and other custom strategies. The mechanism is worth recognising even when the allocator isn't being written by hand, because it appears in production codebases.

A class-scope overload looks like this:

When the compiler sees new Product{...}, it looks up operator new first in Product itself. Because Product defines one, that version is used instead of the global one. The constructor call (step 2 of the new expression) still runs normally on the memory the class-scope operator new returned. The same happens in reverse for delete.

operator new and operator delete can also be overloaded at global scope to replace the default allocator program-wide. That's a heavier hammer and almost always belongs in a dedicated library (memory tracker, leak detector, allocation profiler), not application code. Either way, the rule for users of the class is unchanged: pair every new with one delete and the class's overloads run automatically.

new[] and delete[]: Arrays Are Different

The array forms of new and delete call different underlying functions than the single-object forms. new T calls operator new; new T[n] calls operator new[]. delete p calls operator delete; delete[] p calls operator delete[]. They are four separate functions, and the compiler picks one based on whether brackets were used.

That's why mixing them is undefined behavior.

The diagram is direct on purpose. The only correct pairings are new with delete and new[] with delete[]. Crossing the wires breaks the program. The bugs don't always show up immediately; sometimes a delete on a new[] array seems to work on small tests and only crashes when array sizes grow or the destructor does real work.

To understand why, consider what new Product[3] actually does. It needs to know there are three objects so that delete[] can later call the destructor three times. Most implementations store the element count (and sometimes the size of each element) in a small header in front of the returned array address. delete[] reads that header to figure out how many destructors to run. delete (the single-object form) doesn't know to look for a header, so it doesn't see the count, doesn't run the right number of destructors, and frees memory that doesn't start where the allocator's bookkeeping expects.

A concrete example that shows the destructor count:

delete[] cart runs the destructors in reverse construction order (Lamp, Cable, Mouse), then returns the whole block to the allocator. Replacing delete[] cart with delete cart enters undefined-behavior territory: only one destructor might run, the allocator's internal bookkeeping gets confused, and the failure often shows up later as a crash on a completely unrelated allocation.

delete[] on an array of N objects with non-trivial destructors runs N destructor calls before freeing the block. For a million-element array of objects that each hold a std::string, that's a million string destructors. Worth knowing when sizing arrays.

Allocation Failure: std::bad_alloc

new can fail. If the runtime can't find a large enough free block, operator new throws std::bad_alloc. The program either catches it or terminates with an uncaught exception. On a desktop with plenty of memory, normal-sized allocations rarely fail, but very large allocations and embedded targets do hit the failure path.

std::bad_alloc lives in the <new> header and inherits from std::exception, so a catch (const std::exception&) would also catch it.

The throwing behavior is the default because it's the safest. The expression int* p = new int; is guaranteed to either return a valid pointer or throw. There's no third option where p is null and the next line reads garbage.

The Nothrow Form

For situations where exceptions aren't an option (real-time code, embedded systems, or code that prefers to handle failure inline), C++ provides a nothrow form:

new (std::nothrow) T returns nullptr on failure instead of throwing. The trade-off: the pointer must be checked before every use. Missing the check leads to a null dereference, which is undefined behavior.

The nothrow form isn't faster on the happy path. It just changes how failure is reported. Use it only when avoiding exceptions is specifically required.

Exception Safety: When the Constructor Throws

This is where the two-step nature of new pays off. Consider what should happen if operator new succeeds (raw memory is available) but the constructor throws partway through (the object isn't fully built). The result would be a chunk of memory the runtime gave out, with no object in it, and no pointer available to call delete. That would be a leak baked into the language.

The C++ standard prevents this. If a constructor throws during a new expression, the runtime automatically calls `operator delete` on the memory that step 1 allocated, then lets the exception propagate. The memory is gone before any user code sees it.

The constructor ran, validated the input, found the bad total, and threw. The destructor message never prints because the object was never fully constructed. The memory operator new returned was reclaimed automatically before the exception left the new expression. There is no leak, and bad was never assigned a value because the assignment only happens when the expression on the right completes.

For array forms, the guarantee is stronger. If new Order[5] constructs three orders successfully and then the fourth's constructor throws, the runtime calls the destructors for the first three (in reverse construction order) and then releases the whole block. Partially constructed arrays are cleaned up automatically too.

A and B constructed successfully, C threw partway through, and the runtime destroyed B then A before letting the exception escape. No leaks, no half-built objects in user code, no manual cleanup. This is one of the better-engineered corners of the C++ standard, and it's invisible until the output makes it visible.

Why Modern C++ Discourages Raw new/delete

Even though the language provides new and delete, modern C++ codebases mostly avoid writing them directly. It's not that they're broken; the language guarantees covered above are solid. The reason is that pairing them correctly across real code is harder than it looks, and the language has tools that handle the pairing automatically.

A few patterns that bite raw new/delete code:

  • A function returns early on an error path and skips the delete. Leak.
  • An exception unwinds the stack between new and delete. Leak.
  • Ownership gets handed between functions and no one is sure who deletes. Leak or double-delete.
  • A Product* member of a class isn't deleted in the destructor. Leak.
  • The destructor deletes the same pointer twice across an inheritance chain. Undefined behavior.

Each one has a fix. Together they're a steady source of bugs in C++ codebases that don't use the higher-level abstractions.

The rest of this section is largely about the abstractions that solve these problems: RAII as the foundational technique (tie resource ownership to a stack variable's lifetime so cleanup happens automatically on scope exit), smart pointers like std::unique_ptr and std::shared_ptr that wrap a raw new and call delete automatically when they go out of scope, and the factory functions std::make_unique and std::make_shared that replace new entirely in day-to-day code.

The skill those abstractions teach isn't "stop using new." It's "tie every new to an owner whose destructor will run." Smart pointers are the owners. The new is still happening underneath; the user code doesn't have to see it.

A taste of what's coming:

That's not the topic of this chapter, so it gets one example as a teaser. The point: the raw new/delete covered above is the engine; smart pointers are the steering wheel.

One Loose End: Placement new

Placement new is the form of new that constructs an object in memory that already exists, instead of asking the runtime for a fresh block. It looks like new (existingMemory) T(args) and runs step 2 of a normal new expression while skipping step 1. It's used by containers like std::vector to construct elements in pre-allocated buffers, and by libraries that want full control over allocation. A later lesson covers it properly. It's mentioned here so the two-step picture doesn't leave a gap: those steps are independent, and step 2 can be invoked on its own.

Quiz

new & delete Operators Quiz

10 quizzes