
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