Menu
Coddy logo textTech

Funzioni virtuali: ripasso

Fa parte della sezione Programmazione orientata agli oggetti del percorso C++ di Coddy. Lezione 58 di 104.

Ora che abbiamo compreso la differenza tra polimorfismo a tempo di compilazione e a tempo di esecuzione, esaminiamo più a fondo le funzioni virtuali e la parola chiave override che le rende più sicure da usare.

Quando dichiari una funzione come virtual in una classe base, le classi derivate possono fornire la propria implementazione. Lo specificatore override indica esplicitamente al compilatore che intendi sovrascrivere una funzione virtuale:

class Animal {
public:
    virtual void speak() {
        std::cout << "Some sound" << std::endl;
    }
    virtual ~Animal() = default;
};

class Dog : public Animal {
public:
    void speak() override {
        std::cout << "Woof!" << std::endl;
    }
};

Usare override è fondamentale perché permette di individuare gli errori in fase di compilazione. Se scrivi accidentalmente il nome della funzione in modo errato o usi parametri sbagliati, il compilatore ti avviserà invece di creare silenziosamente una nuova funzione:

class Cat : public Animal {
public:
    void speek() override {  // Errore del compilatore: nessuna funzione di cui eseguire l'override
        std::cout << "Meow!" << std::endl;
    }
};

Lo specificatore final impedisce ulteriori ridefinizioni. Usalo quando una classe derivata dovrebbe essere l'ultima a ridefinire una determinata funzione:

class Bulldog : public Dog {
public:
    void speak() override final {
        std::cout << "Gruff woof!" << std::endl;
    }
};

class TinyBulldog : public Bulldog {
    void speak() override {}  // Errore: impossibile ridefinire una funzione final
};

Usa sempre override quando esegui l'override di funzioni virtuali. Documenta la tua intenzione e permette al compilatore di verificare che tu stia effettivamente eseguendo l'override di una funzione virtuale esistente.

challenge icon

Sfida

Facile

Costruiamo un sistema di notifiche che dimostri la potenza delle parole chiave override e final con le funzioni virtuali. Creerai una gerarchia di gestori di notifiche in cui alcuni metodi possono essere ulteriormente personalizzati e altri vengono bloccati per impedirne la modifica.

Organizzerai il codice in tre file:

  • Notifier.h: definisci una classe base Notifier che rappresenta un qualsiasi mittente di notifiche:
    • Un membro std::string recipient protetto
    • Un costruttore che riceve e memorizza il nome del destinatario
    • Un metodo virtuale send(const std::string& message) che stampa: Notifying <recipient>: <message>
    • Un metodo virtuale getType() che restituisce la stringa "Generic"
    • Un distruttore virtuale
  • EmailNotifier.h: definisci una classe EmailNotifier che eredita da Notifier:
    • Un membro privato std::string domain
    • Un costruttore che riceve destinatario e dominio e passa il destinatario alla classe base
    • Esegui l'override di send() per stampare: Emailing <recipient>@<domain>: <message>
    • Esegui l'override di getType() e contrassegnalo come final. Dovrebbe restituire "Email"

    Definisci quindi una classe UrgentEmailNotifier che eredita da EmailNotifier:

    • Un costruttore che riceve destinatario e dominio e li passa entrambi a EmailNotifier
    • Esegui l'override di send() per stampare: [URGENT] Emailing <recipient>@<domain>: <message>
    • Nota: qui non puoi eseguire l'override di getType() perché è stato contrassegnato come final in EmailNotifier
  • main.cpp: leggi tre input (ciascuno su una riga separata):
    1. Nome del destinatario
    2. Dominio email
    3. Testo del messaggio

    Crea dinamicamente tre oggetti notifica: un Notifier base, un EmailNotifier e un UrgentEmailNotifier, tutti con lo stesso destinatario (e, dove applicabile, lo stesso dominio). Memorizzali in un array di puntatori Notifier*.

    Scorri l'array e, per ciascun oggetto notifica, stampa il suo tipo usando getType(), poi chiama send() con il tuo messaggio. Formatta ogni elemento così:

    Type: <type>
    <send output>

    Stampa una riga vuota tra un oggetto notifica e l'altro. Al termine, libera gli oggetti allocati dinamicamente.

Per esempio, con gli input Alice, company.com e Meeting at 3pm:

Type: Generic
Notifying Alice: Meeting at 3pm

Type: Email
Emailing Alice@company.com: Meeting at 3pm

Type: Email
[URGENT] Emailing Alice@company.com: Meeting at 3pm

Nota come UrgentEmailNotifier può eseguire l'override di send() per personalizzare il formato del messaggio, ma eredita il tipo "Email" da EmailNotifier perché getType() è stato contrassegnato come final. Usa la parola chiave override su tutti i metodi sottoposti a override per rilevare eventuali incompatibilità nelle firme in fase di compilazione.

Provalo tu

#include <iostream>
#include <string>
#include "Notifier.h"
#include "EmailNotifier.h"

using namespace std;

int main() {
    // Leggi gli input
    string recipient;
    string domain;
    string message;
    
    getline(cin, recipient);
    getline(cin, domain);
    getline(cin, message);
    
    // TODO: Crea un array di puntatori Notifier* con 3 elementi
    
    // TODO: Crea dinamicamente:
    // - Un Notifier di base con il destinatario
    // - Un EmailNotifier con il destinatario e il dominio
    // - Un UrgentEmailNotifier con il destinatario e il dominio
    
    // TODO: Scorri l'array e, per ogni notificatore:
    // - Stampa "Type: " seguito dal risultato di getType()
    // - Chiama send() con il messaggio
    // - Stampa una riga vuota tra i notificatori (non dopo l'ultimo)
    
    // TODO: Libera gli oggetti allocati dinamicamente
    
    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