next up previous contents
Next: Exemple : fenêtres Up: Héritage Previous: Classes abstraites

Exemple : recherche linéaire

Pour bien comprendre la liaison dynamique et l'héritage, le mieux est de considérer un exemple. Si on considère l'algorithme classique de la recherche linéaire, cet algorithme peut s'appliquer à n'importe quel type pour lequel on a un accès séquentiel aux éléments. Il est a priori inutile d'être obligé de réécrire cet algorithme pour chacun de ces types. La programmation orientée objets le permet. Pour cela, la notion de base est celle de polymorphisme ; dans un langage typé, les arguments des fonctions sont d'un type donné, et, en principe, le compilateur refusera tout appel avec des paramètres d'un autre type. C'est ici qu'intervient l'héritage, on peut regrouper dans un type abstrait tous les types qui sont des collections d'objets avec un accès séquentiel ; de cette façon, comme un élément d'une classe dérivée peut être considéré comme étant du type de base, la fonction qui réalise pourra malgré la vérification du typage s'appliquer à plusieurs types (tous les héritiers de ce type collection avec accès séquentiel). Mais ce n'est pas tout à fait suffisant : il faut aussi que cette fonction utilise des membres de ce type. Par exemple, comme il y a accès séquentiel, elle devra, sans doute, devoir utiliser la fonction permettant justement d'accéder séquentiellement aux éléments du types ; mais ici, il ne s'agit pas d'une fonction d'accès séquentiel qui peut être déterminée à la compilation, mais de celle correspondant au type sur laquelle cette fonction s'applique au moment de l'appel. C'est, bien sûr, là que s'applique la liaison dynamique.

Pour simplifier (on verra dans le chapitre suivant comment généraliser)gif, on considère que des types correspondant à des collections d'entier avec accès séquentiel. On appellera, Coll_Int ce type collection d'entiers :

#ifndef _Coll_Int
#define _Coll_Int 
 class Coll_Int{
 public:
    virtual int  operator[](int)const =0;
    virtual int &  operator[](int) =0;
    virtual int taille()const =0;
};
#endif
Cette classe est un classe abstraite (elle ne peut pas s'instancier). Elle ne fait que définir une interface pour ces collections d'entier avec accès séquentiel. On a choisi la syntaxe de l'opérateur [] .

Avec cette déclaration de la classe, on peut définir la recherche linéaire:

int cherche(const Coll_Int & ci, int e){
    for(int i=0; i<ci.taille(); i++){
        if(ci[i]==e)return(i);
    }
    return -1;
}

Elle retourne l'indice de l'élément dans la collection. Notons qu'il est essentiel que le paramètre Coll_Int soit passé par référence, c'est ce qui garantit qu'au moment de l'appel, aussi bien ci.taille() que l'opérateur [] soit celui de la classe correspondant réellement à la référence.

On peut maintenant considérer plusieurs réalisations pour ces collections d'entiers. Par exemple, peut-être la plus simple est d'utiliser des tableaux du C.

 #ifndef Coll_Tab
 #define Coll_Tab
 #include "Coll.h"
 class Coll_Tab : public Coll_Int{ 
 private:
    int dim;
    int *valeur;
 public:
    Coll_Tab(int n);
    Coll_Tab(const Coll_Tab &); 
    int & operator[](int);
    int operator[](int) const;
    int taille()const {return dim;}
    virtual void resize(int n); // change la taille
    ~Coll_Tab(){delete valeur;}
 };
#endif

Mais on peut aussi envisager une réalisation basée sur des listes chaînées. On peut supposer que l'on a déjà les classes, Lien, List et un des listes avec élément courant List_Pos

struct Lien{
    Lien * suivant;
    Lien(){ suivant=0;}
    Lien(Lien * l) {suivant=l;}
};
class List{ friend List_Pos;
protected:
    Lien *debut;
public:
    void inserer(Lien*); 
    List(Lien *);
    List();
    //...
};
class List_Pos{
protected:
    Lien* ec;    // element courant
    List* lc;    // la liste
    List_Pos(){ ec=0;lc=0;}
public:
    List_Pos(List & l);
    void reinit();
    Lien * aller_en(int i);
    //...
};

A partir de ces listes on peut construire des listes d'entiers, considérées comme des héritiers des listes.

#include "List.h"
class Lien_Int : public Lien{
public:
    int valeur;
    Lien_Int(int v){ valeur=v;}
};
class List_Int : public List{
friend List_Int_Pos;
public:
    void inserer(int v){
        List::inserer(new Lien_Int(v));
    }
    //...
};
class List_Int_Pos: public List_Pos{
protected:
    List_Int_Pos(){};
public:
    List_Int_Pos(List_Int & l) :List_Pos(l){};
    void reinit(){List_Pos::reinit();}
    Lien_Int* aller_en(int i){
        return(Lien_Int*) List_Pos::aller_en(i);
    };
};

Enfin, on peut définir les collections construites sur de listes d'entiers ; comme on peut le noter, il n'y quasiment pas de nouveau code simplement des redéfinitions :

#ifndef Coll_List_Int
#define Coll_List_Int
#include "List_Int.h"
#include "Coll.h"
class Coll_L_Int : private List_Int_Pos,
                 public Coll_Int {
    int dim;
public:
    Coll_L_Int(int n);
    int operator[](int i) const ;
    int & operator[](int i);
    int taille()const{return dim;}
};
#endif
Le code étant :
#include "Coll_List_Int.h"
#include <assert.h>
Coll_L_Int::Coll_L_Int(int n){  
	lc=new List_Int;
	for(int i=0;i<n;i++)
	   ((List_Int *)lc)->ajouter(i);
	dim=n;
	ec=lc->debut;	
};

int &Coll_L_Int::operator[](int i){
	assert(i>=0 && i<dim);
	return aller_en(i)->valeur;
}

int Coll_L_Int::operator[](int i) const {
		return(int )operator[](i);
}

On a choisi dans cette représentation d'utiliser l'héritage multiple ce qui paraît assez naturel et permet d'utiliser directement aussi bien les noms héritées des collections que les nom issues des listes.

Comme on peut le remarquer avec cet exemple, on a atteint réellement la modularité : les différents codes peuvent être développés séparément (à partir du moment où l'interface a été définie). A partir de l'interface et de sa spécification, les clients de la classe peuvent supposer que cette spécification est assurée, et développer des programmes uniquement en utilisant les propriétés de cette interface, sans aucune connaissance de la réalisation.

D'un autre côté, les réalisations de la classe abstraite auront uniquement à garantir les propriétés de la classe. On peut faire l'analogie avec un contrat : les propriétés de la classes sont les termes du contrat qui lient les clients au fournisseur, pour réaliser son contrat, le fournisseur peut s'adresser à un sous contractant qui réalise le travail dans les conditions garanties aux clients.


next up previous contents
Next: Exemple : fenêtres Up: Héritage Previous: Classes abstraites


Mon Oct 20 14:02:48 MET 1997