36 Intermediate The Hundred
The delete That Freed One Object
three objects in, one funeral
#include <iostream>
struct Noisy {
int id = 0;
~Noisy() { std::cout << " ~Noisy #" << id << std::endl; }
};
int main() {
Noisy* batch = new Noisy[3];
for (int i = 0; i < 3; ++i)
batch[i].id = i;
std::cout << "cleaning up..." << std::endl;
delete batch; // one allocation, one delete
std::cout << "done" << std::endl;
}
Run it. How many destructors run?
Answer
One — and the program never reaches done. Typical output (GCC 11 / glibc on x86-64
Linux, identical from -O0 to -O3):
Why¶
new Noisy[3] quietly over-allocates: for a type with a non-trivial destructor it stashes
the element count in a cookie just before the address it returns, so delete[] knows how
many destructors to run. Plain delete has never heard of cookies — it destroys one object,
then hands the allocator a pointer 8 bytes past the block's start: undefined behavior,
which glibc catches here, though another allocator might corrupt the heap in silence.
Trivially destructible types need no cookie, so int* p = new int[3]; delete p; usually
appears to work — which is how the habit survives to meet a class with a destructor.
GCC catches this one with no -W flags at all (-Wfree-nonheap-object is on by default):
"'operator delete' called on pointer ... with nonzero offset 8", and that offset is
the cookie. Hide the new[] behind a factory in another file and the warning goes quiet
while the abort stays.
The fix¶
Match the shape of the delete to the shape of the new — or write neither:
delete[] batch; // ~Noisy #2, #1, #0, then "done"
std::vector<Noisy> batch(3); // better: nothing left to forget
auto batch = std::make_unique<Noisy[]>(3); // owning array, destroys with delete[]
Takeaway: the brackets are part of the operator — new[] pairs with delete[], always.
Try it: g++ -std=c++17 main.cpp -o demo && ./demo