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.
Sfida
FacileCostruiamo 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 baseNotifierche rappresenta un qualsiasi mittente di notifiche:- Un membro
std::string recipientprotetto - 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
- Un membro
EmailNotifier.h: definisci una classeEmailNotifierche eredita daNotifier:- 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 comefinal. Dovrebbe restituire"Email"
Definisci quindi una classe
UrgentEmailNotifierche eredita daEmailNotifier:- 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 comefinalinEmailNotifier
- Un membro privato
main.cpp: leggi tre input (ciascuno su una riga separata):- Nome del destinatario
- Dominio email
- Testo del messaggio
Crea dinamicamente tre oggetti notifica: un
Notifierbase, unEmailNotifiere unUrgentEmailNotifier, tutti con lo stesso destinatario (e, dove applicabile, lo stesso dominio). Memorizzali in un array di puntatoriNotifier*.Scorri l'array e, per ciascun oggetto notifica, stampa il suo tipo usando
getType(), poi chiamasend()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 3pmNota 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;
}
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