Dans la fiche précédente, nous avons introduit le type C pour la réalisation du type abstrait Liste appelée "listes chaînées dynamiques", puis quelques algorithmes de parcours de listes.
Dans cette fiche, on complète pas à pas les points essentiels de l'implémentation, avec les fonctions d'ajout et de suppression en tête de liste.
La fiche suivante présentera in extenso l'implantation de référence des listes chaînées dynamiques qui vous accompagnera tout le semestre et servira de base aux implantations des autres TADs fonctionnant sur le même principe.
1. Rappels de la fiche précédente
Comme dans la fiche précédente,
on s'intéresse à une liste chaînée dynamique contenant des
éléments de type double
,
dont voici une représentation après l'ajout de 2 éléments :
Une liste chaînée dynamique contenant 2 éléments, donc avec 2 maillons.
Le type C list_t
introduit dans la fiche précédente
est donc défini ainsi :
// Fichier list.h
#ifndef _LIST_H
#define _LIST_H
// définition des types maillon link_t et liste list_t
typedef struct _link {
double val; /* un élément de la liste*/
struct _link * next; /* un pointeur sur le maillon suivant de la liste */
} link_t, * list_t;
#endif
On connait déjà le code des trois fonctions suivantes, que nous avons construites dans la fiche précédente :
// retourne une liste vide
list_t list_new() {
return NULL;
}
// predicat : retourne VRAI si la liste l est vide
int list_is_empty( list_t l ) {
return NULL == l;
}
// Affiche les éléments de la liste - Version finale
void list_print( list_t l ) {
link_t * p = l;
// traitement du cas particulier "liste vide"
if(list_is_empty(p)) {
printf("La liste est vide\n");
return ; // plus rien à faire : on arrête la fonction
}
// cas général
while ( NULL != p ) { // "tant que p pointe un maillon"
printf(" %lf ;", p->val); // on affiche la valeur stockée dans le maillon
p = p->next; // on affecte à p l'adresse du maillon suivant
}
printf("\n"); // fin de la liste. Retour à la ligne pour terminer proprement
}
On construit maintenant les fonctions d'ajout en tête et de suppression en tête de liste.
2. Ajout en tête d'un élément
Maintenant qu'on sait parcourir une liste, voyons comment ajouter un nouvel élément en tête de la liste. On rappelle que le contrat et le prototype de la fonction à écrire est :
// Ajoute l'élément e en tête de la liste l et retourne la nouvelle liste
list_t list_add_first(element_t e, list_t l);
2.1 Principe de l'ajout en tête d'une liste chaînée dynamique
Pour réfléchir, on suppose qu'on a une liste l
contenant déjà un maillon, qui contient l'élement ( 9.0 )
:
La liste initiale ( 9.0 )
Pour ajouter l'élement 8.0
en tête de la liste
contenant 9.0
, il faut :
- Créer et allouer un nouveau maillon, appelé
p
; - Mettre la valeur
8.0
dans ce maillon ; - Insérer le nouveau maillon
p
au bon endroit (devant tous les autres), i.e. indiquer à ce nouveau maillon que son suivant est l'ancienne tête de liste, c'est à dire le maillon qui contient9.0
et qui est actuellement pointé parl
; - Indiquer que le nouveau maillon
p
est la nouvelle tête de la liste, i.e. mettre à jour la variable liste.
La nouvelle liste ( 8.0 9.0 )
.
2.2 Vers la fonction d'ajout en tête list_t list_add_first( element_t e, list_t l )
Dans cette section, c'est vous qui faites le cours !
Vos profs préférés ont préparé chacun
une version de la fonction list_t list_add_first( element_t e, list_t l )
,
qu'ils croient en situation de révolutionner le Monde des Listes Chaînées.
Le(s)quel(s) de vos profs devrai(en)t retourner à l'école ?
Quelle est la bonne version (si il y en a une)?
A vous de jouer !
Pour les listes de double
,
on considère le code suivant pour la fonction d'ajout en tête de liste :
// Dans le fichier list.c
list_t list_add_first(double val, list_t l) {
list_t newPtr ; // On link_t * newPtr, puisque le type liste est défini comme "pointeur sur maillon"
newPtr->val = val; // On stocke la valeur dans le maillon
newPtr->suiv = l; // On pointe sur l'ancien premier maillon
return newPtr; // On retourne l'adresse du nouveau maillon.
// newPtr est bien de type "adresse de maillon" : tout va bien !
}
// Dans le fichier test.c
int main() {
list_t mylist = list_new();
mylist = list_add_first(9.0, mylist);
mylist = list_add_first(8.0, mylist);
list_print(maliste);
}
Que fait le code ?
8.0 ; 9.0
9.0 ; 8.0
Attention, important !
Dans cette fonction, on a bien déclarer une variable locale pointeur
newPtr
pour pointer le nouveau maillon.
Mais :
- que vaut ce pointeur ? Il n'est pas initialisé ! Sa valeur (une adresse) est aléatoire !
- Où pointe-t-il ? N'importe où en mémoire !
- A-t-on le droit d'accéder, par exemple, à
new->val
? Non !
En découle un comportement hératique (aléatoire) du programme.
En fait, ce code n'a jamais créé en mémoire le nouveau maillon !
Rationnel 1 : il est nécessaire, dans la fonction d'ajout en tête, de créer en mémoire le nouveau maillon !
Rationnel 2 : Nicolas, tu sors.
On considère le code suivant :
// Dans le fichier list.c
list_t list_add_first(double val, list_t l) {
link_t m ; // le nouveau maillon.
// Notez bien l'usage de link_t :
// cette fois ci, m n'est pas un pointeur, mais bien une structure maillon
m.val = val; // on stocke la valeur dans le maillon
m.suiv = l; // on pointe sur l'ancien premier maillon
return &m; // on retourne l'adresse du nouveau maillon.
// &m est bien de type "adresse de maillon",
// comme le type de retour de la fonction. Tout va bien... apparemment...
}
// Dans le fichier test.c
int main() {
list_t mylist;
mylist = list_new();
mylist = list_add_first(9.0, mylist);
mylist = list_add_first(8.0, mylist);
list_print(maliste);
}
Que fait le code ?
&m
est bien de type "adresse d'un maillon".
Ceci dit, notez que, si l'option -Wall
était passée au compilateur,
le compilateur afficherait un warning "Attention : retour de l'adresse d'une variable locale".
8.0 ; 9.0
9.0 ; 8.0
Attention, important !
Dans cette fonction, on crée bien une variable tructurée de type link_t pour le nouveau maillon.
Mais il s'agit d'une variable locale. Lorsque la fonction se termine, toutes variables localessont détruites sur la pile. Ce maillon est donc détruit. Et la fonction retourne l'adresse d'une variable (le maillon) qui n'existe plus !
Conséquence : dans la suite du programme principal, on manipule par adresse
une case mémoire
qu'on a plus le droit de manipuler. En découle un comportement hératique,
d'aparence aléatoire,
parfois provoquant une erreur de segmentation (seg fault
).
Rationnel 1 : comme on veut que le maillon reste alloué après l'exécution de la fonction, il faut avoir recours à l'allocation dynamique.
Rationnel 2 : François, dehors !
Des deux QCM précédents, on retiendra (outre le fait que vos profs devraient retourner à l'école... ) :
Pour toutes les fonction d'ajout d'un élément à une liste...
Comme pour les variables classiques de type pointeur, déclarer list_t l;
définit une variable qui contient une adresse.
Mais cela ne réserve PAS la mémoire pour la structure pointée, en l'occurence le maillon qu'on veut créer.
Pour ajouter un élément à une liste, il faut créer un maillon en mémoire.
De plus, le maillon créé doit continuer à exister après la fin de la fonction.
On doit donc avoir recours à l'allocation dynamique pour allouer le nouveau maillon.
On considère le code suivant :
// Dans le fichier list.c
list_t list_add_first_v3(double val, list_t l) {
list_t p; // pour pointer le nouveau maillon
// on alloue dyamiquement le nouveau maillon
p = calloc(1,sizeof(*p));
if (p==NULL) {
// une allocation dynamique peut échouer : on gère ce cas d'erreur !
printf("Allocation de mémoire impossible. Aborts.\n");
exit(1);
}
p->val = val; // on stocke la valeur dans le maillon
p->suiv = l; // le maillon suivant le nouveau est l'ancien premier maillon : on pointe dessus
return p; // on retourne l'adresse du nouveau maillon alloué dynamiquement
}
// Dans le fichier test.c
int main() {
list_t mylist;
mylist = list_new();
list_add_first_v3(9.0, mylist);
list_add_first_v3(8.0, mylist);
list_print(maliste);
}
Que fait ce code ?
8.0 ; 9.0
9.0 ; 98.0
la liste est vide
Le code de la fonction est correct. Mais l'appel de la fonction ne l'est pas !
En C, les paramètres sont passés par valeur, c'est à dire qu'une fonction ne peut jamais modifier ses paramètres d'appel.
Lorsqu'on appelle la fonction avec list_add_first(9.0, mylist);
,
on ne passe pas à la fonction la variable mylist
,
mais la valeur de cette variable (l'adresse du premier maillon, donc).
Donc, la variable mylist
des appels de list_add_first
,
ligne 20 et 21 ne peut pas etre modifiée par ces appels.
Comme elle est initialisée à NULL
par la ligne 19 list_add_first
,
elle aura toujours la meme valeur
apres la ligne 20 (appel à ajoutTeteListeDyn
).
mylist
valant NULL
, c'est une liste vide.
Dans ces conditions, l'execution de la ligne 22 afficheListeDyn(maliste);
affiche une liste vide !
En fait : dans le main()
, le développeur à oublié d'utiliser
la valeur retournée par la fonction d'ajout en tête !.
Il eut fallut écrire :
mylist = list_add_first(9.0, mylist);
Avec cette modication, tout ce code devient correct.
Du QCM précédent, on retiendra (outre le fait que Michel doit aussi retourner en cours... ) :
Pensez à récupérer les valeurs de retour de la fonction
list_add_first()
quand vous l'appelez !
2.3 Version finale de la fonction list_t list_add_first( double e, list_t l )
Vous devriez avoir compris les points clés permettant
d'écrire la fonction list_t list_add_first( double e, list_t l )
.
En voici la version finale
Il vous appartient de bien comprendre le code de cette fonction, de vous assurer qu'elle se comporte bien dans tous les cas (tracer l'algorithme dans le cas général, le cas liste à 1 élément et le cas liste vide) et de le retenir.
3. Suppression de l'élément de tête
La suppression de l'élément de tête consiste simplement à enlever le premier maillon et retourner le reste de la liste. On rappelle que le contrat et le prototype de la fonction à écrire est :
// Supprime le maillon en tête de liste et retourne la nouvelle liste
// PRECONDITION : liste non vide
list_t list_del_first(list_t l);
3.1. Entrainons-nous sur 2 exemples comportant des erreurs courantes
list_t list_del_first( list_t l ) {
if(NULL == l) {
// cas liste vide : Erreur
fprintf(stderr, "Erreur : liste vide\n");
exit(EXIT_FAILURE);
}
return l->next; // il suffit de retourner l'adresse du maillon suivant, nouveau 1er maillon !
}
Que dire de ce code ?
calloc
dans la fonction d'ajout.
Il faut le libérer avec free
.
Sin on ne le fait pas, la mémoire restera allouée, alors qu'on ne l'utilisera plus jamais... C'est une "fuite mémoire"
list_t list_del_first( list_t l ) {
free( l );
return l->next;
}
Quel(s) sont les problèmes de ce code ?
free()
pour libérer une une zone mémoire allouée précédemment allouée dynamiquement,
il est interdit d'accéder à cette zone mémoire.
Or, la ligne 3 accède au champ
next
du maillon qu'on
vient de libérer !
3.2. Construisons la fonction list_t list_del_first( list_t l )
On considère une liste contenant 2 éléments ( 8.0 9.0 )
:
Pour supprimer le maillon situé en tête de liste, qui contient l'élément 8.0
, il faut :
- Sauvegarder un pointeur sur le reste de la liste ;
-
Libérer la mémoire allouée pour le premier maillon contenant
8.0
. Comme la mémoire a été allouée dynamiquement, il faut appelerfree()
pour la libérer. - Retourner la nouvelle tête de liste.
3.3. Version finale de la fonction list_t list_del_first( list_t l )
Voici le code final de la fonction de suppression en tête de liste :
Il vous appartient de bien comprendre le code de cette fonction, de vous assurer qu'elle se comporte bien dans tous les cas (tracer l'algorithme dans le cas général, le cas liste à 1 élément et le cas liste vide) et de la retenir.
4. En conclusion
Cette fiche et la précédente ont progressivement mis en place la structure de donnée "liste chaînée dynamique" : la définition des types C supportant cete structure de donnée, et les principales fonctions réalisant les principales opérations sur les listes.
Nous avons ce faisant expliqué comment travailler pour y parvenir.
La fiche suivante donne in extenso l'implantation de référence des listes chaînées dynamiques, qui vous accompagnera durant tout le semestre et au delà. A bien connaître !
Appel de la fonction pour ajouter le second élément.
Déclaration d'un pointeur
p
, pour le nouveau maillon :p
;8.0
dans le nouveau maillon.Le pointeur
next
dep
doit indiquer l'ancienne tête de listeLa tête de la nouvelle liste le nouveau maillon, pointé par
p
.On retourne cette nouvelle liste, repérée par
p
.Retour au main.
Penser à mettre à a jour la variable liste (ici
maliste
) pour qu'elle pointe le nouveau maillon !