Partager via


Why delete[] matters

Even as a senior software engineer, it seems like I still learn every day.  This is a small story about what I learned today.

Office is an enormous collection of source code.  I have just about every data structure imaginable right at my fingertips.  Smart pointers, smart array handling, hashes, sets, and every other collection type.  Needless to say, I rarely every write a line of code that looks something like:

 MyClass* pClasses = new MyClass[ 10 ];

It will always be wrapped in something like:

 MyArrayHolder< MyClass > classArray( 10 );

STL provides similar functionality:

 std::vector< MyClass > classVector( 10 );

These classes call new[] and delete[] for us.  A side effect of delete[] is that it calls the destructor (if destructible) on every object in the allocation.  Something like this:

 MyClass* pClasses = new MyClass[ 10 ];
... Properly setup all 10 allocations ...
delete[] pClasses;

will lead to ~MyClass() getting called 10 times, one for each MyClass chunk in the array allocation.  How does it know?  Calling new[] puts a nice "count" variable at the beginning of the allocation (again, if it is destructible):

 [count | MyClass1 | MyClass2 | MyClass3 | ... | MyClass10]

When new[] returns, it gives you a pointer to the start of MyClass1, not count.  On my 64-bit OS, count of 8 bytes which seems pretty reasonable.  Furthermore, delete[] is pretty smart.  You pass it the pointer to the beginning of MyClass1 and it will say, "Oh, this is destructible. That means I have a piece of memory allocated for the count at the beginning.  I need to delete that too!" And so, it adjusts the pointer appropriately and deletes the entire block.

As long as you don't do anything "strange", your collection data structure will handle calling new[] and delete[] on your objects.  Let's look at something strange, though.  There are cases when we don't actually want the destructor called on some of the MyClass allocations.  Let's say that we didn't fill them all in and they're left with garbage memory.  Calling a destructor on garbage memory is undefined behavior - aka crash incoming.  To handle this, we do everything in BYTEs.  This is completely valid:

BYTE* pBytes = new BYTE[sizeof(MyClass)];MyClass* pMyClass = static_cast<MyClass>(pBytes);

By extension, you could imagine using this to allocate a large number of bytes to form a MyClass array.  The important thing to note here is that BYTE is not a destructible object.  There is no ~BYTE to call.  The allocator sees this so the memory allocation looks like this:

[BYTE1 | BYTE2 | BYTE3 | ... | BYTE_N]

There is no need to store the count because there's no destructors to call.  This is where the gotchas come in.  Imagine I then did something like this:

BYTE* pBytes = new BYTE[sizeof(MyClass)]; MyClass* pMyClass = static_cast<MyClass>(pBytes);

... tons of code, losing track of memory allocations ...delete[] pMyClass;

What is the allocator going to do there?  I'm calling delete on MyClass which is destructible.  The pointer will get adjusted and, uh oh.  The pointer should not have been adjusted.  In my scenario, the allocator started yelling at me which is a bit scary.  It wasn't until a vastly more experience developer than I came and helped me debug this that we found my error.  He kindly described how new[] and delete[] worked with destructible objects and the world was saved.