Vous venez de découvrir une notion un peu complexe : le polymorphisme. C'est le moment de passer à une application concrète pour comprendre comme l'utiliser par la suite.
Gérez les collections hétérogènes
Je vous ai dit au chapitre précédent 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.
Utilisez 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 (les références ne sont que des étiquettes), nous allons donc devoir utiliser des pointeurs.
int main()
{
vector<Vehicule*> listeVehicules;
return 0;
}
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;
}
Voici, schématiquement, notre tableau :
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 !
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 ! Nous avons des méthodes virtuelles pointeurs (ingrédient 1) et des pointeurs (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 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é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.
Utilisez les fonctions virtuelles pures
Avez-vous essayé de programmer la méthode nbrRoues()
? Si ce n'est pas le cas, il est encore temps de le faire. Elle va beaucoup nous intéresser par 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 Vehicule
et Voiture
uniquement :
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 .hpp
, 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éenbrRoues()
qui renvoie unint
et qui ne prend aucun argument mais, dans la classeVehicule
, cette fonction n'existe pas."
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 supprimer complètement la méthode. L'important étant que son prototype soit présent dans le .hpp
.
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 ?
Oui, oui, vous avez bien lu ! La ligne suivante ne compilera pas :
Vehicule v(10000); //Création d'un véhicule valant 10000 euros.
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 version Vehicule
n'existe pas, il n'y a pas de problème.
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 sera abstraite elle aussi.
À vous de jouer !
Comme le polymorphisme est une notion assez complexe, je vous propose de pratiquer un peu pour vous entraîner. Vous allez devoir créer une classe mère Figure
qui permettra ensuite de créer 4 classes filles afin de travailler l’héritage et le polymorphisme.
Votre mission :
Créer la classe abstraite
Figure
contenant les méthodes virtuelles :afficher()
qui affiche le type de la classe. Exemple :Je suis une figure
;la méthode virtuelle pure
perimetre()
;la méthode virtuelle pure
aire()
.
Créer les 4 classes filles (
Triangle
,Carre
,Rectangle
,Cercle
) qui hériteront de la classe mèreFigure
et qui définissent les trois méthodes virtuelles de la classeFigure
.Créer les constructeurs des 4 classes filles avec les attributs :
Triangle
: base et hauteur ;Carre
: longueur ;Rectangle
: longueur et largeur ;Cercle
: rayon.
Définir l’ensemble des méthodes virtuelles.
Créer un vecteur de
Figure
et ajouter une instance de chaque classe fille.Enfin à l’aide d’une boucle
for
, appeler chacune des méthodes :affiche()
,perimetre()
etaire()
.
Voici une proposition de correction avec deux classes filles : Carre
et Cercle
:
En résumé
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.
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 le prochain chapitre, vous allez voir comment utiliser les éléments statiques et l'amitié. Cela ne vous parle pas ? Tant mieux, on y va !