Un
pointeur est un objet
dont la valeur est égale à l'adresse d'un autre objet. On déclare un pointeur
par l'instruction :
type *nom-du-pointeur;
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 |
Avant
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 nombreoctets octets. 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 <stdlib.h>int *p;
p = (int*)malloc(sizeof(int));
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);
L'usage
des pointeurs en C est, en grande partie, orienté vers la manipulation des
tableaux.
Tout
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[i]. 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
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 <stdlib.h>main(){ int n; int *tab; ... tab = (int*)malloc(n * sizeof(int));...
free(tab);}
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 <stdio.h>main(){ int i;char *chaine;
chaine = "chaine de caracteres";for (i = 0; *chaine != '\0'; i++)
chaine++;
printf("nombre de caracteres = %d\n",i);}
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 <stdlib.h>#include <stdio.h>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).membre
L'expression
précédente est strictement équivalente à
p->membre
Ainsi,
dans le programme précédent, on peut remplacer tab[i].nom et tab[i].date respectivement par (tab + i)->nom et (tab + i)->date.