Te estás metiendo en hipotéticos con estas respuestas, por lo que intentaré hacer una explicación más simple y realista para mayor claridad.
Las relaciones básicas del diseño orientado a objetos son dos: IS-A y HAS-A. No los inventé. Así se llaman.
IS-A indica que un objeto particular se identifica como perteneciente a la clase que está por encima de él en una jerarquía de clases. Un objeto banana es un objeto de fruta si es una subclase de la clase de fruta. Esto significa que en cualquier lugar donde se pueda usar una clase de fruta, se puede usar un plátano. Sin embargo, no es reflexivo. No puede sustituir una clase base por una clase específica si se requiere esa clase específica.
Has-a indicó que un objeto es parte de una clase compuesta y que existe una relación de propiedad. Significa en C ++ que es un objeto miembro y, como tal, corresponde a la clase propietaria deshacerse de él o entregar la propiedad antes de destruirse.
Estos dos conceptos son más fáciles de realizar en lenguajes de herencia única que en un modelo de herencia múltiple como c ++, pero las reglas son esencialmente las mismas. La complicación surge cuando la identidad de la clase es ambigua, como pasar un puntero de clase Banana a una función que toma un puntero de clase Fruit.
Las funciones virtuales son, en primer lugar, una cosa de tiempo de ejecución. Es parte del polimorfismo porque se usa para decidir qué función ejecutar en el momento en que se llama en el programa en ejecución.
La palabra clave virtual es una directiva del compilador para vincular funciones en un cierto orden si existe ambigüedad sobre la identidad de la clase. Las funciones virtuales siempre están en las clases principales (hasta donde yo sé) e indican al compilador que el enlace de las funciones miembro a sus nombres debe tener lugar primero con la función de subclase y después con la función de clase principal.
Una clase Fruit podría tener una función virtual color () que devuelve "NINGUNO" de forma predeterminada. La función Banana class color () devuelve "AMARILLO" o "MARRÓN".
Pero si la función que toma un puntero Fruit llama a color () en la clase Banana que se le envió, ¿qué función color () se invoca? La función normalmente llamaría Fruit :: color () para un objeto Fruit.
Eso sería el 99% de las veces no lo que se pretendía. Pero si Fruit :: color () se declara virtual, entonces Banana: color () se llamaría para el objeto porque la función color () correcta estaría vinculada al puntero de Fruit en el momento de la llamada. El tiempo de ejecución verificará a qué objeto apunta el puntero porque se marcó virtual en la definición de la clase Fruit.
Esto es diferente de anular una función en una subclase. En ese caso, el puntero de Fruit llamará a Fruit :: color () si todo lo que sabe es que es un puntero IS-A a Fruit.
Así que ahora surge la idea de una "función virtual pura". Es una frase bastante desafortunada ya que la pureza no tiene nada que ver con eso. Significa que se pretende que nunca se invoque el método de la clase base. De hecho, no se puede invocar una función virtual pura. Sin embargo, aún debe definirse. Debe existir una firma de función. Muchos codificadores hacen una implementación vacía {} para completar, pero el compilador generará una internamente si no. En ese caso, cuando se llama a la función incluso si el puntero es a Fruit, se llamará a Banana :: color (), ya que es la única implementación de color () que existe.
Ahora la pieza final del rompecabezas: constructores y destructores.
Los constructores virtuales puros son ilegales, completamente. Eso acaba de salir.
Pero los destructores virtuales puros funcionan en el caso de que desee prohibir la creación de una instancia de clase base. Solo se pueden crear instancias de subclases si el destructor de la clase base es puramente virtual. la convención es asignarlo a 0.
virtual ~Fruit() = 0; // pure virtual
Fruit::~Fruit(){} // destructor implementation
Tienes que crear una implementación en este caso. El compilador sabe que esto es lo que está haciendo y se asegura de hacerlo correctamente, o se queja poderosamente de que no puede vincularse a todas las funciones que necesita para compilar. Los errores pueden ser confusos si no está en el camino correcto en cuanto a cómo está modelando su jerarquía de clases.
En este caso, está prohibido crear instancias de Fruit, pero se le permite crear instancias de Banana.
Una llamada para eliminar el puntero de Fruit que apunta a una instancia de Banana llamará primero a Banana :: ~ Banana () y luego a Fuit :: ~ Fruit (), siempre. Porque no importa qué, cuando llama a un destructor de subclase, debe seguir el destructor de la clase base.
¿Es un mal modelo? Es más complicado en la fase de diseño, sí, pero puede garantizar que se realice el enlace correcto en tiempo de ejecución y que se realice una función de subclase donde exista ambigüedad sobre exactamente a qué subclase se está accediendo.
Si escribe C ++ para que solo pase punteros de clase exactos sin punteros genéricos ni ambiguos, entonces las funciones virtuales no son realmente necesarias. Pero si necesita flexibilidad de tipos en tiempo de ejecución (como en Apple Banana Orange ==> Fruit) las funciones se vuelven más fáciles y más versátiles con menos código redundante. Ya no tiene que escribir una función para cada tipo de fruta, y sabe que cada fruta responderá a color () con su propia función correcta.
Espero que esta larga explicación solidifique el concepto en lugar de confundir las cosas. Hay muchos buenos ejemplos por ahí para mirar, mirar lo suficiente y ejecutarlos y meterse con ellos y lo obtendrás.