• Aucun résultat trouvé

Les pointeurs et les listes chainées

N/A
N/A
Protected

Academic year: 2022

Partager "Les pointeurs et les listes chainées"

Copied!
15
0
0

Texte intégral

(1)

42

Huitième Partie

Les pointeurs et les listes chainées

(2)

43

Les pointeurs et les listes chainées

1. Les pointeurs 1. Introduction

Partons d’une considération simple : tout ce que notre ordinateur manipule est chargé en mémoire centrale. La mémoire, comme nous le savons déjà est un ensemble de cellules, chacune connue par un numéro appelé son adresse. Chaque instruction, chaque donnée de notre programme -en cours d’exécution- possède donc une adresse par laquelle elle est accessible. C’est pour manipuler ces adresses que la notion de pointeur a été introduite : un pointeur est une donnée dont la valeur est une adresse en mémoire.

2. Manipulations de base sur les pointeurs

Commençons par une précision concernant le vocabulaire que nous utiliserons : Lorsqu’un pointeur p a comme valeur l’adresse adr d’une donnée d, nous dirons que p pointe (ou

référence) d.

Manipuler les pointeurs suppose que l’on puisse passer d’une donnée d à son adresse adr et inversement. Autrement dit nous devons pouvoir :

• récupérer l’adresse adr d’une donnée d,

• accéder à la donnée d dont on connaît l’adresse adr.

Mais avant tout nous devons pouvoir déclarer des pointeurs. Pour ce faire le pointeur peut avoir comme valeur l’adresse d’une donnée d’un type précis TypePointeur. Nous dirons qu’il s’agit d’un ”pointeur sur TypePointeur”. Le pointeur sera, donc, déclaré de la manière suivante :

TypePointeur * nomdupointeur ; Les opérations possibles avec les pointeurs sont :

• Récupérer l’adresse d’une donnée d, se fait grâce à l’opérateur unaire ”adresse de”

noté & appliqué à d.

• Récupérer la donnée pointée par un pointeur p (c’est-à-dire dont l’adresse est la valeur de p) se fait grâce à l’opérateur ”d’indirection” noté * appliqué à p.

Exemple : ...

/*Déclaration d’un pointeur sur entier */

int *p;

int *q ; int x,y;

...

x=7;

y=10;

(3)

44 ...

/*Nous donnons à ptr la valeur ”adresse de a” */

/*(resp. ”adresse de b”) */

/*ensuite nous affichons la valeur pointée par ptr */

/*C’est donc la valeur 1 (resp 5) qui sera affichée */

p = &x;

printf(”La valeur pointée par p = %d\n”,*p);

q = &y;

printf(”La valeur pointée par q = %d\n”,*q);

Remarque : Il arrive parfois que l’on veuille préciser qu’un pointeur ”ne pointe aucune donnée”. Dans ce cas nous lui affectons la valeur 0 représentée par le symbole NULL défini dans le fichier d’entête stdio.h.

Un exemple d’implementation de pointeurs en mémoire :

3. Allocation dynamique

L’allocation dynamique d’espace c’est la réservation ”dynamique” d’un espace mémoire pour la réception d’une donnée. Pour ce faire nous fournissons en entrée la taille de cet espace et récupérons en sortie la première adresse de l’espace mémoire réservé. Ceci est assurée par la fonction malloc selon la syntaxe :

Nompointeur=(typePointeur *) malloc (tailledemandee) ; Où tailledemandée est la taille en octets à réserver pour les données.

Si cette taille ne peut être définie exactement par le programmeur, ce dernier pourra utiliser la fonction sizeof qui retourne la taille, en octet, du type de pointeur demandé sa syntaxe est :

sizeof(typepointeur).

Le système retourne l’adresse de l’espace alloué qui sera pointé par nompoiteur.

Pour libérer l’espace alloué par malloc on peut utiliser la fonction free selon la syntaxe : Free(nompointeur)

Les deux fonctions malloc et free sont prototypées dans la bibliothèque stdlib.h.

Réserve nb octets consécutifs et retourne la première adresse Nb etier

Fonction malloc

Adresse : nbptr

(4)

45 Exemple :

int *X ;

X=(int*)malloc(sizeof(int)) ;

*X=10 ;

4. Pointeurs et structure

Comme pour une variable de type réel, entier ou caractère, on peut créer un pointeur sur une structure donnée. Ceci se réalise comme suit :

struct Nomstructure * nomvariable ; Exemple :

...

...

/*Définition de la structure SVecteur */

struct SVecteur { float x ; float y ; float z ; };

...

...

/*Déclaration d’un pointeur sur la structure SVecteur */

struct SVecteur *pV ; ...

/*Allocation mémoire au pointeur */

pV=(struct SVecteur * )malloc(sizeof (struct SVecteur));

/*Manipulation du vecteur */

(* pV).x = 1.0;

(* pV).y = 2.0;

(* pV).z = 3.0;

...

...

/*Fin de la durée de vie du vecteur */

free(pV) ;

Nous utilisons les opérateurs * et . pour accéder à un champ de la donnée pointée. Ces deux opérateurs peuvent être remplacés par un seul opérateur : l’opérateur ->.

? 10

X mémoire X mémoire

(5)

46 Donc, les écritures suivantes :

(* pV).x = 1.0;

(* pV).y = 2.0;

(* pV).z = 3.0;

Pourront être remplacées par : pV->x = 1.0;

pV->y = 2.0;

pV->z = 3.0;

5. Nom de tableaux et pointeurs

Soit un tableau tab de taille taille et dont les éléments sont de type T.

La disposition en mémoire des éléments de tab vérifie les propriétés suivantes :

• Chaque élément de tab occupe n cellules en mémoires où n est égal à sizeof (T).

• Les cellules occupées par les éléments de tab sont contigües.

• La différence d’adresses entre deux éléments successifs du tableau est égale à 1.

• La valeur de tab est une adresse et plus précisément l’adresse du premier élément du tableau (c’est-à-dire tab[0] ).

a. Parcours d’un tableau

Il découle de ce qui a été dit, ce qui suit :

• L’adresse du ième élément du tableau tab est égale à &tab[i], compte tenu des propriétés précédentes c’est aussi tab+i.

• Le ième élément du tableau est noté tab[i], compte tenu du point précédent c’est aussi

* (tab+i).

L’utilisation du tableau comme pointeur permet d’écrire : scanf (”%d”,tab+i); et printf (” %d”,* (tab+i));

Exemple :

/*Exemple 1 de parcours d’un tableau*/

...

int i;

int tab[10];

...

...

/*Lecture des éléments du tableau*/

for (i=0;i < 10;i++) {

printf (”tab[%d] = ”,i);

scanf (”%d”,tab+i);

}

/*Affichage des éléments du tableau*/

for (i=0;i < 10;i++) {

printf (”tab[%d] = %d”,i,* (tab+i));

} ...

...

(6)

47

6. Pointeurs et fonctions

Dans ce paragraphe, on abordera différents aspects de l’utilisation des pointeurs dans les arguments et les retours d’une fonction :

• Du fait qu’un tableau ne peut être retourné par une fonction ce qui est parfois utile. La solution viendra des pointeurs.

• Ensuite tableau et pointeurs dans les arguments ....

• Pour finir on étudiera le mode de passage des arguments d’une fonction, les problèmes que cela pose et la solution apportée par les pointeurs.

Une fonction peut retourner un pointeur. Une précaution est toutefois à prendre : le

pointeur retourné ne peut pointer une variable locale de la fonction. Autrement dit le pointeur retourné doit pointer une zone mémoire ayant été allouée dynamiquement ou avoir la valeur NULL.

Ces 3 possibilités sont illustrées ci-dessous :

/*Retourner un pointeur : CE QU’IL NE FAUT PAS FAIRE*/

int * MauvaisRetourPointeur() { int * pret;

int n;

...

...

pret = &n;

...

...

return pret;

}...

/*Retourner un pointeur : CE QU’ON PEUT FAIRE*/

int * BonRetourPointeur() { int * pret;

...

...

pret = malloc(...);

...

...

return pret;

}...

/*Retourner un pointeur : CE QU’ON PEUT FAIRE*/

int * BonRetourPointeur() { int * pret;

...

...

pret = NULL;

...

...

return pret;

}...

Une utilisation importante des pointeurs comme retour de fonctions est de retourner un tableau (dynamique). Une fonction ne peut pas retourner de tableaux (statiques). L’exemple suivant permet d’illustrer cette possibilité.

(7)

48 Dans cet exemple :

• la fonction reçoit en entrée un tableau dynamique et un seuil,

• si le tableau contient des éléments supérieurs au seuil, la fonction retourne un tableau contenant uniquement ces éléments,

sinon la fonction retourne un pointeur nul.

/*Retourner un pointeur/tableau dynamique*/

int * retournerTableau(int * tab, int taille, int seuil ) { int i, cpt, j ;

int * ptret;

cpt = 0;

i = 0;

while(i<taille)

{ if (tab[i]>=seuil ) cpt++;

i++;

} if (cpt==0)

{ ptret = NULL;

}else

{ ptret = (int * )malloc(cpt*sizeof(int));

i = 0;

j = 0;

while(i<taille)

{ if (tab[i]>=seuil ) { ptret[j] = tab[i] ;

j++;

} i++;

} } return ptret;

}

Un autre exemple de fonction retournant un pointeur : /*Retourner un pointeur sur une structure*/

struct SVecteur * creerVecteur(float a, float b, float c) { struct SVecteur * pV ;

/*Début de la durée de vie du vecteur */

pV=(struct SVecteur * )malloc(sizeof (struct SVecteur));

/*Initialisation du vecteur */

pV ->x = a;

pV ->y = b;

pV ->z = c;

return pV ; }

7. Pointeurs dans les arguments d’une fonction

Un pointeur peut être utilisé comme argument d’une fonction.

L’exemple suivant permet de l’illustrer.

int main() { int i;

struct SVecteur * pvect;

struct SVecteur svect;

struct SVecteur tabvect[4] ;

(8)

49 ...

...

/*Normer le vecteur pointé par pvect*/

normer(pvect);

/*Normer svect*/

normer(&svect);

/*Normer tous les éléments de tabvect*/

for (i=0;i<4;i++) normer(tabvect+i);

}

void normer(struct SVecteur * pv) { float norme;

norme = calculerNorme(pv);

pv->x /=norme;

pv->y /=norme;

pv->z /=norme;

}

float calculerNorme(struct SVecteur * pv) { float norme;

norme = (float)sqrt(pv->x*pv->x+pv->y*pv->y+pv->z*pv->z );

return norme;

}

B- Les listes chaînées

1. Introduction

Considérons un ensemble de données (éléments) organisé de la manière suivante :

• L’ensemble est ordonné par la relation ” ... est le suivant de ...”

• Il y a un seul élément qui n’est le suivant d’aucun autre élément. On appellera cet élément le premier élément de la suite.

• Tout autre élément est le suivant d’un et un seul élément.

• Il y a un seul élément qui n’a pas de suivant. On appellera cet élément le dernier élément de la suite.

• Tout autre élément a un et un seul suivant.

• Tout élément de la suite, autre que le premier élément, n’est accessible que via l’élément dont il est le suivant.

Le premier élément est directement accessible. C’est le seul point d’entrée de la suite.

Une telle structure de données est appelée une liste chainée. Son implémentation sera faite de la manière suivante :

• On définira une liste par son point d’entrée, son premier élément.

• On définira chaque élément de la liste par sa valeur et par un moyen de passer à l’élément suivant.

(9)

50 ...

...

/*Définition des éléments de la liste */

struct SElementListe { int elem ;

struct SElementListe * psuivant ; };

typedef struct SElementListe * ElementListe;

/*Définition de la liste */

struct SListe

{ ElementListe pTeteDeListe ; };

typedef struct SListe * Liste;

...

...

La fonction suivante donne un exemple de manipulation d’une liste chaînée.

/*Parcours et affichage des éléments de la liste */

void afficherListe(Liste l) { ElemListe elem;

int p;

elem = l ->pTeteDeListe;

p = 0 ;

while(elem != NULL)

{ printf (”Position = %d, Valeur = %d\n”, p, elem->element);

elem = elem->psuivant;

p++;

} }

2. Ajout d’un élément dans une liste :

On se donne une liste L (pointeur sur la tête) ; un pointeur P sur un élément donné.

L’ajout de l’élément P à la liste L peut se faire en plusieurs positions de cette liste : En tête (au début), en queue (en fin) ou milieu après un élément donné R

1ercas : ajout en tête :

On pointe le suivant de P vers la tête L puis on change la tête en P :

(10)

51

2ème cas : ajout en fin (on aura un pointeur Q qui marque la queue) On pointe le suivant de Q sur P puis on change Q en P.

Ce qui donne

Remarque : pour ne pas devoir parcourir à chaque fois la liste, on maintient un pointeur sur qui marquera la queue de celle-ci.

3ème cas : on ajoute au milieu

On cherche l’élément après lequel on va insérer le nouvel élément puis on l’insère : 1. On pointe le suivant de P vers le suivant de Q.

2. Puis, on pointe le suivant de Q vers P.

(11)

52

3. Suppression d’un élément dans une liste : Trois cas aussi se présentent :

1er cas : la suppression d’un élément en tête :

On change la tête en suivant tête puis on libère l’ancien tête :

On place Q sur L,

1. On déplace L vers le suivant de Q, 2. On libère (free) Q.

2ème cas :On supprime la queue :

1. On positionne R sur l’avant dernier de la liste, 2. On marque le suivant de R sur NULL,

3. On libère Q,

4. On position Q sur R.

3ème cas : on supprime un élément pointé par P

1. On cherche l’élément R dont le suivant est l’élément à supprimer, 2. On positionne un pointeur P sur le suivant de R,

3. On positionne le suivant de R sur le suivant de P, 4. Puis on libère P.

L 1 2 3 4 NULL

Q

L

1 2 3 4 NULL

Q

L 1 2 3 4 NULL

Q R

L 1 2

R

3 NULL Q

1 2 3

P L

R

4 NULL

1 3

P L

R

4 NULL 2

1 2 4 NULL

R

L 3 NULL

Q

(12)

53

4. Extension : liste doublement chaînée (bidirectionnelles)

Dans les listes simples, on ne peut pas revenir en arrière, car on n’a pas l’adresse du précédent.

• Les listes doublement chaînées ou bidirectionnelles n’ont pas cet inconvénient : chaque élément possède deux liens :

o un vers son successeur o un vers son prédécesseur

Exemple : typedef Cliste{

float re, im ;

Cliste * precedent ; Cliste * suivant ; } ;

Cliste * ZListe ;

…….

On peut utiliser toutes les opérations possibles pour les listes simplement chaînées mais avec plus de souplesse vu qu’on peut toujours revenir en arrière.

1- Opérations sur les listes doublement chaînées a. ajout d’un élément

L’ajout d’un élément dans une liste peut se faire en tête, en queue ou après un élément donné de la liste.

1er cas : L’ajout en tête :

1- On pointe le suivant de l’élément à insérer sur la tête, 2- On pointe le précédent de l’élément à insérer sur NULL, 3- On pointe le précédent de la tête sur l’élément à insérer, 4- Puis on pointe la tête sur l’élément insérer.

L

NULL 1 2 3 4 NULL

L

NULL 1 2 3 4 NULL

NULL 5 R

1

L

1 2 3 4 NULL

NULL 5 R

2

NULL 5 R L

1 2 3 4 NULL

3

(13)

54 2ème cas : Ajout en queue :

1- On pointe le dernier de la liste par un pointeur Q, 2- On pointe le précédent de l’élément à insérer R sur Q , 3- On pointe le suivant de R sur NULL,

4- On pointe le suivant de Q sur R.

3ème cas : Insertion après un élément donné Q:

1- On pointe le suivant de R sur le suivant de Q, 2- On pointe le précédent de R sur Q,

3- On pointe le précédent du suivant de Q sur R, 4- En dernier lieu, on pointe le suivant de Q sur R.

b. La suppression d’un élément : 1er cas : Suppression de la tête :

1- On pointe un pointeur Q sur le suivant de la tête,

NULL 5 NULL R

L

NULL 1 2 3 4 NULL

Q

1

5 NULL R

L

NULL 1 2 3 4 NULL

Q

2

5 NULL R

L

NULL 1 2 3 4

Q

3

NULL 5 NULL R

L

NULL 1 2 3 4 NULL

Q

1

L

NULL 1 2 3 4 NULL

Q

5 R

2

5 R

L

NULL 1

Q

2 3 4 NULL

3

4

5 R

L

NULL 1 3 4 NULL

Q

2

(14)

55 2- On pointe le précédent de Q sur NULL, 3- On libère L,

4- On pointe L sur Q.

2ème cas : Supprimer la queue :

1- On pointe avec Q l’avant dernier élément de la liste (élément avant la queue), 2- On pointe libère le suivant de Q,

3- On pointe le suivant de Q sur NULL ;

3ème cas : Supprimer un élément au milieu de la liste : 1- On pointe l’élément à supprimer avec Q,

2- On pointe le suivant du précédent de Q sur le suivant de Q, 3- On pointe le précédent du suivant de Q sur le précédent de Q, 4- On libère Q.

L

NULL 1 2 3 4 NULL

Q

1

Q L

NULL 1 NULL 2 3 4 NULL

2

Q L

NULL 1 NULL 2 3 4 NULL

3

Q

L NULL 2 3 4 NULL

4

L

NULL 1 2 3 4 NULL

Q

1

L

NULL 1 2 3 4 NULL

Q

2

Q L

NULL 1 2 3 NULL

3

L

NULL 1 2 3 4 NULL

Q

1

Q L

NULL 1 2 3 4 NULL

2

L

NULL 1 2 4 NULL

Q

3 3

(15)

56 Remarques :

• D’autres opérations autres que l’ajout et la suppression (recherche, tri, inversion,fusion, …) seront vu au cours des TDs.

Si on pointe le suivant de la queue d’une liste doublement chaînée sur la tête de la liste et on pointe le précédent de la tête sur la queue de la liste on obtient une liste doublement chaînée circulaire :

L

NULL 1 2 4 NULL

Q

3 4

Références

Documents relatifs

liste supprimerSequence ( liste L,char CodeOp[] ): permet de supprimer tous les éléments d’une séquence de la liste

e coursename : afficher le nom d’un professeur le plus ancien souhaitant enseigner le cours coursename f filename : recopie la liste chaînée qui en résulte dans le

Ecrire une fonction qui accepte en argument un pointeur vers un element de type liste (maillon), ainsi qu’un pointeur vers une liste chainée constituée d’éléments de type

L’exemple suivant montre la déclaration d’une variable de type pointeur de fonction qui doit retourné un entier nommé pFonction1 et d’une fonction retournant

9. Le récepteur peut être inséré dans le boîtier du pointeur pour les déplacements 10. Économie d’énergie avec le Auto- Standby et l’interrupteur Marche / Arrêt 13.

4) Appuyez sur le bouton “Haut” pendant 2 secondes afin de faire une pause dans la présentation avec un écran noir. Appuyez à nouveau 2 secondes pour reprendre la

4) Appuyez sur le bouton “Haut” pendant 2 secondes afin de faire une pause dans la présentation avec un écran noir. Appuyez à nouveau 2 secondes pour reprendre la présentation

Ils sont ensuite émis sans vitesse par la source S, puis accélérés par un champ électrostatique uniforme qui règne entre S et P tel que.. U sp