Chapitre 3 Les
pointeurs Toute variable manipulée dans un programme est stockée quelque part en mémoire
centrale. Cette mémoire est constituée d'octets qui sont identifiés de manière
univoque par un numéro qu'on appelle
adresse. Pour retrouver une
variable, il suffit donc de connaître l'adresse de l'octet où elle est stockée
(ou, s'il s'agit d'une variable qui recouvre plusieurs octets contigus,
l'adresse du premier de ces octets). Pour des raisons évidentes de lisibilité,
on désigne souvent les variables par des identificateurs, et non par leur
adresse. C'est le compilateur qui fait alors le lien entre l'identificateur
d'une variable et son adresse en mémoire. Toutefois, il est parfois très pratique
de manipuler directement une variable par son adresse.
3.1 Adresse et valeur d'un objetOn appelle
Lvalue (left value) tout objet pouvant
être placé à gauche d'un opérateur d'affectation. Une
Lvalue est
caractérisée par :
- son adresse, c'est-à-dire
l'adresse-mémoire à partir de laquelle l'objet est stocké ;
- sa valeur, c'est-à-dire ce
qui est stocké à cette adresse.
Dans l'exemple,
int i, j;i = 3;j = i;
Si le compilateur a placé la variable i à l'adresse 4831836000 en mémoire, et la
variable j à l'adresse
4831836004, on a
objet
| adresse
| valeur
|
i
| 4831836000
| 3
|
j
| 4831836004
| 3
|
Deux variables différentes ont des adresses différentes.
L'affectation i = j; n'opère que
sur les valeurs des variables. Les variables i
et j étant de type int, elles sont stockées sur
4 octets. Ainsi la valeur de i
est stockée sur les octets d'adresse 4831836000 à 4831836003.
L'adresse d'un objet étant un numéro d'octet en mémoire, il s'agit d'un entier
quelque soit le type de l'objet considéré. Le format interne de cet entier (16
bits, 32 bits ou 64 bits) dépend des architectures. Sur un DEC alpha, par
exemple, une adresse a toujours le format d'un entier long (64 bits).
L'opérateur & permet
d'accéder à l'adresse d'une variable. Toutefois &i n'est pas une Lvalue mais une constante : on ne
peut pas faire figurer &i à
gauche d'un opérateur d'affectation. Pour pouvoir manipuler des adresses, on
doit donc recourir un nouveau type d'objets, les pointeurs.
3.2 Notion de pointeurUn
pointeur est un objet (Lvalue) dont la valeur
est égale à l'adresse d'un autre objet. On déclare un pointeur par
l'instruction :
type *
nom-du-pointeur;
où
typeest le type de l'objet pointé. Cette déclaration déclare un identificateur,
nom-du-pointeur,
associé à un objet dont la valeur est l'adresse d'un autre objet de type
type.
L'identificateur
nom-du-pointeurest donc en quelque sorte un identificateur d'adresse. Comme pour n'importe
quelle Lvalue, sa valeur est modifiable.
Même si la valeur d'un pointeur est toujours un entier (éventuellement un
entier long), le type d'un pointeur dépend du type de l'objet vers lequel il
pointe. Cette distinction est indispensable à l'interprétation de la valeur
d'un pointeur. En effet, pour un pointeur sur un objet de type char, la valeur donne l'adresse de l'octet
où cet objet est stocké. Par contre, pour un pointeur sur un objet de type int, la valeur donne l'adresse du premier
des 4 octets où l'objet est stocké. Dans l'exemple suivant, on définit un
pointeur p qui pointe vers un
entier i :
int i = 3;int *p; p = &i;
On se trouve dans la configuration
objet
| adresse
| valeur
|
i
| 4831836000
| 3
|
p
| 4831836004
| 4831836000
|
L'
opérateur unaire d'indirection * permet d'accéder directement à la valeur
de l'objet pointé. Ainsi, si p
est un pointeur vers un entier i,
*p désigne la valeur de i. Par exemple, le programme
main(){ int i = 3; int *p; p = &i; printf("*p = %d \n",*p);}
imprime *p = 3.
Dans ce programme, les objets i
et *p sont identiques : ils
ont mêmes adresse et valeur. Nous sommes dans la configuration :
objet
| adresse
| valeur
|
i
| 4831836000
| 3
|
p
| 4831836004
| 4831836000
|
*p
| 4831836000
| 3
|
Cela signifie en particulier que toute modification de *p modifie i. Ainsi, si l'on ajoute l'instruction *p = 0; à la fin du programme précédent,
la valeur de i devient nulle.
On peut donc dans un programme manipuler à la fois les objets p et *p.
Ces deux manipulations sont très différentes. Comparons par exemple les deux
programmes suivants :
main(){ int i = 3, j = 6; int *p1, *p2; p1 = &i; p2 = &j; *p1 = *p2;}
et
main(){ int i = 3, j = 6; int *p1, *p2; p1 = &i; p2 = &j; p1 = p2;}
Avant la dernière affectation de chacun de ces programmes,
on est dans une configuration du type :
objet
| adresse
| valeur
|
i
| 4831836000
| 3
|
j
| 4831836004
| 6
|
p1
| 4831835984
| 4831836000
|
p2
| 4831835992
| 4831836004
|
Après l'affectation *p1 =
*p2; du premier programme, on a
objet
| adresse
| valeur
|
i
| 4831836000
| 6
|
j
| 4831836004
| 6
|
p1
| 4831835984
| 4831836000
|
p2
| 4831835992
| 4831836004
|
Par contre, l'affectation p1
= p2 du second programme, conduit à la situation :
objet
| adresse
| valeur
|
i
| 4831836000
| 3
|
j
| 4831836004
| 6
|
p1
| 4831835984
| 4831836004
|
p2
| 4831835992
| 4831836004
|
3.3 Arithmétique des pointeursLa valeur d'un pointeur étant un entier, on peut lui
appliquer un certain nombre d'opérateurs arithmétiques classiques. Les seules
opérations arithmétiques valides sur les pointeurs sont :
- l'addition d'un entier à un
pointeur. Le résultat est un pointeur de même type que le pointeur de
départ ;
- la soustraction d'un entier
à un pointeur. Le résultat est un pointeur de même type que le pointeur de
départ ;
- la différence de deux
pointeurs pointant tous deux vers des objets de même type. Le résultat est
un entier.
Notons que la somme de deux pointeurs n'est pas autorisée.
Si i est un entier et p est un pointeur sur un objet de type
type,
l'expression p + i désigne un
pointeur sur un objet de type
type dont la valeur est égale à la valeur de p incrémentée de i * sizeof(
type). Il en va de même pour la soustraction d'un entier à un
pointeur, et pour les opérateurs d'incrémentation et de décrémentation ++ et --.
Par exemple, le programme
main(){ int i = 3; int *p1, *p2; p1 = &i; p2 = p1 + 1; printf("p1 = %ld \t p2 = %ld\n",p1,p2);}
affiche p1 = 4831835984
p2 = 4831835988.
Par contre, le même programme avec des pointeurs sur des objets de type double :
main(){ double i = 3; double *p1, *p2; p1 = &i; p2 = p1 + 1; printf("p1 = %ld \t p2 = %ld\n",p1,p2);}
affiche p1 = 4831835984
p2 = 4831835992.
Les opérateurs de comparaison sont également applicables aux pointeurs, à
condition de comparer des pointeurs qui pointent vers des objets de même type.
L'utilisation des opérations arithmétiques sur les pointeurs est
particulièrement utile pour parcourir des tableaux. Ainsi, le programme suivant
imprime les éléments du tableau tab
dans l'ordre croissant puis décroissant des indices.
#define N 5int tab[5] = {1, 2, 6, 0, 7};main(){ int *p; printf("\n ordre croissant:\n"); for (p = &tab[0]; p <= &tab[N-1]; p++) printf(" %d \n",*p); printf("\n ordre decroissant:\n"); for (p = &tab[N-1]; p >= &tab[0]; p--) printf(" %d \n",*p);}
Si p et q sont deux pointeurs sur des objets de
type
type,
l'expression p - q désigne un
entier dont la valeur est égale à (p
- q)/sizeof(
type) .
3.4 Allocation dynamiqueAvant de manipuler un pointeur, et notamment de lui
appliquer l'opérateur d'indirection *,
il faut l'initialiser. Sinon, par défaut, la valeur du pointeur est égale à une
constante symbolique notée NULL
définie dans stdio.h. En
général, cette constante vaut 0. Le test p
== NULL permet de savoir si le pointeur p pointe vers un objet.
On peut initialiser un pointeur p
par une affectation sur p. Par
exemple, on peut affecter à p
l'adresse d'une autre variable. Il est également possible d'affecter
directement une valeur à *p.
Mais pour cela, il faut d'abord réserver à *p
un espace-mémoire de taille adéquate. L'adresse de cet espace-mémoire sera la
valeur de p. Cette opération
consistant à réserver un espace-mémoire pour stocker l'objet pointé s'appelle
allocation
dynamique. Elle se fait en C par la fonction malloc de la librairie standard stdlib.h. Sa syntaxe est
malloc(
nombre-octets)
Cette fonction retourne un pointeur de type char * pointant vers un objet de taille
nombreoctetsoctets. Pour initialiser des pointeurs vers des objets qui ne sont pas de type char, il faut convertir le type de la
sortie de la fonction malloc à
l'aide d'un cast. L'argument
nombre-octets est souvent donné à l'aide de la
fonction sizeof() qui renvoie le
nombre d'octets utilisés pour stocker un objet.
Ainsi, pour initialiser un pointeur vers un entier, on écrit :
#include int *p;p = (int*)malloc(sizeof(int));
On aurait pu écrire également
p = (int*)malloc(4);
puisqu'un objet de type int est
stocké sur 4 octets. Mais on préférera la première écriture qui a
l'avantage d'être portable.
Le programme suivant
#include #include main(){ int i = 3; int *p; printf("valeur de p avant initialisation = %ld\n",p); p = (int*)malloc(sizeof(int)); printf("valeur de p apres initialisation = %ld\n",p); *p = i; printf("valeur de *p = %d\n",*p);}
définit un pointeur p
sur un objet *p de type int, et affecte à *p la valeur de la variable i. Il imprime à l'écran :
valeur de p avant initialisation = 0valeur de p apres initialisation = 5368711424valeur de *p = 3
Avant l'allocation dynamique, on se trouve dans la
configuration
objet
| adresse
| valeur
|
i
| 4831836000
| 3
|
p
| 4831836004
| 0
|
A ce stade, *p
n'a aucun sens. En particulier, toute manipulation de la variable *p générerait une violation mémoire,
détectable à l'exécution par le message d'erreur Segmentation fault.
L'allocation dynamique a pour résultat d'attribuer une valeur à p et de réserver à cette adresse un
espace-mémoire composé de 4 octets pour stocker la valeur de *p. On a alors
objet
| adresse
| valeur
|
i
| 4831836000
| 3
|
p
| 4831836004
| 5368711424
|
*p
| 5368711424
| ? (int)
|
*p est maintenant définie mais
sa valeur n'est pas initialisée. Cela signifie que *p peut valoir n'importe quel entier (celui qui se trouvait
précédemment à cette adresse). L'affectation *p
= i; a enfin pour résultat d'affecter à *p la valeur de i.
A la fin du programme, on a donc
objet
| adresse
| valeur
|
i
| 4831836000
| 3
|
p
| 4831836004
| 5368711424
|
*p
| 5368711424
| 3
|
Il est important de comparer le programme précédent avec
main(){ int i = 3; int *p; p = &i;}
qui correspond à la situation
objet
| adresse
| valeur
|
i
| 4831836000
| 3
|
p
| 4831836004
| 4831836000
|
*p
| 4831836000
| 3
|
Dans ce dernier cas, les variables i et *p
sont identiques (elles ont la même adresse) ce qui implique que toute
modification de l'une modifie l'autre. Ceci n'était pas vrai dans l'exemple
précédent où *p et i avaient la même valeur mais des adresses
différentes.
On remarquera que le dernier programme ne nécessite pas d'allocation dynamique
puisque l'espace-mémoire à l'adresse &i
est déjà réservé pour un entier.
La fonction malloc permet
également d'allouer un espace pour plusieurs objets contigus en mémoire. On
peut écrire par exemple
#include #include main(){ int i = 3; int j = 6; int *p; p = (int*)malloc(2 * sizeof(int)); *p = i; *(p + 1) = j; printf("p = %ld \t *p = %d \t p+1 = %ld \t *(p+1) = %d \n",p,*p,p+1,*(p+1));}
On a ainsi réservé, à l'adresse donnée par la valeur de p, 8 octets en mémoire, qui
permettent de stocker 2 objets de type int.
Le programme affiche
p = 5368711424 *p = 3 p+1 =
5368711428 *(p+1) = 6 .
La fonction calloc de la
librairie stdlib.h a le même rôle
que la fonction malloc mais elle
initialise en plus l'objet pointé *p
à zéro. Sa syntaxe est
calloc(
nb-objets,
taille-objets)
Ainsi, si p est de type int*, l'instruction
p = (int*)calloc(N,sizeof(int));
est strictement équivalente à
p = (int*)malloc(N * sizeof(int));for (i = 0; i < N; i++) *(p + i) = 0;
L'emploi de calloc
est simplement plus rapide.
Enfin, lorsque l'on n'a plus besoin de l'espace-mémoire alloué dynamiquement
(c'est-à-dire quand on n'utilise plus le pointeur p), il faut libérer cette place en mémoire. Ceci se fait à
l'aide de l'instruction free qui
a pour syntaxe
free(
nom-du-pointeur);
A toute instruction de type malloc
ou calloc doit être associée une
instruction de type free.
3.5 Pointeurs et tableauxL'usage des pointeurs en C est, en grande partie,
orienté vers la manipulation des tableaux.
3.5.1 Pointeurs et tableaux à une
dimensionTout tableau en C est en fait un pointeur constant.
Dans la déclaration
int tab[10];
tab est un
pointeur constant (non modifiable) dont la valeur est l'adresse du premier
élément du tableau. Autrement dit, tab
a pour valeur &tab[0]. On
peut donc utiliser un pointeur initialisé à tab
pour parcourir les éléments du tableau.
#define N 5int tab[5] = {1, 2, 6, 0, 7};main(){ int i; int *p; p = tab; for (i = 0; i < N; i++) { printf(" %d \n",*p); p++; }}
On accède à l'élément d'indice i du tableau tab
grâce à l'opérateur d'indexation [],
par l'expression tab
. Cet
opérateur d'indexation peut en fait s'appliquer à tout objet p de type pointeur. Il est lié à
l'opérateur d'indirection * par
la formule
p[i] = *(p + i)
Pointeurs et tableaux se manipulent donc exactement de même
manière. Par exemple, le programme précédent peut aussi s'écrire
#define N 5int tab[5] = {1, 2, 6, 0, 7};main(){ int i; int *p; p = tab; for (i = 0; i < N; i++) printf(" %d \n", p[i]);}
Toutefois, la manipulation de tableaux, et non de pointeurs,
possède certains inconvénients dûs au fait qu'un tableau est un pointeur constant.
Ainsi
- on ne peut pas créer de
tableaux dont la taille est une variable du programme,
- on ne peut pas créer de
tableaux bidimensionnels dont les lignes n'ont pas toutes le même nombre
d'éléments.
Ces opérations deviennent possibles dès que l'on manipule
des pointeurs alloués dynamiquement. Ainsi, pour créer un tableau d'entiers à n éléments où n est une variable du programme, on écrit
#include main(){ int n; int *tab; ... tab = (int*)malloc(n * sizeof(int)); ... free(tab);}
Si on veut en plus que tous les éléments du tableau tab soient initialisés à zéro, on remplace
l'allocation dynamique avec malloc
par
tab = (int*)calloc(n, sizeof(int));
Les éléments de tab
sont manipulés avec l'opérateur d'indexation [],
exactement comme pour les tableaux.
Les deux différences principales entre un tableau et un pointeur sont
- un pointeur doit toujours
être initialisé, soit par une allocation dynamique, soit par affectation
d'une expression adresse, par exemple p
= &i ;
- un tableau n'est pas une
Lvalue ; il ne peut donc pas figurer à gauche d'un opérateur
d'affectation. En particulier, un tableau ne supporte pas l'arithmétique
(on ne peut pas écrire tab++.
3.5.2 Pointeurs et tableaux à plusieurs
dimensions
Un tableau à deux dimensions est, par définition, un tableau
de tableaux. Il s'agit donc en fait d'un pointeur vers un pointeur. Considérons
le tableau à deux dimensions défini par :
int tab[M][N];
tab est un
pointeur, qui pointe vers un objet lui-même de type pointeur d'entier. tab a une valeur constante égale à
l'adresse du premier élément du tableau, &tab[0][0].
De même tab[i], pour i entre 0 et M-1, est un pointeur constant vers un
objet de type entier, qui est le premier élément de la ligne d'indice i. tab[i]
a donc une valeur constante qui est égale à &tab[i][0].
Exactement comme pour les tableaux à une dimension, les pointeurs de pointeurs
ont de nombreux avantages sur les tableaux multi-dimensionnés.
On déclare un pointeur qui pointe sur un objet de type [i]type * (deux
dimensions) de la même manière qu'un pointeur, c'est-à-dire
type **
nom-du-pointeur;
De même un pointeur qui pointe sur un objet de type
type ** (équivalent à un tableau à
3 dimensions) se déclare par
type ***
nom-du-pointeur;
Par exemple, pour créer avec un pointeur de pointeur une
matrice à k lignes et n colonnes à coefficients entiers, on
écrit :
main(){ int k, n; int **tab; tab = (int**)malloc(k * sizeof(int*)); for (i = 0; i < k; i++) tab
= (int*)malloc(n * sizeof(int)); .... for (i = 0; i < k; i++) free(tab[i]); free(tab);}
La première allocation dynamique réserve pour l'objet pointé
par tab l'espace-mémoire
correspondant à k pointeurs sur
des entiers. Ces k pointeurs
correspondent aux lignes de la matrice. Les allocations dynamiques suivantes
réservent pour chaque pointeur tab[i]
l'espace-mémoire nécessaire pour stocker n
entiers.
Si on désire en plus que tous les éléments du tableau soient initialisés à
zéro, il suffit de remplacer l'allocation dynamique dans la boucle for par
tab[i] = (int*)calloc(n, sizeof(int));
Contrairement aux tableaux à deux dimensions, on peut
choisir des tailles différentes pour chacune des lignes tab[i]. Par exemple, si l'on veut que tab[i] contienne exactement i+1 éléments, on écrit
for (i = 0; i < k; i++) tab[i] = (int*)malloc((i + 1) * sizeof(int));
3.5.3 Pointeurs et chaînes de caractères
On a vu précédemment qu'une chaîne de caractères était un
tableau à une dimension d'objets de type char,
se terminant par le caractère nul '\0'.
On peut donc manipuler toute chaîne de caractères à l'aide d'un pointeur sur un
objet de type char. On peut
faire subir à une chaîne définie par
char *chaine;
des affectations comme
chaine = "ceci est une chaine";
et toute opération valide sur les pointeurs, comme
l'instruction chaine++;. Ainsi,
le programme suivant imprime le nombre de caractères d'une chaîne (sans compter
le caractère nul).
#include main(){ int i; char *chaine; chaine = "chaine de caracteres"; for (i = 0; *chaine != '\0'; i++) chaine++; printf("nombre de caracteres = %d\n",i);}
La fonction donnant la longueur d'une chaîne de caractères,
définie dans la librairie standard string.h,
procède de manière identique. Il s'agit de la fonction strlen dont la syntaxe est
strlen([i]chaine);
où
chaineest un pointeur sur un objet de type char.
Cette fonction renvoie un entier dont la valeur est égale à la longueur de la
chaîne passée en argument (moins le caractère '\0').
L'utilisation de pointeurs de caractère et non de tableaux permet par exemple
de créer une chaîne correspondant à la concaténation de deux chaînes de
caractères :
#include #include #include main(){ int i; char *chaine1, *chaine2, *res, *p; chaine1 = "chaine "; chaine2 = "de caracteres"; res = (char*)malloc((strlen(chaine1) + strlen(chaine2)) * sizeof(char)); p = res; for (i = 0; i < strlen(chaine1); i++) *p++ = chaine1
; for (i = 0; i < strlen(chaine2); i++) *p++ = chaine2[i]; printf("%s\n",res);}
On remarquera l'utilisation d'un pointeur intermédiaire p qui est indispensable dès que l'on fait
des opérations de type incrémentation. En effet, si on avait incrémenté
directement la valeur de res, on
aurait évidemment ``perdu'' la référence sur le premier caractère de la chaîne.
Par exemple,
#include #include #include main(){ int i; char *chaine1, *chaine2, *res; chaine1 = "chaine "; chaine2 = "de caracteres"; res = (char*)malloc((strlen(chaine1) + strlen(chaine2)) * sizeof(char)); for (i = 0; i < strlen(chaine1); i++) *res++ = chaine1[i]; for (i = 0; i < strlen(chaine2); i++) *res++ = chaine2[i]; printf("\nnombre de caracteres de res = %d\n",strlen(res));}
imprime la valeur 0,
puisque res a été modifié au
cours du programme et pointe maintenant sur le caractère nul.
3.6 Pointeurs et structures
3.6.1 Pointeur sur une structure
Contrairement aux tableaux, les objets de type structure
en C sont des Lvalues. Ils possèdent une adresse, correspondant à
l'adresse du premier élément du premier membre de la structure. On peut donc
manipuler des pointeurs sur des structures. Ainsi, le programme suivant crée, à
l'aide d'un pointeur, un tableau d'objets de type structure.
#include #include struct eleve{ char nom[20]; int date;}; typedef struct eleve *classe; main(){ int n, i; classe tab; printf("nombre d'eleves de la classe = "); scanf("%d",&n); tab = (classe)malloc(n * sizeof(struct eleve)); for (i =0 ; i < n; i++) { printf("\n saisie de l'eleve numero %d\n",i); printf("nom de l'eleve = "); scanf("%s",&tab[i].nom); printf("\n date de naissance JJMMAA = "); scanf("%d",&tab[i].date); } printf("\n Entrez un numero "); scanf("%d",&i); printf("\n Eleve numero %d:",i); printf("\n nom = %s",tab[i].nom); printf("\n date de naissance = %d\n",tab[i].date); free(tab); }
Si p est un
pointeur sur une structure, on peut accéder à un membre de la structure pointé
par l'expression(*p).[i]membre L'usage de parenthèses est ici indispensable car l'opérateur
d'indirection * à une priorité
plus élevée que l'opérateur de membre de structure. Cette notation peut être
simplifiée grâce à l'opérateur
pointeur de membre de structure, noté ->. L'expression précédente est
strictement équivalente à
p->
membre Ainsi, dans le programme précédent, on peut remplacer tab
.nom et tab[i].date respectivement par (tab + i)->nom et (tab
+ i)->date.
3.6.2 Structures auto-référencées
On a souvent besoin en C de modèles de structure dont
un des membres est un pointeur vers une structure de même modèle. Cette
représentation permet en particulier de construire des listes chaînées. En
effet, il est possible de représenter une liste d'éléments de même type par un
tableau (ou un pointeur). Toutefois, cette représentation, dite [i]contiguë,
impose que la taille maximale de la liste soit connue a priori (on a besoin du
nombre d'éléments du tableau lors de l'allocation dynamique). Pour résoudre ce
problème, on utilise une représentation
chaînée : l'élément de
base de la chaîne est une structure appelée
cellule qui contient la
valeur d'un élément de la liste et un pointeur sur l'élément suivant. Le
dernier élément pointe sur la liste vide NULL.
La liste est alors définie comme un pointeur sur le premier élément de la
chaîne.
Pour représenter une liste d'entiers sous forme chaînée, on crée le modèle
de structure cellule qui a deux
champs : un champ valeur de
type int, et un champ suivant de type pointeur sur une struct cellule. Une liste sera alors un
objet de type pointeur sur une struct
cellule. Grâce au mot-clef typedef,
on peut définir le type liste,
synonyme du type pointeur sur une struct
cellule.
struct cellule{ int valeur; struct cellule *suivant;}; typedef struct cellule *liste;
Un des avantages de la représentation chaînée est qu'il est
très facile d'insérer un élément à un endroit quelconque de la liste. Ainsi,
pour insérer un élément en tête de liste, on utilise la fonction
suivante :
liste insere(int element, liste Q){ liste L; L = (liste)malloc(sizeof(struct cellule)); L->valeur = element; L->suivant = Q; return(L);}
Le programme suivant crée une liste d'entiers et l'imprime à
l'écran :
#include #include struct cellule{ int valeur; struct cellule *suivant;}; typedef struct cellule *liste; liste insere(int element, liste Q){ liste L; L = (liste)malloc(sizeof(struct cellule)); L->valeur = element; L->suivant = Q; return(L);} main(){ liste L, P; L = insere(1,insere(2,insere(3,insere(4,NULL)))); printf("\n impression de la liste:\n"); P = L; while (P != NULL) { printf("%d \t",P->valeur); P = P->suivant; }}
On utilisera également une structure auto-référencée pour
créer un arbre binaire :
struct noeud{ int valeur; struct noeud *fils_gauche; struct noeud *fils_droit;}; typedef struct noeud *arbre;