Plusieurs problèmes surviennent lorsque l’on utilise des tableaux pour stocker des valeurs :
Nous définirons dans ce cours un autre moyen de stocker les données en formant un ensemble ordonné, le listes chaînées.
Soit T un type structuré défini comme suit :
typedef struct T { int i; char c; }T;
Si p est un pointeur de type T*, alors p contient l’adresse
mémoire d’un élément de type T. Soit t une variable de type T,
et soit l’affectation
p = &t
. Alors, p pointe sur t. De ce
fait *p et t sont des alias, et nous pourrons indifférement
utiliser t.i (resp. t.c) et (*p).i (resp (*p).c). Par exemple,
réecrivons le programme de l’exemple du cours sur les structures :
#include<stdio.h> #include<stdlib.h> #define N 10 /**************************************************************/ typedef struct point { double abs; double ord; }point; /**************************************************************/ point nextPoint(point* previous) { point result; result.ord = (*previous).ord + 1.; result.abs = (*previous).abs + 2.; return result; } /**************************************************************/ void initTableauPoints(point* p, int n) { int i; (*p).ord = 0; (*p).abs = 1; for(i = 1 ; i < n ; i++) *(p + i) = nextPoint(p + i - 1); } /**************************************************************/ void affichePoint(point* p) { printf("(%f, %f)", (*p).abs, (*p).ord); } /**************************************************************/ void afficheTableauPoints(point* p, int n) { int i; for(i = 1 ; i < n ; i++) { printf("p[%d] = ", i); affichePoint(p + i); printf("\n"); } } /**************************************************************/ int main() { point* p; p = (point*)malloc(N * sizeof(point)); if (p == NULL) return -1; initTableauPoints(p, N); afficheTableauPoints(p, N); free(p); return 0; }
L’écriture (*p).i permet de désigner le champ i de la variable pointée par p. Cette écriture, peu commode, peut être remplacée par p−>i, par exemple :
#include<stdio.h> #include<stdlib.h> #define N 10 /**************************************************************/ typedef struct point { double abs; double ord; }point; /**************************************************************/ point nextPoint(point* previous) { point result; result.ord = previous->ord + 1.; result.abs = previous->abs + 2.; return result; } /**************************************************************/ void initTableauPoints(point* p, int n) { int i; p->ord = 0; p->abs = 1; for(i = 1 ; i < n ; i++) *(p + i) = nextPoint(p + i - 1); } /**************************************************************/ void affichePoint(point* p) { printf("(%f, %f)", p->abs, p->ord); } /**************************************************************/ void afficheTableauPoints(point* p, int n) { int i; for(i = 1 ; i < n ; i++) { printf("p[%d] = ", i); affichePoint(p + i); printf("\n"); } } /**************************************************************/ main() { point* p; p = (point*)malloc(N * sizeof(point)); if (p == NULL) return -1; initTableauPoints(p, N); afficheTableauPoints(p, N); free(p); }
Attention ! Les opérateurs d’accès aux champs
.
et
->
ont une priorité supérieure à celles de tous les autres
opérateurs du langage ! Si vous manipulez un tableau de structures
t
et que vous souhaitez accéder au champ
t
du
i
-ème élément, est-il intelligent d’écrire
*(i + t).c
? Absolument pas !
.
est prioritaire sur
*
, donc le
parenthèsage implicite est
*((i + t).c)
, essayez de vous
représenter ce que cela fait, et vous comprendrez pourquoi votre
programme plante ! En écrivant
(i + t)->c
, vous obtenez une
expression équivalente à
(*(i + t)).c
, qui est déjà bien
plus proche de ce que l’on souhaite faire.
Considérons le type suivant :
typedef struct maillon { int data; struct maillon* next; }maillon;
On appelle cette forme de type un type récursif, c’est-à-dire qui contient un pointeur vers un élément du même type. Observons l’exemple suivant :
#include<stdio.h> /**************************************************************/ typedef struct maillon { int data; struct maillon* next; }maillon; /**************************************************************/ int main() { maillon m, p; maillon* ptr; m.data = 1; m.next = &p; p.data = 2; p.next = NULL; for (ptr = &m ; ptr != NULL ; ptr = ptr->next) printf("data = %d\n", ptr->data); return 0; }
m
et
p
sont deux maillons, et
m->next
pointe vers
p
, cela signifie que lorsque l’on initialise
ptr
à
&m
, alors
ptr
pointe sur
m
. Donc, lors de la première itération de la boucle
for
, la valeur
prt->data
est
m.data
, à
savoir
1
. Lorsque le pas de la boucle est exécuté,
ptr
reçoit la valeur
ptr->next
, qui n’est autre que
m->next
, ou encore
&p
. Donc
ptr
pointe
maintenant sur
p
. Dans la deuxième itération de la boucle,
ptr->data
est la valeur
p.data
, à savoir
2
. Ensuite le pas de la boucle est exécuté, et
ptr
prend la valeur
ptr->next
, à savoir
p->next
, ou
encore
NULL
. Comme
ptr == NULL
, alors la boucle
s’arrête. Ce programme affiche donc :
data = 1 data = 2
La façon de renseigner les valeurs des pointeurs
next
observée dans l’exemple précédent s’appelle
le chaînage. Une liste de structures telle que chaque
variable structurée contienne un pointeur vers vers une autre variable
du même type s’appelle une liste chaînée. Utilisons un
tableau pour stocker les éléments d’une liste chaînée à
10
éléments.
#include<stdio.h> #include<stdlib.h> #define N 10 /**************************************************************/ typedef struct maillon { int data; struct maillon* next; }maillon; /**************************************************************/ void printData(maillon* ptr) { for( ; ptr != NULL ; ptr = ptr->next) printf("data = %d\n", ptr->data); } /**************************************************************/ int main() { maillon* l; int i; l = (maillon*)malloc(N * sizeof(maillon)); if (l == NULL) return -1; l->data = 0; for(i = 1 ; i < N ; i++) { (l + i)->data = i; (l + i - 1)->next = l + i; } (l + N - 1)->next = NULL; printData(l); free(l); return 0; }
Les 10 maillons de la liste chaînée ont été placés dans le tableau
l, la première boucle dispose le chaînage des éléments dans le même
ordre que dans le tableau, l’affichage de la liste est fait dans le
sous-programme
printfData
, et seul le chaînage y est
utilisé. Comme le champ
data
du i-ème élément de la liste
contient la valeur i, alors ce programme affiche :
data = 0 data = 1 data = 2 data = 3 data = 4 data = 5 data = 6 data = 7 data = 8 data = 9
Pour modifier l’ordre de parcours des maillons, il suffit de modifier le chaînage, par exemple,
#include<stdio.h> #include<stdlib.h> #define N 10 /**************************************************************/ typedef struct maillon { int data; struct maillon* next; }maillon; /**************************************************************/ void printData(maillon* ptr) { for( ; ptr != NULL ; ptr = ptr->next) printf("data = %d\n", ptr->data); } /**************************************************************/ int main() { maillon* l; int i; l = (maillon*)malloc(N * sizeof(maillon)); if (l == NULL) { printf("Plus de mémoire"); return 1; } l->data = 0; (l + 1)->data = 1; (l + N - 2)->next = l + 1; (l + N - 1)->next = NULL; for(i = 2 ; i < N ; i+=1) { (l + i)->data = i; (l + i - 2)->next = l + i; } printData(l); free(l); return 0; }
Ce programme affiche
data = 0 data = 2 data = 4 data = 6 data = 8 data = 1 data = 3 data = 5 data = 7 data = 9
Pour le moment, nous avons stocké les éléments dans un tableau, les maillons étaient donc regroupés dans des zones mémoire contigües. Il est possible de stocker les maillons dans les zones non contigües, tous peuvent être retrouvés à l’aide du chaînage. Par exemple,
#include<stdio.h> #include<stdlib.h> #define N 10 /**************************************************************/ typedef struct maillon { int data; struct maillon* next; }maillon; void printData(maillon* ptr) { for( ; ptr != NULL ; ptr = ptr->next) printf("data = %d\n", ptr->data); } /**************************************************************/ void freeLL(maillon* l) { maillon* n; while(l != NULL) { n = l->next; free(l); l = n; } } /**************************************************************/ int main() { maillon* l; maillon* current; maillon* previous; int i; l = (maillon*)malloc(sizeof(maillon)); if(l == NULL) return -1; l->data = 0; previous = l; for(i = 1 ; i < N ; i++) { current = (maillon*)malloc(sizeof(maillon)); if(current == NULL) exit(0); current->data = i; previous->next = current; previous = current; } current->next = NULL; printData(l); freeLL(l); return 0; }
Ce programme affiche
data = 0 data = 1 data = 2 data = 3 data = 4 data = 5 data = 6 data = 7 data = 8 data = 9
Pour plus de clarté, on placera l’initialisation de la liste dans une fonction :
#include<stdio.h> #include<stdlib.h> #define N 10 /**************************************************************/ typedef struct maillon { int data; struct maillon* next; }maillon; /**************************************************************/ void printLL(maillon* ptr) { for( ; ptr != NULL ; ptr = ptr->next) printf("data = %d\n", ptr->data); } /**************************************************************/ maillon* initLL(int n) { maillon* first; maillon* current; maillon* previous; int i; first = (maillon*)malloc(sizeof(maillon)); if(first == NULL) return NULL; first->data = 0; previous = first; for(i = 1 ; i < n ; i++) { current = (maillon*)malloc(sizeof(maillon)); if(current == NULL) exit(0); current->data = i; previous->next = current; previous = current; } current->next = NULL; return first; } /**************************************************************/ void freeLL(maillon* l) { maillon* n; while(l != NULL) { n = l->next; free(l); l = n; } } /**************************************************************/ int main() { maillon* l; l = initLL(N); if (l == NULL) return -1; printLL(l); freeLL(l); return 0; }
Observons de quelle façon il est possible d’effectuer certaines opérations sur une liste chaînée. Les sous-programmes ci-dessous ont été pensé et combinés pour être les plus simples possibles...
maillon* creeMaillon(int n) { maillon* l; l = (maillon*)malloc(sizeof(maillon)); if(l == NULL) exit(0); l->data = n; l->next = NULL; return l; }
maillon* insereDebutLL(maillon* l, int n) { maillon* first = creeMaillon(n); first->next = l; return first; }
maillon* initLL(int n) { maillon* l = NULL; int i; for(i = n - 1 ; i >= 0 ; i--) l = insereDebut(l, i); return l; }
Un maillon d’une liste doublement chaînée contient deux pointeurs, un vers le maillon précédent, et un vers le maillon suivant.
typedef struct dmaillon { int data; struct dmaillon* previous; struct dmaillon* next; }dmaillon;
Aussi paradoxal que cela puisse paraître, bon nombre d’opérations sont davantage aisées sur des listes doublement chaînées. Pour se faciliter la tâche, nous manipulerons les listes chaînées à l’aide de deux pointeurs, un vers son premier élément, et un vers son dernier :
typedef struct dLinkedList { struct dmaillon* first; struct dmaillon* last; }dLinkedList;
Le soin d’ecrire les opérations sur ces listes vous est laissé dans le TP...