Polymorphisme par héritage

Nous allons réutiliser notre exemple du chapitre 6, où nous avions donné deux implantations différentes d’un type liste reproduisant (extérieurement) une liste chaînée. La représentation interne de ces classes n’était pas forcément une vraie liste chaînée.

Il se peut que dans un même programme on souhaite disposer des deux types de listes chaînées. Une première solution consiste à donner des noms différents aux deux classes (et non identiques comme on l’a fait au chapitre 6), et à utiliser l’une ou l’autre selon les besoins.

Cela pose toutefois des problèmes spécifiques. En effet, si l’on veut par exemple utiliser un tableau de listes (ou plutôt de pointeurs de listes), il faudra choisir un des deux types, et faire constamment des changements de types, avec les risques que cela suppose.

Une méthode bien meilleure consiste à faire dériver l’une des classes de l’autre, comme ceci :

class liste {     noeud* courant;     protected :     int nombre;     public :     liste() { nombre = 0; courant = 0; }     liste(int n, const element*);    // consructeur avec table     virtual ~liste();     virtual void avance(int combien = 1);     void recule(int combien = 1)         { avance(-combien); }     virtual element& valeur(void)         { if (courant) return courant->contenu(); }     unsigned nombre_elt(void) { return  nombre; }     void affiche(unsigned combien = 65535);     virtual int insere(const element&);     virtual void supprime(int n = 1);     };class listetab : public liste {     element *tab, *courant;     public :     listetab() { courant = tab = 0; }     listetab(int n, const element*);    // consructeur avec table     ~listetab();     void avance(int combien = 1);     element& valeur(void)         { if (courant) return *courant; }     int insere(const element&);     void supprime(int n = 1);     };

On observe d’abord que l’on gagne du temps et du code, puisque certaines méthodes n’ont pas besoin d’être redéfinies ; il suffit généralement de bien choisir les méthodes virtuelles.

Exercice 8.1

On a supposé que la méthode d’affichage utilisait la méthode valeur, au lieu de faire référence directement aux membres privés comme c’est le cas dans l’implantation du chapitre 6. Écrire la méthode correspondante.

Voir solution
Exercice 8.2

Quelles sont les méthodes virtuelles et celles qui ne le sont pas dans les classes liste et listetab ? Que pensez-vous de ce choix ?

Voir solution

Cela fait, rien n’empêche plus de définir un tableau de pointeurs :

element table[5] = { 1, 3, 5, 7, 11 };liste *listes[3] = {    new liste(5, table),                         new listetab(2, table +3),                         new listetab(3, table) };for (int i = 0; i < 3; i++)     if (listes[i]) listes[i]->affiche();// ...for (i = 0; i < 3; i++) delete listes[i];

Bien remarquer que ce sont les bonnes fonctions d’affichage et de destruction qui sont appelées dans cet exemple.

Exercice 8.3

Quel est l’affichage produit par cet exemple ? Quelle est la place mémoire totale occupée dans le tas, en fonction de la taille S = sizeof(element) des éléments de liste ?

Voir solution

L’inconvénient de cette méthode simple, c’est que le champ noeud* est inutilisé dans listetab, ce qui gaspille de la mémoire (en faible quantité cependant). Évidemment, on aurait pu le réutiliser pour tenir le rôle de tab par exemple, mais cela aurait exigé de désagréables changements de types à tout moment. En outre, il aurait fallu le déclarer protégé et non privé. Et il est clair que dans tous les exemples la situation ne sera pas si simple.

Un autre inconvénient important résulte de ce qu’il devient impossible de définir le type listetab avant l’autre, et difficile de recommencer avec de nouveaux types de listes. Pour régler ce problème plus élégamment, le langage fournit les classes abstraites.

Précédent Précédent Sommaire Sommaire Suivant Suivant