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, C++ détermine quelle version appeler en fonction du type réel de l’objet lors de l’exécution, 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!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 remplacer une fonction virtuelle, en détectant 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 recherche cachée 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.
Lorsque vous appelez une fonction virtuelle via un pointeur, le programme recherche l’adresse de fonction correcte dans la vtable au moment de l’exécution. Cela s’appelle la liaison dynamique. Cela ajoute une légère surcharge par rapport aux appels de fonctions ordinaires, mais permet un comportement polymorphe puissant.
Une règle importante : si une classe possède des fonctions virtuelles et est utilisée comme classe de base, son destructeur doit également être virtuel. Cela garantit un nettoyage correct lors de la suppression d’objets dérivés via des pointeurs vers la classe 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 multimédias où chacun est lu différemment, et voir comment les fonctions virtuelles permettent d'obtenir le comportement correct même lorsqu'elles sont appelées via des pointeurs vers la classe de base.
Vous organiserez votre code sur trois fichiers :
Media.h: définir une classe de baseMediaqui représente tout média lisible :- un membre
std::string titleprotégé - un constructeur qui reçoit un titre et le stocke
- une méthode virtual
play()qui affiche :Playing media: <title> - un destructeur virtuel qui affiche :
Media [<title>] destroyed
- un membre
AudioTrack.h: définir une classeAudioTrackqui hérite publiquement deMedia:- un membre privé
std::string artist - un constructeur qui reçoit un titre et un artiste, transmet le titre à la classe de base et stocke l'artiste
- redéfinir
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: lire trois entrées (chacune sur une ligne distincte) :- titre de la piste audio (chaîne)
- nom de l'artiste (chaîne)
- titre de la vidéo (chaîne)
Définir directement dans main.cpp une classe
VideoClipqui hérite publiquement deMedia:- un membre privé
int duration(en secondes) - un constructeur qui reçoit un titre et une durée (avec une durée par défaut de 120)
- redéfinir
play()pour afficher :Playing video: <title> (<duration>s) - un destructeur qui affiche :
VideoClip [<title>] destroyed
Créer un tableau de trois pointeurs
Media*dans la portée d'un bloc. Allouer 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
Parcourir le tableau et appeler
play()sur chaque pointeur. Supprimer ensuite tous les objets dans l'ordre inverse. Après le bloc, afficher :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 de la classe dérivée appropriée grâce aux fonctions virtuelles. Observez également comment le destructeur virtuel garantit un nettoyage correct : les destructeurs de la classe dérivée et de la classe de base sont tous deux exécutés lors de la suppression via un pointeur vers la classe de base.
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)
// - Override play() to print: Playing video: <title> (<duration>s)
// - Destructor prints: 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: Redéfinir 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 un bloc de portée avec des accolades
// À l'intérieur du bloc :
// 1. Créer un tableau de trois pointeurs Media*
// 2. Allouer dynamiquement :
// - A base Media object with title "Generic Media"
// - Un objet AudioTrack avec le titre et l'artiste en entrée
// - Un objet VideoClip avec le titre vidéo en entrée
// 3. Parcourir le tableau et appeler play() sur chaque pointeur
// 4. Supprimer tous les objets dans l'ordre inverse
// TODO: After the block, print: 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 externesGénération et compilation en C++Fichiers d’en-tête et fichiers sourceEspaces de noms et portéeIntroduction à la POO en C++Classes et objetsLe pointeur « this »Méthodes (fonctions membres)Attributs (membres de données)Bases des constructeurs et destructeursRécapitulatif - Calculatrice simple4Propriétés de classe
Membres d’instance et statiquesAccesseurs et mutateursFonctions membres constMot-clé mutableMéthodes et variables statiquesFonctions et classes amiesRécapitulatif - Gestionnaire de compte bancaire7Héritage
Héritage de baseNiveaux d’accès de l’héritageOrdre d’appel des constructeurs et destructeursRedéfinition des méthodesFonctions virtuelles et VTableHéritage multipleHéritage virtuelRécapitulatif : hiérarchie des employés10Vue d’ensemble de la STL
Vue d’ensemble et philosophie de la STLConteneurs de la STLItérateursAlgorithmes de la STLFoncteurs et expressions lambdaRécapitulatif - fréquence des mots13Modèles de conception, partie 1
Introduction aux modèles de conceptionModèle SingletonFabrique et fabrique abstraiteModèle BuilderModèle ObserverModèle Strategy2Gestion de la mémoire
Mémoire de pile ou de tasPointeurs et référencesMémoire dynamique (new/delete)Pointeurs intelligents 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 de l’informationStruct vs classeClasses imbriquées et internesRécapitulatif - Système de gestion des dossiers étudiants8Polymorphisme
Polymorphisme à la compilation vs à l’exécutionSurcharge de fonctionsRetour sur les fonctions virtuellesFonctions virtuelles puresClasses abstraitesConception d’interfaces en C++Conversion dynamique et RTTIRécapitulatif - Calculateur de formes3Constructeurs et destructeurs
Constructeur par défautConstructeur paramétréConstructeur de copieConstructeur de déplacementListes d’initialisation des constructeursConstructeurs déléguésApprofondissement des destructeursRègle des trois / cinq / zéroRécapitulatif - Classe String6Surcharge des opérateurs
Introduction à la surcharge des 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 des templatesTemplates variadiquesBases de SFINAE et des traits de typesRécapitulatif - Conteneur génériqueEntraînez-vous par vous-même : Compilateur C++ en ligne