Compartir a través de


Destructores (C++)

Un destructor es una función miembro que se invoca automáticamente cuando el objeto sale del ámbito o se destruye explícitamente mediante una llamada a delete o delete[]. Un destructor tiene el mismo nombre que la clase y va precedido de una tilde (~). Por ejemplo, el destructor de la clase String se declara como: ~String().

Si no define un destructor, el compilador proporciona uno predeterminado y, para algunas clases, esto es suficiente. Debe definir un destructor personalizado cuando la clase mantiene recursos que se deben liberar explícitamente, como identificadores de recursos del sistema o punteros a la memoria que se deben liberar cuando se destruye una instancia de la clase.

Considere la siguiente declaración de una clase String:

// spec1_destructors.cpp
#include <string> // strlen()

class String
{
    public:
        String(const char* ch);  // Declare the constructor
        ~String();               // Declare the destructor
    private:
        char* _text{nullptr};
};

// Define the constructor
String::String(const char* ch)
{
    size_t sizeOfText = strlen(ch) + 1; // +1 to account for trailing NULL

    // Dynamically allocate the correct amount of memory.
    _text = new char[sizeOfText];

    // If the allocation succeeds, copy the initialization string.
    if (_text)
    {
        strcpy_s(_text, sizeOfText, ch);
    }
}

// Define the destructor.
String::~String()
{
    // Deallocate the memory that was previously reserved for the string.
    delete[] _text;
}

int main()
{
    String str("We love C++");
}

En el ejemplo anterior, el destructor String::~String usa el operador delete[] para desasignar el espacio asignado dinámicamente para el almacenamiento de texto.

Declaración de destructores

Los destructores son funciones con el mismo nombre que la clase pero precedidos por una tilde (~).

Varias reglas rigen la declaración de destructores. Destructores:

  • No acepte argumentos.
  • No devuelva un valor (o void).
  • No se puede declarar como const, volatileo static. Aun así, se pueden invocar para la destrucción de objetos declarados como const, volatile o static.
  • Se pueden declarar como virtual. Mediante destructores virtuales, puede destruir objetos sin conocer su tipo: el destructor correcto para el objeto se invoca mediante el mecanismo de función virtual. Los destructores también se pueden declarar como funciones virtuales puras para clases abstractas.

Usar destructores

Se llama a los destructores cuando se produce alguno de los eventos siguientes:

  • Un objeto local (automático) con ámbito de bloque sale de ámbito.
  • Use delete para desasignar un objeto asignado mediante new. El uso de delete[] resultados en un comportamiento indefinido.
  • Use delete[] para desasignar un objeto asignado mediante new[]. El uso de delete resultados en un comportamiento indefinido.
  • La duración de un objeto temporal termina.
  • Un programa termina y hay objetos globales o estáticos.
  • Se llama explícitamente al destructor mediante el nombre completo de la función de destructor.

Los destructores pueden llamar libremente a funciones miembro de clase y acceder a datos de miembros de clase.

Hay dos restricciones en el uso de destructores:

  • No puede tomar su dirección.

  • Las clases derivadas no heredan el destructor de su clase base.

Orden de destrucción

Cuando un objeto sale del ámbito o se elimina, la secuencia de eventos para su completa destrucción es la siguiente:

  1. Se llama al destructor de clase y se ejecuta el cuerpo de la función destructora.

  2. Los destructores de los objetos miembro no estáticos se llaman en el orden inverso al que aparecen en la declaración de clase. La lista opcional de inicialización de miembros usada en la construcción de estos miembros no afecta al orden de construcción o destrucción.

  3. Los destructores para las clases base no virtuales se llaman en el orden inverso al de la declaración.

  4. Los destructores para las clases base virtuales se llaman en el orden inverso al de la declaración.

// order_of_destruction.cpp
#include <cstdio>

struct A1      { virtual ~A1() { printf("A1 dtor\n"); } };
struct A2 : A1 { virtual ~A2() { printf("A2 dtor\n"); } };
struct A3 : A2 { virtual ~A3() { printf("A3 dtor\n"); } };

struct B1      { ~B1() { printf("B1 dtor\n"); } };
struct B2 : B1 { ~B2() { printf("B2 dtor\n"); } };
struct B3 : B2 { ~B3() { printf("B3 dtor\n"); } };

int main() {
   A1 * a = new A3;
   delete a;
   printf("\n");

   B1 * b = new B3;
   delete b;
   printf("\n");

   B3 * b2 = new B3;
   delete b2;
}
A3 dtor
A2 dtor
A1 dtor

B1 dtor

B3 dtor
B2 dtor
B1 dtor

Clases base virtuales

Los destructores de las clases base virtuales se llaman en orden inverso al de su aparición en un gráfico acíclico dirigido (recorrido con prioridad de profundidad, de izquierda a derecha y en postorden). La ilustración siguiente representa un gráfico de herencia.

Inheritance graph that shows virtual base classes.

Cinco clases, etiquetadas A a E, se organizan en un gráfico de herencia. La clase E es la clase base de B, C y D. Las clases C y D son la clase base de A y B.

A continuación se enumeran las definiciones de clase de las clases que se muestran en la ilustración:

class A {};
class B {};
class C : virtual public A, virtual public B {};
class D : virtual public A, virtual public B {};
class E : public C, public D, virtual public B {};

Para determinar el orden de destrucción de las clases base virtuales de un objeto de tipo E, el compilador compila una lista aplicando el algoritmo siguiente:

  1. Recorrer el gráfico izquierdo, desde el punto más profundo del gráfico (en este caso, E).
  2. Realizar recorridos hacia la izquierda hasta que se hayan visitado todos los nodos. Anotar el nombre del nodo actual.
  3. Revisitar el nodo anterior (abajo y a la derecha) para averiguar si el nodo recordado es una clase base virtual.
  4. Si el nodo recordado es una clase base virtual, examinar la lista para ver si ya se ha especificado. Si no es una clase base virtual, omitala.
  5. Si el nodo recordado aún no está en la lista, agréguelo a la parte inferior de la lista.
  6. Recorrer el gráfico hacia arriba y a lo largo de la ruta siguiente a la derecha.
  7. Vaya al paso 2.
  8. Cuando se agote la última ruta ascendente, anotar el nombre del nodo actual.
  9. Vaya al paso 3.
  10. Continuar este proceso hasta que el nodo inferior sea de nuevo el nodo actual.

Por consiguiente, para la clase E, el orden de destrucción es:

  1. La clase base no virtual E.
  2. La clase base no virtual D.
  3. La clase base no virtual C.
  4. La clase base virtual B.
  5. La clase base virtual A.

Este proceso produce una lista ordenada de entradas únicas. Ningún nombre de clase aparece dos veces. Una vez construida la lista, se recorre en orden inverso y se llama al destructor para cada una de las clases de la lista del último al primero.

El orden de construcción o destrucción es principalmente importante cuando los constructores o destructores de una clase dependen del otro componente que se va a crear primero o conservar más tiempo, por ejemplo, si el destructor de A (en la figura mostrada anteriormente) se basaba en B estar todavía presente cuando se ejecuta su código o viceversa.

Tales interdependencias entre clases en un gráfico de herencia son inherentemente peligrosas, porque las últimas clases derivadas pueden cambiar cuál es la ruta más a la izquierda y, en consecuencia, pueden cambiar el orden de construcción y destrucción.

Clases base no virtuales

Los destructores para clases base no virtuales se invocan en orden inverso al que se declaran los nombres de clase base. Considere la siguiente declaración de clase:

class MultInherit : public Base1, public Base2
...

En el ejemplo anterior, el destructor para Base2 se invoca antes que el destructor para Base1.

Llamadas de destructor explícitas

Raras veces se necesita llamar explícitamente al destructor. Sin embargo, puede ser útil realizar la limpieza de los objetos colocados en direcciones absolutas. Estos objetos suelen asignarse mediante un operador new definido por el usuario que toma un argumento de ubicación. El delete operador no puede desasignar esta memoria porque no está asignada desde el almacén gratuito (para obtener más información, vea Operadores nuevos y de eliminación). Sin embargo, una llamada al destructor puede realizar la limpieza adecuada. Para llamar explícitamente al destructor para un objeto, s, de clase String, utilice una de las instrucciones siguientes:

s.String::~String();     // non-virtual call
ps->String::~String();   // non-virtual call

s.~String();       // Virtual call
ps->~String();     // Virtual call

La notación para las llamadas explícitas a destructores, que se muestra anteriormente, puede utilizarse con independencia de que el tipo defina un destructor. Esto permite realizar llamadas explícitas sin saber si un destructor está definido para el tipo. Una llamada explícita a un destructor donde no se ha definido ninguna no tiene ningún efecto.

Programación sólida

Una clase necesita un destructor si adquiere un recurso, y para administrar el recurso de forma segura probablemente tiene que implementar un constructor de copia y una asignación de copia.

Si el usuario no define estas funciones especiales, el compilador las define implícitamente. Los constructores y operadores de asignación generados implícitamente realizan una copia superficial miembro a miembro, que casi con total seguridad será incorrecta si un objeto está administrando un recurso.

En el ejemplo siguiente, el constructor de copia generado implícitamente hará que los punteros str1.text y str2.text hagan referencia a la misma memoria y, cuando devolvamos de copy_strings(), esa memoria se eliminará dos veces, lo que es un comportamiento indefinido:

void copy_strings()
{
   String str1("I have a sense of impending disaster...");
   String str2 = str1; // str1.text and str2.text now refer to the same object
} // delete[] _text; deallocates the same memory twice
  // undefined behavior

La definición explícita de un destructor, un constructor de copia o un operador de asignación de copia impide la definición implícita del constructor de movimiento y el operador de asignación de movimiento. En este caso, si la copia es costosa, el hecho de no ofrecer operaciones de movimiento suele ser una oportunidad de optimización perdida.

Consulte también

Constructores de copia y operadores de asignación de copia
Constructores de movimiento y operadores de asignación de movimiento