Sie sind auf Seite 1von 19

Chapitre 3

Pointeur, structure et liste chaînée

3.1 Introduction

Les programmes procéduraux étudiés dans les Chapitre 1 et 2, utilisent la donnée la plus
élémentaire en programmation, à savoir la variable simple, celle qui permet de mémoriser
une seule valeur à la fois ; ou à la limite, un agencement linéaire et contigu de variables
de même type, qu'on appelle tableau . Cependant, les données qu'on pourrait être amené à
manipuler par programme peuvent avoir une structure qui est bien plus complexe qu'un
simple agencement homogène de variables.
Dans le présent chapitre, nous commençons par étudier un type très particulier de
variable simple qu'on appelle pointeur . Ensuite, nous passons à une structures de données
composites et hétérogènes qu'on appelle structure . Enn, à partir des notions de pointeur
et de structure, nous présentons la première structure de données dynamique de ce cours,
à savoir leslistes chaînées .

3.2 Les pointeurs

En plus des types de base présentés dans le Chapitre 1, le langage C ore au program-
meur une toute autre catégorie de types qui est emblématique de ce langage ; il s'agit des
pointeurs. La notion de pointeur a déjà été évoquée dans le Chapitre 2 lors de la descrip-
tion du mode de passage de paramètres par adresse. En fait, les pointeurs interviennent
dans plusieurs aspects de la programmation C, notamment, dans l'implémentation des
structures de données dites dynamiques.

Dénition 1
 Un pointeur est une adresse d'une zone mémoire.
 Une variable de type pointeur est une variable simple dont les valeurs pos-
sibles sont des adresses.
Le type pointeur est, en réalité, un type générique construit a partir d'un type prédéni
(char, int, float,...) et du symbole étoile (*). Ainsi, on peut construire le type pointeur
sur caractère, pointeur sur entier, pointeur sur réel et ainsi de suite. An de déclarer une
variable de type pointeur sur un type donné, le langage C utilise la syntaxe suivante :

1
2 CHAPITRE 3. POINTEUR, STRUCTURE ET LISTE CHAÎNÉE

RAM
ptr adresse
adresse *ptr
variable pointeur zone pointée

Figure 3.1  Schéma illustrant deux variables, la première étant de type pointeur, pointe
sur la deuxième.

type *nom_variable ;
Notez que, dans la déclaration précédente, l'étoile (*) est associée au nom de la variable
et non pas au type de référence. Par conséquent, dans la déclaration suivante :

int *ptr, ctr, *tab;

La variable ctr est de type entier alors que ptr et tab sont de type pointeur sur entier.

Par ailleurs, les valeurs de pointeur qui, rappelons le, sont des adresses de zone mé-
moire, peuvent être achées à l'aide des fonctions d'achage du langage C moyennant
le format d'impression %p. Un exemple de l'achage de la valeur d'un pointeur se trouve
dans la Ligne 10 du programme de la Figure 3.2. Nous précisons, à ce niveau, que cet
achage utilise la base hexadécimale.

3.2.1 Pointeurs et adresses mémoire

Il est possible d'accéder à des expressions de type pointeur sans avoir à déclarer des
variables pointeurs de manière explicite, comme dans le paragraphe précédent. En eet,
ceci peut se faire à l'aide de l'opérateur d'adresse &, qui donne accès à l'adresse en mémoire
d'une variable. Ainsi, si var est une variable de type type alors &var est une expression de
type pointeur sur type type
( *). Notez que ce moyen d'accéder à l'adresse d'une variable
est fréquemment utilisé par le mode de passage de paramètre par adresse, lors de l'appel
de la fonction scanf (), par exemple.
Réciproquement, à partir d'une expression de type pointeur, donc une adresse, on peut
accéder au contenu de cette adresse. Pour ce faire, on utilise encore une fois l'opérateur
étoile (*). Il s'agit là de l'opération principale qu'on peut eectuer sur un pointeur, à
indirection
savoir l' , c'est-à-dire, l'accès à la valeur d'un objet pointé par un pointeur.
Ainsi, si ptr est une expression de type pointeur sur type alors l'expression *ptr a pour
type type et sa valeur sera le contenu de la zone mémoire dont l'adresse est stockée dans
ptr.
Le programme C de la Figure 3.2, illustre quelques manipulations de variables et ex-
pressions de type pointeur. On peut voir que si ptr est initialisé a &var alors *ptr et var
désignent le même objet.

De part la réciprocité entre, d'une part, l'action d'accéder à une adresse et d'autre
part, l'action de l'indirection, on a ce qui suit : si var est une variable alors
3.2. LES POINTEURS 3

1 #include < s t d i o . h>

int
2

3 main ( )

int
4 {

5 var , ∗ ptr ;
6

7 var = 2019;

8 ptr = &v a r ;

10 p r i n t f ( "%d %p %d %p %d %p \ n " , v a r ,& v a r , ∗ ptr , ptr , ∗ & v a r ,& p t r ) ;


11 var −= 2;

12 ∗ ptr += 3;

∗ ptr ∗& v a r ) ;
return
13 p r i n t f ( "%d %p %d %p %d \ n " , v a r ,& v a r , , ptr ,

14 0;

15 }

Figure 3.2  Manipulation de variables et expression de type pointeur.

*&var ⇔ var

Cette équivalence est illustrée dans la Ligne 13 du programme de la Figure 3.2.

3.2.2 Pointeurs et passage de paramètres

Nous avons déjà abordé l'utilisation des pointeurs pour implémenter le mode de passage
de paramètre par adresse. Les règles à respecter si l'on veut passer une variable simple
var de type type_var
par adresse à une fonction f se résument comme suit :
 Faire précéder le nom de cette variable par l'opérateur d'adresse & lors de l'appel
de f ; on écrira alors quelque chose comme f(...,&var,...).
 Du coté de l'entête de la fonction f, il faut tenir compte du fait que l'on a passé à
f, l'adresse de var, et donc l'entête de f aura la forme suivante :
type_retour f(..., type_var *adr_var,...)
Pour le passage des paramètres du type tableau statique, (les tableau déclarés à l'aide
des crochets []), il n'y pas besoin d'utiliser l'opérateur d'adressage & lors de l'appel d'une
fonction, car une variable tableau a déjà une adresse (un pointeur) comme valeur. Du coté
des paramètres formelles, c'est à dire, dans les entêtes des fonctions utilisant des tableaux
statiques comme paramètre, la forme générale de la déclaration d'un paramètre de type
tableau est la suivante :

type_retour f(..., type tab[][tail_dim_2]...[tail_dim_d],...)


Notez que seul la taille de la première dimension peut être omise. La taille des autres
dimensions sont nécessaires pour que le compilateur sache calculer le décalage à eectuer
pour accéder aux diérentes cases du tableau à partir du nom de ce dernier et des indices.
4 CHAPITRE 3. POINTEUR, STRUCTURE ET LISTE CHAÎNÉE

3.2.3 Pointeurs et tableaux

Il existe un autre moyen d'obtenir des objets de type pointeur avec le langage C et ceci
en utilisant l'opérateur de construction de tableau, qui sont les crochets []. Par exemple,
la déclaration du tableau uni-dimensionnel suivant :

oat tab[MAX_TAIL];

crée une variable tab qui est de type pointeur sur oat
. Cette variable est initialisée, dès
sa déclaration, par une adresse. Puisqu'un tableau est une variable de type pointeur, il
est possible de donner un deuxième nom, notes, au tableau tab et d'utiliser ce nouveau
nom pour saisir les éléments du tableau, comme le montre le bout de code suivant :
..
.
oat tab[MAX_TAIL], *notes;
notes = tab;
for (i = 0 ; i < MAX_TAIL ; i++) scanf("%f",&notes[i]);
..
.
Bien que tab et notes sont de même type, leurs déclarations respectives ne sont pas
du tout équivalentes. En eet, la déclaration de tab, qui utilise des crochets avec une
constante entière, réserve un espace mémoire permettant de stocker MAX_TAIL réels simple
précision (oat) alors que la déclaration de la variable notes, qui utilise l'opérateur *, ne
fait que réserver un espace mémoire pour une variable simple dédiée au stockage d'une
seule adresse à la fois.

3.2.4 Pointeur et allocation dynamique de mémoire

À l'aide des variables de type pointeur il est possible d'allouer de la mémoire de manière
dynamique , c'est-à-dire, allouer de la mémoire dont la taille n'est pas déterminée lors de
la compilation du programme mais plus tard, lors de son exécution. L'avantage d'une telle
allocation, par rapport à une allocation statique utilisant les crochets ([]), est qu'il est
désormais possible de demander juste la quantité de mémoire dont le programme a besoin.

An de coder l'allocation dynamique de manière simple et concise, nous commençons


par dénir une marco, nalloc(n,t), qui prend comme paramètres, un entier n et un type
t et retourne l'adresse d'une zone mémoire dont la taille permet de stocker n données de
type t.

#define nalloc(n,t) (t*) malloc((n)∗sizeof(t))

Comme on peut le constater, la macro nalloc(.,.) est dénie à l'aide de deux fonctions ;
tout d'abord la fonction prédénie malloc(.) qui réserve, dans le tas 1 , un espace mémoire
dont la taille, en octet, est égale au nombre qui lui est passé en paramètre. L'utilisation
de malloc(.) requiert, toutefois, l'inclusion de la bibliothèque stdlib.h. La fonction
malloc() retourne l'adresse de l'espace mémoire réservé sous-forme d'un pointeur sur
1. Le tas est un espace mémoire qui se trouve dans la RAM qui est réservé aux allocations dynamiques

de mémoire.
3.3. LES STRUCTURES 5

#include <stdio.h>
#include <stdlib.h>
#dene nalloc(n,t) (t*) malloc(n*sizeof(t))
..
.
int i;
1 oat *notes;
printf("Saisissez le nombre de notes à saisir : ");
scanf("%d",&n);
notes = nalloc(n,oat);
for (i = 0 ; i < n ; i++) scanf("%f",&notes[i]);
..
.
Programme 1: Un bout de programme montrant une allocation dynamique d'un
tableau de réels rendue possible grâce à l'utilisation d'une variable de type pointer.

t
void. Par conséquent, an d'avoir un pointeur sur le type , on procède à une conversion
explicite de type via l'utilisation de l'opérateur de conversion de type (t*).
La macro nalloc(.,.) utilise aussi la fonction sizeof(.) qui retourne le nombre d'oc-
tets nécessaires pour mémoriser une valeur du type passé en paramètre. À titre d'exemple,
sizeof(double) retourne l'entier 8, si on travaille sur une machine 64 bits.
Une fois dénie, la macro nalloc(.,.) peut être appelée au besoin, comme dans le
bout de code du Programme 1, où il s'agit d'une allocation dynamique d'un tableau de
réels simple précision. Observez que cette allocation dynamique a été rendue possible grâce
à l'utilisation d'une variable de type pointeur sur oat (voir la variable notes déclarée
dans la Ligne 1).
D'une manière générale, l'allocation dynamique d'un tableau d-dimensionnel tab dont
les éléments sont de type type nécessite une variable dont le type est type
*| .{z
. . *}. Il
d fois
s'ensuit que l'expression tab[i1][i2]...[ip], où i1,i2,. . .,ip sont des indices, est de type
type *| .{z
. . *}. En particulier, tab[i1][i2]...[id] est de type type
.
d−p fois

Exemple 1 On se propose d'allouer de manière dynamique l'espace mémoire nécessaire


pour stocker une matrice rectangulaire de réels double précision de taille m × n. Pour ce
faire, on utilise une variable, mat, de type double**, comme on peut le voir dans la Ligne 1
du Programme 2. Dans mat, on mémorise l'adresse d'un vecteur qui permettra de stocker
m pointeurs sur double. Chaque case mat[i] du vecteur pointé par mat permettra de
stocker l'adresse d'un vecteur de n réels double précision (voir Ligne 2 du Programme 2).

3.3 Les structures

En pratique, les données ayant un type élémentaire, comme les entiers par exemple, sont
souvent associées à des données ayant d'autres types pour former une description complète
d'un objet donné. À titre d'exemple, an de décrire l'identité d'une personne de manière
précise, on peut avoir besoin de préciser son numéro de carte d'identité nationale, son
nom et prénom et, parfois même, sa date et lieu de naissance. Notez que le numéro de la
6 CHAPITRE 3. POINTEUR, STRUCTURE ET LISTE CHAÎNÉE

..
.
int i, j, m, n;
1 double **mat;
printf("Donnez la taille de la matrice : ");
scanf("%d %d",&m,&n);
// Allocation dynamique d'une matrice
mat = nalloc(m, double*);
for (i = 0 ; i < m ; i++)2 mat[i] = nalloc(n, double);
;
// Saisie des éléments de la matrice
for (i = 0 ; i < m ; i++)
{
for (j = 0 ; j < n ; j++) scanf("%lf",&mat[i][j]);
}
..
.
Programme 2: Allocation dynamique d'une matrice rectangulaire.

carte d'identité nationale est un entier alors que les nom et prénom d'une personne sont
des chaînes de caractères. Il s'agit donc de données hétérogènes
.
Le langage C, comme la plus part des langages de programmation de haut niveau,
permet de regrouper des données hétérogènes en une seule entité grâce au constructeur
de structure
struct.

La syntaxe exacte permettent de dénir d'une structure dans le langage C est la


suivante :
struct nom_structure
{
type _1 nom _champ _1;
type
..
_2 nom _champ _2;
.
type _n nom _champ _n;
};
Le terme utilisé pour désigner les composants d'une structure est le terme champ (voir Fi-
gure 3.3). Le programme de la Figure 3.4 dénit une structure dont le nom est sPersonne,
qui est composée des champs cin., nom et prenom auxquels nous avons fait référence plus
haut.

Une fois dénie, une structure peut être utilisée pour déclarer des variables de type
structure suivant la syntaxe C suivante :

struct nom_structure liste_de_variables ;


A titre d'exemple, l'instruction suivante dénit deux variables de type structure sPersonne
et une variable de type pointeur sur structure sPersonne :

struct sPersonne p, cp, *ptr ;


3.3. LES STRUCTURES 7

Structure
champ1 champ2 ... champn

Figure 3.3  Illustration d'une structure composée de plusieurs champs.

L'accès aux diérents champs d'une variable de type structure s'eectue à l'aide de l'opé-
rateur point (.). Ces champs sont traités comme des variables à part entière et on pourra,
par exemple, y saisir des données comme suit :

scanf("%d %s %s",&p.cin,p.nom,p.prenom);

Les remarques suivantes concernant les structures :


 La déclaration de nouvelles structure se fait, de préférence, au niveau global à
l'extérieur de toute fonction du programme. Ceci permet d'utiliser ces structures
dans toutes les fonctions du programme.
 Le nom d'un champ doit toujours être précédé par le nom d'une variable de type
structure et de l'opérateur point (.), sauf lors de la dénition de la structure elle
même.
 Une diérence notoire avec les tableaux c'est que, avec les variables de type struc-
ture, on peut eectuer une copie d'une structure avec une simple aectation, comme
dans la Ligne 18 du programme de la Figure 3.4.

La notion de structure permet aussi de dénir de nouveaux types, qui iront enrichir la
collection de types de base, (caractère, entier, réel et chaîne de caractères). Ces nouveaux
types pourront, par la suite, être utilisés comme n'importe quel type de base pour déclarer
des variables.
Le langage C ore deux syntaxes diérentes pour dénir de nouveaux types sur la
base des structures. Toutefois, les deux syntaxes s'appuient sur le même mot clé, à sa-
voir typedef. La première syntaxe passe, tout d'abord, par la dénition d'une nouvelle
structure, à laquelle on donne un nom, pour aboutir ensuite à un nouveau type.
struct nom_structure
{
type _1 nom _champ _1;
type
..
_2 nom _champ _2;
.
type _n nom _champ _n;
};
typedef struct nom_structure nom_type ;
La deuxième syntaxe, qui est plus concise, ne passe pas par la dénition d'une nouvelle
structure :
8 CHAPITRE 3. POINTEUR, STRUCTURE ET LISTE CHAÎNÉE

#include
#define
1 < s t d i o . h>

2 MAX_CHAINE 64

struct
3

4 sPersonne

int
5 {

char
6 cin ;

char
7 nom [MAX_CHAINE ] ;

8 prenom [MAX_CHAINE ] ;

9 };

int
10

11 main ( )

struct
12 {

13 sPersonne p, cp , ∗ ptr ;
14

15 printf (" Saisissez le cin , le nom et le prenom : ") ;

16 s c a n f ( "%d %s %s " ,& p . c i n , p . nom , p . prenom ) ;

17 cp = p;

18 printf (" cin : %d , nom : %s , prenom : %s \ n " , c p . c i n , c p . nom , c p . prenom ) ;

19 ptr = &p ;

20 printf (" cin : %d , nom : %s , prenom :

∗ ptr ) . cin ∗ p t r ) . nom , ( ∗ p t r ) . prenom ) ;


return
%s \ n " , ( ,(

21 0;

22 }

Figure 3.4  Dénition d'un enregistrement, sPersonne et saisie des champs d'une va-
riable ayant la structure sPersonne.
3.3. LES STRUCTURES 9

typedef struct
{
type nom champ _1;
_1 _

..
type nom champ _2;
_2 _
.
type nom champ _n;
_n _
} nom_type
;
Un exemple de dénition d'un nouveau type suivant la deuxième syntaxe se trouve
dans le programme de la Figure 3.5, qui introduit un nouveau type appelé tPersonne.
Une fois déni, le nouveau type peut être utilisé pour déclarer des variables structurées.
La manipulation d'une variable ayant un type construit à l'aide d'une structure se fait
à travers les noms des divers champs de la structure et de l'opérateur point (.), comme
précédement.

Exemple 2 Le programme de la Figure 3.5 dénit un nouveau type structuré appelé


tPersonne et montre comment des variables de ce type sont manipulées. Le programme
utilise aussi la fonction prédénie du langage C, strcpy, dont l'entête se trouve dans le
chier string.h, qu'il faudra inclure an de copier des chaînes de caractères.

Exemple 3 On se propose de dénir un nouveau type tTableau construit à l'aide d'une


structure et comportant deux champs : un champ de type entier servant à mémoriser la
taille du tableau et un champ de type pointeur sur entier servant à mémoriser l'adresse
d'un tableau d'entier qui sera allouer de manière dynamique. Le programme de la Fi-
gure 3.6 montre comment dénir le type tTableau et comment manipuler des variables de
ce type, notamment, comment copier un tableau dans un autre sans recourir au partir du
tableau original.

Exemple 4 Le présent exemple montre comment utiliser des variables structurées comme
paramètres et résultat de fonction. Il s'agit de manipuler des nombres complexes. Il y aura
une fonction pour la saisie, une procédure pour l'achage, une fonction pour le calcul de
la somme, du produit de deux nombres complexes et une autre pour le calcul du module.
Toutes ces routines ont été développés dans le programme montré dans les Figures 3.7 et
3.9.

Exercice 1 Le produit de nombre complexes peut être obtenu en eectuant 4 mul-


tiplications réelles (voir le programme de la Figure 3.7). Proposez une fonction C
qui calcule ce même produit en eectuant uniquement 3 multiplications réelles.
10 CHAPITRE 3. POINTEUR, STRUCTURE ET LISTE CHAÎNÉE

#include
#include
1 < s t d i o . h>

#define
2 < s t r i n g . h>

3 MAX_CHAINE 64

typedef struct
4

int
6 {

char
7 cin ;

char
8 nom [MAX_CHAINE ] ;

9 prenom [MAX_CHAINE ] ;

10 } tPersonne ;

int
11

12 main ( )

char
13 {

14 rep ;

15 tPersonne p , cp ;

16

17 printf (" Saisissez le cin , le nom et le prenom : ") ;

18 s c a n f ( "%d %s %s " ,& p . c i n , p . nom , p . prenom ) ;

19 fflush ( stdin ) ;

do
20

21 {

22 p r i n t f ( "%s est −c e bien le nom? o /n : " , p . nom ) ;

while
23 s c a n f ( "%c " ,& r e p ) ;

24 } ( r e p != ' o ' && rep != ' n ' ) ;

if
25

26 ( r e p== ' n ' )

27 {

28 cp . c i n = p . cin ;

29 s t r c p y ( c p . nom , p . prenom ) ;

30 s t r c p y ( c p . prenom , p . nom ) ;

31 p = cp ;

32 }

33 printf (" cin : %d , nom : %s , prenom : %s " , p . c i n , p . nom , p . prenom ) ;

return
34

35 0;

36 }

Figure 3.5  Dénition d'un nouveau type, tPersonne, en s'appuyant sur la notion d'en-
registrement et saisie des champs d'une variable de ce type.
3.3. LES STRUCTURES 11

#include
#define
1 < s t d i o . h>

2 MAXTAB 128

typedef struct
3

int
5 {

int
6 taille ;

7 e l t s [MAXTAB ] ;

8 } tTableau ;

int
9

10 main ( )

int
11 {

12 i ;

13 tTableau tab , copie ;

14 printf (" Saisissez le nombre d ' entier du tableau : ") ;

15 s c a n f ( "%d " ,& t a b . t a i l l e ) ;

for
16

17 ( i = 0 ; i <t a b . t a i l l e ; i ++)

18 {

19 printf (" Saisissez l ' entier %d : " , i ) ;

20 s c a n f ( "%d " ,& t a b . e l t s [ i ] ) ;

21 }

22

23 // Copie d ' un tableau dans un autre à l ' aide d ' un e simple

24 // affectation

25 copie = tab ;

for
26 printf (" Affichage de la copie du t a b l e a u \n" ) ;

27 ( i = 0 ; i <c o p i e . t a i l l e ; i ++)

28 p r i n t f (" case %d : %d \ n " , i , c o p i e . e l t s [ i ] ) ;

return
29

30 0;

31 }

Figure 3.6  Programme montrant comment il est possible de copier un tableau dans un
autre sans parcourir le tableau original.
12 CHAPITRE 3. POINTEUR, STRUCTURE ET LISTE CHAÎNÉE

#include
#include
1 < s t d i o . h>

#define
2 <math . h>

3 aigu 130

typedef struct
4

float
6 {

float
7 re ;

8 im ;

9 } tComplexe ;

10

11 t C o m p l e x e SaisieComp ( )

12 {

13 tComplexe c ;

14

15 printf (" Saisissez la partie réelle : ") ;

16 s c a n f ( "%f " ,& c . r e ) ;

17 printf (" Saisissez la partie imaginaire : ") ;

return
18 s c a n f ( "%f " ,& c . im ) ;

19 c ;

20 }

void
21

22 AfficheComp ( tComplexe c)

23 {

24 p r i n t f ( " Le nombre complexe e s t : \ n" ) ;

25 p r i n t f ( " La partie réelle : %f \ n " , c . r e ) ;

26 p r i n t f ( " La partie imaginaire : %f \ n " , c . im ) ;

27 }

28

29 t C o m p l e x e ProduitComp ( t C o m p l e x e c , tComplexe d)

30 {

31 tComplexe p;

32

33 p . re = c . re ∗d . r e − c . im ∗ d . im ;
∗ d . im + ∗d . r e ;
return
34 p . im = c . re c . im

35 p;

36 }

37

38 t C o m p l e x e ConjugueComp ( t C o m p l e x e c)

39 {

−c . im ;
return
40 c . im =

41 c ;

Figure 3.7  Programme manipulant des nombres complexes.


3.3. LES STRUCTURES 13

42 }

float
43

44 ModuleComp ( t C o m p l e x e c)

return
45 {

46 sqrt ( c . re ∗c . re + c . im ∗ c . im ) ;
47 }

48

49 t C o m p l e x e SommeComp ( t C o m p l e x e c , tComplexe d)

50 {

51 tComplexe s ;

52

53 s . re = c . re + d . re ;

return
54 s . im = c . im + d . im ;

55 s ;

56 }

57

58 t C o m p l e x e RacineComp ( t C o m p l e x e c)

59 {

60 // on part de c ^2 = ( x+i y ) ^2 = a + ib et | x+i y | ^ 2 = | a+ i b |

float
61 tComplexe r ;

62 m = ModuleComp ( c ) ;

63

64 r . re = s q r t ( (m+c . r e ) / 2 ) ;


if
65 r . im = s q r t ( (m c . r e ) / 2 ) ;

66 ( c . im < 0.0)

− r . im ;
return
67 r . im =

68 r ;

69 }

70

71 t C o m p l e x e QuotientComp ( t C o m p l e x e a , tComplexe b)

72 {

73 // a/b = a ∗ conjugué ( b ) / | b |^2

float
74 tComplexe q = ProduitComp ( a , ConjugueComp ( b ) ) , b;

75 m = ModuleComp ( b ) ;

76 m ∗= m;

77 q . re /= m;

return
78 q . im /= m;

79 q;

80 }

int
81

82 main ( )

83 {

84 tComplexe a , b , c , d , b2 , x1 , x 2 ;

85

86 a = SaisieComp ( ) ;

87 b = SaisieComp ( ) ;

88 c = SaisieComp ( ) ;

Figure 3.8  Programme manipulant des nombres complexes.


14 CHAPITRE 3. POINTEUR, STRUCTURE ET LISTE CHAÎNÉE

89

90 b = ProduitComp ( b , ( tComplexe ) { . 5 , .0}) ;

91 b2 = ProduitComp ( b , b ) ;

92 d = ProduitComp ( ProduitComp ( a , c ) , ( t C o m p l e x e ) { −1 , .0}) ;

93 d = RacineComp ( SommeComp ( b2 , d ) ) ;

94

95 x1 = SommeComp ( ProduitComp ( b , ( t C o m p l e x e ) { −1 , 0.0}) , d) ;

96 x1 = QuotientComp ( x1 , a ) ;

97

98 x2 = ProduitComp ( SommeComp ( b , d ) , ( tComplexe ) { −1 , 0.0}) ;

99 x2 = QuotientComp ( x2 , a ) ;

100

101 A f f i c h e C o m p ( x1 ) ;

102 A f f i c h e C o m p ( x2 ) ;

return
103

104 0;

105 }

Figure 3.9  Programme manipulant des nombres complexes.

Exercice 2 On voudrait résoudre des équations du second degré dans le corps des
nombres complexes (C). Cette tâche nécessite la dénition d'opérations classiques
sur les nombres complexes, dont la racine carrée et le quotient complexe.
 Proposez une fonction qui implémente chacune des opérations citées ci-
dessus.
 À l'aide des fonctions somme, produit et module complexe déjà dénies aupa-
ravant ainsi que les fonctions développées en réponse à la première question,
proposez un programme complet qui résout des équations du second degré
dans C.

3.4 Les listes chaînées

Un ensemble d'éléments qui évolue pendant le déroulement du programme, en augmen-


tant et en diminuant de taille, peut être implémenté à l'aide d'une structure de données
dynamique appelée liste chaînée
. Une liste chaînée est une structure linéaire composée
de celluleschaînées entre elles. Chaque cellule est composée de deux parties, une partie
qui sert à contenir l'élément de la liste, proprement dit, et une autre partie qui assure le
chaînage entre les cellules. Concrètement, la deuxième partie sert à mémoriser l'adresse
de la cellule suivante dans la liste (voir Figure 3.10). Rappelons aussi que l'ordre d'accès
aux éléments d'une liste chaînée est déterminé par le chaînage
, qui aboutit forcement à
un ordre d'accès linéaire. L'accès à une liste commence, donc, par le premier élément de
la liste, puis le second, et ainsi de suite, jusqu'au dernier. Il en résulte que, pour pouvoir
accéder aux éléments d'une liste, il sut de disposer de l'adresse de la tête de la liste.
C'est pour cette raison qu'une liste chaînée sera identiée et désignée par l'adresse de sa
tête.
3.4. LES LISTES CHAÎNÉES 15

cellule de tête

Figure 3.10  Illustration d'une liste chaînée composée de cinq cellules.


3.4.1 Implémentation

L'implémentation des listes chaînées s'appuie sur l'allocation dynamique de mémoire. Ceci
implique que l'espace mémoire nécessaire au stockage des éléments d'une liste chaînée sera
réservés dans le tas 2 . Les cellules de la liste seront donc allouées, au fur et à mesure, à
chaque fois qu'on aura besoin d'ajouter un élément à la liste et non pas une fois pour
toute. Inversement, si un des éléments de la liste n'est plus utilise au déroulement de
l'algorithme alors la cellule contenant cet élément peut être libérée et l'espace qui lui été
réservé est ajouté à l'espace libre du tas.
Mais commençons, tout d'abord, par dénir les types nécessaires à la création d'une
liste chaînée générique, c'est-à-dire que le type des éléments de cette liste ne sera pas déni
avec précision. Le point de départ est la dénition du type d'une cellule
de la liste. Ce
type va bien sûr dépendre du type des éléments de la liste, qu'on désignera par tElt, qui
est un type générique qui pourra être remplacé par entier, réel ou tout autre type. À cette
composante, il faut ajouter la composante qui assurera le chaînage. Il est donc naturel
d'utiliser le constructeur de structure struct pour intégrer les deux composantes en une
seule entité. On aboutit ainsi à une structure composée de deux champs, un champ pour
l'élément à mémoriser et un autre pour l'adresse de la cellule suivante comme indiqué
dans le Programme 3. Comme on peut le constater, la structure sCell est récursive, car
le champ succ est de type pointeur sur struct sCell elle même. A partir de cette structure,
il est possible de dénir un nouveau type que nous appelons tCell. Une fois déni, le type
tCell peut être utilisé pour déclarer des variables du type tCell ou pointeur sur tCell
(tCell*).

An d'allouer de l'espace mémoire pour une nouvelles cellules, on convient d'utiliser
la macro alloc(t), dont la dénition est la suivante

#dene alloc(t) (t*) malloc(sizeof(t))


Un appel à alloc(tCell) retourne un pointeur sur une nouvelle cellule de type tCell. Par
soucis de modularité et de concision, nous créons la fonction InitCell(cell,elt,succ) dé-
taillée dans le Programme 4, dont le rôle est d'initialiser les deux champs de la cellule
pointée par cell par les paramètres etelt adr
.
Nous terminons ce paragraphe par un raccourci qu'ore le langage C pour accéder aux
diérents champs d'une structure à partir d'un pointeur sur celle-ci. Soit ptr un pointeur
sur un objet structuré ayant un champ appelée chp alors les deux écritures suivantes sont
équivalentes :
(*ptr).chp ⇔ ptr->chp
Dans ce qui suit, nous utiliserons l'écriture avec la èche (->) car elle est plus concise.
2. Le tas est espace mémoire dans la RAM réservé à l'allocation dynamique de mémoire et qui peut

être géré par le programmeur à l'aide, notamment, des fonctions C malloc() et free().
16 CHAPITRE 3. POINTEUR, STRUCTURE ET LISTE CHAÎNÉE

.. int main()
.
#dene alloc(t) (t*) malloc(sizeof(t)) {
..
// Définition d'un type générique .
// pour les cellules d'une liste tCell *cell;
// chaînée // Allocation + initialisation
typedef struct sCell // d'une cellule
{ cell = alloc(tCell);
tElt elt; InitCell(cell,elt,NULL);
struct sCell *succ; ..
.
} tCell; }
Programme 3: Dénition du type générique tCell, déclaration d'une variable
pointeur sur cellule et création puis initialisation d'une nouvelle cellule.

tCell* InitCell(tCell *cell, tElt elt, tCell *succ)


{
(*cell).elt = elt;
(*cell).succ = succ;
return cell;
}
Programme 4: Initialisation d'une nouvelle cellule avec un élément donné et un
successeur donné.

3.4.2 Parcours et insertion

Comme on peut avoir besoin d'accéder à n'importe quel élément d'une liste, il est indis-
pensable de savoir la parcourir
. L'Algorithme 5 permet d'eectuer le parcours total d'une
liste chaînée grâce à une procédure qui utilise la tête
de la liste comme unique paramètre.
Le parcour est eectué à l'aide d'une boucle tant-que, qui s'arrête quand elle atteint une
adresse nulle, désignée par la constante NULL du langage C. Le traitement de l'élément
courant de la liste est eectué par une procédure générique, Traiter(.), qui est appelée
à chaque itération.
Par soucis de simplicité, nous avons choisis de présenter le parcours total d'une liste
chaînée avant sa création. En réalité, avant d'être parcourue, une liste chaînée doit tout
d'abord être créée. La création d'une liste chaînée peut se faire via plusieurs insertions
successives d'élément. À son tour, l'insertion d'un élément dans une liste chaînée, éven-
tuellement vide, pointée par la variable tete
peut se faire via les fonctions alloc(.) et
InitCell(.,.), en exécutant l'instruction suivante :

tete = InitCell(alloc(tCell),elt, tete)

Cette aectation insère l'élément reçu en paramètre, elt, dans une nouvelle cellule qui est
ajoutée à la tête de la liste chaînée pointée par la variable tete et cette dernière pointera,
désormais, sur la nouvelle cellule créée par l'appel de fonction alloc(tCell).
Insérer des éléments de cette façon présente, toutefois, un inconvénient. C'est que les
éléments insérés vont être stockés dans la liste dans l'ordre inverse de leur insertion. Ainsi,
3.4. LES LISTES CHAÎNÉES 17

void ParcourList(tCell* tete)


{
while (tete)
{
Traiter(tete->elt);
tete = tete->succ;
}
}
Programme 5: Parcours total d'une liste chaînée. La procédure Traiter, qui n'est
pas détaillée, permet de traiter l'élément qui se trouve dans la cellule courante.

l'élément inséré en premier dans une liste vide va se trouver à la n de la liste, c'est-à-dire,
dans la dernière cellule de la liste ; celui inséré en deuxième lieu va se trouvé dans l'avant
dernière cellule, et ainsi de suite.

Exercice 3 On voudrait insérer un élément donné à la n d'une liste chaînée iden-


tiée par sa tête. Proposez une fonction C qui réalise cette opération.

3.4.3 Suppression de cellule

Une autre opération, fréquemment exécutée sur les listes chaînées, est celle qui consiste à
supprimer un élément particulier, ou plus précisément, de la cellule qui contient l'élément
en question. Cette opération est plus complexe que l'insertion d'un élément, car la cellule
à supprimer peut être n'importe où dans la liste, ce qui nécessite un parcours partiel
de la
liste an de localiser la cellule à supprimer. Ce parcours est eectué par la boucle while du
Programme 6. La suppression d'une cellule peut donc introduire des changements sur le
chaînage des cellules restantes, comme on peut le constater à partir du code de la fonction
SupprimeList(.,.) (voir Ligne 14 du Programme 6). Pour que cette dernière fonctionne
correctement, il faut lui faire appel comme suit :

tete = SupprimeList( tete, elt);

où le paramètre tetecontient l'adresse de la première cellule de la liste et le paramètre elt


désigne l'élément à supprimer.

Une autre action qu'il faut exécuter lors de la suppression de cellule est la libération
de l'espace mémoire qui été alloué à la cellule supprimée. Étant donné que cette espace
n'est plus utilisé, il est possible de le libérer physiquement pour qu'il soit ajouter à l'espace
libre du tas. Le langage C ore à cet eet la fonction free(.), qui permet de libérer l'espace
mémoire dont l'adresse est reçue en paramètre. Ainsi, l'instruction

free(cour);

exécutée par la fonction SupprimeList(.,.), au niveau de la ligne 15 permet de libérer


l'espace mémoire qui été alloué à la cellule dont l'adresse se trouve dans cour. Signalons,
enn, que la fonction free(.) requiert l'inclusion de la bibliothèque stdlib.h.
18 CHAPITRE 3. POINTEUR, STRUCTURE ET LISTE CHAÎNÉE

1 tCell *SupprimeList(tCell* tete, tElt elt)


2 {
3 tCell *pred, *cour;
4 pred = NULL;
5 cour = tete;
6 while (cour)
7 {
8 if (cour->elt == elt) break ;
9 pred = cour;
10 cour = cour->succ;
11 }
12 if (cour)
13 {
14 if (pred) pred->succ = cour->succ else tete = tete->succ ;
15 free(cour);
16 }
17 return tete
18 }
Programme 6: Suppression d'une cellule d'une liste chaînée.

Exercice 4 Cet exercice porte sur les opérations usuelles qu'on peut avoir à eec-
tuer sur une liste chaînée.
 Proposez une fonction C qui permet de tester l'appartenance d'un entier à
un ensemble d'entier stockés dans une liste chaînée identiée par l'adresse de
sa tête. On supposera que les éléments sont stockés suivant l'ordre croissant
des entiers.
 Proposez une fonction C qui permet d'insérer, au bon endroit, un entier
donné dans une liste chaînée et triée d'entiers.
 Proposez une fonction C qui permet de retirer un entier d'une liste chaînée
et triée d'entiers, sachant que l'entier en question peut gurer plusieurs fois
dans la liste.

Exercice 5 Proposez un programme C qui saisit une liste d'entiers, la mémorise


dans une liste simplement chaînée puis supprime les éléments ayant une parité
donnée et libère l'espace mémoire qui leurs a été réservé.
3.4. LES LISTES CHAÎNÉES 19

cellule de tête pred

succ

Figure 3.11  Illustration d'une liste doublement chaînée.

Exercice 6 Dans une liste doublement chaînée, chaque cellule contient trois
champs, un champ pour stocker l'information est deux autres champs pour stocker
deux adresses : l'adresse du successeur, comme dans les listes simplement chaînées,
et l'adresse du prédécesseur (voir Figure 3.11).
 En tenant compte du chaînage double, proposez une nouvelle procédure pour
l'insertion d'un élément et une nouvelle procédure pour la suppression d'une
cellule donnée.
 Proposez une procédure qui permet d'insérer, au bon endroit, un entier donné
dans une liste doublement chaînée et triée d'entiers.
 Proposez une fonction qui permet de retirer un entier d'une liste doublement
chaînée et triée d'entiers, sachant que l'élément en question peut gurer zéro
ou plusieurs fois dans la liste.

Das könnte Ihnen auch gefallen