Menu
Coddy logo textTech

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() {}
};
challenge icon

Défi

Facile

Construisons 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 base Media qui 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
  • AudioTrack.h : Définissez une classe AudioTrack qui hérite publiquement de Media :
    • 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é override pour afficher : Playing audio: <title> by <artist>
    • Un destructeur qui affiche : AudioTrack [<title>] destroyed
  • main.cpp : Lisez trois entrées (chacune sur une ligne séparée) :
    1. Titre de la piste audio (chaîne de caractères)
    2. Nom de l'artiste (chaîne de caractères)
    3. Titre de la vidéo (chaîne de caractères)

    Définissez une classe VideoClip directement dans main.cpp qui hérite publiquement de Media :

    • 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 Media 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

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