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 montar un objeto pieza a pieza, primero llamando al constructor base y después a los constructores más derivados en el orden de la herencia (también debe llamar por el camino a los constructores de los objetos miembro). 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 a aquel en que los llama el constructor. Es decir, el destructor 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 actual 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 propia limpieza, y entonces llamar al siguiente destructor, el cual hará su propia limpieza, etc. Cada destructor sabe de qué clase deriva su clase, pero no qué clases derivan de ella.

Hay que tener en cuenta que los constructores y los destructores son los únicos lugares donde tiene que producirse esta jerarquía de llamadas (y por eso el compilador genera automáticamente la jerarquía adecuada). 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 de que se llame a las versiones de la clase base de la misma función en las funciones ordinarias (virtuales o no) consiste en llamar de forma explícita a esa función.

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 actividad es uno de los objetivos principales de la programación orientada a objetos. El problema viene cuando se quiere hacer un delete (eliminar) de un puntero de este tipo a un objeto que ha sido creado en el montículo (heap) con new. Si el puntero apunta a la clase base, el compilador sólo puede saber que debe llamar a la versión del destructor de la clase base durante el delete. ¿Le suena? Es el mismo problema que las funciones virtuales se crearon para resolver en el caso general. Afortunadamente, las funciones virtuales funcionan con los destructores igual que con todas las demás 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 deseado. Olvidar hacer virtual un destructor es un error insidioso porque a menudo no afecta directamente al comportamiento del programa, pero puede introducir de forma silenciosa fugas de memoria. Además, el hecho de que alguna destrucción esté teniendo lugar puede enmascarar aún más el problema.

Aunque el destructor, como el constructor, es una función «excepcional», es posible que el destructor sea virtual porque el objeto ya sabe de qué tipo es (mientras que no lo sabe durante la construcción). Una vez que el objeto ha sido construido, su VPTR está inicializado, por lo que pueden producirse llamadas a funciones virtuales.