Virtual destructor and memory deallocation

2019-07-15 20:41发布

问题:

I'm not quite sure I understand virtual destructors and the concept of allocating space on the heap right. Let's look at the following example:

class Base
{
public:
    int a;
};

class Derived : public Base
{
public:
    int b;
};

I imagine that if I do something like this

Base *o = new Derived;

that 8 Bytes (or whatever two integers need on the system) are allocated on the heap, which looks then something like this: ... | a | b | ...

Now if I do this:

delete o;

How does 'delete' know, which type o is in reality in order to remove everything from the heap? I'd imagine that it has to assume that it is of type Base and therefore only deletes a from the heap (since it can't be sure whether b belongs to the object o): ... | b | ...

b would then remain on the heap and be unaccessible.

Does the following:

Base *o = new Derived;
delete o;

truly provoke memory leaks and do I need a virtual destructor here? Or does delete know that o is actually of the Derived class, not of the Base class? And if so, how does that work?

Thanks guys. :)

回答1:

You're making a lot of assumptions about the implementation, which may or may not hold. In a delete expression, the dynamic type must be the same as the static type, unless the static type has a virtual destructor. Otherwise, it is undefined behavior. Period. That's really all you have to know—I've used with implementations where it would crash otherwise, at least in certain cases; and I've used implementations where doing this would corrupt the free space arena, so that the code would crash sometime later, in a totally unrelated piece of code. (For the record, VC++ and g++ both fall in the second case, at least when compiled with the usual options for released code.)



回答2:

Firstly, the classes you declared in your example have trivial internal structure. From purely practical point of view, in order to destroy object of such classes properly the run-time code does not need to know the actual type of the object being deleted. All it needs to know is the proper size of the memory block to be deallocated. This is actually something that is already achieved by C-style library functions like malloc and free. As you probably know, free implicitly "knows" how much memory to deallocate. Your example above does not involve anything in addition to of that. In other words, your example above is not elaborate enough to truly illustrate anything C++-specific.

However, formally the behavior of your examples is undefined, since virtual destructor is formally required by C++ language for polymorphic deletion regardless of how trivial the internal structure of the class is. So, your "how delete knows..." question simply does not apply. Your code is broken. It does not work.

Secondly, the actual tangible C++-specific effects begin to appear when you begin to require non-trivial destruction for your classes: either by defining an explicit body for the destructor or by adding non-trivial member subobjects to your class. For example, if you add a std::vector member to your derived class, the destructor of the derived class will become responsible for (implicit) destruction of that subobject. And in order for that to work, you will have to declare you destructors virtual. A proper virtual destructor is called through the same mechanism as any other virtual function is called. That's basically the answer to your question: the run-time code does not care about the actual type of the object simply because the ordinary virtual dispatch mechanism will ensure that the proper destructor is called (just like it works with any other virtual function).

Thirdly, another significant effect of virtual destruction appears when you define dedicated operator delete functions for your classes. The language specification requires that the proper operator delete function is selected as if it is looked up from inside the destructor of the class being deleted. And many implementations implement this requirement literally: they actually implicitly call operator delete from inside the class destructor. In order for that mechanism to work properly, the destructor has to be virtual.

Fourthly, a part of your question seems to suggest that you believe that failing to define a virtual destructor will lead to "memory leaks". This a popular, but completely incorrect and totally useless urban legend, perpetuated by low-quality sources. Performing polymorphic deletion on a class that has no virtual destructor leads to undefined behavior and to completely unpredictable devastating consequences, not to some "memory leaks". "Memory leaks" are not the issue in such cases.



回答3:

There is no problem in the size of the object being deleted - it is known. The problem which virtual destructors solve can be demonstrated as follows:

class Base
{
public:
    Base() { x = new char[1]; }
    /*virtual*/ ~Base() { delete [] x; }

private:
    char* x;
};

class Derived : public Base
{
public:
    Derived() { y = new char[1]; }
    ~Derived() { delete [] y;}
private:
    char* y;
};

Then having:

Derived* d = new Derived();
Base* b = new Derived();

delete d;   // OK
delete b;   // will only call Base::~Base, and not Derived::~Derived

The second delete will not finalize the object properly. If the "virtual" keyword was uncommented, then the second delete statement will behave as expected, and it will call Derived::~Derived along with Base::~Base.

EDIT: as pointed out in the comments, to be strict, the second delete yields an undefined behavior.