30 Destructores

Los destructores son funciones miembro especiales que sirven para eliminar un objeto de una determinada clase. El destructor realizará procesos necesarios cuando un objeto termine su ámbito temporal, por ejemplo liberando la memoria dinámica utilizada por dicho objeto o liberando recursos usados, como ficheros, dispositivos, etc.

Al igual que los constructores, los destructores también tienen algunas características especiales:

  • También tienen el mismo nombre que la clase a la que pertenecen, pero tienen el símbolo ˜ delante.
  • No tienen tipo de retorno, y por lo tanto no retornan ningún valor.
  • No tienen parámetros.
  • No pueden ser heredados.
  • Deben ser públicos, no tendría ningún sentido declarar un destructor como privado, ya que siempre se usan desde el exterior de la clase, ni tampoco como protegido, ya que no puede ser heredado.
  • No pueden ser sobrecargados, lo cual es lógico, puesto que no tienen valor de retorno ni parámetros, no hay posibilidad de sobrecarga.

Cuando se define un destructor para una clase, éste es llamado automáticamente cuando se abandona el ámbito en el que fue definido. Esto es así salvo cuando el objeto fue creado dinámicamente con el operador new, ya que en ese caso, cuando es necesario eliminarlo, hay que hacerlo explícitamente usando el operador delete.

En general, será necesario definir un destructor cuando nuestra clase tenga datos miembro de tipo puntero, aunque esto no es una regla estricta.

Ejemplo:

#include <iostream>
#include <cstring>
using namespace std;
 
class cadena {
  public:
   cadena();        // Constructor por defecto
   cadena(const char *c); // Constructor desde cadena c
   cadena(int n);   // Constructor de cadena de n caracteres
   cadena(const cadena &);   // Constructor copia
   ~cadena();       // Destructor

   void Asignar(const char *dest);
   char *Leer(char *c);
  private:
   char *cad;       // Puntero a char: cadena de caracteres
};
 
cadena::cadena() : cad(NULL) {}
 
cadena::cadena(const char *c) {
   cad = new char[strlen(c)+1];// Reserva memoria para cadena
   strcpy(cad, c);             // Almacena la cadena
}
 
cadena::cadena(int n) {
   cad = new char[n+1]; // Reserva memoria para n caracteres
   cad[0] = 0;          // Cadena vacía   
}
 
cadena::cadena(const cadena &Cad) {
   // Reservamos memoria para la nueva y la almacenamos
   cad = new char[strlen(Cad.cad)+1];
   // Reserva memoria para cadena
   strcpy(cad, Cad.cad);             // Almacena la cadena
} 
 
cadena::~cadena() {
   delete[] cad;        // Libera la memoria reservada a cad
}
 
void cadena::Asignar(const char *dest) {
   // Eliminamos la cadena actual:
   delete[] cad;
   // Reservamos memoria para la nueva y la almacenamos
   cad = new char[strlen(dest)+1]; 
   // Reserva memoria para la cadena
   strcpy(cad, dest);              // Almacena la cadena
}
 
char *cadena::Leer(char *c) {
   strcpy(c, cad);
   return c;
}

int main() {
   cadena Cadena1("Cadena de prueba");
   cadena Cadena2(Cadena1);   // Cadena2 es copia de Cadena1
   cadena *Cadena3;           // Cadena3 es un puntero
   char c[256];
   
   // Modificamos Cadena1:
   Cadena1.Asignar("Otra cadena diferente"); 
   // Creamos Cadena3:
   Cadena3 = new cadena("Cadena de prueba nº 3");
   
   // Ver resultados
   cout << "Cadena 1: " << Cadena1.Leer(c) << endl;
   cout << "Cadena 2: " << Cadena2.Leer(c) << endl;
   cout << "Cadena 3: " << Cadena3->Leer(c) << endl;
   
   delete Cadena3;  // Destruir Cadena3. 
   // Cadena1 y Cadena2 se destruyen automáticamente

   return 0;
}

Ejecutar este código en codepad.

Voy a hacer varias observaciones sobre este programa:

  1. Hemos implementado un constructor copia. Esto es necesario porque una simple asignación entre los datos miembro "cad" no copiaría la cadena de un objeto a otro, sino únicamente los punteros.

    Por ejemplo, si definimos el constructor copia como:

    cadena::cadena(const cadena &Cad) {
       cad = Cad.cad;
    }
    

    en lugar de cómo lo hacemos en el ejemplo, lo que estaríamos copiando sería el valor del puntero cad, con lo cual, ambos punteros estarían apuntando a la misma posición de memoria. Esto es desastroso, y no simplemente porque los cambios en una cadena afectan a las dos, sino porque al abandonar el programa se intenta liberar automáticamente la misma memoria dos veces.

    Lo que realmente pretendemos al asignar cadenas es crear una nueva cadena que sea copia de la cadena antigua. Esto es lo que hacemos con el constructor copia del ejemplo, y es lo que haremos más adelante, y con más elegancia, sobrecargando el operador de asignación.

    La definición del constructor copia que hemos creado en este último ejemplo es la equivalente a la del constructor copia por defecto.

  2. La función Leer, que usamos para obtener el valor de la cadena almacenada, no devuelve un puntero a la cadena, sino una copia de la cadena. Esto está de acuerdo con las recomendaciones sobre la programación orientada a objetos, que aconsejan que los datos almacenados en una clase no sean accesibles directamente desde fuera de ella, sino únicamente a través de las funciones creadas al efecto. Además, el miembro cad es privado, y por lo tanto debe ser inaccesible desde fuera de la clase. Más adelante veremos cómo se puede conseguir mantener la seguridad sin crear más datos miembro.
  3. La Cadena3 debe ser destruida implícitamente usando el operador delete, que a su vez invoca al destructor de la clase. Esto es así porque Cadena3 es un puntero, y la memoria que se usa en el objeto al que apunta no se libera automáticamente al destruirse el puntero Cadena3.

Comentarios de los usuarios (6)

ACANEL
2014-08-06 03:30:16

No tengo claro,como funciona el destructor en este caso, solo con llamar a cadena3 es suficiente para destruir los demas?

hace falta colocar el delete?...

porque destruyo el puntero y falla con los objetos directamente ejemplo

delete Cadena2;

Angel
2015-05-13 14:16:38

Al hacer las permutaciones con repeticion recursivamente se me plantea la duda de como borrar la matriz copia creada con new. ¿Hay alguna solución?

#ifndef VARIACIONES_H

#define VARIACIONES_H

#include <iostream>

#include <list>

using namespace std;

template <class T>

class Variaciones

{

public:

list<T*> VarConRep(T original[], int m, int n);

private:

void ImplementVarConRep(T original[], T temp[], int &m, int &n, list<T*> &lista, int pos);

};

template <class T>

list<T*> Variaciones<T>::VarConRep(T original[], int m, int n)

{

list<T*> lista;

T temp[n];

ImplementVarConRep(original, temp, m, n, lista, 0);

return lista;

}

template <class T>

void Variaciones<T>::ImplementVarConRep(T original[], T temp[], int &m, int &n, list<T*> &lista, int pos)

{

if(pos == n)

{

T *copia = new T[pos];

copy(temp, temp+pos, copia);

lista.push_back(copia);

//delete copia; // Si se usa no funciona

}

else

{

for(int i=0;i<m;i++)

{

temp[pos] = original[i];

ImplementVarConRep(original, temp, m, n, lista, pos+1);

}

}

}

#endif // VARIACIONES_H

Steven R. Davidson
2015-05-13 20:10:35

Hola Ángel,

Tal y como has implementado el diseño, deberás liberar la memoria creada fuera de esta clase; seguramente en el programa en sí: en 'main()'. Por ejemplo,

Variaciones obj;
list<int *> lista = obj.VarConRep( ... );
...
for( list<int *> :: iterator it = lista.begin(); it != lista.end(); it++ )
  delete *it;

Si no quieres hacer esto explícitamente, podrías elegir crear una clase para gestionar implícita y automáticamente una lista. Por ejemplo,

template< typename T >
class lista
{
private:
  list<T *> obj;

public:
  ~lista()
  {
    for( list<int *> :: iterator it = obj.begin(); it != obj.end(); it++ )
      delete *it;
  }
};

Ahora al crear cualquier objeto 'lista' automáticamente liberará su memoria cuando deje de existir.

Otra alternativa es usar punteros inteligentes en lugar de punteros directos. Por ejemplo,

list< unique_ptr<T> > VarConRep(T original[], int m, int n);

Espero que esto te oriente.

Steven

Steven R. Davidson
2015-05-13 20:20:19

Hola Ángel,

Se me olvidó comentar que tienes un error al escribir:

template <class T>
list<T*> Variaciones<T>::VarConRep(T original[], int m, int n)
{
    list<T*> lista;
    T temp[n];
    ...
}

No puedes crear un array estático con una cantidad variable de elementos. Debes crear memoria dinámicamente para crear un array dinámico. O bien creas un array dinámico o bien usas un contenedor estándar, como 'vector'. Por ejemplo,

vector<T> temp( n );

Steven

Fabian veschi
2016-03-17 18:50:58

En el ejemplo utiliza cadena 1, cadena 2 y cadena 3. Entiendo que cadena 3 es puntero de cadena, mi consulta es la siguiente: si puedo hacer y aplicar los mismos metodos para cadena1 y cadena3, que diferencia existe en crear a cadena3 como puntero y no crearla como creamos a cadena1?

Steven R. Davidson
2016-03-19 16:38:17

Hola Fabián,

En este ejemplo, no hay mucha diferencia, excepto para exponer la posibilidad de hacerlo y obviamente el hecho de usar el operador ->. La diferencia principal es que al crear un objeto dinámico, podemos controlar su destrucción y por tanto, podemos ver cuándo se invoca su destructor. Dicho esto, sí será importante el uso de punteros y posiblemente objetos dinámicos cuando lleguemos al tema de polimorfismo, en el capítulo 37. Un objeto polimórfico debe ser tratado mediante un puntero o con una referencia, aunque personalmente prefiero usar punteros. Si el objeto polimórfico es también dinámico, entonces la solución directa es obvia: usamos un puntero.

Espero que esto aclare la duda.

Steven