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.