Menu
Coddy logo textTech

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() {}
};
challenge icon

Sfida

Facile

Costruiamo 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 base Media che rappresenta qualsiasi contenuto multimediale riproducibile:
    • Un membro std::string title protetto
    • 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
  • AudioTrack.h: definisci una classe AudioTrack che eredita pubblicamente da Media:
    • 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 chiave override per stampare: Playing audio: <title> by <artist>
    • Un distruttore che stampa: AudioTrack [<title>] destroyed
  • main.cpp: leggi tre input (ciascuno su una riga separata):
    1. Titolo della traccia audio (stringa)
    2. Nome dell'artista (stringa)
    3. Titolo del video (stringa)

    Definisci direttamente in main.cpp una classe VideoClip che eredita pubblicamente da Media:

    • 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 Media della classe base con il titolo "Generic Media"
    • Un oggetto AudioTrack con il titolo e l'artista inseriti
    • Un oggetto VideoClip con 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;
}
quiz iconMettiti alla prova

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

Esercitati da solo: Compilatore C++ online