CPGE Oujda                                                                                                                                                                                                                                                                                  Spe

Les pointeurs

  Notion de pointeur

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

Allocation dynamique

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);

 Pointeurs et tableaux

L'usage des pointeurs en C est, en grande partie, orienté vers la manipulation des tableaux.

Pointeurs et tableaux à une dimension

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 5
int 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 5
int 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);
}

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 <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);
}

 Pointeurs et structures

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.