Los destructores virtuales son necesarios en C++ para garantizar la destrucción adecuada de objetos de clases derivadas a través de un puntero o referencia a la clase base, evitando fugas de memoria y comportamiento indefinido.

Vayamos al código. Por ejemplo hagamos un delete de un objeto derivado usando un puntero a la clase base:

#include <iostream>

class Base {
    public:
        Base() {
            std::cout << "Base::ctor" << std::endl;
        }
        ~Base() {
            std::cout << "Base::dtor" << std::endl;
        }
};

class Derived: public Base {
    public:
        Derived() {
            std::cout << "Derived::ctor" << std::endl;
        }
        ~Derived() {
            std::cout << "Derived::dtor" << std::endl;
        }
};

int main() {
    Base* b = new Derived;
    delete b;
    return 0;
}

El resultado de este fragmento de código es:

Base::ctor
Derived::ctor
Base::dtor

Como vemos el destructor de la clase Derived no se ejecuta. Esto se soluciona marcando como virtual el destructor de la clase Base.

virtual ~Base() {
    std::cout << "Base::dtor" << std:endl;
}

Da como resultado:

Base::ctor
Derived::ctor
Derived::dtor
Base::dtor

Ahora sí se está destruyendo el objeto Derived.

Conclusiones

Al no ejecutarse el destructor de un objeto, se pueden crear fugas de recursos ya que el constructor de Base podría reservar memoria dinámica al construir Base y nunca ser liberadas al borrar el objeto de la clase Base un puntero de la clase base, (en este caso la clase Derived

Para resumir, siempre declarar como virtual todos los destructores de las clases bases que se van a tratar de manera polifórmica.