4.11.2d2 Destructores
§1 Sinopsis
Los destructores son un tipo especial de función miembro, estrechamente relacionados con los constructores. Son también funciones que no devuelven nada (ni siquiera void). Tampoco aceptan ningún parámetro, ya que la destrucción de un objeto no acepta ningún tipo de opción o especificación particular y es idéntica para todos los objetos de la clase. Los destructores no pueden ser heredados, aunque una clase derivada puede llamar a los destructores de su superclase si no han sido declarados privados (son públicos o protegidos). Lo mismo que ocurre con los constructores, tampoco puede obtenerse su dirección, por lo que no es posible establecer punteros a este tipo de funciones.
La misión más común de los destructores es liberar la memoria asignada por los constructores, aunque también puede consistir en desasignar y/o liberar determinados recursos asignados por estos. Por ejemplo, cerrar un fichero; una línea de comunicación, o desbloquear un recurso compartido que hubiera sido bloqueado previamente por el constructor.
Se ha señalado que, si el programador no define uno
explícitamente, el compilador C++ proporciona un destructor de oficio, que es declarado público y puede ser invocado
sin argumentos. Por lo general en la mayoría de los casos este destructor de oficio es suficiente, por lo que el programador no
necesita definir uno por sí mismo, a no ser que la clase incluya la inicialización de objetos persistentes
(
1.3.2). Por ejemplo,
matrices que necesiten del operador new en el constructor para su inicialización, en cuyo caso es
responsabilidad del programador definir un destructor adecuado (ver ejemplo
).
Los destructores son invocados automáticamente (de forma implícita) por el programa en multitud de ocasiones; de hecho es muy raro que sea necesario invocarlos explícitamente. Su misión es limpiar los miembros del objeto antes que el propio objeto se auto-destruya.
§2 Declaración
Los destructores se distinguen porque tienen el mismo nombre que la clase a que pertenecen precedido por la tilde ~ para simbolizar su estrecha relación con los constructores que utilizan el mismo nombre (son el "complemento" de aquellos). Ejemplo:
class X {
public:
~X(); // destructor de la clase X
};
...
X::~X() { // definición (off-line) del destructor
...
}
Ejemplo:
La clase Punto definida en el epígrafe anterior (
4.11.2d1) sería un buen exponente del caso
en que es necesario definir un destructor explícito que se encargue de las
correcta destrucción de los miembros. En efecto, manteniendo aquella definición, una sentencia del tipo:
{
...
Punto p1(2,3);
...
}
provoca la creación de un objeto en memoria dinámica. El miembro coord es un puntero-a-int que señala un área en el montón capaz para albergar dos enteros. Cuando la ejecución sale del ámbito del bloque en que se ha creado el objeto, es invocado el destructor de oficio y el objeto es destruido, incluyendo su único componente, el puntero coord; sin embargo el área señalada por este permanece reservada en el montón, y por tanto irremediablemente perdida.
La forma sensata de utilizar tales objetos sería modificando la definición de la clase para añadirle un destructor adecuado. La versión correcta tendría el siguiente aspecto:
class Punto {
public: int* coord;
Punto(int x = 0, int y = 0) { // construtor
coord = new int[2];
coord[0] = x; coord[1] = y;
}
~Punto() { // destructor
delete [] coord; // L.8:
};
En este caso, la sentencia de la línea 8 provoca que al ser invocado el destructor del objeto, se desasigne el área del montón señalada por el puntero (recuerde que, al igual que el resto de las funciones-miembro, los destructores también tienen un argumento oculto this, por lo que la función sabe sobre que objeto tiene que operar en cada caso).
§3 Invocación
Como hemos señalado, los destructores son invocados automáticamente por el compilador, y es muy raro que sea necesario invocarlos explícitamente.
§3.1 Invocación explícita de destructores
En caso necesario los destructores pueden ser invocados explícitamente de dos formas: indirectamente, mediante una llamada
a delete ( 4.9.21) o
directamente utilizando el nombre cualificado completo.
class X {...}; // X es una clase
...
{
X obj1; // L.4: objeto automático
X* ptr = new(X) // L.5: objeto persistente
X* pt2 = &obj1; // Ok: pt2 es puntero a obj1 de la clase X
...
pt2–>X::~X(); // L.8: Ok: llamada legal del destructor
// pt2->~X(); L.9: Ok: variación sintáctica de la anterior
// obj1.~X(); L.10: Ok otra posibilidad análoga
X::~X(); // L.11: Error: llamada ilegal al destructor [1]
delete ptr; // L.12: Ok. invocación implícita al destructor
}
Comentario
L.4 crea el objeto obj1 en la pila (
1.3.2), se trata de un objeto automático, y en cuanto el bloque salga de ámbito, se producirá una
llamada a su destructor que provocará su eliminación. Sin embargo, el objeto anónimo señalado por ptr es
creado en el montón.
Observe que mientras ptr es también un objeto automático, que será eliminado al salir del bloque, el objeto al que señala es persistente y su destructor no será invocado al salir de ámbito el bloque. Como se ve en el punto siguiente, en estos casos es imprescindible una invocación explícita al destructor mediante el operador delete (cosa que hacemos en L.12), en caso contrario, el espacio ocupado por el objeto se habrá perdido.
Es muy importante advertir que la invocación explícita al destructor de obj1 en L.8 (o su versiones equivalentes L.9 y L.10) son correctas, aunque muy peligrosas [2]. En efecto, en L.8 se produce la destrucción del objeto, pero en el estado actual de los compiladores C++, que no son suficientemente "inteligentes" en este sentido [3], al salir el bloque de ámbito vuelven a invocar los destructores de los objetos automáticos creados en su interior, por lo que se producirá un error de ejecución irrecuperable (volcado de memoria si corremos bajo Linux).
§3.1.1 Los objetos que han sido creados con el operador new
(4.9.20) deben destruirse
obligatoriamente con una llamada explícita al destructor. Ejemplo:
#include <stdlib.h>
class X { // clase
public:
...
~X(){}; // destructor de la clase
};
void* operator new(size_t size, void *ptr) {
return ptr;
}
char buffer[sizeof(X)]; // matriz de caracteres, del tamaño de X
void main() {
// ========================
X* ptr1 = new X;
// puntero a objeto X creado con new
X* ptr2;
// puntero a objeto X
ptr2 = new(&buffer) X; // se inicia con la dirección de buffer
...
delete ptr1;
// delete destruye el puntero
ptr2–>X::~X();
// llamada directa, desasignar el espacio de buffer
}
§3.2 Invocación implícita de destructores
Además de las posibles invocaciones explícitas, cuando una variable sale del ámbito para el que ha sido declarada, su
destructor es invocado de forma implícita. Los destructores de las variables locales son invocados cuando el bloque en el que
han sido declarados deja de estar activo. Por su parte, los destructores de las variables globales son invocados como parte del
procedimiento de salida (
1.5) después de la función main
(
4.4.4).
§3.2.1 En el siguiente ejemplo se muestra claramente como se invoca el destructor cuando un objeto sale de
ámbito al terminar el bloque en que ha sido declarado.
#include <iostream>
using namespace std;
class A {
public:
int x;
A(int i = 1) { x = i; } // constructor por defecto
~A() {
// destructor
cout << "El destructor ha sido invocado" << endl;
}
};
int main() { // =========================
{
A a; // se instancia un objeto
cout << "Valor de a.x: " << a.x << endl;
}
// punto de invocación del destructor de a
return 0;
}
Salida:
Valor de a.x: 1
El destructor ha sido invocado
§3.2.2 En el ejemplo que sigue se ha modificado ligeramente el código anterior para demostrar como
el destructor es invocado incluso cuando la salida de ámbito se realiza mediante una sentencia de salto (omitimos la salida,
que es idéntica a la anterior):
#include <iostream>
using namespace std;
class A {
public:
int x;
A(int i = 1) { x = i; } // constructor por defecto
~A() {
// destructor
cout << "El destructor ha sido invocado" << endl;
}
};
int main() { // =========================
{
A a; // se instancia un objeto
cout << "Valor de a.x: " << a.x << endl;
goto FIN;
}
FIN:
return 0;
}
§3.2.3 Una tercera versión, algo más sofisticada, nos muestra como la invocación del destructor se realiza
incluso cuando la salida de ámbito se realiza mediante el mecanismo de salto del manejador de excepciones, y cómo la invocación
se realiza para cualquier objeto, incluso temporal, que deba ser destruido
( Ejemplo).
Recordar que que cuando los punteros a objetos
salen de ámbito, no se invoca implícitamente ningún destructor para el objeto, por lo que se hace necesaria una invocación
explícita al operador delete (
4.9.21) para destruir el objeto (§3.1.1
).
§4 Propiedades de los destructores
Cuando se tiene un destructor explícito, las sentencias del cuerpo se ejecutan antes que la destrucción de los miembros. A
su vez, la invocación de los destructores de los miembros se realiza exactamente en orden inverso en que se realizó la
invocación de los constructores correspondientes (
4.11.2d1). La destrucción de los miembros estáticos se ejecuta después
que la destrucción de los miembros no estáticos.
Los destructores no pueden ser declarados const
( 3.2.1c) o
volatile (
3.2.1d), aunque
pueden ser invocados desde estos objetos. Tampoco pueden ser declarados static
(
4.11.7), lo que supondría poder
invocarlos sin la existencia de un objeto que destruir.
§5 Destructores virtuales
Como cualquier otra función miembro, los destructores pueden ser declarados virtual
( 4.11.8a).
El destructor de una clase derivada de otra cuyo destructor
es virtual, también es virtual (
4.11.8a).
La existencia de un destructor virtual permite que un objeto de una subclase pueda ser correctamente destruido por un puntero a su clase-base [4].
Ejemplo:
class B { // Superclase (polimórfica)
...
virtual ~B(); // Destructor virtual
};
class D : public B { // Subclase (deriva de B)
...
~D(); // destructor también virtual
};
void func() {
B* ptr = new D; // puntero a superclase asignado a objeto de subclase
...
delete ptr; // Ok: delete es necesario siempre que se usa new
}
Comentario
Aquí el mecanismo de llamada de las funciones virtuales permite que el operador delete invoque al destructor correcto, es decir, al destructor ~D de la subclase, aunque se invoque mediante el puntero ptr a la superclase B*. Si el destructor no hubiese sido virtual no se hubiese invocado el destructor derivado ~D, sino el de la superclase ~B, dando lugar a que los miembros privativos de la subclase no hubiesen sido desasignados. Tendríamos aquí un caso típico de "misteriosas" pérdidas de memoria, tan frecuentes en los programas C++ como difíciles de depurar.
A pesar de todo, el mecanismo de funciones virtuales puede ser anulado utilizando un operador de resolución adecuado
( 4.11.8a). En el ejemplo
anterior podría haberse puesto:
void foo (B&, B&); // prototipo
-observe que son referencias a la superclase!!-
void func() {
D d1, d2;
foo(d1, d2); // usamos referencias a
la subclase!!
}
void foo(B& b1, B& b2) {
b1.~B(); // invocación
-virtual- a ~D()
b2.B::~B(); // invocacion estática a B::~B()
}
§5.1 En el siguiente ejemplo se refiere a un caso concreto de la hipótesis anterior. Muestra como
virtual afecta el orden de llamada a los destructores. Sin un destructor virtual en la clase base, no se produciría
una invocación al destructor de la clase derivada.
#include <iostream>
class color { // clase base
public:
virtual ~color() { // destructor virtual
std::cout << "Destructor de color" <<
std::endl;
}
};
class rojo : public color { // clase derivada (hija)
public:
~rojo() {
// también destructor virtual
std::cout << "Destructor de rojo" <<
std::endl;
}
};
class rojobrillante : public rojo { // clase derivada (nieta)
public:
~rojobrillante() { // también destructor virtual
std::cout << "Destructor de rojobrillante"
<< std::endl;
}
};
int main() { // ===========
color* palette[3]; // matriz de tres punteros a tipo color
palette[0] = new rojo; // punteros a tres objetos en algún sitio
palette[1] = new rojobrillante;
palette[2] = new color;
// llamada a los destructores de rojo y color (padre).
delete palette[0];
// llamada a destructores de rojobrillante, rojo (padre) y color (abuelo)
delete palette[1];
// llamada al destructor de la clase raíz
delete palette[2];
return 0;
}
Salida:
Destructor de rojo
Destructor de color
Destructor de rojobrillante
Destructor de rojo
Destructor de color
Destructor de color
Comentario
Si los destructores no se hubiesen declarado virtuales, las sentencias delete palette[0], delete palette[1], y delete palette [2] solamente hubiesen invocado el destructor de la clase raíz color. Lo que no hubiese destruido correctamente los dos primeros elementos, que son del tipo rojo (hija) y rojobrillante (nieta).
§6 Los destructores y las funciones virtuales
El mecanismo de llamada de funciones virtuales está deshabilitado en los destructores por las razones ya expuestas al
tratar de los constructores (
4.11.2d1)
§7 Los destructores y exit
Cuando se invoca exit (
&1.5.1) desde un programa, no son invocados los destructores de ninguna variable local del
ámbito actual. Las globales son destruidas en su orden normal.
§8 Los destructores y abort
Cuando se invoca la función abort (
&1.5.1) en cualquier punto de un programa no se invoca ningún
destructor, ni aún
para las variables de ámbito global.
[1] Falta indicación del objeto sobre el que actuará el destructor.
[2] Recuerde que desde el punto de vista del lenguaje C++, dejar de destruir un objeto persistente no es un error, a lo sumo una pérdida de espacio. Sin embargo, intentar destruir un objeto dos veces sí lo es. La mayoría de las veces se producirá un error fatal (core dump) en tiempo de ejecución.
[3] El propio Stroustrup (
TC++PL &10.4.5) nos informa que "desgraciadamente, las implementaciones (de
los compiladores C++) no pueden detectar eficazmente este tipo de errores".
[4] Ellis y Stroustrup (
ACRM §12.4)
aconsejan que como regla general, se declare un destructor virtual en cualquier clase que tenga una función virtual.