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]
http://www.mescours.ma/C/c4.html
ntounsi@emi.ac.ma
Version: Sept. 2000, Dernière MàJ: Dec. 2021
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.
*
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.
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.
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:
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
.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).
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.
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
.
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).
Exemple: Inversion d'un tableau. On parcourt un tableau dans les deux sens jusqu'au milieu, et on permute les éléments rencontrés.p = &t[3];
q = &t[7];
i = q - p; /* i reçoit 4 */
Exercicesfor(p= &t[0], q= &t[MAX-1]; abs(p-q)>1; p++,q--){
i= *q;
*q= *p;
*p=i;
}
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.
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>
.
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.
Il est possible en C de déclarer un pointeur sur un pointeur (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?.
*
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:
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.
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).
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
Donc*(p+i) = 5;
est équivalent àt[i] = 5;
désignent le même objet ( si*(p + i)
ett[i]
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
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'écrirep = t; et
peut donc s'écrire (ce que fait un compilateur)
t[i]*(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'écriret + 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.
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?
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 adresset[2]
et qui est 2 ici, ou bien
**(t+2)
, valeur pointée par le contenu de l'adresset+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
.
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"
danst[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.
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 {
(1)
int jour;
char mois[3];
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)
où 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;
(3)
char mois[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)
où 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 ;
où 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:
Utilisation:struct personne {
char nom[10],prenom[10];
float salaire;
struct date DateNaissance;
};
notation doublement pointée dans le deuxième cas.struct personne p;
p.nom = "toto\0";
p.DateNaissance.jour = 10;
Remarque: On peut déclarer des variables structure sans nommer celle-ci.
struct {int x,y} z;
où 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 ? )
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 (structurepersonne
). La dimension[100]
suit donc le nom de la variable structure comme dansint 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.
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.
Soit la structure:
struct {
int x;
int *y;
} *p, q[10];
p
pointe vers un couple: entier x
et un
pointeur d'entiers y
.
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:
On a:
tout commep->x
est 1 et
*p->y
est 2 (->
étant prioritaire, l'indirection concerney
),
Attention à la syntaxe:q[0].x
est 1 et
*q[0]->y
est 2.est 3 et
(p+1)->x*(p+1)->y
est 4. (Ce seraitq[1].x
et*q[1]->y
ici)est 3 et
(++p)->x*(++p)->y
est 4. (p
aura été incrémenté de 1 avant d'accéder àx
et ce à quoiy
pointe)
++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).
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).
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;
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.
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;
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;.
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);
};
}
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é:
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);
}
}
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:
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)));
}
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 !
[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.