Você está entrando em hipóteses com essas respostas, então tentarei fazer uma explicação mais simples e mais realista por uma questão de clareza.
Os relacionamentos básicos do design orientado a objetos são dois: IS-A e HAS-A. Eu não inventei isso. É assim que eles são chamados.
IS-A indica que um objeto específico se identifica como sendo da classe que está acima dele em uma hierarquia de classes. Um objeto de banana é um objeto de fruta se for uma subclasse da classe de fruta. Isso significa que em qualquer lugar que uma classe de frutas possa ser usada, uma banana pode ser usada. Não é reflexivo, no entanto. Você não pode substituir uma classe base por uma classe específica se essa classe específica for solicitada.
Has-a indicou que um objeto faz parte de uma classe composta e que existe um relacionamento de propriedade. Significa em C ++ que é um objeto membro e, como tal, o ônus recai sobre a classe proprietária para descartá-lo ou transferir a propriedade antes de se destruir.
Esses dois conceitos são mais fáceis de entender em linguagens de herança única do que em um modelo de herança múltipla como c ++, mas as regras são essencialmente as mesmas. A complicação ocorre quando a identidade da classe é ambígua, como passar um ponteiro de classe Banana para uma função que leva um ponteiro de classe Fruit.
As funções virtuais são, em primeiro lugar, uma coisa em tempo de execução. Faz parte do polimorfismo, pois é usado para decidir qual função executar no momento em que é chamada no programa em execução.
A palavra-chave virtual é uma diretiva de compilador para vincular funções em uma determinada ordem, se houver ambiguidade sobre a identidade da classe. As funções virtuais estão sempre nas classes pai (tanto quanto eu sei) e indicam ao compilador que a ligação das funções membro aos seus nomes deve ocorrer com a função subclasse primeiro e a função classe pai depois.
Uma classe Fruit pode ter uma função virtual color () que retorna "NONE" por padrão. A função color class () da classe Banana retorna "AMARELO" ou "MARROM".
Mas se a função que usa um ponteiro Fruit chama color () na classe Banana enviada para ele - qual função color () é chamada? A função normalmente chamaria Fruit :: color () para um objeto Fruit.
Isso não seria 99% do tempo pretendido. Mas se Fruit :: color () fosse declarado virtual, Banana: color () seria chamada para o objeto, porque a função color () correta seria vinculada ao ponteiro Fruit no momento da chamada. O tempo de execução verificará para qual objeto o ponteiro aponta, porque foi marcado como virtual na definição da classe Fruit.
Isso é diferente de substituir uma função em uma subclasse. Nesse caso, o ponteiro Fruit chamará Fruit :: color () se tudo o que souber é que ele é um ponteiro para Fruit.
Então agora surge a idéia de uma "função virtual pura". É uma frase bastante infeliz, pois a pureza não tem nada a ver com isso. Isso significa que se pretende que o método da classe base nunca seja chamado. Na verdade, uma função virtual pura não pode ser chamada. Ainda deve ser definido, no entanto. Uma assinatura de função deve existir. Muitos codificadores fazem uma implementação vazia {} para garantir a integridade, mas o compilador gerará uma internamente, se não. Nesse caso, quando a função é chamada, mesmo que o ponteiro seja para Fruit, Banana :: color () será chamada, pois é a única implementação de color () que existe.
Agora a peça final do quebra-cabeça: construtores e destruidores.
Construtores virtuais puros são ilegais, completamente. Isso acabou de sair.
Mas destruidores virtuais puros funcionam no caso em que você deseja proibir a criação de uma instância de classe base. Somente subclasses podem ser instanciadas se o destruidor da classe base for puro virtual. a convenção é atribuí-lo a 0.
virtual ~Fruit() = 0; // pure virtual
Fruit::~Fruit(){} // destructor implementation
Você precisa criar uma implementação neste caso. O compilador sabe que é isso que você está fazendo e garante que você faça o que é certo, ou queixa-se poderosamente de que não pode vincular a todas as funções necessárias para compilar. Os erros podem ser confusos se você não estiver no caminho certo sobre como está modelando sua hierarquia de classes.
Portanto, neste caso, você é proibido de criar instâncias de Fruit, mas tem permissão para criar instâncias de Banana.
Uma chamada para excluir o ponteiro Fruit que aponta para uma instância de Banana chama Banana :: ~ Banana () primeiro e depois chama Fuit :: ~ Fruit (), sempre. Porque não importa o que, quando você chama um destruidor de subclasse, o destruidor da classe base deve seguir.
É um modelo ruim? É mais complicado na fase de design, sim, mas pode garantir que a vinculação correta seja executada em tempo de execução e que uma função de subclasse seja executada onde houver ambiguidade quanto a exatamente qual subclasse está sendo acessada.
Se você escreve C ++ para passar apenas ponteiros de classe exatos sem ponteiros genéricos nem ambíguos, as funções virtuais não são realmente necessárias. Porém, se você precisar de flexibilidade de tipos de tempo de execução (como em Apple Banana Orange ==> Frutas), as funções se tornarão mais fáceis e versáteis com código menos redundante. Você não precisa mais escrever uma função para cada tipo de fruta e sabe que todas as frutas responderão a color () com sua própria função correta.
Espero que essa explicação extenuante solidifique o conceito em vez de confundir as coisas. Existem muitos bons exemplos por aí, e o suficiente e, na verdade, executá-los e mexer com eles, e você conseguirá.