Menu

Destructores en C++ explicados: ~NombreClase, RAII y limpieza

Un destructor se ejecuta automáticamente cuando un objeto se destruye. Aprende la sintaxis ~NombreClase(), cuándo se dispara, por qué libera recursos y la Regla de Tres/Cinco.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

Qué es un destructor

En la página anterior viste los constructores: funciones especiales que se ejecutan cuando un objeto nace para establecer su estado inicial. Un destructor es la imagen reflejada: una función especial que se ejecuta cuando un objeto muere, para limpiar después de él.

Se declara con el nombre de la clase precedido por una tilde (~). No recibe parámetros, no devuelve nada, y una clase puede tener exactamente uno. Casi nunca lo llamas a mano: C++ lo llama por ti en el momento adecuado.

Observa que el mensaje del destructor se imprime después de que main termina el cuerpo de su función, pero antes de que el programa salga. Cuando log sale de su ámbito en la llave de cierre, C++ ejecuta ~Logger() por ti.

Cuándo se ejecutan los destructores

El momento exacto depende de dónde vive el objeto:

  • Los objetos en la pila (locales) se destruyen cuando salen de su ámbito, en la llave de cierre } del bloque.
  • Los objetos en el montón (creados con new) se destruyen cuando llamas a delete. Si olvidas el delete, el destructor nunca se ejecuta y provocas una fuga.

Este ejemplo hace visible la diferencia:

Los objetos se destruyen en orden inverso al de su construcción. a se construyó primero, así que muere el último. Este orden LIFO (último en entrar, primero en salir) importa cuando los objetos dependen unos de otros.

Por qué importan los destructores: RAII

El verdadero poder de los destructores es que hacen que la limpieza sea automática y segura ante excepciones. En lugar de acordarte de liberar un recurso en cada ruta de código, pones la liberación en un destructor y dejas que el lenguaje garantice que se ejecuta. Este patrón se llama RAII (Resource Acquisition Is Initialization, la adquisición de recursos es inicialización) y es la columna vertebral del C++ moderno.

Aquí una clase posee un búfer en el montón: lo reserva en el constructor y lo libera en el destructor, de modo que quien la usa nunca toca new/delete directamente.

La idea clave: incluso si se lanzara una excepción después de crear squares, la pila se desenrollaría y ~IntArray() aun así se ejecutaría. Esa garantía es lo que hace a RAII tan fiable, y la razón por la que rara vez escribes un delete suelto en buen código de C++.

La Regla de Tres (y de Cinco)

Una clase con un destructor personalizado casi siempre posee un recurso crudo, y eso crea un peligro oculto. El constructor de copia y la asignación de copia generados por el compilador hacen una copia superficial: copian el puntero, no el búfer al que apunta. Ahora dos objetos contienen el mismo puntero, y ambos destructores harán delete sobre él, provocando un fallo por doble liberación.

IntArray a(5);
IntArray b = a;   // copia superficial: a.data y b.data son el MISMO puntero
// al final del ámbito: el destructor de b libera el búfer,
// luego el destructor de a lo libera DE NUEVO -> comportamiento indefinido (doble liberación)

Esto lleva a la Regla de Tres: si escribes cualquiera de los tres —destructor, constructor de copia u operador de asignación de copia— casi con certeza necesitas los tres. En C++11 y posteriores se amplía a la Regla de Cinco, añadiendo el constructor de movimiento y la asignación de movimiento.

Sin embargo, existe una regla aún mejor: la Regla de Cero: diseña las clases de forma que no gestiones recursos crudos en absoluto. Usa un std::vector, un std::string o un puntero inteligente, y el destructor generado por el compilador hará lo correcto gratis.

Recurre a la Regla de Cero por defecto. Escribe un destructor personalizado solo cuando realmente poseas un recurso crudo que ningún tipo estándar envuelva por ti.

Destructores virtuales

Cuando borras un objeto a través de un puntero a la clase base, el destructor debe ser virtual; de lo contrario solo se destruye la parte base y la parte derivada se fuga. Este es uno de los errores más comunes en el código polimórfico, y el compilador no te avisa de ello por defecto.

Sin virtual en ~Base, delete p llamaría solo a ~Base(): comportamiento indefinido, y la parte Derived del objeto nunca se limpia. Regla práctica: cualquier clase con funciones virtuales (una clase base polimórfica) necesita un destructor virtual. Verás exactamente por qué importa esto en cuanto empieces a derivar clases.

Errores y trampas comunes

Algunas trampas hacen tropezar a casi todo el mundo:

new/delete que no coinciden. Si reservas con new[], libera con delete[]. Mezclar new[] con un delete simple (o viceversa) es comportamiento indefinido.

Olvidar virtual en el destructor de una base. Como se vio arriba, borrar un objeto derivado a través de un puntero a la base sin un destructor virtual fuga la parte derivada. Si escribes una clase pensada para ser heredada, haz el destructor virtual.

Dejar que las excepciones escapen de un destructor. Un destructor que lanza una excepción durante el desenrollado de la pila termina tu programa. En el C++ moderno los destructores son noexcept de forma implícita: evita que el código de limpieza lance excepciones o traga la excepción dentro del destructor.

Escribir un destructor que no necesitas. Si tus miembros ya se limpian solos, un ~NombreClase() {} vacío añade ruido y puede desactivar silenciosamente las operaciones de movimiento. Cuando no hay nada que limpiar, no escribas ningún destructor.

Siguiente: Herencia

Ya has visto el ciclo de vida completo de un objeto: los constructores le dan vida, los destructores lo limpian y los destructores virtual mantienen esa limpieza correcta cuando una clase se construye sobre otra. Ese último punto es un anticipo de la siguiente gran idea: la herencia, donde una clase reutiliza y extiende los datos y el comportamiento de otra. La siguiente página muestra cómo derivar una clase de otra, cómo la construcción y la destrucción se encadenan a través de la jerarquía, y cómo encajan las piezas que acabas de aprender.

Preguntas frecuentes

¿Qué es un destructor en C++?

Un destructor es una función miembro especial llamada ~NombreClase() que se ejecuta automáticamente cuando un objeto se destruye, ya sea al salir de su ámbito o al hacerle delete. Su tarea es la limpieza: liberar memoria, cerrar archivos o liberar cualquier recurso que el objeto posea. No recibe parámetros ni tiene tipo de retorno, y una clase solo puede tener uno.

¿Cuándo se ejecuta un destructor en C++?

Para un objeto local (en la pila), el destructor se ejecuta cuando sale de su ámbito, en la llave de cierre }. Para un objeto en el montón creado con new, se ejecuta cuando llamas a delete. Los miembros y las clases base se destruyen automáticamente después, en orden inverso al de su construcción.

¿Siempre necesito escribir un destructor en C++?

No. Si tu clase solo contiene miembros que se limpian solos (como std::string, std::vector o punteros inteligentes), el destructor generado por el compilador es suficiente; no escribas uno. Solo necesitas un destructor personalizado cuando tu clase posee un recurso crudo, como memoria reservada con new o un descriptor de archivo abierto.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR