Funzioni virtuali e VTable
Fa parte della sezione Programmazione orientata agli oggetti del percorso C++ di Coddy. Lezione 52 di 104.
La parola chiave virtual risolve il problema che abbiamo visto nella lezione precedente. Quando dichiari un metodo come virtuale, C++ determina quale versione chiamare in base al tipo effettivo dell’oggetto durante l’esecuzione, non al tipo del puntatore.
class Animal {
public:
virtual void speak() {
std::cout << "Some sound" << std::endl;
}
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "Woof!" << std::endl;
}
};
Dog d;
Animal* ptr = &d;
ptr->speak(); // Output: Woof!Ora viene chiamato il metodo corretto! La parola chiave override è facoltativa, ma consigliata. Comunica al compilatore che intendi eseguire l'override di una funzione virtuale, rilevando gli errori se le firme non corrispondono.
Come funziona? Quando una classe ha funzioni virtuali, il compilatore crea una tabella virtuale (vtable). Questa è una tabella di ricerca nascosta che contiene puntatori alle funzioni virtuali della classe. Ogni oggetto con funzioni virtuali memorizza un puntatore nascosto (vptr) alla vtable della propria classe.
Quando chiami una funzione virtuale tramite un puntatore, il programma cerca l'indirizzo della funzione corretta nella vtable durante l'esecuzione. Questo si chiama dispatch dinamico. Aggiunge un piccolo sovraccarico rispetto alle normali chiamate di funzione, ma consente un potente comportamento polimorfico.
Una regola importante: se una classe ha funzioni virtuali e verrà usata come classe base, anche il suo distruttore dovrebbe essere virtuale. Questo garantisce una corretta pulizia quando si eliminano oggetti derivati tramite puntatori alla classe base:
class Animal {
public:
virtual ~Animal() {} // Distruttore virtuale
virtual void speak() {}
};Sfida
FacileCostruiamo un sistema di riproduzione multimediale che dimostri la potenza delle funzioni virtuali e del polimorfismo a runtime. Creerai una gerarchia di tipi multimediali, ognuno dei quali viene riprodotto in modo diverso, e vedrai come le funzioni virtuali consentano di ottenere il comportamento corretto anche quando si accede agli oggetti tramite puntatori alla classe base.
Organizzerai il codice in tre file:
Media.h: definisci una classe baseMediache rappresenta qualsiasi contenuto multimediale riproducibile:- Un membro
std::string titleprotetto - Un costruttore che accetta un titolo e lo memorizza
- Un metodo virtual
play()che stampa:Playing media: <title> - Un distruttore virtuale che stampa:
Media [<title>] destroyed
- Un membro
AudioTrack.h: definisci una classeAudioTrackche eredita pubblicamente daMedia:- Un membro privato
std::string artist - Un costruttore che accetta un titolo e un artista, passa il titolo alla classe base e memorizza l'artista
- Sovrascrivi
play()usando la parola chiaveoverrideper stampare:Playing audio: <title> by <artist> - Un distruttore che stampa:
AudioTrack [<title>] destroyed
- Un membro privato
main.cpp: leggi tre input (ciascuno su una riga separata):- Titolo della traccia audio (stringa)
- Nome dell'artista (stringa)
- Titolo del video (stringa)
Definisci direttamente in main.cpp una classe
VideoClipche eredita pubblicamente daMedia:- Un membro privato
int duration(in secondi) - Un costruttore che accetta un titolo e una durata (la durata predefinita è 120)
- Sovrascrivi
play()per stampare:Playing video: <title> (<duration>s) - Un distruttore che stampa:
VideoClip [<title>] destroyed
Crea un array di tre puntatori
Media*all'interno di un blocco. Alloca dinamicamente:- Un oggetto
Mediadella classe base con il titolo "Generic Media" - Un oggetto
AudioTrackcon il titolo e l'artista inseriti - Un oggetto
VideoClipcon il titolo del video inserito
Scorri l'array e chiama
play()tramite ogni puntatore. Poi elimina tutti gli oggetti in ordine inverso. Dopo il blocco, stampa:Playback complete!
Ad esempio, con gli input Bohemian Rhapsody, Queen e Nature Documentary:
Playing media: Generic Media
Playing audio: Bohemian Rhapsody by Queen
Playing video: Nature Documentary (120s)
VideoClip [Nature Documentary] destroyed
Media [Nature Documentary] destroyed
AudioTrack [Bohemian Rhapsody] destroyed
Media [Bohemian Rhapsody] destroyed
Media [Generic Media] destroyed
Playback complete!Nota come chiamare play() tramite puntatori Media* invochi il metodo corretto della classe derivata grazie alle funzioni virtuali. Osserva anche come il distruttore virtuale garantisca una corretta pulizia: quando si elimina tramite un puntatore alla classe base, vengono eseguiti sia i distruttori della classe derivata sia quelli della classe base.
Provalo tu
#include <iostream>
#include <string>
#include "Media.h"
#include "AudioTrack.h"
using namespace std;
// TODO: Definisci qui la classe VideoClip che eredita pubblicamente da Media
// - Membro privato int duration (in secondi)
// - Il costruttore accetta title e duration (valore predefinito di duration: 120)
// - Sovrascrivi play() per stampare: Playing video: <title> (<duration>s)
// - Il distruttore stampa: VideoClip [<title>] destroyed
class VideoClip : public Media {
private:
int duration;
public:
// TODO: Implementa il costruttore
VideoClip(const std::string& t, int d = 120) : Media(t) {
// TODO: Memorizza duration
}
// TODO: Sovrascrivi il metodo play()
void play() override {
// TODO: Implementa questo metodo
}
// TODO: Implementa il distruttore
~VideoClip() {
// TODO: Implementa questo distruttore
}
};
int main() {
// Leggi gli input
string audioTitle;
string artist;
string videoTitle;
getline(cin, audioTitle);
getline(cin, artist);
getline(cin, videoTitle);
// TODO: Crea un ambito di blocco con parentesi graffe
// All'interno del blocco:
// 1. Crea un array di tre puntatori Media*
// 2. Alloca dinamicamente:
// - Un oggetto Media di base con titolo "Generic Media"
// - Un oggetto AudioTrack con il titolo e l'artista forniti in input
// - Un oggetto VideoClip con il titolo del video fornito in input
// 3. Scorri l'array e chiama play() su ciascun puntatore
// 4. Elimina tutti gli oggetti in ordine inverso
// TODO: Dopo il blocco, stampa: Playback complete!
return 0;
}
Questa lezione include un breve quiz. Inizia la lezione per rispondere e tenere traccia dei tuoi progressi.
Tutte le lezioni di Programmazione orientata agli oggetti
1Fondamenti della programmazione orientata agli oggetti
File esterniBuild e compilazione in C++File header e file sorgenteNamespace e ambitoIntroduzione alla programmazione orientata agli oggetti in C++Classi e oggetti a confrontoIl puntatore 'this'Metodi (funzioni membro)Attributi (membri dati)Fondamenti di costruttori e distruttoriRiepilogo - Calcolatrice semplice4Proprietà delle classi
Membri di istanza e staticiGetter e setterFunzioni membro constParola chiave mutableMetodi e variabili staticiFunzioni e classi friendRiepilogo - Gestore di conti bancari7Ereditarietà
Ereditarietà di baseLivelli di accesso nell’ereditarietàOrdine di chiamata di costruttori e distruttoriRidefinizione dei metodiFunzioni virtuali e VTableEreditarietà multiplaEreditarietà virtualeRiepilogo - Gerarchia dei dipendenti2Gestione della memoria
Memoria Stack vs HeapPuntatori e riferimentiMemoria dinamica (new/delete)Puntatori intelligenti in C++RAII in C++Riepilogo - Gestore di array dinamico5Incapsulamento
Specificatori di accesso in C++Specificatori di accesso in dettaglioOccultamento delle informazioniStruct vs classClassi annidate e interneRiepilogo - Sistema di registrazione degli studenti8Polimorfismo
Polimorfismo a compile time e a runtimeOverload delle funzioniFunzioni virtuali: ripassoFunzioni virtuali pureClassi astratteProgettazione delle interfacce in C++Dynamic casting e RTTIRipasso: calcolatrice di forme11Concetti avanzati di OOP
Composizione vs ereditarietàMixin tramite CRTPIdiom PimplType ErasureEnum class e tipizzazione forteGestione delle eccezioni in OOPGerarchie personalizzate di eccezioni14Pattern di progettazione - Parte 2
Pattern CommandPattern AdapterPattern DecoratorPattern Template MethodPattern StatePattern CompositeRAII come pattern3Costruttori e distruttori
Costruttore predefinitoCostruttore con parametriCostruttore di copiaCostruttore di spostamentoListe di inizializzazione dei costruttoriCostruttori delegantiApprofondimento sui distruttoriRegola del Tre / Cinque / ZeroRipasso - classe String6Sovraccarico degli operatori
Introduzione al sovraccarico degli operatoriSovraccarico degli operatori aritmeticiSovraccarico degli operatori di confrontoOperatori di flussoSovraccarico dell'operatore di assegnazioneSovraccarico degli operatori [] e ()Operatori di conversione di tipoRipasso - Classe Matrix9Template
Template di funzioneTemplate di classeSpecializzazione dei templateTemplate variadiciBasi di SFINAE e dei trait di tipoRiepilogo - Contenitore genericoEsercitati da solo: Compilatore C++ online