15.10.3. Destructores y destructores virtuales

No se puede usar la palabra reservada virtual con los constructores, pero los destructores pueden, y a menudo deben, ser virtuales.

El constructor tiene el trabajo especial de iniciar un objeto poco a poco, primero llamando al constructor base y después a los constructores derivados en el orden de la herencia. De manera similar, el destructor tiene otro trabajo especial: desmontar un objeto, el cual puede pertenecer a una jerarquía de clases. Para hacerlo, el compilador genera código que llama a todos los destructores, pero en el orden inverso al que son llamados en los constructores. Es decir, el constructor empieza en la clase más derivada y termina en la clase base. ésta es la opción deseable y segura debido a que el destructor siempre sabe que los miembros de la clase base están vivos y activos. Si se necesita llamar a una función miembro de la clase base dentro del destructor, será seguro hacerlo. De esta forma, el destructor puede realizar su propio limpiado, y entonces llamar al siguiente destructor, el cual hará su propio limpiado, etc. Cada destructor sabe de que clase deriva, pero no cuales derivan de él.

Hay que tener en cuenta que los constructores y los destructores son los únicos lugares donde tiene que funcionar ésta jerarquía de llamadas (que es automáticamente generada por el compilador). En el resto de las funciones, sólo esa función, sea o no virtual, será llamada (y no las versiones de la clase base). La única forma para acceder a las versiones de la clase base de una función consiste en llamar de forma explicita a esa funciones.

Normalmente, la acción del destructor es adecuada. Pero ¿qué ocurre si se quiere manipular un objeto a través de un puntero a su clase base (es decir, manipular al objeto a través de su interfaz genérica)? Este tipo de actividades es uno de los objetivos de la programación orientada a objetos. El problema viene cuando se quiere hacer un delete (eliminar) de un puntero a un objeto que ha sido creado en el montón (>heap) con new. Si el puntero apunta a la clase base, el compilador sólo puede conocer la versión del destructor que se encuentre en la clase base durante el delete. ¿Suena familiar? Al fin y al cabo, es el mismo problema por las que fueron creadas las funciones virtuales en el caso general. Afortunadamente, las funciones virtuales funcionan con los destructores como lo hacen para las otras funciones excepto los constructores.

//: C15:VirtualDestructors.cpp
// Behavior of virtual vs. non-virtual destructor
#include <iostream>
using namespace std;

class Base1 {
public:
  ~Base1() { cout << "~Base1()\n"; }
};

class Derived1 : public Base1 {
public:
  ~Derived1() { cout << "~Derived1()\n"; }
};

class Base2 {
public:
  virtual ~Base2() { cout << "~Base2()\n"; }
};

class Derived2 : public Base2 {
public:
  ~Derived2() { cout << "~Derived2()\n"; }
};

int main() {
  Base1* bp = new Derived1; // Upcast
  delete bp;
  Base2* b2p = new Derived2; // Upcast
  delete b2p;
} ///:~

Listado 15.12. C15/VirtualDestructors.cpp


Cuando se ejecuta el programa, se ve que delete bp sólo llama al destructor de la clase base, mientras que delete b2p llama al destructor de la clase derivada seguido por el destructor de la clase base, que es el comportamiento que deseamos. Olvidar hacer virtual a un destructor es un error peligroso porque a menudo no afecta directamente al comportamiento del programa, pero puede introducir de forma oculta agujeros de memoria. Además, el hecho de que alguna destrucción está teniendo lugar puede enmascarar el problema.

Es posible que el destructor sea virtual porque el objeto sabe de que tipo es (lo que no ocurre durante la construcción del objeto). Una vez que el objeto ha sido construido, su VPTR es inicializado y se pueden usar las funciones virtuales.