← All posts
memory

new, operator new, Placement new, and malloc: Allocation vs Construction

There are four ways to get memory in C++, and most people only ever use one. Then they write a custom allocator or a memory pool, and the difference between “allocate” and “construct” becomes the entire problem.

The Four Mechanisms

new new T(args) allocate + construct · T* · throws bad_alloc · delete p Allocate Construct operator new operator new(n) allocate only · void* throws bad_alloc no header (nothrow: <new>) operator delete(p) malloc malloc(size) allocate only · void* returns nullptr on fail <cstdlib> free(p) placement new new (ptr) T(args) construct only · T* uses existing memory · <new> no allocation happens p->~T() only, never delete new uses operator new internally for its allocation step, then constructs via placement new operator new and malloc differ only in C++ vs C origin and how they signal failure

new T(args): the only one that does both jobs. Allocates memory by calling operator new(sizeof(T)) internally, then calls T’s constructor on the returned memory. Returns T*. Throws std::bad_alloc on allocation failure. Freed with delete, which calls the destructor then operator delete. No header required for plain new and delete.

operator new(size): allocates only. Returns void*, raw uninitialized bytes, no constructor runs. Throws std::bad_alloc on failure. This is the function new calls internally, and it is a real function subject to overloading, not a language keyword. Overloading it per class redirects all new expressions for that type to a custom allocator without any change at the call site. Freed with operator delete. Also available in a nothrow form: operator new(size, std::nothrow) returns nullptr instead of throwing.

Placement new new (ptr) T(args): constructs only. Takes memory that already exists and builds an object directly inside it. No allocation happens, no memory is reserved. Returns T* pointing to the same address as ptr. Since nothing was allocated, delete cannot be used to clean up. The destructor must be called explicitly: p->~T(). Requires #include <new>.

malloc(size): the C function. Allocates raw bytes, returns void*, must be cast. Returns nullptr on failure, no exception. No constructor ever runs. Freed with free, never delete. Requires #include <cstdlib>.

// allocates AND constructs
Widget* a = new Widget();
delete a;   // destructs AND deallocates

// allocates ONLY
void* raw = operator new(sizeof(Widget));
operator delete(raw);   // deallocates ONLY, no destructor

// constructs ONLY, in existing memory
alignas(Widget) char buffer[sizeof(Widget)];
Widget* p = new (buffer) Widget();
p->~Widget();   // must call destructor explicitly

// raw bytes ONLY, no constructor
Widget* m = static_cast<Widget*>(malloc(sizeof(Widget)));
free(m);   // raw bytes only, no destructor
Name Syntax Allocates Constructs Returns On Failure Cleanup Header
Regular new new T(args) T* throws bad_alloc delete none
Operator new operator new(size) void* throws bad_alloc operator delete none
Placement new new (ptr) T(args) T* constructor throws p->~T() <new>
malloc malloc(size) void* returns nullptr free <cstdlib>

operator new and malloc both allocate raw bytes with no constructor. new calls operator new internally for its allocation step, then runs the constructor via placement new on the returned memory.

Memory Management

Valid Create / Cleanup Pairs

Every valid combination — and what is UB if you mix incorrectly.

✓ valid ✗ undefined behavior
new T(args)
allocates + constructs
delete p
destructs + deallocates
p->~T() + operator delete(p)
equivalent to delete when no operators are overloaded, but dispatches to wrong operator delete if they are — always use delete directly
p->~T() + free(p)
free cannot release memory from operator new; different allocator families
free(p)
destructor never called and free cannot release operator new memory
operator new(sizeof(T))
+ new (raw) T(args)
manual allocate + manual construct
p->~T()
then operator delete(raw)
destruct then deallocate
delete p
delete calls operator delete internally, not raw deallocation; pointer must come from a new expression
p->~T() + free(raw)
free cannot release memory from operator new; different allocator families
operator delete(raw)
destructor never called; object abandoned in memory before it is released
malloc(sizeof(T))
+ new (ptr) T(args)
C allocate + manual construct
p->~T()
then free(ptr)
destruct then deallocate
delete p
delete calls operator delete internally which cannot release malloc memory; pointer must come from a new expression
p->~T() + operator delete(ptr)
wrong deallocator: allocator families cannot be mixed
free(ptr)
destructor never called; object abandoned in memory before it is released
external buffer
+ new (buf) T(args)
no allocation + manual construct
p->~T() only
destruct only — memory is external
delete p
delete calls operator delete on a non-heap address; stack or static memory cannot be freed this way
operator delete(p)
operator delete on non-heap memory is undefined; may corrupt allocator state
free(p)
free on non-malloc memory is undefined; may corrupt allocator state
operator new(sizeof(T))
(no construction)
raw bytes only
operator delete(raw)
raw memory release, no destructor
free(raw)
free cannot release memory from operator new; different allocator families
delete raw
delete tries to call a destructor on memory that was never an object; no constructor ran here
malloc(sizeof(T))
(no construction)
raw bytes only
free(ptr)
raw memory release, no destructor
operator delete(ptr)
operator delete cannot release malloc memory; different allocator families
delete ptr
delete tries to call a destructor on memory that was never an object; no constructor ran here

The matching rule

The deallocation function must match the allocation function: new → delete, operator new → operator delete, malloc → free. Objects constructed with placement new must have their destructor called explicitly before the memory is released. External buffers require no deallocation step at all.

Header Requirements

The header requirements are subtle and worth being precise about:

new and delete (plain, standalone): no header required. They are language keywords with built-in compiler support.

operator new and operator delete (the function forms): no header required for the global versions. #include <new> is needed for the nothrow overloads (std::nothrow) and for placement new syntax.

Placement new new (ptr) T(args): requires #include <new>. The same header covers placement delete if needed.

malloc, free, calloc, realloc: #include <cstdlib>.

The <memory> header covers std::construct_at, std::destroy_at, std::destroy, std::destroy_n, and std::allocator, covered later in this post.

The Delete Side: Three Separate Mechanisms

The same split exists on the release side.

delete p: destructs the object at p by calling p->~T(), then calls operator delete(p) to release the memory. Two operations in one keyword. Calling delete on a nullptr is defined and does nothing. Calling it on a pointer that was not obtained from new (a stack pointer, a malloc result, a placement-new pointer) is undefined behavior. Double delete is also undefined behavior.

operator delete(p): releases memory only. No destructor called. The counterpart to operator delete obtained from operator new. Like operator new, it can be overloaded per class.

Placement delete: exists as a concept but is almost never called directly. When a placement new expression (new (ptr) T(args...)) throws during the constructor, the compiler automatically looks for a matching operator delete(void*, void*) to undo any side effects the allocation step may have had. For plain placement new on a raw buffer this is a no-op: no memory was allocated, so there is nothing to release. The compiler calls placement delete as cleanup in error paths, not as a normal cleanup mechanism. Calling it directly in user code is almost always a mistake.

std::construct_at and Friends

C++20 added a set of constexpr-capable wrappers in <memory> that cover the same operations as placement new and explicit destructor calls, with cleaner syntax and constexpr support.

std::construct_at(ptr, args...): the safe constexpr version of placement new. Constructs an object at the already-allocated memory pointed to by ptr. Returns T*. Works in constexpr contexts where placement new syntax cannot be used directly.

#include <memory>

alignas(Widget) char buffer[sizeof(Widget)];
Widget* p = std::construct_at(reinterpret_cast<Widget*>(buffer), args...);

std::destroy_at(ptr): calls ptr->~T() explicitly. The safe wrapper around a manual destructor call. Does not free memory. Constexpr-capable.

std::destroy_at(p);   // destructs, does not free

std::destroy(first, last): destructs a range of objects. Calls std::destroy_at on each element from first up to but not including last. Both arguments are pointers or iterators.

std::destroy(widgets, widgets + count);

std::destroy_n(first, n): destructs n objects starting at first. Equivalent to std::destroy(first, first + n). Useful when you have a count rather than an end iterator.

std::destroy_n(widgets, count);

construct_at and destroy_at are the building blocks. destroy and destroy_n are convenience wrappers for ranges. All four are in <memory> and all are constexpr since C++20, making them usable inside constexpr functions where the C++20 transient allocation rules apply.

std::allocator: The STL’s Four-Operation Split

std::allocator<T> is the default allocator used by every STL container. Its design separates memory management from object lifetime management, which is why std::vector can reserve capacity without constructing elements in the reserved slots.

The four operations map directly to the primitives above:

allocate(n): calls operator new(n * sizeof(T)). Reserves raw memory for n objects. No constructor is called. Returns T* pointing to the reserved block.

construct(p, args...): calls placement new on p. Constructs an object in place at the already-allocated location. This is how std::vector::push_back builds the new element in the buffer’s next slot without allocating new memory.

destroy(p): calls p->~T(). Destructs the object without releasing memory. This is how std::vector::pop_back removes an element without shrinking the buffer.

deallocate(p, n): calls operator delete(p). Releases the raw memory. No destructor is called.

The usage order in a container is: allocate first (reserve), then construct as objects are added, then destroy as objects are removed, then deallocate when the container releases its buffer. Memory and object lifetime are managed independently at every step.

In C++17 and later, construct and destroy moved to std::allocator_traits rather than being called directly on the allocator itself, but the underlying operations are identical.

The Pattern They All Enable

Once allocation and construction are separated, one pattern falls out naturally: allocate a large block once up front, then use placement new to construct objects into slots within that block, destroy them with explicit destructor calls when done, and never touch the OS allocator again in the hot path.

// allocate once
void* pool = operator new(N * sizeof(Widget));
Widget* slots = static_cast<Widget*>(pool);

// construct into slots as needed
Widget* w = std::construct_at(slots + i, args...);

// destroy when done with the slot
std::destroy_at(w);

// release the whole block at once
operator delete(pool);

This is the foundation of memory pools, arena allocators, and any lock-free data structure that needs to avoid malloc in its hot path. As covered in the prefaulting post, calling into the allocator at runtime can trigger a page fault on first touch. Pre-allocating the pool and prefaulting it before entering the hot path eliminates both the allocator overhead and the fault latency.

Run: main.cpp

g++ -O2 -std=c++26 main.cpp -o main
./main

Expect Widget constructed and Widget destructed to appear for the new/delete section and the placement new section, and to be absent from the operator new/operator delete and malloc/free sections.

Run: overload.cpp

g++ -O2 -std=c++26 overload.cpp -o overload
./overload

Expect the custom operator new and operator delete messages to appear even though main uses plain new PooledWidget() and delete w syntax.

Output

$ ./main
=== new / delete: allocates and constructs, destructs and deallocates ===
  Widget constructed
  Widget destructed
=== operator new / operator delete: allocation only, no constructor or destructor ===
  (no constructor ran)
  (no destructor ran)
=== placement new: construction only, no allocation ===
  Widget constructed
  (no allocation happened, buffer already existed)
  Widget destructed
=== malloc / free: raw bytes only, never touches a constructor or destructor ===
  (no constructor ran)
  (no destructor ran)

$ ./overload
=== plain 'new PooledWidget()', calling code unchanged ===
  custom operator new called, size=1
  PooledWidget constructed
  PooledWidget destructed
  custom operator delete called

Widget constructed and Widget destructed appear only where a constructor and destructor were actually invoked, absent from the operator new and malloc sections. The overload output confirms redirection: the call site uses plain new PooledWidget() and delete w unchanged, but both the custom operator new and operator delete intercept the call. size=1 because PooledWidget has no data members — an empty class still requires a minimum allocation size.

Quick Reference

Coming from other languages

Most languages with automatic memory management hide the allocation/construction split entirely. The runtime handles both as one step and the programmer never sees either separately. C++ exposes the split because it has value semantics, deterministic destruction, and the ability to place objects in arbitrary memory, all of which require being able to construct an object somewhere other than the standard heap. The placement new pattern is how every C++ memory pool is implemented, and understanding it is necessary for writing any container or allocator from scratch.

The 90% mental model

new = allocate + construct. delete = destruct + deallocate. operator new = allocate only, returns void*, can be overloaded per class, no header needed for the global form. Placement new = construct only, into existing memory, requires #include <new>, must call destructor manually, never use delete on it. malloc = raw bytes only, no constructor ever, returns nullptr on failure, freed with free. std::construct_at and std::destroy_at in <memory> are the constexpr-capable wrappers for placement new and explicit destructor calls. std::allocator uses all four primitives internally: allocate and deallocate handle memory, construct and destroy handle object lifetime, kept separate so containers can reserve capacity without constructing objects.