Destructores virtuales en C++: por qué siguen siendo relevantes (y cuándo puedes olvidarte)
\n\nSi alguna vez has programado en C++ y te has topado con la herencia, seguramente has visto esta pregunta en Stack Overflow con decenas de miles de visitas: «When to use virtual destructors?». En 2025, el tema no ha perdido relevancia: las fugas de memoria por omitir virtual siguen causando estragos en producción. Pero también hay una cara opuesta: añadir virtual sin pensar en todos lados tampoco es una buena práctica.
Aclaremos esto de una vez por todas: cuándo un destructor virtual es obligatorio, cuándo es solo una buena práctica y cuándo su presencia indica una arquitectura deficiente. Allá vamos.
\n\nRespuesta corta (para quienes tienen prisa)
\n\nUn destructor virtual es necesario si y solo si vas a eliminar un objeto de una clase derivada a través de un puntero a la clase base. Si no existe ese escenario en tu código, no se requiere virtual. Pero hay matices que analizaremos a continuación.
¿Por qué es un problema? Anatomía de una fuga
\n\nVeamos el antipatrón clásico que rompe el código de los principiantes (y no solo de ellos).
\n\n#include <iostream>\n\nclass Base {\npublic:\n ~Base() { \n std::cout << "Base destructor\\n"; \n }\n};\n\nclass Derived : public Base {\nprivate:\n int* big_data = new int[1000]; // recursos que deben liberarse\npublic:\n Derived() { std::cout << "Derived created\\n"; }\n ~Derived() {\n delete[] big_data;\n std::cout << "Derived destructor\\n";\n }\n};\n\nint main() {\n Base* ptr = new Derived();\n delete ptr; // ¡Ups! Solo se llamará a ~Base()\n return 0;\n}\n\nSalida:
\nDerived created\nBase destructor\n\nEl array big_data se ha fugado. El destructor de Derived no se llamó porque el compilador, al ver el tipo Base*, llamó al destructor de esa clase. Y no es virtual.
Solución: añadir virtual antes del destructor de Base:
class Base {\npublic:\n virtual ~Base() = default; // o cuerpo vacío\n};\n\nAhora, al hacer delete ptr, primero se ejecutará ~Derived() y luego ~Base(). Los recursos se liberan, no hay fugas de memoria y el analizador estático está contento.
Cuándo es obligatorio el destructor virtual (regla de hierro)
\n\nSolo hay un caso incondicional: si la clase está diseñada para uso polimórfico mediante un puntero o referencia a la clase base, y los objetos de las clases derivadas se eliminarán a través de ese puntero.
\n\nEn la práctica, esto significa:
\n- \n
- La clase tiene al menos una función virtual (pura o no) \n
- Planeas usar
Base*oBase¶ trabajar conDerived\n - Los objetos se crean en el heap (
new) y se eliminan condelete\n
Ejemplo: la jerarquía clásica de figuras:
\n\nclass Shape {\npublic:\n virtual double area() const = 0;\n virtual ~Shape() = default; // obligatorio para una clase abstracta\n};\n\nclass Circle : public Shape {\n double radius;\npublic:\n explicit Circle(double r) : radius(r) {}\n double area() const override { return 3.14159 * radius * radius; }\n};\n\nint main() {\n std::vector<std::unique_ptr<Shape>> shapes;\n shapes.push_back(std::make_unique<Circle>(5.0));\n // unique_ptr se encargará del delete, pero el destructor de Shape debe ser virtual\n return 0;\n}\n\nObserva: incluso si usas std::unique_ptr (que es el enfoque recomendado en 2025), el destructor virtual sigue siendo necesario. unique_ptr llama a delete a través del puntero a la clase base —