CHAPITRE IV

Structures de Données C

Pointers are like jumps, leading wildly from one part of the data structure to another. Their introduction into high-level languages has been a step backwards from which we may never recover.
-- C.A.R. HOARE [0]


Najib TOUNSI,   Original: http://www.mescours.ma/C/c4.html

ntounsi@emi.ac.ma
Version: Sept. 2000, Dernière MàJ: Dec. 2021



< Précédent Sommaire Suivant >

1. Les Pointeurs

Le langage C permet de manipuler non seulement les variables, représentant des données en mémoire, mais aussi leur adresse, emplacement mémoire où elles se trouvent.

1.1. Opérateurs * et &

Ces deux opérateurs * et &, inverse l'un de l'autre, sont liés au mécanisme de pointeurs, adressage indirect des objets: & est l'adresse d'un objet, * est le contenu d'une adresse.

1.1.1. Notion d'Objet et d'Adresse

Un objet est une information, donnée ou programme, manipulable se trouvant en mémoire. Il n'a d'intérêt que s'il est accessible et donc adressable. Cela se fait à travers un identificateur qui est un nom associé à un objet. La valeur de l'objet est l'information qui y est représentée. L'adresse d'un objet est alors la désignation de l'emplacement où il se trouve.

Exemple:

int v;

déclare un nom (identificateur v) associé à un objet variable (dont la valeur peut changer), de type entier. V désigne une case mémoire contenant la valeur de cet objet.

Une Variable V

Cette valeur peut être 5 à un moment donné comme sur la figure. Durant l'exécution du programme, v sera associé à cet objet en désignant cette case. On dira, par abus de langage, l'objet v.

Remarques:

  1. Ceci est vraiment un abus de langage: d'abord un identificateur disparaît après la compilation, sa trace à l'exécution reste justement comme adresse. Ensuite, selon qu'un identificateur est en partie gauche d'une affectation ou en partie droite (i.e. une expression), il s'évalue différemment. Dans x = v, v s'évalue comme la valeur à affecter à x, et dans v = 5, v s'évalue comme l'adresse de la case mémoire devant contenir la valeur 5. En C on parle de «l_value» (Left) pour v dans cette dernière écriture. On peut dire «r_value» pour la première écriture, x = v.
  2. Il convient de bien distinguer entre objet et valeur: une valeur est une des caractéristiques, en plus de l'adresse (et du type), d'un objet [1].
  3. Un objet peut aussi bien être une fonction. Sa valeur est constituée de son code et son interface constitue son type. On verra dans le chapitre suivant comment on peut manipuler l'adresse d'une fonction.

En C l'expression

&v

représente l'adresse de l'objet V, c'est-à-dire l'emplacement mémoire (numéro de case, entier positif) où il se trouve, alors que l'expression

v

représente le contenu de cet emplacement, c'est-à-dire la valeur de l'objet. C'est la notation traditionnelle pour manipuler des objets dans la plupart des langages de programmation procéduraux. Du moins quand V est une r_value, ce qui est le cas dans une expression. Voir remarque 1. précédente.

(Exercice: A ce propos, &v peut-elle être une l_value? Regarder le message du compilateur dans ce cas).

1.1.2. Notion de Pointeur

Un pointeur est un objet dont la valeur est l'adresse (ou la référence) d'un autre objet. Ce dernier pouvant à l'occasion être lui même une autre adresse (mais d'un usage en général limité aux tableaux de pointeurs que nous présenterons plus loin.)

La déclaration

int* p;

déclare un nom p associé à un objet qui est une adresse d'un entier. p est en quelque sorte un identificateur de type adresse. Sa valeur est variable (voir plus bas arithmétique des pointeurs).

Si l'expression p désigne une valeur de type adresse donc, l'expression

*p

désigne, l'objet dont l'adresse est dans p i.e. l'entier supposé. p est déréférencé par le signe *. Voir figure IV-1. (NB. Le signe * est utilisé aussi bien dans la déclaration que comme opérateur .). Il s'en suit que l'instruction

p = &v;

met dans p, objet de type adresse, l'adresse de l'objet v (noter l'espace entre = et &). On dira «p pointe vers v». On schématise cela par une figure comme en IV-1.

Un Pointeur

Fig-IV-1, Un Objet p Contenant l'Adresse
d'un Autre Objet v.

Si on fait ensuite

v = 5;

l'expression

*p

donnera la valeur 5 et

*p + 3

donnera la valeur 8 etc....

Aussi, si p est initialisé à &v

*p = 5;

produira le même effet que v = 5, car elle affectera 5 à l'objet v pointé par p.

1.2. Arithmétique des Pointeurs

Les pointeurs supportent une arithmétique mais de façon très restreinte. Si p est un pointeur,

p++ ou

p = p + 1

etc.... sont valides. p++ déplace le pointeur une «position plus loin», i.e. un objet plus loin. Déplacement d'autant d'octets que la longueur de l'objet pointé; sizeof() permet de connaître cette longueur le cas échéant. De même que p = p+i, où i est un entier; p pointera i objets plus loin dans le sens de i.

Exemple: Soit la déclaration

#define MAX 10
int t[MAX]={0,1,2,3,4,5,6,7,8,9};

La séquence suivante:

{   int *p;
    int *q;
    int i;
    p = &t[1];
    printf("%d %d ", *p, *(p+2));
    p++;
    printf("%d %d", *p, *(p+2)) ;
}

imprime

1 3 2 4

p étant initialisé à l'adresse de t[1], p+2 est alors l'adresse de t[3]. *p et *(p+2) correspondent donc aux valeurs t[1] et t[3]. Ensuite, p++ fait pointer p vers t[2] et p+2 sera alors l'adresse de t[4]. D'où le résultat.

En général on rajoute, ou on retranche, un entier à un pointeur. C'est un des moyens pour parcourir un tableau comme l'illustre l'exemple suivant:

for (i=0, p= &t[0]; i<10; i++)
      printf("%d ", *p++);

L'expression *p++, déréférence un pointeur et le fait avancer ensuite. p est en quelque sorte un curseur sur le tableau. Avec p++, on peut le faire glisser sur les éléments successifs de t. Avec p+i, on peut le faire pointer vers un élément d'indice i arbitraire dans le tableau.

Par contre, ajouter par exemple ou retrancher deux pointeurs, donne un entier. La séquence suivante donne à i la valeur 4, distance entre l'adresse de t[7] et celle de t[3] (l'unité étant la taille des objets pointés).

p = &t[3];
q = &t[7];
i = q - p; /* i reçoit 4 */

Exemple: Inversion d'un tableau. On parcourt un tableau dans les deux sens jusqu'au milieu, et on permute les éléments rencontrés.

for(p= &t[0], q= &t[MAX-1]; abs(p-q)>1; p++,q--){
       i= *q;
       *q= *p;
       *p=i;
}

Exercices

IV.1.1 Soit

{ int v = 1; int* p; p = &v;}.

Que veut dire *(&v), &(*p)? Expérimenter sur machine.

IV.1.2 Soit un tableau de caractères. Ecrire un programme qui teste si le tableau est un palindrome (identique à son inverse, e.g. «ici», «radar», «rever», «a man a plan a canal panama» si on ignore les blancs. «esope reste ici et se repose» est un autre palindrome célèbre).

N.B. Parcourir une seule fois le tableau. La taille du tableau est une donnée. Pour les derniers exemples, on peut utiliser gets(t) pour lire une ligne clavier dans un tableau t.

VI.1.3 Pour s'habituer aux expressions de type *p++, ++*p, etc... Soit int t[2]; avec t[0]=1; et t[1]=10; et la séquence

p = &t[0];
x = *++p;
printf("%d %d\n", x, *p);

qui imprime le résultat de l'expression *++p (valeur de x) et la nouvelle valeur (*p) pointée par p.

Parmi les expressions suivantes:

*p++, *++p (celle de l'exemple), ++*p, *(p++), *(++p), (*p)++ et ++(*p)

a) Quelles sont celles qui incrémentent x et celles qui incrémentent p (autrement dit dans quels cas l'indirection est évaluée avant l'incrémentation)?

b) Quand est-ce que l'incrémentation prend effet: avant l'affectation à x ou après l'affectation à x?

c) Quelles expressions sont équivalentes?

 Indication: Les deux opérateurs sont de même priorité, et s'associent de droite à gauche.

1.3. Valeur NULL

NULL est une constante symbolique de type pointeur à qui on peut faire jouer le rôle d'indéfini à la façon du nil de PASCAL. Cela sert pour tester si un pointeur pointe vers un objet, comme tester la fin d'une liste dans une structure de données liste chaînée ou tester la valeur retour des fonctions rendant un pointeur (e.g. fopen(), gets()). NULL est macro-définie égale à 0 dans <stdio.h>.

1.4. Initialisation d'un Pointeur

Un pointeur doit –faut-il le dire– toujours être initialisé avant d'être utilisé. Sinon, comme il peut contenir n'importe quelle valeur au début, il pointe au hasard et le programme se comportera bizarrement ou générera une violation mémoire (le fameux Segmentation fault core dumped de UNIX) etc ...dès le premier accès avec *.

Dans tous les exemples précédents, nous avons écrit

p = & <variable>;

pour initialiser un pointeur: on lui affecte une valeur d'adresse, celle d'une variable dans l'espace des données du programme. Cela sert pour manipuler un tableau par exemple, variable indicée, comme on vient de le voir sur les exemples précédents.

Il y a une autre façon d'initialiser un pointeur. On demande une allocation d'un nouvel espace mémoire pour contenir le ou les objets (non encore initialisés!) à pointer. L'adresse de cet espace sera alors valeur initiale du pointeur considéré. La fonction C pour cela est malloc() de profile:

char *malloc(unsigned taille);

qui a pour effet d'allouer un bloc mémoire de taille cases (au moins) d'un octet et de retourner son adresse. Le résultat est un pointeur vers char dans le sens octet (qui peut être converti vers le type de pointeur approprié). Ce bloc alloué peut contenir n'importe initialement quelle valeur.

Exemple: Le programme suivant lit une chaîne de caractères et l'imprime.

#include <malloc.h> 
#include <stdio.h> 
main(){ 
char *s, *p; 
       s = (char*) malloc(1000); 
       gets(s); 
       puts(s); 
}

Il commence par allouer 1000 octets dans un bloc dont l'adresse est affectée à s pointeur char, lit une chaîne qui sera mise dans cet espace (pointé par s donc) et l'imprime. Le bloc alloué par malloc() se trouve dans un espace mémoire réservé à cet effet appelé tas «heap» ou espace dynamique.

Avec s pointeur entier, s = (int*) malloc(1000) allouerait pour s un tableau pour contenir 1000 entiers.

Remarque: Il convient de distinguer entre valeur initiale d'un pointeur et celle de l'espace alloué (objets pointés). malloc() initialise le pointeur et non l'espace alloué.

int *p;          /* p non initialisé */
p = malloc(4);   /* p initialisé, mais pas *p */
*p = 4;          /* tout est initialisé p pointe vers 4 */

Dans l'exemple ci-dessus, c'est gets(s) qui initialise la chaîne pointée s. On peut noter au passage l'autre façon d'initialiser un pointeur: par la valeur retour d'une fonction. En l'occurrence la fonction gets() ici qui lit une chaîne, la place en mémoire à l'adresse fournie en paramètre et retourne cette même adresse, laquelle peut servir pour initialiser p par

p = gets(s);

p (et s) désigneront ainsi la même chaîne lue. Il y a beaucoup de fonctions C qui retournent des adresses.

En PASCAL normal, l'opérateur adresse & n'existe pas. C'est pour cela qu'on utilise toujours la procédure new avec les pointeurs. En C malloc() est en quelque sorte l'équivalent de new de Pascal [2].

L'inverse de malloc() est free()

void free(char* ptr);

qui restitue au système l'espace, désigné par ptr, précédemment alloué par malloc(). A charge pour lui de le récupérer par un ramasse-miettes (garbage collector). L'adresse dans ptr devient virtuellement inexistante quoique physiquement elle contient peut être toujours la même valeur. Mais l'espace désigné peut contenir plus tard un autre objet et ptr devient donc obsolète. Le pointeur ptr est alors dit «pendant» (dangling).

Autres Fonctions sur Pointeurs

La fonction

char *realloc(ptr, nouvelleTaille)
char *ptr; unsigned nouvelleTaille;

change la taille de l'espace mémoire référencé par ptr pour le ramener à nouvelleTaille octets, et retourne l'adresse du nouvel espace (éventuellement déplacé). Le contenu de l'espace est inchangé, sauf peut être s'il a subi une réduction passant à une nouvelle taille plus petite.

La fonction

char *calloc(nelem, tailleElem)
unsigned nelem, tailleElem;

utilise malloc() pour allouer de l'espace pour un tableau de nelem éléments, chacun de taille tailleElem, l'initialise à zéro et retourne son adresse. On peut donc la préférer à malloc().

char *s;
s = calloc (20, 1)

alloue 20 octets pour s. Cette fonction est particulièrement utile pour créer des tableaux dynamiques, dont on ne connaît la taille qu'à l'exécution.

int *t;
int nelem;
t = (int *) calloc (nelem, sizeof(int));

alloue nelem entiers (chacun occupant 4 octets en principe) au tableau t. Remarquer la conversion du retour de la fonction vers un pointeur entier.

Voir la documentation malloc(3) par la commande UNIX " man 3 malloc " pour d'autres fonctions de ce type. L'exemple qui suit est un programme qui fait 20 tentatives d'allocation mémoire pour contenir 1000 réels doubles. Retour NULL en cas d'échec de malloc().

#define TAILLE 1000 
double t[TAILLE]; 
... 
for (i=0; i<20; i++) 
      if ( (t = (double*) calloc(TAILLE, sizeof(double))) != NULL) break; 
if (i == 20) 
      printf("Que vous êtes gourmand!\n"); 
...

Dans la plupart des exemples qui suivent nous n'utiliserons pas malloc() (on travaillera sur des tableau) pour nous concentrer sur le phénomène des pointeurs.

1.5. Pointeurs **

Il est possible en C de déclarer un pointeur sur un pointeur (double indirection).

Double Indirection

Fig-IV-2, Double Indirection.

Soit les déclarations

int v=5; int *p, *q; int **pp;

pp est un pointeur sur un pointeur d'entier. Si on fait

p = &v;

alors

pp = &p;

est une écriture valide et fait pointer pp sur p. pp contient l'adresse de p qui contient lui-même l'adresse de v. Si donc l'expression *p est de valeur 5,

**pp

est un expression valide de même valeur. On a ici deux niveaux d'indirection. Par ailleurs, l'écriture

q = *pp;

est valide: q reçoit la valeur pointée par pp, i.e. l'adresse de v, et l'expression *q est de valeur 5.

Mais une écriture comme q = pp ou **q est bien sûr incorrecte: combinaison illégale de pointeurs et indirection illégale (en paraphrasant le compilateur). C'est tout simplement des erreurs de type de données avec des opérations incompatibles.

Exercice IV.1.4: p étant un pointeur, faire le schéma (semblable à la figure IV-2) correspondant à p = &p; Sous quelle condition cette écriture est correcte?.

pointeur * vs pointeur **

En général un seul niveau d'indirection suffit. Un pointeur sert à manipuler des objets dynamiques. Un objet dynamique est un objet dont l'emplacement par exemple est changeant: caractéristique très importante quand on ne peut pas prévoir à l'avance l'existence par exemple ou la taille d'un objet. Pour ne pas lier un programme à des contraintes de ce type, et pour rendre les manipulations faites sur un objet indépendantes de son emplacement (ou sa taille), le mécanisme d'indirection est le moyen adéquat: le programme utilise un emplacement fixe qui contient comme valeur l'adresse de l'emplacement actuel de l'objet manipulé. Adresse pouvant varier au gré des objets auxquels il faut accéder (ou des déplacements de ces mêmes objets à cause d'un tassage par exemple), mais l'emplacement qui la contient est fixe.

Un deuxième niveau d'indirection ne se justifie que si l'emplacement fixe voulu par le programme est en fait un ensemble d'emplacements fixes, c'est-à-dire, un tableau de pointeurs. Il serait alors très intéressant de manipuler le dit tableau comme un premier niveau d'indirection, donc via un pointeur, selon le schéma:

Pointeur sur Tableau de Pointeurs

Fig-IV-2-bis Pointeur sur Tableau de Pointeurs

Nous reprendrons cette discussion plus loin au paragraphe §4. Pour l'instant intéressons-nous au lien que fait le langage C justement, entre un tableau habituel et un pointeur.

2. Pointeurs et Tableaux

Les exemples cités précédemment lors de la discussion sur l'arithmétique des pointeurs, illustrent l'intérêt des pointeurs pour manipuler des tableaux. En programmation C, on peut manipuler les tableaux aussi bien par des pointeurs que par indexation. Le compilateur le fait d'ailleurs à votre place dans ce dernier cas (Il ne faut pas bouleverser les habitudes du programmeur).

Pointeur Tableau

Fig-IV-3, Pointeur p (= &t[0]) Vers le Premier
élément d'un Tableau.

Soit les déclarations:

int t[10];
int *p,x;

et l'instruction

p = &t[0];

qui correspondent au diagramme de la figure IV-3. On a alors

x = *p;

affecte à x la valeur t[0]. C'est comme l'écriture x = t[0];

x = *(p + i);

affecte à x la valeur t[i]. C'est comme l'écriture x = t[i];

Inversement

*(p+i) = 5; est équivalent à t[i] = 5;
Donc
*(p + i) et t[i]
désignent le même objet ( si i est toutefois non négatif). C'est pour cela que les indices des tableaux commencent par 0 en C. Si on a p = &t[0] alors

Tableaux et Pointeurs

Règle générale:

Un nom de tableau en C est en réalité un pointeur. Dans notre cas ici, t se comporte comme p, i.e. t contient une adresse (comme l'illustre fig-IV-3). Adresse devant rester constante, rappelons-nous.

p = &t[0];   peut s'écrire   p = t; et
t[i]        
peut donc s'écrire (ce que fait un compilateur) *(t+i)

De même et inversement

*(p+i)       peut s'écrire p[i], pointeur indicé (intéressant), et

l'expression (r_value)

&t[i]        peut s'écrire t + i.

Attention!

La différence, entre un pointeur et un tableau, est que ce dernier ne supporte pas d'arithmétique. t est un nom (associé à l'adresse) de tableau qui doit rester constant pour que t[i] ait toujours le même sens, classique encore, dans tout le programme. Donc

si p++ a un sens, t++ n'en a pas

si p = t; a un sens, t = p; n'en a pas ( modification du contenu de t).

Pas plus que  &t (t est déjà une adresse). Ne pas confondre avec &t[i].

Autrement dit t est une adresse constante durant l'exécution. Voilà pourquoi les tableaux ne sont pas affectables en C. A ce propos l'expression (déjà vue).

"abcdefgh" [4]

est juste et désigne la lettre e, 5ième élément d'une constante tableau de caractères. Exemple qui montre, si besoin est, le privilège dont jouit le cas particulier d'un tableau de caractères en C, c'est-à-dire une chaîne.

En conséquence, le passage d'un tableau en paramètre à une fonction, ne se fait pas par valeur (copie du tableau) mais par adresse (copie du contenu de t, i.e. l'adresse du 1er élément du tableau). Nous parlerons de cet aspect dans le chapitre V avec le passage de tableaux en paramètres.

Une autre différence entre un tableau et un pointeur, c'est qu'un pointeur doit être initialisé. Par malloc() ou par affectation d'une expression adresse (p = &...). En effet, un tableau est alloué dans l'espace d'adressage d'un programme (son nom pointe vers une zone accessible et n'a donc pas besoin d'être initialisé), alors qu'un pointeur reste «pendant», sa valeur est indéfinie, tant qu'il n'a pas reçu une valeur d'adresse. Mais ladite valeur, elle, se trouve dans l'espace d'adressage du programme.

3. Tableaux et Chaînes de Caractères

Une chaîne de caractères étant un tableau de type char, on la manipule par un pointeur vers char. Chose relativement aisée. Il suffit de déclarer un identificateur tableau de char

char TaChaine[26];

ou encore mieux, un identificateur de type char*

char *MaChaine;

pour lui faire subir des affectations comme

MaChaine = "Gai Luron";
MaChaine = TaChaine + 4;
      /* MaChaine pointe sur TaChaine a partir du caractere 4 */

ou des déplacements vers un autre caractère comme

MaChaine++;
MaChaine += 10;

Exemple: La fonction suivante calcule la longueur d'une chaîne de caractères: on parcourt la chaîne s jusqu'à la rencontre du caractère \0, et on compte au fur et à mesure dans n.

strlen (char *s) {
    int n;
    for (n=0; *s++ != '\0'; n++); /* boucle sans corps */
    return n;
}

On peut l'utiliser, si on déclare

char* MaChaine = "toto";

par

strlen (MaChaine) qui retourne 4,

strlen (&MaChaine[2]) qui retourne 2, la longueur du deuxième "to"

ou

strlen (MaChaine + 2) qui retourne 2 pour les mêmes raisons.

En effet, MaChaine étant déclarée pointeur sur char et initialisée "toto", l'expression MaChaine est l'adresse du premier caractère 't' de "toto", &MaChaine[2] est l'adresse du troisième caractère 't', de même que MaChaine + 2.

Ainsi une chaîne de caractères se manipule directement par son nom et il n'y a pas besoin de l'opérateur * d'indirection qui est transparent:

MaChaine = TaChaine;
strlen(MaChaine);
strcmp(Machaine, TaChaine);

etc.... L'usage de typedef (renommage d'un type en C) peut renforcer cette transparence:

typedef char* STRING;  /* STRING est équivalent à char* */
STRING MaChaine;       /* comme char* Machaine */

C'est quand on veut accéder à un caractère de la chaîne qu'on a besoin de l'opérateur * d'indirection comme *s dans l'exemple strlen() ci-dessus.

Exercices:

IV.3.1 Constater que la fonction suivante qui calcule la longueur d'une chaîne en utilisant un tableau, est équivalente à la précédente (exemple ci-dessus).

strlen (char s[]){ 
       /* s[] sans bornes, pour dire que c'est un tableau */ 
       int n = 0; 
       while (s[n++]) ; 
       return (--n); 
}

Cette fonction est-elle vraiment équivalente à la précédente? (Penser à l'effet de bord sur le paramètre s dans le premier cas).

IV.3.2 Ecrire la fonction strcat(s1,s2) qui rend une chaîne égale à la concaténation de s1 et s2:

a) Sans changer s1
b)
s1 devient (aussi) le résultat de la concaténation.
c)
Quels sont les effets de bord de votre fonction?

4. Tableaux et Pointeurs

Reprenons la discussion entamée en fin du §1 de ce chapitre. Un tableau de pointeurs (d'entiers par exemple) se déclare par:

int* t[10];

qui déclare t comme un tableau de 10 pointeurs sur entiers. Exemple:

% cat tableauPointeur.c 
main(){ 
       int *t[9]; 
       int i=2; 
       t[2]= &i; /* t[2] est une adresse! */ 
       printf("%u \t%d \t%d\n", t[2], *t[2], **(t+2)); 
} 

% cc tableauPointeur.c 
% a.out 
4160748720    2    2

Un élément de tableau (e.g. t[2]) contient une adresse d'entier (celle de i) comme l'illustre la valeur 41607... du premier argument t[2] de printf(). Pour accéder à cet entier on dispose de deux écritures:

*t[2], valeur pointée par cette adresse t[2] et qui est 2 ici, ou bien

**(t+2), valeur pointée par le contenu de l'adresse t+2 (double indirection).

C'est une simple extension des notations déjà connues pour un tableau E d'entiers: E[indice]et son équivalent *(E+indice)qui sont l'entier de rang indice. L'extension étant l'usage de l'opérateur * pour une première indirection. (Exercice: A ce propos, quelle est la valeur de l'expression **t ?)

Donc là il y a une double indirection, matérialisée par **(t+2). Si maintenant on rajoute les instructions suivantes:

{
       int **p; 
       int j; 
       t[3] = &j;  /* chaque élément de t doit être initialise 
                    pour son propre compte! */ 
       *t[3] = 3;  /*   *t[indice] en partie gauche    */ 
       p = &t[2]; 
       printf("%d \n", **p); 
       p++; 
       printf("%d \n", **p); 
}

on obtient en plus les valeurs 2 et 3

% a.out 
4160748720    2    2
2 
3

Quelle est alors la différence entre p et t? p est un pointeur vers un pointeur d'entier, en l'occurrence ici le 2e élément de t et qui donc peut supporter l'arithmétique (p++) ou l'affectation (p = ...). la variable t est un nom de tableau qui est pointeur en C, mais constant. Si **t est possible (désigne la valeur pointée par t[0], réponse à l'exercice juste proposé), t++ ou t = ... n'est pas possible.

Remarque: Dans un tableau de pointeurs, un élément qui ne pointe nulle-part doit par convention valoir la constante NULL, à affecter explicitement.

Exercice:

IV.4.1 Un tableau int *t[3] contient les adresses de trois variables entières i, j et k. Sans modifier les valeurs de ces variables, ranger ce tableau selon les valeurs croissantes de i, j et k.

Comme on a utilisé un tableau de pointeurs d'entiers, on peut utiliser un tableau de pointeurs vers des objets quelconques en particulier char.

4.1. Tableau de Chaînes de Caractères

C'est une conséquence directe de ce qui précède. On va se contenter d'un exemple pour illustrer cela:

% cat tabString.c 
main(){ 
       char *s; 
       char **ps; 
       char *t[10]; 
       t[0] = "toto\0"; 
       t[1] = "titi\0"; 
       t[2] = "tintin\0"; 
       t[3] = "tutu\0"; 
       s = t[1]; 
       ps = &t[2]; 
       printf("%s %s %s %s\n", t[0], *t, s, *ps); 
       printf("%c %c %c %c\n", *t[0], **t, *s, **ps); 
       printf("%s %s\n", ++s, *++ps); 
} 
% cc tabString.c 
% a.out
toto toto titi tintin 
t t t t 
iti tutu

Dans ce programme, char *t[10] déclare t comme tableau de 10 chaînes de caractères (char*). Les quatre premiers éléments du tableau sont donc des chaînes comme le montrent les initialisations. Or une chaîne de caractères se manipule directement et sans indirection par son identificateur ( qu'on sait être un pointeur vers un caractère). Le résultat de cet exemple le montre bien:

t[0] est la chaîne "toto" (il faut un indice ici) et

s est la chaîne "titi" dans t[1] (cela on le sait déjà).

De plus, t étant tableau donc pointeur vers son premier élément et ps étant double pointeur vers char, on a:

*t est aussi la chaîne "toto"

*ps est la chaîne "tintin" (ps valant &t[2])

D'où la présence de * pour la première indirection dans ces deux derniers cas. Cependant *t[0], **t, *s et **ps sont des doubles indirections vers le premier caractère 't' de chaque chaîne (Ce qui revient aux cas de l'exemple de tableau de pointeurs d'entiers avec char au lieu de int).

Remarquer que ++s fait avancer la chaîne s vers son prochain caractère alors que ++ps fait avancer ps (pointeur vers une chaîne élément de tableau) vers le prochain élément(chaîne) du dit tableau! C'est la différence entre char* et char** . Se rappeler enfin aussi que t++ ou t=... sont interdits. C'est la différence entre les déclarations char* t[10] etchar **ps.

Notations syntaxiques:

L'opérateur d'indexation [] est de priorité supérieure à l'opérateur d'indirection *. Reprenons int comme exemple de type.

1) int *t[10]; déclare t tableau de 10 pointeurs entiers.

2) int (*t)[10]; déclare (*t) comme un tableau de 10 entiers et donc t est un pointeur vers un tel tableau. Ici, t supportera l'affectation et l'arithmétique.

3) int *(*t)[10]; déclare t comme pointeur vers tableau de 10 pointeurs d'entiers.

Aspects sémantiques:

Le deuxième cas ici, pointeur vers un tableau de «quelque chose», mérite l'attention (dans les cas 1 et 3, ce quelque chose n'étant autre qu'un entier et une adresse d'entier respectivement). Un pointeur vers un tableau d'entier, ne se comporte pas comme un pointeur d'entier initialisé vers un tableau d'entier. Soit

int tab[10],*p;
p= &tab[0];

Ici, p est déclaré devant pointer vers un entier, donc l'initialisation est correcte, et en outre p++ fait avancer p de un entier.

Par contre dans

int (*p)[10];

p est déclaré devant pointer vers un tableau de 10 entiers, et comme tel, il doit être initialisé par quelque chose d'horrible comme:

p = (int (*)[10]) &tab[0];

pour convertir l'adresse &tab[0] vers un type compatible avec p. Cela a pour conséquence que p++ fait avancer p de la longueur d'un tableau de 10 entiers. (Quelle est l'initialisation par malloc() dans ce cas?)

Exemple: Soit int tab[50] un tableau de 50 entiers, et int (*p)[10] un pointeur vers un tableau de 10 entiers. On initialise p à &t[0] et ensuite on le fait avancer de deux crans par p +=2. Dans les deux cas on imprime les 10 entiers pointés.

% cat pointToTab.c 
main(){ 
       int tab[50]; 
       int (*p)[10]; 
       int i; 
       /* tab initialisé aux 50 premiers entiers */ 
       for(i=0; i<50; i++) tab[i]=i; 

       p = (int (*)[10]) &tab[0]; 
       for(i=0; i<10; i++) 
              printf("%d ", (*p)[i]); printf("\n"); 
       p +=2; 
       for(i=0; i<10; i++) 
              printf("%d ", (*p)[i]); 
} 
% cc pointToTab.c 
% a.out 
0 1 2 3 4 5 6 7 8 9 
20 21 22 23 24 25 26 27 28 29

Le pointeur p a bien avancé vers la troisième tranche de 10 éléments de tab. Noter dans ce cas que la taille 10 dans l'initialisation de p doit être la même que celle dans sa déclaration. Une autre initialisation est:

p = (int (*)[10]) malloc (1000);

pour allouer un espace pour 100 tranches de 10 entiers (réponse à la question juste proposée).

Exercices:

IV.4.1 Soit t un tableau de 6 chaînes non vides de longueurs croissantes. Ecrire un programme qui a pour résultat la chaîne obtenue par concaténation des chaînes Si (i=0..5), Si = les i premiers caractères de chaque chaîne t[i].

Exemple:

t[0] = "do";
t[1] = "ali";
t[2] = "bob";
t[3] = "minime";
t[4] = "ablette";
t[5] = NULL;

doit donner

NULL
abominable

(comme une syntaxe...)

IV.4.2 Reprendre le même exercice pour un tableau de N chaînes.

IV.4.3 Idem, avec Si débutant au caractère i.

5. Types Structurés

5.1. La Structure struct

Jusqu'à présent la seule structure d'objet complexe qu'on ait vu est la structure tableau à une ou plusieurs dimensions. C'est une structure d'objet complexe, dans le sens objet composé d'objets plus élémentaires. Dans le cas du tableau ces objets élémentaires sont des objets de même type. C'est pour cela qu'on peut les ranger dans des cases consécutives de même taille et donc numérotées.

Une autre structure d'objet complexe est celle d'enregistrement (cf. RECORD de Pascal Cobol). C'est une structure composée d'une juxtaposition d'objets quelconques appelés champs. En C une structure est déclarée par la forme générale:

struct [<nom>] { <ListeDeclarations> };

Le nom <nom> est celui de la structure et il va jouer un rôle de type pour déclarer des objets de cette structure. <ListeDeclarations> donne les déclarations des différents champs de la structure.

Exemple:

struct date {
       int jour;
       char mois[3];         
(1)
       int annee; };

est une structure d'objet d'un nouveau type date, composé d'un champ entier jour, un champ mois de 3 caractères, e.g. "jan" "fev" etc..., et d'un champ annee qui est entier aussi. Une écriture comme struct{...} s'appelle un constructeur de type -- comme d'ailleurs [] la déclaration de tableau. jour, mois et annee sont des variables, mais date non. C'est le nom de la structure.

Maintenant, pour déclarer des variables du type de cette structure date on peut écrire:

struct date d;                 (2)

d est la variable structurée composée de trois champs: jour, mois et année. C'est toujours l'écriture

<type> <variable>;

où le type est struct date. On se réfère aux champs individuels de d par l'opérateur point «.» Ainsi:

d.jour        est l'entier jour.

d.mois[1]    est le deuxième caractère du mois.

d.annee       est l'entier année.

La définition d'une variable structurée est faite donc en deux phases: la construction du type par la description de la structure avec ses champs comme en (1), et la déclaration de la variable structurée comme en (2). C permet néanmoins de combiner ces deux écritures comme suit:

struct date { int jour;
char mois[3];               
(3)
int annee;
} d ;

La structure date est définie et une instance, variable d, en est déclarée. D'autres variables qui ont la même structure, peuvent être déclarées ultérieurement sans redéfinir la structure par

struct date d1, d2;           (4)

d1 et d2 sont deux nouvelles variables ayant cette structure.

Néanmoins il peut paraître plus discipliné de définir d'abord la structure comme en (1) et ensuite déclarer les variables correspondantes séparément comme en (2). Une écriture comme (3) risque en effet de cacher la déclaration de la variable d, pour ne voir que celle des champs, alors que (4) est plus explicite.

On peut imbriquer les structures les unes dans les autres:

struct DatePrecise {
       int jour;
       char mois[3];
       int annee;
       struct moment{
              int heure,
              minute,sec;
       } m;

} dp ;

m est un champ structuré faisant partie de la structure dp. Noter que l'apparition de m dans la description de la structure moment est nécessaire ici, car c'est le nom d'un champ; ce qui contrarie ce qui vient juste d'être dit... Mais la structure moment peut être décrite par ailleurs et utilisée ensuite dans la structure qui la requiert.

Autre exemple:

struct personne {
       char nom[10],prenom[10];
       float salaire;
       struct date DateNaissance;
};
Utilisation:
struct personne p;
p.nom = "toto\0";
p.DateNaissance.jour = 10;
notation doublement pointée dans le deuxième cas.

Remarque: On peut déclarer des variables structure sans nommer celle-ci.

struct {int x,y} z;
z est une variable structurée composée de deux entiers x et y. La structure n'étant pas nommée, elle est inutilisable ailleurs. Cela fait quand même assez de liberté dans C. (Exercice: à ce propos que pensez-vous de struct {int x, y}; qui est correcte en C ? )

5.2. Tableaux de Structures

On peut déclarer des tableaux de structures

<type> <variable> [<dimension>] ...

Par exemple:

struct personne employes[100];

pour avoir un tableau (variable employes) constitué de 100 personnes (structure personne). La dimension [100] suit donc le nom de la variable structure comme dans int x[100];. On utilise cette variable simplement comme tout autre tableau avec le point «.» pour chaque champ.

s = employes[i].salaire;
c = employes[i].nom[0]

sont ainsi le salaire et la première lettre du nom du ième employé. Mais il faut faire attention à l'affectation du genre:

employes[2].nom = employes[1].nom

pour affecter le nom du deuxième employé au troisième. Se rappeler que les tableaux ne sont pas affectables (il faut utiliser un pointeur char* nom dans ce cas ou la fonction strcpy()). Mais les structures, elles, sont affectables même au niveau sous-structure. Tout l'objet est copié (si toutefois il ne contient pas de pointeurs. C'est eux qui serait copiés au lieu... des objets pointés «shallow copie» ).

employes[100].DateNaissance = d;

Ce qui nous ramène aux pointeurs vers une structure.

5.3. Pointeurs vers les Structures

C étant un langage orthogonal, pour avoir un pointeur vers une structure il suffit de le déclarer.

struct personne *pp;

déclare pp comme pointeur vers une structure personne. On peut écrire

(*pp).salaire

pour accéder au champ salaire. L'opérateur . étant prioritaire sur l'indirection *, il faut parenthéser celle-ci. Mais il y a une autre notation d'utilisation. Au lieu de (*pp).salaire, on utilise l'opérateur -> pour écrire plus simplement

pp->salaire

pour accéder au champ salaire de la structure pointée par pp. Le signe -> est là pour le refléter. Dans (*pp).salaire les parenthèses sont obligatoires à cause de la précédence de . sur *. Auparavant on aura initialisé pp (ne pas l'oublier) par

pp = &employes[0]

ou

pp = employes;

par exemple.

Comme les autres pointeurs, une variable qui pointe vers une structure peut aussi subir de l'arithmétique. L'incrémenter par exemple, la fait «pointer une structure plus loin».

x = (pp + 1)->salaire;

accède au salaire de l'employé suivant du tableau et l'affecte à x;

(++pp)->salaire;

fait pointer pp vers l'employé suivant et accède à son salaire. Dans le cas précédent pp n'a pas bougé.

L'opérateur -> a la même priorité que le point «.». Dans ces deux dernières expressions, il faut parenthéser (pp+1) et (++p). Voir plus bas.

5.4. Combinaison de pointeurs tableaux et structures

Soit la structure:

struct {
        int x;
        int *y;
} *p, q[10];

p pointe vers un couple: entier x et un pointeur d'entiers y.

Illustration

q est un tableau de 10 couples x et y, ainsi définis.

Soit i=2 et j=4 deux entiers, et les initialisations

q[0].x = 1;
q[0].y = &i;
q[1].x = 3;
q[1].y = &j;
p = q;

correspondant à la figure:

Illustration

On a:

p->x            est 1 et
*p->y          est 2 (-> étant prioritaire, l'indirection concerne y),
tout comme
q[0].x          est 1 et
*q[0]->y      est 2.
(p+1)->x
      est 3 et *(p+1)->y est 4.   (Ce serait q[1].x et *q[1]->y ici)
(++p)->x
      est 3 et *(++p)->y est 4. (p aura été incrémenté de 1 avant d'accéder à x et ce à quoi y pointe)
Attention à la syntaxe:

++p->x aurait été considéré comme ++(p->x) i.e. incrémenter x, car -> est prioritaire sur ++ (ainsi que sur l'indirection * comme déjà signalé). Par contre p++->x est pareil que (p++)->x (pourquoi?): dans les deux cas on incrémente p après avoir accédé à x (post-incrémentation).

De même, *p->y prend ce à quoi y pointe comme *(p->y) (la valeur 2 ci-dessus). *(++p)->y incrémente p avant de prendre ce à quoi y pointe (la valeur 4 ci-dessus). *p++->y, comme *(p++)->y prend ce à quoi y pointe et incrémente p après.

Mais l'écriture *p->y++ prend ce à quoi y pointe et ensuite (post)incrémente y (syntaxiquement, c'est comme *t++, où t serait ici (p->y)). Ce serait utile si y était un tableaux ou une chaîne dans la structure. (Exercice: que serait *++p->y? Réponse: (Pre)incrémenter y et aller vers quoi il pointe. *p->++y est syntaxiquement incorrecte).

5.5. Structures Récursives

Une structure peut faire référence à elle-même, mais par pointeur.

Struct arbre {
       int valeur;
       struct arbre *FilsGauche;
       struct arbre *FilsDroit;
};

Les champs FilsDroit et FilsGauche ne sont pas une structure arbre, mais un pointeur vers structure arbre. Nous y reviendrons (§ 5.7).

5.6. Unions

L'union est une structure qui contient une donnée dont le type varie selon les circonstances de son utilisation. Sa syntaxe est semblable à celle de struct mais avec le mot clé union.

union ent_car {
       int i;
       char c;
} x;

Ici on a deux variables x.i et x.c, qui occupent la même place en mémoire donc une seule donnée. Avec struct au lieu de union, il y aurait eu deux emplacements différents. La taille de x ici est celle du plus grand composant de l'union, donc 4 octets (de int).

x.c = 'A';
printf("i = %d c = %c\n", x.i, x.c);

donne i = 65 et c = 'A' (i=65 si le compilateur est bien gentil de remplir les 3 octets restants par 0). C'est en fait i considéré caractère dans le second cas , i.e. x.i et x.c désignent un même objet. &x.i est donc égale à &x.c.

Se pose alors le problème de comment savoir à tout moment si on doit utiliser un entier ou un caractère ? Quel type est significatif ? Une solution consiste à associer une autre donnée qui pourrait être utilisée pour indiquer que la valeur est significative mais pour tel type, comme dans la figure IV-5.

Cette structure StrNoeud, et la variable associée x, contient deux éléments : le champ type et le champ valeur. Le champ type servira, si égale à 'C' par exemple, pour indiquer que c'est valeur.opérateur qui est significative et, si égale à 'I', ce serait valeur.opérande. Plus précisément x.valeur.operande et x.valeur.opérateur. On peut reconnaître ici un arbre binaire représentant une expression arithmétique où un noeud est parfois opérande parfois opérateur.


      struct StrNoeud { 
          char type; 
          union { 
             int operande; 
              char operateur; 
          } valeur; 
       } x;
Fig-IV-5, Une structure avec Union, pour le Noeud
d'un Arbre Expression Arithmétique.

On peut utiliser cette structure par

switch (x.type) { 
       case 'I' : printf (" Operande %d \n", x.valeur.operande); break; 
       case 'C' : printf (" Operateur %c \n", x.valeur.operateur); break; 
       default : printf (" Pardon? \n"); 
}

Moralité: l'union n'est utile que si elle est associée à une structure comme ici.

Elle peut aussi être combinée avec des pointeurs et des tableaux comme pour les structures. La même notation est appliquée

<NomUnion>.<Champs> ou <NomUnion> -> <Champs>

En d'autres termes, l'union est une structure où tous les champs sont situés au même endroit et se superposent.

5.7. Typedef

C permet de définir de nouveaux noms de types. Ce n'est pas de nouveaux types, mais juste un nom pour se référer à un type défini par combinaison de types existants. Ce nom sert pour simplifier des déclarations ou pour avoir un type mnémonique.

typedef int NOMBRE;

rend NOMBRE synonyme de int. On peut l'utiliser pour déclarer

NOMBRE effectif, taille;
NOMBRE dimensions [4];

On peut aussi créer un type STRING

typedef char* STRING;

et déclarer

STRING nom; STRING classe[30];

C'est peut être mieux que char* classe[30];

typedef est très utile avec les structures. En voici deux exemples.

typedef struct {
     float re;
     float im;
} COMPLEXE, *PCOMPLEXE;

De struct à } c'est la définition du type (une structure), et COMPLEXE c'est son nom. PCOMPLEX est un type pointeur vers cette structure. On peut donc déclarer:

COMPLEXE x, y, z;
PCOMPLEXE px, py, pz;

Cette dernières écriture est du reste la même que COMPLEXE *px, *py, *pz.

Mais attention aux opérations permises pour les nouveaux types. z = x + y; est bien sûr incorrecte. Voici une fonction qui ajoute deux complexes:

PCOMPLEXE cxPlus (PCOMPLEXE x, PCOMPLEXE y){
       COMPLEXE z;
       z.re = x->re + y->re;
       z.im = x->im + y->im;
       return (&z);
}

et qu'on peut utiliser par

pz = cxPlus (px, py); ou par
pz = cxPlus (&x, &y);

Autrement dit, les complexes étant de nouveaux objets, il faut en fournir les opérateurs de manipulations (cf. C++).

Un deuxième exemple de typedef est donné par la figure IV-6.

typedef struct StrArbre {
       struct StrNoeud nd;
       struct StrArbre *fg;
       struct StrArbre *fd;
} ARBRE, *PTRARBRE;
Figure IV-6, Structure Arbre Binaire (définition
des types arbre et pointeur vers arbre)

Ici la structure est nommée par StrArbre car il y est fait référence. StrNoeud est la structure déjà définie plus haut (figure IV-5). Voici un exemple d'utilisation:

PTRARBRE ArbreAllouer(){
       /* allocation d'un arbre */
       return ( (PTRARBRE) malloc(sizeof(ARBRE)));
}

qui alloue l'espace pour un arbre (sa racine plus exactement). Pour les fils il doit y avoir la même allocation. Noter la conversion vers le type PTRARBRE du résultat char* de malloc(), et l'usage de sizeof() pour calculer la taille du type ARBRE. D'où l'usage de deux noms: ARBRE et PTRARBRE dans typedef.

Utiliser typedef a donc certains avantages. Il permet d'utiliser des nom courts est mnémoniques (COMPLEXE, ARBRE) pour une bonne compréhension de programme, et surtout il peut aider à assurer une meilleur portabilité pour les types machine-dépendents ( ce qui le rend très utilisé dans les fichiers include). Le changement sera fait une seule fois dans la ligne typedef. Imaginer qu'il faut partout remplacer int par short, si int est prévu sur 2 octets et il se trouve qu'il est sur 4. Si on avait

typedef int ENTIERCOURT;

on n'aura qu'à réécrire et une seule fois:

typedef short ENTIERCOURT;.

5.8. Exemples de Programmes

Nous allons illustrer des structures de données sur deux exemples. L'exemple de la figure IV-7 est une fonction qui évalue le résultat d'une expression donnée sous forme d'arbre (paramètre e de type PTRARBRE). Les déclarations utilisées sont celles des figures IV-5 et IV-6.
EvalExp( PTRARBRE e ){
       if (e == NULL) /* arbre vide au premier appel */
             return;
       else if (e->nd.type == 'I') /* c'est une feuille */
             return (e->nd.valeur.operande);
       else /* c'est un noeud operateur */
       switch (e->nd.valeur.operateur) {
              case '+' : return ( EvalExp(e->fg) + EvalExp(e->fd));
                         break;
              case '-' : return ( EvalExp(e->fg) - EvalExp(e->fd));
                         break;
              case '/' : return ( EvalExp(e->fg) / EvalExp(e->fd));
                         break;
              case '*' : return ( EvalExp(e->fg) * EvalExp(e->fd));
                         break;
              default : printf("Operateur bizarre! %c\n",
                                e->nd.valeur.operateur);
       };
}
Fig-IV-7, Evaluation d'une Expression Arithmétique
                     (représentée sous forme d'arbre).

L'algorithme commence par tester si e est NULL, auquel cas l'arbre est vide. Ensuite si l'information du noeud (racine de e) est du type entier alors c'est une feuille (un arbre réduit à la racine, i.e. expression réduite à un opérande), et la valeur de e est celle de ce noeud. Sinon, le noeud est un opérateur et le résultat de l'évaluation de e est celui de l'application de cet opérateur aux 2 sous-expressions: sous-arbre gauche et sous-arbre droit, dans cet ordre. Ces sous-expressions sont bien sûr évaluées par appel récursif à la même fonction. Noter que ces appels récursifs ne se font jamais sur une expression NULL. Du moins si l'arbre est bien formé. Voici un exemple d'arbre bien formé:

Illustration

dont l'évaluation donne la valeur 11, i.e. (3-1) + 3*(6/2).

Un deuxième exemple, beaucoup plus simple --pour rester au milieu des arbres...-- est bien sûr le parcours pré/post/infixé d'un arbre. Voir figure IV-8 pour un programme réalisant le parcours préfixé. La spécification de l'algorithme est

PreFixe (A) = Racine (A) ; PreFixe(A.FG) ; PreFixe(A.FD)

où A est un arbre et Racine(A) un traitement quelconque sur le noeud racine de A, comme l'impression de sa valeur dans cet exemple. Le point virgule signifie l'enchaînement. Les autres parcours s'en déduisent simplement en mettant Racine(A) au bon endroit:

PostFixe (A) = PostFixe(A.FG) ; PostFixe(A.FD) ; Racine(A)

InFixe (A) = InFixe(A.FG) ; Racine(A) ; InFixe(A.FD)

Exercice: Vérifier par programme les parcours post/infixés.

PreFixe(PTRARBRE e) {
      if (e != NULL) {
            if (e->nd.type == 'I')
                   printf("%d ",e->nd.valeur.operande);
            else
                   printf("%c",e->nd.valeur.operateur);
                   pre(e->fg);
                   pre(e->fd);
      }
}
Fig-IV-8, Parcours Prefixé d'Arbre Binaire.

Voici le résultat donné pour le parcours préfixé du même arbre que ci-dessus.

+ - 3 1 * 3 / 6 2

Un programme principale pour ces fonctions est donné par la figure IV-9. On y a initialisé un arbre par affectations dans les différents nœuds pour illustrer l'usage des structures avec des pointeurs. Les variables a, b,..., i sont des pointeurs pour les différents nœuds. L'arbre construit est (a, (b, (d, e), c, (f, (g, (h, i))))) en notation LISP. Soit:

Illustration

Attention, ne pas commettre l'erreur facile de vouloir condenser et écrire a=b=c=...=i=ArbreAllouer(); (Pourquoi c'est faux?)

#include <stdio.h>
#include <malloc.h>
struct StrNoeud {
       char type;
       union {
            int operande;
            char operateur;
       } valeur;
} ;

typedef struct StrArb {
               struct StrNoeud nd;
               struct StrArb *fg;
               struct StrArb *fd;
        } ARBRE, *PTRARBRE;

main()
{
PTRARBRE a,b,c,d,e,f,g,h,i, ArbreAllouer();
/* allocations des noeuds */
a = ArbreAllouer(); b = ArbreAllouer(); c = ArbreAllouer();
d = ArbreAllouer(); e = ArbreAllouer(); f = ArbreAllouer();
g = ArbreAllouer(); h = ArbreAllouer(); i = ArbreAllouer();
/* initialisations des feuilles (fils = NULL) */
d->fg = NULL; d->fd = NULL;
e->fg = NULL; e->fd = NULL;
f->fg = NULL; f->fd = NULL;
h->fg = NULL; h->fd = NULL;
i->fg = NULL; i->fd = NULL;
/* initialisations des feuilles Opérateurs/
a->nd.type ='C'; a->nd.valeur.operateur ='+';
b->nd.type ='C'; b->nd.valeur.operateur ='-';
c->nd.type ='C'; c->nd.valeur.operateur ='*';
g->nd.type ='C'; g->nd.valeur.operateur ='/';
/* initialisations des feuilles Opérandes/
d->nd.type ='I'; d->nd.valeur.operande = 3;
e->nd.type ='I'; e->nd.valeur.operande = 1;
f->nd.type ='I'; f->nd.valeur.operande = 3;
h->nd.type ='I'; h->nd.valeur.operande = 6;
i->nd.type ='I'; i->nd.valeur.operande = 2;
/* Et des noeud restants jusqu'à la racine */
b->fg = d; b->fd = e;
g->fg = h; g->fd = i;
c->fg = f; c->fd = g;
a->fg = b; a->fd = c;
/* les appels */
printf(" RESULTAT %d \n",EvalExp(a));
PreFixe(a);
}

PTRARBRE ArbreAllouer(){
    /* allocation d'un arbre */
    return ( (PTRARBRE) malloc(sizeof(ARBRE)));
}

Fig-IV-9 Programme Principale pour les Figures IV-7 et IV-8.

6. Types Enumérés

Autre caractéristique de C, un type énuméré définit un ensemble d'objets dont les seules valeurs possibles sont données en extension, i.e. énumérées. Ce sont donc des objets élémentaires. La syntaxe est identique à celle de la structure avec le mot clé enum

enum [<nom>] { <listeEnum> } [<variables>];

Le rôle de <nom> est le même que pour struct. Il sert à déclarer des variables enum. Par exemple

enum color {blue, green, red};
enum color coul, *pcoul;

coul est une variable de type énuméré pouvant recevoir les valeurs blue, green ou red.

coul = blue;

pcoul est un pointeur vers une couleur pouvant en recevoir l'adresse.

pcoul = &coul.

Les types énumérés se comportent comme les types scalaires, on peut les affecter, les comparer (l'ordre d'énumération est l'ordre croissant: blue < red < green ici), les passer en paramètres etc....

Par ailleurs, les valeurs (énumérées) d'un type enum, sont équivalentes à des constantes entières 0, 1, 2, ...(dans l'ordre d'énumération) et peuvent être traitées (ce que fait le compilateur) comme telles. Par exemple blue est équivalent à 0, green à 1 et red à 2. Ces constantes peuvent être redéfinies dans l'énumération.

enum color {blue, green=3, red=20};

Ici green est 3, red est 20 et blue reste 0 (l'ordre est croissant). Si on fait par exemple

switch (coul){...}

on peut écrire case 3 : ...  ou

case green : ...

That's all folks !



[0] C.A.R.Hoare "Hints on Programming Language Design", 1973, Prentice-Hall, collection of essays and papers by Tony Hoare

[1] Plus exactement, un objet peut être défini comme un triplet <adresse, valeur, type>. L'adresse servant pour identifier l'objet et designer son emplacement. Cet emplacement contient la valeur, d'un certain type, de l'objet.

[2] En C++, il existe une instruction new, semblable à malloc() et d'usage plus facile.

[2] Se rappeler néanmoins qu'un fichier C peut être compilé avec seulement l'une des parties citées.


< Précédent Sommaire Suivant >


Copyleft © Najib TOUNSI
ntounsi@emi.ac.ma
Version : Sept 2000, Dernière MàJ: Dec 2021