How C++ delete Differentiates Single Objects and Arrays
In the C++ programming language, the runtime does not automatically
deduce whether a pointer refers to a single object or a contiguous array
of objects. Instead, the language enforces this distinction at compile
time through two distinct operators: delete and
delete[]. While single-object delete simply
invokes a single destructor and frees the memory, array destruction
requires tracking the number of allocated elements, which compilers
implement using low-level mechanisms such as "array cookies" or
allocator lookup tables.
The Programmer's Responsibility
C++ does not store dynamic type metadata within raw pointers. A
pointer of type T* looks identical to the compiler whether
it points to a single instance of T allocated via
new T or an array of instances allocated via
new T[N].
Because raw pointers lack this context, the language requires the programmer to explicitly differentiate between the two operations:
delete ptr;is reserved for pointers allocated withnew.delete[] ptr;is reserved for pointers allocated withnew[].
Mismatching these operators—such as calling delete on an
array or delete[] on a single object—results in undefined
behavior.
How Single-Object
Destruction Works (delete)
When delete ptr is executed, the compiler generates
instructions to perform two specific tasks:
- Destruction: It invokes the destructor for the
single object:
ptr->~T(). If the static type ofptrhas a virtual destructor, the compiler uses the object's virtual method table (vtable) to invoke the correct derived destructor. - Deallocation: It releases the underlying memory by
calling the deallocation function:
operator delete(ptr).
Because only one object exists, the compiler does not need to look up an element count.
How Array Destruction Works
(delete[])
When delete[] ptr is executed, the compiler must destroy
every element in the array before freeing the memory block. To
accomplish this, the runtime must determine:
- The number of elements in the array.
- The true starting address of the allocated block.
The compiler then generates a loop that calls the destructor
~T() on each element in reverse order of construction.
Finally, it invokes operator delete[](raw_pointer) to
release the entire memory block.
Tracking Array Size: The "Array Cookie"
The primary mechanism defined by major C++ Application Binary Interfaces (ABIs), such as the Itanium C++ ABI and the MSVC ABI, to track the number of elements is known as an array cookie (or overhead allocation).
When dynamic memory is allocated using new T[N] for a
type T that has a non-trivial destructor:
- Over-allocation: The runtime requests more bytes
than
N * sizeof(T). It adds padding equivalent to the size of a size-tracking integer (typicallysizeof(std::size_t)). - Storing the Count: The runtime writes the value
Ninto the initial bytes of the allocated memory block. - Pointer Adjustment: The pointer returned to the
user is offset forward by the size of the cookie, pointing directly to
the first actual element of type
T.
When delete[] ptr is subsequently invoked:
- Reading the Count: The compiler generates code that
offsets backward from
ptrto read the stored valueN. - Destruction Loop: The compiler executes a loop
calling
~T()exactlyNtimes. - Pointer Readjustment: The base pointer (including
the cookie) is passed to
operator delete[]to reclaim the memory.
If a type has a trivial destructor (such as int or a
plain-old-data struct), compilers often optimize this process by
omitting the array cookie entirely. Because no destructors need to be
executed, the underlying memory allocator only needs the original
pointer to release the raw memory block.
Why Mismatching Causes Undefined Behavior
Because of the architectural differences between delete
and delete[], mixing them leads to severe runtime
failures:
- Calling
deleteon an array: The compiler will only invoke the destructor for the first element (ptr[0]). If an array cookie was stored, the base address passed to the memory allocator will be offset by the size of the cookie rather than the true start of the allocation, leading to heap corruption. - Calling
delete[]on a single object: The compiler will attempt to read memory immediately preceding the single object, misinterpreting whatever bytes reside there as an element count. It will then attempt to execute destructors on unallocated or unrelated memory, leading to segmentation faults or memory corruption.