I would like to know how the delete operator figures out the memory location that needs to be freed when it is given a base class pointer that is different from the true memory location of the object.
I want to duplicate this behavior in my own custom allocator/deallocator.
Consider the following hierarchy:
struct A { unsigned a; virtual ~A() { } }; struct B { unsigned b; virtual ~B() { } }; struct C : public A, public B { unsigned c; };
I want to allocate an object of type C and delete it through a pointer of type B. As far as I can tell this is a valid use of operator delete, and it works under Linux/GCC:
C* c = new C; B* b = c; delete b;
The interesting thing is that the pointers 'b' and 'c' actually point to different addresses because of how the object is laid out in memory, and the delete operator "knows" how to find and free the correct memory location.
I know that, in general, it is not possible to find the size of a polymorphic object given a base class pointer: Find out the size of a polymorphic object. I suspect that it is not generally possible to find the true memory location of the object either.
Notes:
operator delete, operator delete[] Deallocates storage previously allocated by a matching operator new. These deallocation functions are called by delete-expressions and by new-expressions to deallocate memory after destructing (or failing to construct) objects with dynamic storage duration.
The delete operator is used to deallocate the memory. User has privilege to deallocate the created pointer variable by this delete operator.
When delete is used to deallocate memory for a C++ class object, the object's destructor is called before the object's memory is deallocated (if the object has a destructor). If the operand to the delete operator is a modifiable l-value, its value is undefined after the object is deleted.
Delete is an operator that is used to destroy array and non-array(pointer) objects which are created by new expression. New operator is used for dynamic memory allocation which puts variables on heap memory. Which means Delete operator deallocates memory from heap.
This is clearly implementation specific. In practice there are a relatively small number of sensible ways to implement things. Conceptually there are a few problems here:
You need to be able to get a pointer to the most derived object, that is the object that (conceptually) encompasses all of the other types.
In standard C++ you can do this with a dynamic_cast
:
void *derrived = dynamic_cast<void*>(some_ptr);
Which gets the C*
back from just a B*
, for example:
#include <iostream> struct A { unsigned a; virtual ~A() { } }; struct B { unsigned b; virtual ~B() { } }; struct C : public A, public B { unsigned c; }; int main() { C* c = new C; std::cout << static_cast<void*>(c) << "\n"; B* b = c; std::cout << static_cast<void*>(b) << "\n"; std::cout << dynamic_cast<void*>(b) << "\n"; delete b; }
Gave the following on my system
0x912c008 0x912c010 0x912c008
Once that's done it then becomes a standard memory allocation tracking problem. Usually that's done in one of two ways, either a) record the size of the allocation just before the allocated memory, finding the size is just a pointer subtraction then or b) record allocations and free memory in a data structure of some sort. For more details see this question, which has a good reference.
With glibc you can query the size of a given allocation fairly sensibly:
#include <iostream> #include <stdlib.h> #include <malloc.h> int main() { char *test = (char*)malloc(50); std::cout << malloc_usable_size(test) << "\n"; }
That information is available to free/delete similarly and used to figure out what to do with the returned chunk of memory.
The exact details of the implementation of malloc_useable_size
are given in the libc source code, in malloc/malloc.c:
(The following includes lightly edited explanations by Colin Plumb.)
Chunks of memory are maintained using a `boundary tag' method as described in e.g., Knuth or Standish. (See the paper by Paul Wilson ftp://ftp.cs.utexas.edu/pub/garbage/allocsrv.ps for a survey of such techniques.) Sizes of free chunks are stored both in the front of each chunk and at the end. This makes consolidating fragmented chunks into bigger chunks very fast. The size fields also hold bits representing whether chunks are free or in use.
An allocated chunk looks like this:
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Size of previous chunk, if allocated | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Size of chunk, in bytes |M|P| mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | User data starts here... . . . . (malloc_usable_size() bytes) . . | nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Size of chunk | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Destroying a base class pointer requires that you've implemented a virtual destructor. If you didn't do that, all bets are off.
The first destructor called will be the one for the most derived object as determined by the virtual mechanism (vtable). This destructor knows the size of the object! It can squirrel that information away someplace, or pass it down the chain of destructors.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With