Fonctions virtuelles et VTable
Fait partie de la section Programmation Orientée Objet du Journey C++ de Coddy — leçon 52 sur 104.
Le mot-clé virtual résout le problème que nous avons vu dans la leçon précédente. Lorsque vous déclarez une méthode comme virtuelle, le C++ détermine quelle version appeler en fonction du type d'objet réel au moment de l'exécution, et non du type de pointeur.
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(); // Sortie : Woof!Maintenant, la méthode correcte est appelée ! Le mot-clé override est facultatif mais recommandé. Il indique au compilateur que vous avez l'intention de redéfinir une fonction virtuelle, ce qui permet de détecter les erreurs si les signatures ne correspondent pas.
Comment cela fonctionne-t-il ? Lorsqu'une classe possède des fonctions virtuelles, le compilateur crée une table virtuelle (vtable). Il s'agit d'une table de correspondance masquée contenant des pointeurs vers les fonctions virtuelles de la classe. Chaque objet doté de fonctions virtuelles stocke un pointeur masqué (vptr) vers la vtable de sa classe.
Lorsque vous appelez une fonction virtuelle via un pointeur, le programme recherche l'adresse de la fonction correcte dans la vtable au moment de l'exécution. C'est ce qu'on appelle la résolution dynamique. Cela ajoute une légère surcharge par rapport aux appels de fonction classiques, mais permet un comportement polymorphe puissant.
Une règle importante : si une classe possède des fonctions virtuelles et sera utilisée comme classe de base, son destructeur doit également être virtuel. Cela garantit un nettoyage approprié lors de la suppression d'objets dérivés via des pointeurs de base :
class Animal {
public:
virtual ~Animal() {} // Destructeur virtuel
virtual void speak() {}
};Défi
FacileConstruisons un système de lecteur multimédia qui démontre la puissance des fonctions virtuelles et du polymorphisme à l'exécution. Vous allez créer une hiérarchie de types de médias où chacun se lit différemment, et voir comment les fonctions virtuelles permettent d'obtenir le comportement correct même lorsqu'elles sont accédées via des pointeurs de la classe de base.
Vous organiserez votre code sur trois fichiers :
Media.h: Définissez une classe de baseMediaqui représente tout média lisible :- Un membre protégé
std::string title - Un constructeur qui prend un titre et le stocke
- Une méthode virtuelle
play()qui affiche :Playing media: <title> - Un destructeur virtuel qui affiche :
Media [<title>] destroyed
- Un membre protégé
AudioTrack.h: Définissez une classeAudioTrackqui hérite publiquement deMedia:- Un membre privé
std::string artist - Un constructeur qui prend un titre et un artiste, transmet le titre à la classe de base et stocke l'artiste
- Surchargez
play()en utilisant le mot-cléoverridepour afficher :Playing audio: <title> by <artist> - Un destructeur qui affiche :
AudioTrack [<title>] destroyed
- Un membre privé
main.cpp: Lisez trois entrées (chacune sur une ligne séparée) :- Titre de la piste audio (chaîne de caractères)
- Nom de l'artiste (chaîne de caractères)
- Titre de la vidéo (chaîne de caractères)
Définissez une classe
VideoClipdirectement dans main.cpp qui hérite publiquement deMedia:- Un membre privé
int duration(en secondes) - Un constructeur qui prend un titre et une durée (durée par défaut à 120)
- Surchargez
play()pour afficher :Playing video: <title> (<duration>s) - Un destructeur qui affiche :
VideoClip [<title>] destroyed
Créez un tableau de trois pointeurs
Media*à l'intérieur d'une portée de bloc. Allouez dynamiquement :- Un objet de base
Mediaavec le titre "Generic Media" - Un objet
AudioTrackavec le titre et l'artiste saisis - Un objet
VideoClipavec le titre de la vidéo saisi
Bouclez sur le tableau et appelez
play()sur chaque pointeur. Ensuite, supprimez tous les objets dans l'ordre inverse. Après le bloc, affichez :Playback complete!
Par exemple, avec les entrées Bohemian Rhapsody, Queen, et 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!Remarquez comment l'appel de play() via des pointeurs Media* invoque la méthode correcte de la classe dérivée grâce aux fonctions virtuelles. Observez également comment le destructeur virtuel assure un nettoyage approprié — les destructeurs de la classe dérivée et de la classe de base s'exécutent tous deux lors de la suppression via un pointeur de base.
Aide-mémoire
Le mot-clé virtual permet le polymorphisme à l'exécution en déterminant quelle méthode appeler en fonction du type réel de l'objet, et non du type du pointeur :
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(); // Sortie : Woof!Le mot-clé override est optionnel mais recommandé. Il indique au compilateur que vous avez l'intention de redéfinir une fonction virtuelle, ce qui permet de détecter des erreurs si les signatures ne correspondent pas.
Comment fonctionnent les fonctions virtuelles : Le compilateur crée une table virtuelle (vtable) contenant des pointeurs vers les fonctions virtuelles de la classe. Chaque objet possédant des fonctions virtuelles stocke un pointeur caché (vptr) vers la vtable de sa classe. Lors de l'appel d'une fonction virtuelle via un pointeur, le programme recherche l'adresse de la fonction correcte dans la vtable au moment de l'exécution. C'est ce qu'on appelle la répartition dynamique (dynamic dispatch).
Destructeurs virtuels : Si une classe possède des fonctions virtuelles et doit être utilisée comme classe de base, son destructeur doit également être virtuel. Cela garantit un nettoyage approprié lors de la suppression d'objets dérivés via des pointeurs de base :
class Animal {
public:
virtual ~Animal() {} // Destructeur virtuel
virtual void speak() {}
};Essayez vous-même
#include <iostream>
#include <string>
#include "Media.h"
#include "AudioTrack.h"
using namespace std;
// TODO: Définir la classe VideoClip ici qui hérite publiquement de Media
// - Membre privé int duration (en secondes)
// - Le constructeur prend title et duration (duration par défaut à 120)
// - Surcharger play() pour afficher : Playing video: <title> (<duration>s)
// - Le destructeur affiche : VideoClip [<title>] destroyed
class VideoClip : public Media {
private:
int duration;
public:
// TODO: Implémenter le constructeur
VideoClip(const std::string& t, int d = 120) : Media(t) {
// TODO: Stocker la duration
}
// TODO: Surcharger la méthode play()
void play() override {
// TODO: Implémenter cette méthode
}
// TODO: Implémenter le destructeur
~VideoClip() {
// TODO: Implémenter ce destructeur
}
};
int main() {
// Lire les entrées
string audioTitle;
string artist;
string videoTitle;
getline(cin, audioTitle);
getline(cin, artist);
getline(cin, videoTitle);
// TODO: Créer une portée de bloc avec des accolades
// À l'intérieur du bloc :
// 1. Créer un tableau de trois pointeurs Media*
// 2. Allouer dynamiquement :
// - Un objet Media de base avec le titre "Generic Media"
// - Un objet AudioTrack avec le titre et l'artiste saisis
// - Un objet VideoClip avec le titre de la vidéo saisi
// 3. Parcourir le tableau et appeler play() sur chaque pointeur
// 4. Supprimer tous les objets dans l'ordre inverse
// TODO: Après le bloc, afficher : Playback complete!
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