Disponible la nueva versión "donationware" 7.3 de OrganiZATOR
Descubre un nuevo concepto en el manejo de la información.
La mejor ayuda para sobrevivir en la moderna jungla de datos la tienes aquí.

Curso C++

[Home]  [Inicio]  [Índice]


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.

Ejemplo

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.

  Inicio.


[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.