• 50 heures
  • Difficile

Ce cours est visible gratuitement en ligne.

Ce cours existe en livre papier.

Vous pouvez obtenir un certificat de réussite à l'issue de ce cours.

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Mis à jour le 19/02/2019

Mettez en oeuvre le polymorphisme

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Vous avez bien compris le chapitre sur l'héritage ? C'était un chapitre relativement difficile. Je ne veux pas vous faire peur mais celui que vous êtes en train de lire est du même acabit. C'est sans doute le chapitre le plus complexe de tout le cours mais vous allez voir qu'il va nous ouvrir de nouveaux horizons très intéressants.

Mais au fait, de quoi allons-nous parler ? Le titre est simplement « le polymorphisme », ce qui ne nous avance pas vraiment.
Si vous avez fait un peu de grec, vous êtes peut-être à même de décortiquer ce mot. « Poly » signifie « plusieurs », comme dans polygone ou polytechnique, et « morphe » signifie « forme » comme dans… euh… amorphe ou zoomorphe.
Nous allons donc parler de choses ayant plusieurs formes. Ou, pour utiliser des termes informatiques, nous allons créer du code fonctionnant de différentes manières selon le type qui l'utilise.

La résolution des liens

Commençons en douceur avec un peu d'héritage tout simple. Vous en avez marre de notre RPG ? Moi aussi. Prenons un autre exemple pour varier un peu. Attaquons donc la création d'un programme de gestion d'un garage et des véhicules qui y sont stationnés. Imaginons que notre fier garagiste sache réparer à la fois des voitures et des motos.
Dans son programme, il aurait les classes suivantes :Vehicule,Voitureet 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
};

L'exemple est bien sûr simplifié au maximum : il manque beaucoup de méthodes, d'attributs ainsi que les constructeurs. Je vous laisse compléter selon vos envies.
Le corps des fonctionsaffiche()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. Et si vous avez bien suivi le chapitre précédent, vous aurez remarqué que j'utilise ici le masquage pour redéfinir la fonctionaffiche()deVehiculedans les deux classes filles.

Essayons donc ces fonctions avec un petitmain()tout bête :

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 cela va venir.

La résolution statique des liens

Créons une fonction supplémentaire qui reçoit en paramètre unVehiculeet modifions lemain()afin d'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;
}

A 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 fonctionpresenter()reçoit en argument unVehicule. Ce peut être un objet réellement de typeVehiculemais aussi uneVoitureou, comme dans l'exemple, uneMoto. Souvenez-vous de la dérivation de type introduite au chapitre précédent.
Ce qui est important c'est que, pour le compilateur, à l'intérieur de la fonction, on manipule unVehicule. Peu importe sa vraie nature. Il va donc appeler la « versionVehicule» de la méthodeafficher()et pas la « versionMoto» 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 fonctionpresenter(), pas moyen de savoir ce que sont réellement les véhicules reçus en argument.

En termes techniques, on parle de résolution statique des liens. La fonction reçoit unVehicule, c'est donc toujours la « versionVehicule» des méthodes qui sera utilisée.

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

Ce qu'on aimerait, c'est que la fonctionpresenter()appelle la bonne version de la méthode. C'est-à-dire qu'il faut que la fonction connaisse la vraie nature duVehicule. C'est ce qu'on appelle la résolution dynamique des liens. Lors de l'exécution, le programme utilise la bonne version des méthodes car il sait si l'objet est de type mère ou de type fille.

Pour faire cela, il faut deux ingrédients :

  • utiliser un pointeur ou une référence ;

  • utiliser des méthodes virtuelles.

Les fonctions virtuelles

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

Déclarez une méthode virtuelle…

Cela a l'air effrayant en le lisant mais c'est très simple. Il suffit d'ajouter le mot-clévirtualdans le prototype de la classe (dans le fichier.hdonc). 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
};

Jusque là, rien de bien difficile. Notez bien qu'il n'est pas nécessaire que toutes les méthodes soient virtuelles. Une classe peut très bien proposer des fonctions « normales » et d'autres virtuelles.

… et utilisez 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 fonctionpresenter()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 fonctionpresenter()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 fonctionpresenter()a pu correctement choisir la méthode à appeler.

On aurait obtenu le même comportement avec des pointeurs à la place des références, comme sur le schéma suivante :

Fonctions virtuelles et pointeurs
Fonctions virtuelles et pointeurs

Un même morceau de code a eu deux comportements différents suivant le type passé en argument. C'est donc du polymorphisme. On dit aussi que les méthodes affiche()ont un comportement polymorphique.

Les méthodes spéciales

Bon, assez parlé. À mon tour de vous poser une petite question de théorie :

Quelles sont les méthodes d'une classe qui ne sont jamais héritées ?

La réponse est simple :

  • tous les constructeurs ;

  • le destructeur.

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

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. D'où la règle suivante : on ne peut pas appeler de méthode virtuelle dans un constructeur. Si on essaye quand même, la résolution dynamique des liens ne se fait pas.

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 lignev->affiche()affiche donc le message que l'on souhaitait. Le problème de ce programme se situe au moment dudelete. Nous avons un pointeur mais la méthode appelée n'est pas virtuelle. C'est donc le destructeur deVehiculequi est appelé et pas celui deVoiture!
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 ! Cela nous permet de formuler une nouvelle règle importante : un destructeur doit toujours être virtuel si on utilise le polymorphisme.

Le code amélioré

Ajoutons 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()
{}

Nous sommes donc prêts à aborder un exemple concret d'utilisation du polymorphisme. Attachez vos ceintures !

Les collections hétérogènes

Je vous ai dit tout au début du chapitre que nous voulions créer un programme de gestion d'un garage. Par conséquent, nous allons devoir gérer une collection de voitures et de motos. Nous ferons donc appel à… des tableaux dynamiques !

vector<Voiture> listeVoitures;
vector<Moto> listeMotos;

Bien ! Mais pas optimal. Si notre ami garagiste commence à recevoir des commandes pour des scooters, des camions, des fourgons, des vélos, etc. il va falloir déclarer beaucoup de vectors. Cela veut dire qu'il va falloir apporter de grosses modifications au code à chaque apparition d'un nouveau type de véhicule.

Le retour des pointeurs

Il serait bien plus judicieux de mettre le tout dans un seul tableau ! Comme les motos et les voitures sont des véhicules, on peut déclarer un tableau de véhicules et mettre des motos dedans.
Mais si nous procédons ainsi, nous allons alors perdre la vraie nature des objets. Souvenez-vous des deux ingrédients du polymorphisme ! Il nous faut donc un tableau de pointeurs ou un tableau de références. On ne peut pas créer un tableau de références (rappelez-vous, les références ne sont que des étiquettes), nous allons donc devoir utiliser des pointeurs.

Vous vous rappelez du chapitre sur les pointeurs ? Je vous avais présenté trois cas d'utilisations. En voici donc un quatrième. J'espère que vous ne m'en voulez pas trop de ne pas en avoir parlé avant…

int main()
{
    vector<Vehicule*> listeVehicules;
    return 0;
}

C'est ce qu'on appelle une collection hétérogène puisqu'elle contient, d'une certaine manière, des types différents.

Utilisez la collection

Commençons par remplir notre tableau. Comme nous allons accéder à nos véhicules uniquement via les pointeurs, nous n'avons pas besoin d'étiquettes sur nos objets et nous pouvons utiliser l'allocation dynamique pour les créer. En plus, cela nous permet d'avoir directement un pointeur à mettre dans notre vector.

int main()
{
    vector<Vehicule*> listeVehicules;

    listeVehicules.push_back(new Voiture(15000, 5));
    //J'ajoute à ma collection de véhicules une voiture
    //Valant 15000 euros et ayant 5 portes
    listeVehicules.push_back(new Voiture(12000, 3));  //…
    listeVehicules.push_back(new Moto(2000, 212.5));
    //Une moto à 2000 euros allant à 212.5 km/h

    //On utilise les voitures et les motos
   
    return 0;
}

La figure suivante représente notre tableau.

Une collection hétérogène
Une collection hétérogène

Les voitures et motos ne sont pas réellement dans les cases. Ce sont des pointeurs. Mais en suivant les flèches, on accède aux véhicules.

Bien ! Mais nous venons de faire une grosse faute ! Chaque fois que l'on utilise new, il faut utiliser deletepour vider la mémoire. Nous allons donc devoir faire appel à une boucle pour libérer la mémoire allouée.

int main()
{
    vector<Vehicule*> listeVehicules;

    listeVehicules.push_back(new Voiture(15000, 5));
    listeVehicules.push_back(new Voiture(12000, 3));
    listeVehicules.push_back(new Moto(2000, 212.5));  

    //On utilise les voitures et les motos
   
    for(int i(0); i<listeVehicules.size(); ++i)
    {
        delete listeVehicules[i];  //On libère la i-ème case mémoire allouée
        listeVehicules[i] = 0;  //On met le pointeur à 0 pour éviter les soucis
    }

    return 0;
}

Il ne nous reste plus qu'à utiliser nos objets. Comme c'est un exemple basique, ils ne savent faire qu'une seule chose : afficher des informations. Mais essayons quand même !

int main()
{
    vector<Vehicule*> listeVehicules;

    listeVehicules.push_back(new Voiture(15000, 5));
    listeVehicules.push_back(new Voiture(12000, 3));
    listeVehicules.push_back(new Moto(2000, 212.5));  

    listeVehicules[0]->affiche();
    //On affiche les informations de la première voiture
    
    listeVehicules[2]->affiche();
    //Et celles de la moto
   
    for(int i(0); i<listeVehicules.size(); ++i)
    {
        delete listeVehicules[i];  //On libère la i-ème case mémoire allouée
        listeVehicules[i] = 0;  //On met le pointeur à 0 pour éviter les soucis
    }

    return 0;
}

Je vous invite, comme toujours, à tester. Voici ce que vous devriez obtenir :

Ceci est une voiture avec 5 portes valant 15000 euros.
Ceci est une moto allant a 212.5 km/h et valant 2000 euros.

Ce sont les bonnes versions des méthodes qui sont appelées ! Cela ne devrait pas être une surprise à ce stade. Nous avons des pointeurs (ingrédient 1) et des méthodes virtuelles (ingrédient 2).

Je vous propose d'améliorer un peu ce code en ajoutant les éléments suivants :

  • Une classeCamionqui aura comme attribut le poids qu'il peut transporter.

  • Un attribut représentant l'année de fabrication du véhicule. Ajoutez aussi des méthodes pour afficher cette information.

  • Une classeGaragequi aura comme attribut levector<Vehicule*>et proposerait des méthodes pour ajouter/supprimer des véhicules ou pour afficher des informations sur tous les éléments contenus.

  • Une méthodenbrRoues()qui renvoie le nombre de roues des différents véhicules.

Après ce léger entraînement, terminons ce chapitre avec une évolution de notre petit programme.

Les fonctions virtuelles pures

Avez-vous essayé de programmer la méthodenbrRoues()du mini-exercice ? Si ce n'est pas le cas, il est encore temps de le faire. Elle va beaucoup nous intéresser dans la suite.

Le problème des roues

Comme c'est un peu répétitif, je vous donne ma version de la fonction pour les classes Vehiculeet Voitureuniquement.

class Vehicule
{
    public:
    Vehicule(int prix);           
    virtual void affiche() const;
    virtual int nbrRoues() const;  //Affiche le nombre de roues du véhicule
    virtual ~Vehicule();         

    protected:
    int m_prix;
};

class Voiture : public Vehicule
{
    public:
    Voiture(int prix, int portes);
    virtual void affiche() const;
    virtual int nbrRoues() const;  //Affiche le nombre de roues de la voiture
    virtual ~Voiture();

    private:
    int m_portes;
};

Du côté du.h, pas de souci. C'est le corps des fonctions qui risque de poser problème.

int Vehicule::nbrRoues() const
{  
  //Que mettre ici ????
}

int Voiture::nbrRoues() const
{
    return 4;
}

Vous l'aurez compris, on ne sait pas vraiment quoi mettre dans la « version Vehicule» de la méthode. Les voitures ont 4 roues et les motos 2 mais, pour un véhicule en général, on ne peut rien dire ! On aimerait bien ne rien mettre ici ou carrément supprimer la fonction puisqu'elle n'a pas de sens.
Mais si on ne déclare pas la fonction dans la classe mère, alors on ne pourra pas l'utiliser depuis notre collection hétérogène. Il nous faut donc la garder ou au minimum dire qu'elle existe mais qu'on n'a pas le droit de l'utiliser. On souhaiterait ainsi dire au compilateur : « Dans toutes les classes filles de Vehicule, il y a une fonction nommée nbrRoues()qui renvoie un intet qui ne prend aucun argument mais, dans la classe Vehicule, cette fonction n'existe pas. »

C'est ce qu'on appelle une méthode virtuelle pure.

Pour déclarer une telle méthode, rien de plus simple. Il suffit d'ajouter « = 0 » à la fin du prototype.

class Vehicule
{
    public:
    Vehicule(int prix);           
    virtual void affiche() const;
    virtual int nbrRoues() const = 0;  //Affiche le nombre de roues du véhicule
    virtual ~Vehicule();         

    protected:
    int m_prix;
};

Et évidemment, on n'a rien à écrire dans le .cpppuisque, justement, on ne sait pas quoi y mettre. On peut carrément supprimer complètement la méthode. L'important étant que son prototype soit présent dans le.h.

Les classes abstraites

Une classe qui possède au moins une méthode virtuelle pure est une classe abstraite. Notre classeVehiculeest donc une classe abstraite.

Pourquoi donner un nom spécial à ces classes ? Eh bien parce qu'elles ont une règle bien particulière : on ne peut pas créer d'objet à partir d'une classe abstraite.

Oui, oui, vous avez bien lu ! La ligne suivante ne compilera pas.

Vehicule v(10000); //Création d'un véhicule valant 10000 euros.

Dans le jargon des programmeurs, on dit qu'on ne peut pas créer d'instance d'une classe abstraite.
La raison en est simple : si je pouvais créer un Vehicule, alors je pourrais essayer d'appeler la fonctionnbrRoues()qui n'a pas de corps et ceci n'est pas possible.
Par contre, je peux tout à fait écrire le code suivant :

int main()
{
    Vehicule* ptr(0);  //Un pointeur sur un véhicule
    
    Voiture caisse(20000,5);
    //On crée une voiture
    //Ceci est autorisé puisque toutes les fonctions ont un corps
    
    ptr = &caisse;  //On fait pointer le pointeur sur la voiture

    cout << ptr->nbrRoues() << endl;
    //Dans la classe fille, nbrRoues() existe, ceci est donc autorisé

    return 0;
}

Ici, l'appel à la méthode nbrRoues()est polymorphique puisque nous avons un pointeur et que notre méthode est virtuelle. C'est donc la « versionVoiture» qui est appelée. Donc même si la « versionVehicule» n'existe pas, il n'y a pas de problèmes.

Si l'on veut créer une nouvelle sorte de Vehicule(Camionpar exemple), on sera obligé de redéfinir la fonction nbrRoues(), sinon cette dernière sera virtuelle pure par héritage et, par conséquent, la classe sera abstraite elle aussi.

On peut résumer les fonctions virtuelles de la manière suivante :

  • une méthode virtuelle peut être redéfinie dans une classe fille ;

  • une méthode virtuelle pure doit être redéfinie dans une classe fille.

Dans la bibliothèque Qt, que nous allons très bientôt aborder, il y a beaucoup de classes abstraites. Il existe par exemple une classe par sorte de bouton, c'est-à-dire une classe pour les boutons normaux, une pour les cases à cocher, etc. Toutes ces classes héritent d'une classe nommée QAbstractButton, qui regroupe des propriétés communes à tous les boutons (taille, texte, etc.). Mais comme on ne veut pas autoriser les utilisateurs à mettre des QAbstractButtonsur leurs fenêtres, les créateurs de la bibliothèque ont rendu cette classe abstraite.

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.

  • Si l'on ne sait pas quoi mettre dans le corps d'une méthode de la classe mère, on peut la déclarer virtuelle pure.

  • Une classe avec des méthodes virtuelles pures est dite abstraite. On ne peut pas créer d'objet à partir d'une telle classe.

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