Menu
Coddy logo textTech

Retour sur les fonctions virtuelles

Fait partie de la section Programmation Orientée Objet du Journey C++ de Coddy. Leçon 58 sur 104.

Maintenant que nous comprenons la différence entre le polymorphisme à la compilation et le polymorphisme à l’exécution, examinons de plus près les fonctions virtuelles et le mot-clé override qui les rend plus sûres à utiliser.

Lorsque vous marquez une fonction comme virtual dans une classe base, les classes dérivées peuvent fournir leur propre implémentation. Le spécificateur override indique explicitement au compilateur que vous avez l’intention de remplacer une fonction virtuelle :

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;
    }
};

Utiliser override est essentiel, car cela permet de détecter les erreurs lors de la compilation. Si tu fais accidentellement une faute d’orthographe dans le nom de la fonction ou utilises de mauvais paramètres, le compilateur t’en avertira au lieu de créer silencieusement une nouvelle fonction :

class Cat : public Animal {
public:
    void speek() override {  // Erreur du compilateur : aucune fonction à redéfinir
        std::cout << "Meow!" << std::endl;
    }
};

Le spécificateur final empêche toute redéfinition ultérieure. Utilisez-le lorsqu’une classe dérivée doit être la dernière à redéfinir une fonction particulière :

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

class TinyBulldog : public Bulldog {
    void speak() override {}  // Erreur : impossible de redéfinir la fonction final
};

Utilisez toujours override lorsque vous redéfinissez des fonctions virtuelles. Cela documente votre intention et permet au compilateur de vérifier que vous redéfinissez effectivement une fonction virtuelle existante.

challenge icon

Défi

Facile

Construisons un système de notification qui démontre la puissance des mots-clés override et final avec des fonctions virtuelles. Vous allez créer une hiérarchie de gestionnaires de notifications, dans laquelle certaines méthodes peuvent être personnalisées davantage tandis que d’autres sont verrouillées pour empêcher toute modification.

Vous organiserez votre code dans trois fichiers :

  • Notifier.h : définir une classe de base Notifier qui représente n’importe quel expéditeur de notifications :
    • Un membre std::string recipient protégé
    • Un constructeur qui prend et stocke le nom du destinataire
    • Une méthode virtuelle send(const std::string& message) qui affiche : Notifying <recipient>: <message>
    • Une méthode virtuelle getType() qui renvoie la chaîne "Generic"
    • Un destructeur virtuel
  • EmailNotifier.h : définir une classe EmailNotifier qui hérite de Notifier :
    • Un membre privé std::string domain
    • Un constructeur qui prend le destinataire et le domaine, en transmettant le destinataire à la classe de base
    • Redéfinir send() pour afficher : Emailing <recipient>@<domain>: <message>
    • Redéfinir getType() et le marquer final. Il doit renvoyer "Email"

    Définir ensuite une classe UrgentEmailNotifier qui hérite de EmailNotifier :

    • Un constructeur qui prend le destinataire et le domaine, en transmettant les deux à EmailNotifier
    • Redéfinir send() pour afficher : [URGENT] Emailing <recipient>@<domain>: <message>
    • Remarque : vous ne pouvez pas redéfinir getType() ici, car il a été marqué final dans EmailNotifier
  • main.cpp : lire trois entrées, chacune sur une ligne distincte :
    1. Nom du destinataire
    2. Domaine de l’adresse e-mail
    3. Texte du message

    Créer dynamiquement trois objets de notification : un Notifier de base, un EmailNotifier et un UrgentEmailNotifier, tous avec le même destinataire (et le même domaine lorsque cela s’applique). Les stocker dans un tableau de pointeurs Notifier*.

    Parcourir le tableau et, pour chaque objet de notification, afficher son type à l’aide de getType(), puis appeler send() avec votre message. Formater chaque entrée ainsi :

    Type: <type>
    <send output>

    Afficher une ligne vide entre chaque objet de notification. Libérer les objets alloués dynamiquement une fois terminé.

Par exemple, avec les entrées Alice, company.com et 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

Remarquez comment UrgentEmailNotifier peut redéfinir send() pour personnaliser le format du message, mais hérite du type "Email" de EmailNotifier, car getType() a été marqué final. Utilisez le mot-clé override sur toutes les méthodes redéfinies afin de détecter toute incompatibilité de signature au moment de la compilation.

Essayez vous-même

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

using namespace std;

int main() {
    // Lire les entrées
    string recipient;
    string domain;
    string message;
    
    getline(cin, recipient);
    getline(cin, domain);
    getline(cin, message);
    
    // TODO: Créer un tableau de pointeurs Notifier* avec 3 éléments
    
    // TODO: Créer dynamiquement :
    // - Un Notifier de base avec le recipient
    // - Un EmailNotifier avec recipient et domain
    // - Un UrgentEmailNotifier avec recipient et domain
    
    // TODO: Parcourir le tableau et pour chaque notifier :
    // - Afficher "Type: " suivi du résultat de getType()
    // - Appeler send() avec le message
    // - Afficher une ligne vide entre les notifiers (pas après le dernier)
    
    // TODO: Nettoyer les objets alloués dynamiquement
    
    return 0;
}
quiz iconTestez-vous

Cette leçon comprend un petit quiz. Commencez la leçon pour y répondre et suivre votre progression.

Toutes les leçons de Programmation Orientée Objet

Entraînez-vous par vous-même : Compilateur C++ en ligne