Publi

Destructores virtuales en C++

papel destruido

Muchas veces, cuando leemos código de otras personas, hemos visto destructores declarados como virtual en algunas clases. Es una de esas cosas que si hacemos siempre no pasa nada, pero si no hacemos nunca, en ocasiones nos llevamos sorpresas; pero muchos desarrolladores optan por poner sólo en los casos justos.

¿ Para qué valían los métodos virtual ?

Eso está en un post anterior, pero básicamente valen para que las subclases tengan un método propio que se llame de la misma forma que el método declarado como virtual. Aunque cuando se trata de los destructores… en cada clase tendremos un destructor que se llama como mi clase, por lo que parece que esto no tiene demasiado sentido. Aunque está relacionado.

¡Demostración de la destrucción!

Aunque esto difiera de cómo construiríamos esta estructura en realidad, vamos a imaginarnos que nuestras clases formarán un edificio. Tendremos la clase Edificio, la clase Planta, y la clase Habitación, donde Planta será subclase de Edificio y Habitación subclase de Planta.

Para construir la habitación, todo va bien, primero creamos el Edificio, luego la Planta y finalmente la Habitación; pero para destruirlo todo, depende del tipo de objeto que creemos y, en ocasiones, como con las Mascotas, nos interesará declarar los objetos con la clase padre (o superclase) y a la hora de construirlo hacerlo con la clase hija (o subclase) correspondiente.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <iostream>

using namespace std;

class Edificio
{
public:
  Edificio()
  {
    cout << "Se construye el edificio" << endl;
  }

  virtual ~Edificio()
  {
    cout << "Sacamos las bombillas del ascensor" << endl;
    cout << "Se destruye el edificio" << endl;
  }

private:
};

class Planta : public Edificio
{
public:
  Planta()
  {
    cout << "Se construye la planta" << endl;
  }

  ~Planta()
  {
    cout << "Sacamos los muebles de los pasillos" << endl;
    cout << "Se destruye la planta" << endl;
  }

private:
};

class Habitacion : public Planta
{
public:
  Habitacion()
  {
    cout << "Se construye la habitación" << endl;
  }

  ~Habitacion()
  {
    cout << "Saco las cosas de la habitación "<<endl;
    cout << "Se destruye la habitación" << endl;
  }

private:
};


int main()
{

  Edificio *h = new Habitacion();

  delete h;
  return 0;
}

La respuesta esperada es la siguiente:

Se construye el edificio
Se construye la planta
Se construye la habitación
Saco las cosas de la habitación
Se destruye la habitación
Sacamos los muebles de los pasillos
Se destruye la planta
Sacamos las bombillas del ascensor
Se destruye el edificio

Pero si quitamos el virtual, la respuesta será la siguiente:

Se construye el edificio
Se construye la planta
Se construye la habitación
Sacamos las bombillas del ascensor
Se destruye el edificio

Muchas veces, los destructores no tendrán nada, cuando no hay nada que hacer, pero imaginad que el Edificio reserva memoria para almacenar ciertos datos, la Planta, reserva también memoria para algo, y la Habitación por su parte también, en ese caso sería necesario pasar por todos los destructores para que cada uno libere su región de información. Así evitamos memory leaks.

En el caso de poner virtual en todos los destructores, no pasa nada.

Más ejemplos

Otro ejemplo puede ser cuando queremos crear un árbol, y los nodos pueden ser de dos tipos (nodo con ramificaciones, o sin ellas):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
#include <iostream>
#include <string>
#include <vector>

using namespace std;

class Nodo
{
public:
  Nodo(string n)
  {
    nombre=n;
    cout << "Construyo el nodo "<<nombre<<endl;
  }

  virtual ~Nodo()
  {
    cout<< "Destruyo el nodo "<<nombre<<endl;
  }
 
  virtual void print(int espacios)
  {
    cout << "Nodo: "<<nombre<<endl;
  }

public:
  string nombre;
};

class Rama: public Nodo
{
public:
  Rama(string n):Nodo(n)
  {  
    cout << "Construyo la rama "<<n<<endl;
  }

  ~Rama()
  {
    while (!nodos.empty())
      {
    delete nodos.back();
    nodos.pop_back();
      }
    cout <<"Destruyo la rama " << nombre << endl;
  }

  void addNodo(Nodo *n)
  {
    nodos.push_back(n);
  }

  void print()
  {
    cout << "Rama: "<<nombre<<endl;
    for (vector<Nodo*>::iterator it=nodos.begin(); it!=nodos.end(); ++it)
      {
    (*it)->print();
      }
  }

public:
  vector<Nodo*> nodos;
};

int main()
{
  Nodo *arbol= new Rama("main");

  Nodo *nodo= new Nodo("Abuelo sin nietos");
  static_cast<Rama*>(arbol)->addNodo(nodo);
  nodo= new Rama("Abuelo 2");
  static_cast<Rama*>(arbol)->addNodo(nodo);
  Nodo *nodo2= new Nodo("Hijo 1");
  static_cast<Rama*>(nodo)->addNodo(nodo2);
  nodo2= new Rama("Padre 1");
  static_cast<Rama*>(nodo)->addNodo(nodo2);
  Nodo *nodo3= new Nodo("Hijo 3");
  static_cast<Rama*>(nodo2)->addNodo(nodo3);
  nodo3= new Nodo("Hijo 4");
  static_cast<Rama*>(nodo2)->addNodo(nodo3);
  nodo3= new Nodo("Hijo 5");
  static_cast<Rama*>(nodo2)->addNodo(nodo3);
  nodo3= new Nodo("Hijo 6");
  static_cast<Rama*>(nodo2)->addNodo(nodo3);
  nodo2= new Nodo("Hijo 2");
  static_cast<Rama*>(nodo)->addNodo(nodo2);

  nodo= new Rama("Abuelo 3");
  static_cast<Rama*>(arbol)->addNodo(nodo);
  nodo2= new Nodo("Hijo 7");
  static_cast<Rama*>(nodo)->addNodo(nodo2);
  nodo2= new Rama("Padre 2");
  static_cast<Rama*>(nodo)->addNodo(nodo2);
  nodo3= new Nodo("Hijo 8");
  static_cast<Rama*>(nodo2)->addNodo(nodo3);

  cout << endl;
  arbol->print();
  cout << endl << endl;

  delete arbol;
}

En este caso, si quitamos el virtual al destructor, podemos ver que sólo se destruye el nodo main, quedándose todo lo demás en memoria.

Otros ejemplos de este uso pueden ser:

  • Sistemas que mantentan archivos, sockets, pipes abiertos desde la superclase (todo tiene que ser cerrado)
  • Instancias cuyas superclases tengan cadenas char* , tenemos que liberar todas las cadenas
  • Estoy abierto a sugerencias en los comentarios

Foto:  <a href=”http://www.flickr.com/photos/randomurl/445352972/”>WagsomeDog</a> (Flickr) Compartido con CC-by a 28/Nov/2011

También podría interesarte...

There are 6 comments left Ir a comentario

  1. Pingback: Bitacoras.com /

  2. Pingback: Poesía binaria » ¿ Y tú qué eres ? Identificando clases heredadas en C++ /

  3. Jeremias /
    Usando Mozilla Firefox Mozilla Firefox 14.0.1 en Windows Windows 7

    buen post!! muy bien explicado!
    Te comento lo que me pasó:
    Tengo una lista en C++ con visual studio 2010, siguiendo tu ejemplo sería algo asi:
    list listaEdificios;
    y luego en el código ejecuto varios listaEdificios.push_back(new Planta()), sabiendo que la clase Planta hereda de Edificio.
    He definido virtuales tanto el destructor de Edificio como el de Planta.
    Pero luego de poner un break point en el destructor de Planta, noté que éste no se llama al hacer un listaEdificio.pop_back().
    Leí la documentación del método pop_back() cpluplus.com y dice que el pop_back() llama al destructor del objeto que se quita de la lista.
    Sinceramente no se cual es el problema. No se llama al destructor Planta a menos haga lo siguiente p = listaEdificios.back(); listaEdificios.pop_back(); delete(p);
    Siguiendo la documentación, eso no debería ser necesario.
    Si puedes explicarme que pasa con el pop_back() te lo agradezco!!

    1. Gaspar Fernández / Post Author
      Usando Mozilla Firefox Mozilla Firefox 13.0.1 en Ubuntu Linux Ubuntu Linux

      Este caso con los destructores es especial. Si lo que incluyes en el vector son objetos tipo Planta, sí que los destruyes cuando haces pop_back, pero lo que estás introduciendo son punteros a objeto (Planta*) y éstos no se destruyen. Se sacan de la lista, tal vez luego necesites acudir a ellos.
      Yo creo que no se implementa lo que tú quieres, aunque siempre puedes hacer una clase derivada de list que cuando haces pop_back elimine el elemento y no tengas que hacerlo tú.

      Suerte! Ya me contarás cómo solucionaste el problema.

  4. Eduardo Zepeda /
    Usando Google Chrome Google Chrome 53.0.2785.143 en Linux Linux

    ¡Qué bien explicado está! Intenté leer varios resultados de google pero parece que todos se drogan antes de explicarlo. Tu explicación es tan amena y sencilla de entender.

    Muchas gracias por compartirlo

    1. Gaspar Fernández / Post Author
      Usando Mozilla Firefox Mozilla Firefox 49.0 en Ubuntu Linux Ubuntu Linux

      ¡¡ Muchas gracias por tu comentario Eduardo !! Realmente me ayuda a seguir compartiendo este tipo de contenidos 🙂

Leave a Reply