* * *
Programmation : Langage C
* * *


Polymorphisme

 Puisque l'initiation au C est terminé, nous allons voir une manière de programmer en C très efficace et subtile. Puisque que vous avez réussi à lire jusqu'ici, vous vous demandez : mais c'est quoi le polymorphisme? Le polymorphisme c'est la faculté qu'ont certains objets de réagir de la même manière aux mêmes actions. Je m'explique, prenez deux nombres complexes, vous voudriez que quelque soit la représentation (rectangulaire ou polaire), une addition fonctionne sans vous préoccupez du type du complexe. Pour ceux qui ont fait de la programmation GEM, le même problème se pose lorsque l'on désire, par exemple à chaque rafraîchissement de la fenêtre ne pas vouloir (ou ne pas pouvoir) tester quel type de fenêtre est-ce, et donc faire le rafraîchissement correspondant. Mais la liste peut-être longue et je n'ai pas envie de poursuivre, donc je laisse votre imagination prendre le relais!

 Mais comment réussir se tour de passe-passe? Il faut donc pouvoir appeler la bonne fonction, sans regarder le type de l'objet. Si l'on ne regarde pas le type de l'objet, il faut que le moyen de trouver la fonction soit stockée quelque part, par exemple son adresse pour l'exécuter. Donc pour chaque action, il faut donc l'adresse de la fonction, et puisque toutes ces fonctions qui nous intéressent sont communes, il faut donc les stocker dans des variables à structures communes. D'après ce petit paragraphe, on voit pointer les pointeurs sur des fonctions, ainsi que les structures. Je pense que les structures sont bien connues, par contre un petit rappel sur les pointeurs de fonctions me semble nécessaire.
Un pointeur de fonction est définie comme suit :

(*nom_du pointeur)(paramètres_de_la_fonction)
 Par exemple un pointeur sur une fonction du type : "float add(float a, float b)", s'écrit : "float (*pointeur)(float a, float b)". L'affectation du pointeur se fait avec le nom de la fonction, ainsi si l'on veut faire pointer pointeur sur la fonction add : "pointeur=add", car add représente l'adresse de la fonction. Une petite remarque si vous écrivez "add;" dans un programme, il n'y aura pas appel de la fonction add, car le C considère que vous manipulez un pointeur, sans appel! L'appel de la fonction pointée se fait comme un simple appel : "pointeur(a,b)". Rien de très étonnant, passons à la structure qui contient toutes ces informations :

typedef struct obj_polymorphe {
   void (*add)(struct obj_polymorphe *objg, struct obj_polymorphe *objd);
                                                /* addition */
   void (*aff)(struct obj_polymorphe *obj);     /* affichage */
   float (*p_reel)(struct obj_polymorphe *obj); /* partie réelle */
   float (*p_imag)(struct obj_polymorphe *obj); /* partie imaginaire */
   void *objet;                                 /* objet */
   } OBJ_polymorphe;

 Nous avons donc un pointeur (add) sur une fonction d'addition, qui prend comme paramètre deux adresses de variable OBJ_polymorphe. On se trouve dans le cas de structure définie récursivement, donc on ne peut pas utiliser le type OBJ_polymorphe, car il n'est pas encore définit, il faut donc utiliser le type struct obj_polymorphe, qui lui est défini (partiellement, mais ce n'est pas important). Cette fonction va se charger d'additionner deux valeurs (réelle, complexe rectangulaire ou polaire), et de stocker le résultat dans objg. De même aff qui est une fonction qui affiche la valeur de la variable, p_reel qui retourne la partie réelle du nombre et p_imag qui retourne la partie imaginaire. Le pointeur objet est un pointeur sur l'objet de la variable. Puisque les champs de la variable sont différents d'un type à l'autre (il n'y pas le même nombre de champ pour un réel et pour un complexe), le type ne peut être définit que lorsque l'on sait de quel type il s'agit. Mais comment savoir de quel type il s'agit? Et bien c'est très simple chaque action sait quelle s'applique sur un objet de type donné, et bien avec cette connaissance, on peut donc savoir comment est structuré l'objet (de quel type il est). Ainsi lors de la création d'un réel, chaque fonctions (add, aff, p_reel, p_imag) utilisera objet comme un pointeur sur un réel. Bon voici les différents type des objets :

typedef struct obj_reel {
        float reel;
        } OBJ_reel;



typedef struct obj_imag_rec {
        float reel;
        float imag;
        } OBJ_imag_rec;

typedef struct obj_imag_pol {
        float modu;
        float angl;
        } OBJ_imag_pol;


 Maintenant, voyons la fonction de création, car c'est là ou tout se joue :

 Pour un réel :
void c_OBJ_reel(OBJ_polymorphe *obj, float init)
{ if ((obj->objet=malloc(sizeof(OBJ_reel)))==NULL)
        { printf("erreur de création !\n");
        }
        else
        { ((OBJ_reel *)(obj->objet))->reel=init;
          obj->add=reel_add;
          obj->aff=reel_aff;
          obj->p_reel=reel_p_reel;
          obj->p_imag=reel_p_imag;
        }
}

 On commence par allouer la place pour un objet de type réel. Si il n'y a pas d'erreur, on l'initialise, et on affecte tous les pointeurs aux bonnes fonctions. Une petite remarque : puisque la structure OBJ_polymorphe contient un pointeur de type void, lors de l'affectation de la valeur d'initialisation, il faut dire au C que le champ objet pointe sur une structure de type OBJ_reel. Donc il faut faire un forçage de type au type OBJ_reel * pour pouvoir accéder correctement au champ. Le même problème se pose pour les nombres complexes. L'initialisation de l'objet polymorphe se passe donc comme suit :

  OBJ_polymorphe a,b,c,d;

  c_OBJ_reel(&a, 10.25);

 Donc a est un réel qui a pour valeur 10.25. L'utilisation se passe de la même manière :

  printf("valeur de a : ");
  a.aff(&a);

 Ainsi, on n'a même pas besoin de savoir de quel type est a, pour pouvoir l'afficher! De même une addition entre d et c (deux nombres complexes, l'un rectangulaire, l'autre polaire) s'écrit :

  d.add(&d,&c);

 Le principe est extrêmement puissant et peut être appliqué à plusieurs autres cas. Bon maintenant, on va attaquer la partie des remarques :

  - au lieu de stocker dans chaque objet les adresses des différentes fonctions, pourquoi ne pas les stocker dans des structures qui seront partagées par toutes les variables de même type. Ainsi un objet pourrait avoir la structure suivant :

typedef struct objet {
     OBJ_adr_fnct *fonction;
     /* différent champ de l'objet */
     } OBJET;

 Ceci permet de gagner de la place en mémoire, et c'est plus beau!

  - l'utilisation de macro peut-être un plus, par exemple au lieu de "d.add(&d,&c);", il est plus agréable d'écrire "addition(d,c);", avec addition une macro qui s'écrit :

#define addition(a,b) (a).add(&(a), &(b))

  - un problème n'a pas été soulevé : on fait des allocations dynamique de mémoire, mais aucune libération n'est faite! Le problème peut être résolu grâce, pourquoi pas à une fonction polymorphe de destruction! Cette fonction serait appelée pour chaque objet créé.

  - si par hasard, vous aviez envie de définir des couples de points en complexe, ou réel, rien ne vous empêche de définir une structure comme suit :

typedef struct couple {
     OBJ_polymorphe *a;
     OBJ_polymorphe *b;
     } COUPLE;

et par exemple de faire de COUPLE un nouveau type polymorphe : tout est possible!

 Bon je vous laisse méditer sur ce court article, mais qui je le pense est très intéressante (enfin pour moi, car cette idée est aussi applicable en assembleur, et elle permet de construire un gros projet petit bout par petit bout!). Bon C !



Golio Junior