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 à 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 de 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 redéfinir 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;
}
};L'utilisation de override est cruciale car elle permet de détecter les erreurs au moment de la compilation. Si vous faites accidentellement une faute de frappe dans le nom de la fonction ou si vous utilisez des paramètres incorrects, le compilateur vous alertera au lieu de créer silencieusement une nouvelle fonction :
class Cat : public Animal {
public:
void speek() override { // Erreur de compilation : 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 une fonction final
};Utilisez toujours override lors de la redéfinition de fonctions virtuelles. Cela documente votre intention et permet au compilateur de vérifier que vous redéfinissez réellement une fonction virtuelle existante.
Défi
FacileConstruisons un système de notification qui démontre la puissance des mots-clés override et final avec les fonctions virtuelles. Vous allez créer une hiérarchie de gestionnaires de notifications où certaines méthodes peuvent être personnalisées davantage et d'autres sont verrouillées pour empêcher toute modification.
Vous organiserez votre code sur trois fichiers :
Notifier.h: Définissez une classe de baseNotifierqui représente n'importe quel expéditeur de notification :- Un membre protégé
std::string recipient - 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 retourne la chaîne"Generic" - Un destructeur virtuel
- Un membre protégé
EmailNotifier.h: Définissez une classeEmailNotifierqui hérite deNotifier:- Un membre privé
std::string domain - Un constructeur qui prend le destinataire et le domaine, en passant le destinataire à la classe de base
- Surchargez
send()pour afficher :Emailing <recipient>@<domain>: <message> - Surchargez
getType()et marquez-la commefinal— elle doit retourner"Email"
Ensuite, définissez une classe
UrgentEmailNotifierqui hérite deEmailNotifier:- Un constructeur qui prend le destinataire et le domaine, en passant les deux à
EmailNotifier - Surchargez
send()pour afficher :[URGENT] Emailing <recipient>@<domain>: <message> - Remarque : Vous ne pouvez pas surcharger
getType()ici car elle a été marquéefinaldansEmailNotifier
- Un membre privé
main.cpp: Lisez trois entrées (chacune sur une ligne séparée) :- Nom du destinataire
- Domaine de l'e-mail
- Texte du message
Créez trois objets de notification dynamiquement : un
Notifierde base, unEmailNotifier, et unUrgentEmailNotifier— tous utilisant le même destinataire (et le même domaine le cas échéant). Stockez-les dans un tableau de pointeursNotifier*.Bouclez sur le tableau et pour chaque notification, affichez son type en utilisant
getType(), puis appelezsend()avec votre message. Formatez chaque entrée comme suit :Type: <type> <send output>Affichez une ligne vide entre chaque notification. Libérez vos 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 3pmRemarquez comment UrgentEmailNotifier peut surcharger send() pour personnaliser le format du message, mais il hérite du type "Email" de EmailNotifier car getType() a été marquée final. Utilisez le mot-clé override sur toutes les méthodes surchargées pour détecter toute erreur de signature au moment de la compilation.
Aide-mémoire
Le spécificateur override indique explicitement au compilateur que vous avez l'intention de redéfinir une fonction virtuelle d'une classe de base :
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;
}
};L'utilisation de override permet de détecter les erreurs au moment de la compilation. Si vous faites une faute de frappe dans le nom de la fonction ou si vous utilisez les mauvais paramètres, le compilateur vous alertera :
class Cat : public Animal {
public:
void speek() override { // Compiler error: no function to override
std::cout << "Meow!" << std::endl;
}
};Le spécificateur final empêche toute redéfinition ultérieure dans les classes dérivées :
class Bulldog : public Dog {
public:
void speak() override final {
std::cout << "Gruff woof!" << std::endl;
}
};
class TinyBulldog : public Bulldog {
void speak() override {} // Error: cannot override final function
};Utilisez toujours override lors de la redéfinition de fonctions virtuelles pour documenter votre intention et permettre la vérification par le compilateur.
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 notificateur :
// - Afficher "Type: " suivi du résultat de getType()
// - Appeler send() avec le message
// - Afficher une ligne vide entre les notificateurs (pas après le dernier)
// TODO: Libérer les objets alloués dynamiquement
return 0;
}
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
1Fondamentaux de la POO
Fichiers externesBuild et Compilation C++Fichiers d'en-tête et Fichiers sourceEspaces de noms et PortéeIntroduction à la POO en C++Classes vs ObjetsLe pointeur 'this'Méthodes (Fonctions membres)Attributs (Membres de données)Bases des Ctors et DtorsRécapitulatif - Calculatrice simple4Propriétés de classe
Membres d'instance vs membres statiquesGetters et SettersFonctions membres constMot-clé mutableMéthodes et variables statiquesFonctions et classes amiesRécapitulatif - Gestionnaire de compte bancaire7Héritage
Héritage de baseNiveaux d'accès à l'héritageOrdre d'appel des Ctor et DtorRedéfinition de méthodesFonctions virtuelles et VTableHéritage multipleHéritage virtuelRécapitulatif - Hiérarchie des employés10Présentation de la STL
Présentation et philosophie de la STLConteneurs de la STLItérateursAlgorithmes de la STLFoncteurs et expressions lambdaRécapitulatif - Fréquence des mots13Patrons de conception, Partie 1
Introduction aux patrons de conceptionPatron SingletonFabrique & Fabrique abstraitePatron MonteurPatron ObservateurPatron Stratégie2Gestion de la mémoire
Mémoire Pile vs TasPointeurs et RéférencesMémoire dynamique (new/delete)Smart Pointers en C++RAII en C++Récapitulatif - Gestionnaire de tableaux dynamiques5Encapsulation
Spécificateurs d'accès en C++Spécificateurs d'accès en profondeurMasquage d'informationStruct vs ClassClasses imbriquées et internesRécapitulatif - Système de gestion d'étudiants8Polymorphisme
Polymorphisme : Compilation vs ExécutionSurcharge de fonctionsRetour sur les fonctions virtuellesFonctions virtuelles puresClasses abstraitesConception d'interfaces en C++Dynamic Casting & RTTIRécapitulatif - Calculateur de formes11Concepts avancés de la POO
Composition vs HéritageMixins via CRTPIdiome PimplEffacement de typeEnum Classes & Typage fortGestion des exceptions en POOHiérarchies d'exceptions personnalisées14Patrons de conception, Partie 2
Patron CommandePatron AdaptateurPatron DécorateurPatron Template MethodPatron ÉtatPatron CompositeRAII en tant que patron3Constructeurs et Destructeurs
Constructeur par défautConstructeur paramétréConstructeur de copieConstructeur de déplacementListes d'initialisation du constructeurConstructeurs déléguésAnalyse approfondie du destructeurRègle de trois / cinq / zéroRécapitulatif - Classe String6Surcharge d'opérateurs
Introduction à la surcharge d'opérateursSurcharge des opérateurs arithmétiquesSurcharge des opérateurs de comparaisonOpérateurs de fluxSurcharge de l'opérateur d'affectationSurcharge des opérateurs [] et ()Opérateurs de conversion de typeRécapitulatif - Classe Matrix9Templates
Templates de fonctionsTemplates de classesSpécialisation de templatesTemplates variadiquesBases de SFINAE & Type TraitsRécapitulatif - Conteneur générique