Stai entrando in ipotesi con queste risposte, quindi cercherò di fare una spiegazione più semplice, più concreta per amor di chiarezza.
Le relazioni di base del design orientato agli oggetti sono due: IS-A e HAS-A. Non li ho inventati. Questo è come si chiamano.
IS-A indica che un particolare oggetto si identifica come appartenente alla classe che si trova sopra di esso in una gerarchia di classi. Un oggetto banana è un oggetto frutta se è una sottoclasse della classe frutta. Ciò significa che ovunque una classe di frutta può essere utilizzata, una banana può essere utilizzata. Tuttavia, non è riflessivo. Non è possibile sostituire una classe base per una classe specifica se tale classe specifica è richiesta.
Has-a ha indicato che un oggetto fa parte di una classe composita e che esiste una relazione di proprietà. Significa in C ++ che è un oggetto membro e in quanto tale spetta alla classe proprietaria disporne o consegnarne la proprietà prima di distruggersi.
Questi due concetti sono più facili da realizzare in linguaggi a eredità singola che in un modello di ereditarietà multipla come c ++, ma le regole sono essenzialmente le stesse. La complicazione arriva quando l'identità di classe è ambigua, come passare un puntatore di classe Banana in una funzione che accetta un puntatore di classe Fruit.
Le funzioni virtuali sono, in primo luogo, una cosa di runtime. Fa parte del polimorfismo in quanto viene utilizzato per decidere quale funzione eseguire nel momento in cui viene chiamata nel programma in esecuzione.
La parola chiave virtuale è una direttiva del compilatore per associare le funzioni in un determinato ordine in caso di ambiguità sull'identità della classe. Le funzioni virtuali sono sempre nelle classi principali (per quanto ne so) e indicano al compilatore che l'associazione delle funzioni membro ai loro nomi dovrebbe avvenire prima con la funzione della sottoclasse e successivamente con la funzione della classe genitore.
Una classe Fruit potrebbe avere una funzione virtuale color () che restituisce "NONE" per impostazione predefinita. La funzione color () della classe Banana restituisce "GIALLO" o "MARRONE".
Ma se la funzione che assume un puntatore Fruit chiama color () sulla classe Banana che gli viene inviata - quale funzione color () viene invocata? La funzione normalmente chiamerebbe Fruit :: color () per un oggetto Fruit.
Sarebbe il 99% delle volte non quello che era previsto. Ma se Fruit :: color () fosse dichiarato virtuale, allora Banana: color () verrebbe chiamato per l'oggetto perché la funzione color () corretta sarebbe legata al puntatore Fruit al momento della chiamata. Il runtime controlla l'oggetto a cui punta il puntatore perché è stato contrassegnato come virtuale nella definizione della classe Fruit.
Ciò è diverso dall'override di una funzione in una sottoclasse. In tal caso, il puntatore Fruit chiamerà Fruit :: color () se tutto ciò che sa è che è un puntatore IS-A a Fruit.
Quindi ora arriva l'idea di una "pura funzione virtuale". È una frase piuttosto sfortunata poiché la purezza non ha nulla a che fare con essa. Significa che è inteso che il metodo della classe base non deve mai essere chiamato. In effetti una funzione virtuale pura non può essere chiamata. Deve comunque essere definito. Deve esistere una firma di funzione. Molti programmatori eseguono un'implementazione vuota {} per completezza, ma il compilatore ne genererà uno internamente in caso contrario. In quel caso quando la funzione viene chiamata anche se il puntatore è su Fruit, verrà chiamato Banana :: color () in quanto è l'unica implementazione di color () che esiste.
Ora l'ultimo pezzo del puzzle: costruttori e distruttori.
I costruttori virtuali puri sono illegali, completamente. Questo è appena uscito.
Ma i puri distruttori virtuali funzionano nel caso in cui si desideri vietare la creazione di un'istanza di classe base. Solo le sottoclassi possono essere istanziate se il distruttore della classe base è virtuale puro. la convenzione è assegnarla a 0.
virtual ~Fruit() = 0; // pure virtual
Fruit::~Fruit(){} // destructor implementation
In questo caso devi creare un'implementazione. Il compilatore sa che è ciò che stai facendo e si assicura che lo faccia nel modo giusto, oppure si lamenta con forza che non può collegarsi a tutte le funzioni che deve compilare. Gli errori possono essere fonte di confusione se non sei sulla buona strada per modellare la gerarchia di classi.
Quindi in questo caso è vietato creare istanze di Fruit, ma è consentito creare istanze di Banana.
Una chiamata per eliminare il puntatore Fruit che punta a un'istanza di Banana chiamerà prima Banana :: ~ Banana () e poi chiamerà Fuit :: ~ Fruit (), sempre. Perché, qualunque cosa accada, quando chiami un distruttore di sottoclasse, il distruttore della classe base deve seguire.
È un cattivo modello? È più complicato in fase di progettazione, sì, ma può garantire che venga eseguito il collegamento corretto in fase di esecuzione e che venga eseguita una funzione di sottoclasse in cui vi sia ambiguità su quale sottoclasse si acceda esattamente.
Se si scrive C ++ in modo da passare solo intorno a puntatori di classe esatti senza puntatori generici né ambigui, le funzioni virtuali non sono realmente necessarie. Ma se si richiedono flessibilità di runtime dei tipi (come in Apple Banana Orange ==> Fruit) le funzioni diventano più facili e più versatili con un codice meno ridondante. Non devi più scrivere una funzione per ogni tipo di frutto e sai che ogni frutto risponderà a color () con la sua funzione corretta.
Spero che questa spiegazione prolissa consolidi il concetto piuttosto che confondere le cose. Ci sono molti buoni esempi là fuori da guardare e guardare abbastanza e in realtà eseguirli e pasticciare con loro e lo otterrai.