• 10 heures
  • Difficile

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 10/02/2022

Découvrez la notion de polymorphisme

Vous avez bien compris le chapitre sur l'héritage ? C'était un chapitre relativement difficile. Je ne veux pas vous faire peur, mais ce qui suit est du même acabit. C'est sans doute le moment le plus complexe de tout le cours, mais vous allez voir, ça va vous ouvrir de nouveaux horizons très intéressants.

Je vous conseille de relire le chapitre sur les pointeurs avant de commencer.

Pour commencer, ça veut dire quoi, "polymorphisme" ?

Découvrez la résolution des liens

Commençons en douceur avec un peu d'héritage tout simple. Nous allons créer un programme de gestion d'un garage et des véhicules qui y sont stationnés.

Dans le programme, il y aurait les classes suivantes : Vehicule  , Voiture et Moto  :

class Vehicule
{
    public:
    void affiche() const;  //Affiche une description du Vehicule

    protected:
    int m_prix;  //Chaque véhicule a un prix
};

class Voiture : public Vehicule //Une Voiture EST UN Vehicule
{
    public:
    void affiche() const;

    private:
    int m_portes;  //Le nombre de portes de la voiture
};

class Moto : public Vehicule  //Une Moto EST UN Vehicule
{
    public:
    void affiche() const;
 
    private:
    double m_vitesse;  //La vitesse maximale de la moto
};

Le corps des fonctions affiche()  est le suivant :

void Vehicule::affiche() const
{
    cout << "Ceci est un vehicule." << endl;
}

void Voiture::affiche() const
{
    cout << "Ceci est une voiture." << endl;
}

void Moto::affiche() const
{
    cout << "Ceci est une moto." << endl;
}

Chaque classe affiche donc un message différent.

Essayons donc ces fonctions avec un main() :

int main()
{
    Vehicule v;
    v.affiche();    //Affiche "Ceci est un vehicule."

    Moto m;
    m.affiche();    //Affiche "Ceci est une moto."

    return 0;
}

Je vous invite à tester, vous ne devriez rien observer de particulier. Mais ça va venir.

La résolution statique des liens

Créons une fonction supplémentaire qui reçoit en paramètre un Vehicule  . Nous allons modifier le main() pour utiliser cette fonction :

void presenter(Vehicule v)  //Présente le véhicule passé en argument
{
    v.affiche();
}

int main()
{
    Vehicule v;
    presenter(v);

    Moto m;
    presenter(m);

    return 0;
}

À priori, rien n'a changé. Les messages affichés devraient être les mêmes. Voyons cela :

Ceci est un vehicule.
Ceci est un vehicule.

Le message n'est pas correct pour la moto ! C'est comme si, lors du passage dans la fonction, la vraie nature de la moto s'était perdue, et qu'elle était redevenue un simple véhicule.

Comment est-ce possible ?

Comme il y a une relation d'héritage, nous savons qu'une moto est un véhicule, un véhicule amélioré en quelque sorte puisqu'il possède un attribut supplémentaire. La fonction presenter() reçoit en argument un Vehicule  . Cela peut être un objet de type Vehicule  , mais aussi une Voiture ou une Moto  . Souvenez-vous de la dérivation de type.

Ce qui est important c'est que, pour le compilateur, à l'intérieur de la fonction, on manipule un Vehicule  . Peu importe sa vraie nature. Il va donc appeler la version Vehicule de la méthode afficher()  , et pas la version Moto comme on aurait pu l'espérer.

Dans l'exemple du chapitre précédent, c'est la bonne version qui était appelée puisque, à l'intérieur de la fonction, le compilateur savait s'il avait affaire à un simple personnage ou à un guerrier. Ici, dans la fonction presenter()  , pas moyen de savoir ce que sont réellement les véhicules reçus en argument.

C'est le type de la variable qui détermine quelle fonction membre appeler, et non sa vraie nature. Mais vous vous doutez bien que, si je vous parle de tout cela, c'est qu'il y a un moyen de changer ce comportement.

La résolution dynamique des liens

Pour faire cela, il faut deux ingrédients :

  1. Une méthode virtuelle.

  2. Un pointeur ou une référence.

Déclarez une méthode virtuelle et utilisez une référence

Je vous ai donné la liste des ingrédients, allons-y pour la préparation du menu. Commençons par les méthodes virtuelles.

Ingrédient n° 1 : une méthode virtuelle

Pour déclarer une méthode virtuelle, il suffit d'ajouter le mot-clé virtual dans le prototype de la classe (dans le fichier .hpp  , donc). Pour notre garage, cela donne :

class Vehicule
{
    public:
    virtual void affiche() const;  //Affiche une description du Vehicule

    protected:
    int m_prix;  //Chaque véhicule a un prix
};

class Voiture: public Vehicule  //Une Voiture EST UN Vehicule
{
    public:
    virtual void affiche() const;

    private:
    int m_portes;  //Le nombre de portes de la voiture
};

class Moto : public Vehicule  //Une Moto EST UN Vehicule
{
    public:
    virtual void affiche() const;
 
    private:
    double m_vitesse;  //La vitesse maximale de la moto
};

Ingrédient n° 2 : une référence

Le deuxième ingrédient est un pointeur ou une référence. Vous êtes certainement comme moi, vous préférez la simplicité et, par conséquent, les références. On ne va quand même pas s'embêter avec des pointeurs juste pour le plaisir.

Réécrivons donc la fonction presenter() avec comme argument une référence :

void presenter(Vehicule const& v)  //Présente le véhicule passé en argument
{
    v.affiche();
}

int main()  //Rien n'a changé dans le main()
{
    Vehicule v;
    presenter(v);

    Moto m;
    presenter(m);

    return 0;
}

Voilà. Il ne nous reste plus qu'à tester :

Ceci est un vehicule.
Ceci est une moto.

Cela marche ! La fonction presenter() a bien appelé la bonne version de la méthode. En utilisant des fonctions virtuelles ainsi qu'une référence sur l'objet, la fonction presenter()  a pu correctement choisir la méthode à appeler.

On aurait obtenu le même comportement avec des pointeurs à la place des références :

Fonctions virtuelles et pointeurs
Fonctions virtuelles et pointeurs

Reprenons le même code et voyons dans le screencast suivant comment créer un comportement polymorphique. Nous allons voir comment résoudre étape par étape le problème que nous avons vu au début du chapitre :

Mais au fait, quelles sont les méthodes d'une classe qui ne sont jamais héritées ?

La réponse est simple :

  1. Tous les constructeurs.

  2. Le destructeur.

Toutes les autres méthodes peuvent être héritées et avoir un comportement polymorphique si on le souhaite. Mais qu'en est-il pour ces méthodes spéciales ?

Utilisez les méthodes spéciales

Comprenez le cas des constructeurs

Un constructeur virtuel a-t-il du sens ? Non ! Quand je veux construire un véhicule quelconque, je sais lequel je veux construire. Je peux donc à la compilation déjà savoir quel véhicule construire. Je n'ai pas besoin de résolution dynamique des liens et, par conséquent, pas besoin de virtualité. Un constructeur ne peut pas être virtuel.

Et cela va même plus loin. Quand je suis dans le constructeur, je sais quel type je construis, je n'ai donc à nouveau pas besoin de résolution dynamique des liens.

Comprenez le cas du destructeur

Ici, c'est un petit peu plus compliqué… malheureusement.

Créons un petit programme utilisant nos véhicules et des pointeurs, puisque c'est un des ingrédients du polymorphisme :

int main()
{
    Vehicule *v(0);
    v = new Voiture;
    //On crée une Voiture et on met son adresse dans un pointeur de Vehicule

    v->affiche();  //On affiche "Ceci est une voiture."

    delete v;      //Et on détruit la voiture

    return 0;
}

Nous avons un pointeur et une méthode virtuelle. La ligne v->affiche() affiche donc le message que l'on souhaitait.

Le problème de ce programme se situe au moment du delete  . Nous avons un pointeur, mais la méthode appelée n'est pas virtuelle. C'est donc le destructeur de Vehicule qui est appelé, et pas celui de Voiture  !

Dans ce cas, cela ne porte pas vraiment à conséquence, le programme ne plante pas. Mais imaginez que vous deviez écrire une classe pour le maniement des moteurs électriques d'un robot. Si c'est le mauvais destructeur qui est appelé, vos moteurs ne s'arrêteront peut-être pas. Cela peut vite devenir dramatique.

Il faut donc impérativement appeler le bon destructeur. Et pour ce faire, une seule solution : rendre le destructeur virtuel !

Ajoutez des constructeurs et destructeurs aux classes

En ajoutant donc des constructeurs et des destructeurs à nos classes, tout sera alors correct :

class Vehicule
{
    public:
    Vehicule(int prix);           //Construit un véhicule d'un certain prix
    virtual void affiche() const;
    virtual ~Vehicule();          //Remarquez le 'virtual' ici

    protected:
    int m_prix;
};

class Voiture: public Vehicule
{
    public:
    Voiture(int prix, int portes);
    //Construit une voiture dont on fournit le prix et le nombre de portes
    virtual void affiche() const;
    virtual ~Voiture();

    private:
    int m_portes;
};

class Moto : public Vehicule 
{
    public:
    Moto(int prix, double vitesseMax);
    //Construit une moto d'un prix donné et ayant une certaine vitesse maximale
    virtual void affiche() const;
    virtual ~Moto();
 
    private:
    double m_vitesse;
};

Il faut bien sûr également compléter le fichier source :

Vehicule::Vehicule(int prix)
    :m_prix(prix)
{}

void Vehicule::affiche() const
//J'en profite pour modifier un peu les fonctions d'affichage
{
    cout << "Ceci est un vehicule coutant " << m_prix << " euros." << endl;
}

Vehicule::~Vehicule() //Même si le destructeur ne fait rien, on doit le mettre !
{}

Voiture::Voiture(int prix, int portes)
    :Vehicule(prix), m_portes(portes)   
{}

void Voiture::affiche() const
{
    cout << "Ceci est une voiture avec " << m_portes << " portes et coutant " << m_prix << " euros." << endl;
}

Voiture::~Voiture()
{}

Moto::Moto(int prix, double vitesseMax)
    :Vehicule(prix), m_vitesse(vitesseMax)
{}

void Moto::affiche() const
{
    cout << "Ceci est une moto allant a " << m_vitesse << " km/h et coutant " << m_prix << " euros." << endl;
}

Moto::~Moto()
{}

En résumé

  • Le polymorphisme permet de manipuler des objets d'une classe fille via des pointeurs ou des références sur une classe mère.

  • Deux ingrédients sont nécessaires : des fonctions virtuelles et des pointeurs ou références sur l'objet.

Vous êtes prêts à aborder un exemple concret d'utilisation du polymorphisme. Attachez vos ceintures, c'est l'objet du prochain chapitre (sans mauvais jeu de mots) !

Exemple de certificat de réussite
Exemple de certificat de réussite