Aller au menu - Aller au contenu

Icône Le polymorphisme

Mise à jour : 27/05/2011
Difficulté : Difficile Difficile Durée d'étude : 1 jour Creative Commons BY-NC-SA
75 639 visites depuis 7 jours, dont 1 194 sur ce chapitre classé 5/786
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 un petit peu ce mot. « Poly » signifie « plusieurs » comme dans polygone ou polytechnique et « morphe » signifie « forme » comme... euh... amorphe ou zoomorphe. :)
Nous allons donc parler de choses ayant plusieurs formes. Ou pour utiliser des termes d'informatique, nous allons créer du code fonctionnant de manière différente selon le type qui l'utilise.

Nous verrons plus tard dans ce cours un autre moyen de faire cela, grâce aux templates. Pour l'instant, nous allons nous contenter de créer des fonctions qui s'exécutent différemment selon qu'on utilise un objet d'une classe mère ou d'une classe fille.

Courage ! C'est le dernier chapitre vraiment difficile avant d'attaquer la pratique du C++ et notamment la création de programmes avec des vraies fenêtres à la place de cette vieille console.

Je vous conseille vivement de relire le chapitre sur les pointeurs avant de continuer.
Sommaire du chapitre :
Icône du chapitre
Chapitre précédent Sommaire Chapitre suivant

La résolution des liens

Commençons en douceur avec un peu d'héritage tout simple. :soleil: 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, Voiture et Moto.

Code : C++ - Définition des classes
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 simplifié au maximum. Il manque bien sûr beaucoup de méthodes, d'attributs et les constructeurs. Je vous laisse compléter selon vos envies.
Le corps des fonctions affiche() est le suivant :

Code : C++ - Corps des fonctions affiche()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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 reconnu que j'utilise ici le masquage pour redéfinir la fonction affiche() de Vehicule dans les deux classes filles.

Essayons donc ces fonctions avec un petit main tout bête.

Code : C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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. :pirate:

La résolution statique des liens



Créons une fonction supplémentaire qui reçoit en paramètre un Vehicule et modifions le main de sorte à utiliser la fonction :

Code : C++ - Une fonction en apparence inoffensive
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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;
}


Et testons là. A priori, rien n'a changé. Les messages affichés devraient être les mêmes. Voyons ça :

Code : Console
Ceci est un vehicule.
Ceci est un vehicule.


:waw: :waw: 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. Ça peut être un vrai objet de type Vehicule, mais aussi une Voiture ou comme dans l'exemple, une Moto. 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, il 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 si 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.

En termes techniques, on parle de résolution statique des liens. La fonction reçoit un Vehicule, c'est donc toujours la "version Vehicule" des méthodes qui sera utilisée.

C'est le type de la variable qui détermine quelle fonction membre appeler et pas sa vraie nature.


Mais vous vous doutez bien que si je vous parle de tout ça, c'est qu'il y a moyen de changer ce comportement. ;)

La résolution dynamique des liens



Ce qu'on aimerait nous, c'est que la fonction presenter() appelle la bonne version de la méthode. C'est-à-dire qu'il faut que la fonction connaisse la vraie nature du Vehicule. C'est ce qu'on appelle la résolution dynamique des liens. Lors de l'exécution, le programme va utiliser la bonne version des méthodes car il saura 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.

Si ces deux ingrédients ne sont pas réunis, alors on retombe dans le premier cas et l'ordinateur n'aura aucun moyen d'appeler la bonne méthode.

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éclarer une méthode virtuelle...



Ça a l'air effrayant en le lisant, mais c'est très simple. Il suffit d'ajouter le mot-clé virtual dans le prototype de la classe (dans le fichier .h donc). Donc pour notre garage, cela donne :

Code : C++ - Quelques méthodes virtuelles
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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
};


Il n'est pas nécessaire de mettre le virtual devant les méthodes des classes filles. Elles sont automatiquement virtuelles par héritage.
Personnellement, je préfère le mettre pour me souvenir de leur particularité.


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.

Il ne faut pas mettre virtual dans le fichier .cpp, mais uniquement dans le .h Si vous essayez, votre compilateur se vengera en vous insultant copieusement !


... et utiliser 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.

Code : C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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;
}


J'ai aussi ajouté un const. Comme on ne modifie pas l'objet dans la fonction, autant le faire savoir au compilateur et au programmeur en déclarant la référence constante.


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

Code : Console
Ceci est un vehicule.
Ceci est une moto.


:magicien: Ça marche ! La fonction presenter() a bien appelé la bonne version de la méthode. En utilisant des fonctions virtuelles et 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. Comme sur le schéma suivant.

Image utilisateur


Un même bout de code a eu deux comportements différents selon 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é. A 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 ?

Secret (cliquez pour afficher)
  • Tous les constructeurs.
  • Le destructeur.


Vous avez trouvé ? C'est bien. Toutes les autres méthodes peuvent être héritées et peuvent avoir un comportement polymorphique si on le souhaite. 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équence 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éthodes virtuelles 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.

Code : C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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 notre voiture

    return 0;
}


Nous avons un pointeur et une méthode virtuelle. La ligne v->affiche() va donc afficher 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 va pas planter. 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é, peut-être que vos moteurs ne vont pas s'arrêter. Cela peut vite devenir dramatique.

Il faut donc impérativement appeler le bon destructeur. Et pour se faire, une seule solution : rendre le destructeur virtuel ! Ce qui 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.

Code : C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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 :

Code : C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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êt à 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. Nous allons donc devoir gérer une collection de voitures et de motos. Nous allons donc utiliser des ... tableaux dynamiques !

Code : C++ - Des listes de voitures et de motos
1
2
vector<Voiture> listeVoitures;
vector<Moto> listeMotos;


Bien ! Mais pas optimal. :euh: 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 faire de grosses modifications au code à chaque fois qu'un nouveau type de véhicule apparaît.

Le retour des pointeurs



Il serait bien mieux 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 on fait ça, alors nous allons 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'utilisation. En voici donc un quatrième. J'espère que vous ne m'en voulez pas trop de ne pas en avoir parlé avant... ;)

Code : C++ - Un tableau de pointeurs sur des véhicules
1
2
3
4
5
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.

Utiliser 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, nous pouvons utiliser l'allocation dynamique pour créer nos objets. En plus, cela nous permet d'avoir directement un pointeur à mettre dans notre vector.

Code : C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
int main()
{
    vector<Vehicule*> listeVehicules;

    listeVehicules.push_back(new Voiture(15000, 5));  //J'ajoute une voiture valant 15000 euros 
                                                      // et ayant 5 portes à ma collection de véhicules
    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;
}


Voici à quoi ressemble notre tableau :

Image utilisateur


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

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

Code : C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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;       //Et 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 !

Code : C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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;       //Et on met le pointeur à 0 pour éviter les soucis
    }

    return 0;
}


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

Code : Console
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 ! :D Ce 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 classe Camion qui 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 classe Garage qui aura comme attribut le vector<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éthode nbrRoues() 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éthode nbrRoues() du mini-exercice ? Non ! 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 véhicules et voiture uniquement.

Code : C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 soucis. C'est le corps des fonctions qui risque de poser problème.

Code : C++
1
2
3
4
5
6
7
8
9
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 aura une fonction nommée nbrRoues() qui renvoie un int et 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.

Code : C++ - Une fonction virtuelle pure
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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 .cpp puisque 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 classe Vehicule est 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.

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

Code : C++
1
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 est simple. Si je peux créer un Vehicule, alors je pourrais essayer d'appeler la fonction nbrRoues() qui n'a pas de corps et ceci n'est pas possible.
Je peux par contre tout à fait écrire le code suivant :

Code : C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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 donc ceci est 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 "version Voiture" qui est appelée. Donc même si la "version Vehicule" n'existe pas, il n'y a pas de problèmes.

Si l'on veut créer une nouvelle sorte de Vehicule (Camion par 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 abstraite 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, ...). Mais comme on ne veut pas autoriser les utilisateurs à mettre des QAbstractButton sur leurs fenêtres, les créateurs de la bibliothèque ont rendu cette classe abstraite. ;)

Q.C.M.

Où se place le mot-clé virtual ?
Quelles méthodes ne peuvent pas être virtuelles ?
Combien d'instances de classes abstraites peut-on créer ?
Quels sont les ingrédients nécessaires à la résolution dynamique des liens ?

Statistiques de réponses au QCM

Pffouu... :-°

Nous voilà arrivé au bout. C'était un chapitre vraiment complexe et vous êtes certainement dans la même situation que moi lorsqu'on m'a parlé pour la première fois du polymorphisme, vous êtes perplexe et pas sûr d'avoir bien compris.
Vous verrez, avec un peu de pratique, cela va presque devenir naturel.

Courage ! Nous sommes proches de la fin des chapitres théoriques. Vous verrez que faire des programmes avec des fenêtres sera un vrai régal après ça. ;)
Chapitre précédent Sommaire Chapitre suivant

Partager

11 commentaires pour "Le polymorphisme"
Note moyenne : 3.85 / 4 (1749 votes)
Pseudo Commentaire
Hors ligne ouzair # Posté le 28/09/2011 à 22:34:53
Avatar

Ville : Virginia water
Pays : Royaume-Uni

Bon Tutoriel. Cependant j'ai trouve le chapitre sur la surcharge d'operateurs plus... complexe. Merci Mateo et Nanoc.
Hors ligne Innocenti # Posté le 01/11/2011 à 21:07:10

J'ai finalement reussi à créer la méthode pour ajouter des vehicules dans Garage, c'est très simple mais j'en ai mis du temps :p

Prototype :
Code : C++
1
Garage& ajouterVehicule(Vehicule *v);


Garage.cpp
Code : C++
1
2
3
4
5
Garage& Garage::ajouterVehicule(Vehicule *v)
{
	m_listeVehicules.push_back(v);
	return *this; // On renvoie l'objet Garage modifié (tableau de pointeurs)
}


main.cpp
Code : C++
1
2
Garage monGarage;
monGarage.ajouterVehicule(new Voiture(20000, 4)); // 20.000 = m_prix, 4 = m_portes
Hors ligne MonsieurPlus # Posté le 15/01/2012 à 18:20:45

A-t-on vraiment besoin (dans le code source au tout début) de taper "void affiche() const;" pour chaque classe fille? o_O
Normalement elles héritent toutes de cette fonction automatiquement non?
Hors ligne Nanoc # Posté le 15/01/2012 à 18:39:47
Aimez-vous le C++ ?
Avatar
Validateurs

Ville : Durham
Pays : Royaume-Uni
Études : EPFL

Elles ne font pas toutes la même chose. Chaque classe fille s'affiche d'une manière différente.
 
Connecté Kiwywywy # Posté le 24/05/2012 à 14:27:00

Bonjour,

Je suis à la fin de la partie sur "les collection hétérogènes", et je me pose une question :

Secret (cliquez pour afficher)
N'y aurais t'il pas un piège dans la consigne :Une classe Garage qui aura comme attribut le vector<Vehicule*> et proposerait des méthodes pour ajouter/supprimer des véhicules ou pour afficher des informations sur tous les éléments contenus. ?

J'ai commencé par faire comme indiqué une classe "classique", puis je me suis renseigné sur vector qui est une classe qui gère déjà les ajout/suppression. Donc est ce que ce n'est pas superflus ? Ou alors il suffit de créer un vector de vector ?


Merci.

Voir tous les commentaires