Cette fiche précise comment les pointeurs permettent, en C, de manipuler les éléments d'un tableau.

Elle introduit le parcours de tableau "par indice pointeurs", une manière de parcourir les tableaux très idiomatique en langage C.

1. Rappel : tableau et adresse

Une variable de type tableau a pour valeur l'adresse du début du tableau et les cases du tableau sont contigües en mémoire, à partir de cette adresse.

Il est donc légitime de dire que la variable tableau pointe le tableau.

Considérons le programme suivant


int main() {
  short tab[100];  // tab est de type "tableau de 100 short"
  // Rappel : un short est un entier sur 2 octets

  printf("Valeur de tab :                 %p\n", tab);
  printf("Adresse de la case d'indice 0 : %p\n", &tab[0]); // eh bien... affiche la même chose !
  printf("Adresse de la case d'indice 1 : %p\n", &tab[1]); // Adresse précédente + 2 octets
  printf("Adresse de la case d'indice 2 : %p\n", &tab[2]); // Adresse précédente + 2 octets
  printf("Adresse de la case d'indice 99: %p\n", &tab[99]);// Adresse du début du tableau + 2*99 octets
}
          

Lorsqu'on l'exécute, en supposant que le tableau a été placé en mémoire, par exemple, à l'adresse 0x7fff56690a00, on obtient la sortie suivante :


  Valeur de tab :                 0x7fff56690a00
  Adresse de la case d'indice 0 : 0x7fff56690a00
  Adresse de la case d'indice 1 : 0x7fff56690a02
  Adresse de la case d'indice 2 : 0x7fff56690a04
  Adresse de la case d'indice 99: 0x7fff56690aC6
          

On observe donc bien que :

  • la valeur de tab est bien l'adresse de la première case (&tab[0]);
  • toutes les cases sont contigües en mémoire ;
  • et chaque case occupe bien 2 octets (la taille d'un short).

De même, le nombre d'octet entre le début du tableau et l'adresse de la dernière case, c'est à dire (0xC6-0x00) en hexa, ou encore (198) en décimal, vaut bien (2 octets)*(99 cases).

Enfin, l'adresse qui suit la dernière case du tableau de 100 cases est donc tab + 100.

Puisque tab pointe le début du tableau, il est possible d'utiliser l'arithmétique de pointeur et l'opérateur d'indirection * pour accéder aux éléments, comme dans l'exemple suivant :


int main() {
  double tab[10];  // tab est de type "tableau de 10 double"

  // tab <=> &tab[0] ; donc *tab <=> tab[0]
  *tab = 45.2; // et ceci affecte 45.2 à la première case du tableau, tab[0]

  // Arithmétique de pointeur
  // tab+1 est l'adresse du tableau + 1 case
  // c'est à dire l'adresse du tableau + 8 octets (puisque tab pointe un double)
  // Donc : (tab+1) <=> & tab[1] et *(tab+1) <=> tab[1]
  // Donc :
  *(tab+1) = 76.2 ; // affecte 76.2 à la case 2ème case du tableau, d'indice 1

  // ou encore
  *(tab+9) = 9. ; // affecte 9.0 à la dernière case du tableau

  // Evidémment, ce qui suit est une erreur : on sort des limites du tableau !!!
  *(tab+10) = 98 ;
  // Pour rappel, à l'exécution, cette ligne va "aléatoirement" :
  // * soit provoquer une erreur de segmentation (si jamais votre programme n'a pas le droit d'accéder à la case mémoire qui suit le tableau)
  // * soit changer la valeur d'autre chose que le tableau (si jamais une de vos variable suit le tableau en mémoire)
}
          

Eh bien, en s'appuyant sur la proximité, en C, entre tableau et adresse, il est possible de parcourir un tableau en utilisant un pointeur.

2. Parcours de tableau par indice pointeur

"Parcourir un tableau par indice pointeur", c'est écrire une boucle qui utilise un pointeur, au lieu d'un indice entier, pour désigner une a une les cases du tableau.

Voici comment on procède, ici sur l'exemple d'un tableau de float.


int main() {
  float tab[10];  // tab est de type "tableau de 10 double"
  float *p=NULL;  // on déclare un pointeur qui va pointeur dans le tableaux
                  // Notez qu'on a préfèré l'initialiser à NULL :
                  // pour éviter que, même ne serait-ce qu'entre deux instructions,
                  // il pointe aléatoirement en mémoire

  // et maintenant, le parcours du tableau par indice pointeur
  p = tab;
  while(p < tab + 10) {
    *p = 3.14 ;
    p++;
  }
  return EXIT_SUCCES;
}

          

Dans le slide suivant, on étend un peu ce code en le commentant pas à pas. On va aussi utiliser des boucles for, plus concises que le while.

Parcours de tableau par indice pointeur

/**
 * Parcours de tableau par indice pointeur
 */

#define NB 10

int main() {
  float tab[NB];  // tab est de type "tableau de 10 double"
  float *p=NULL;

  // Un premier parcours du tableau par "indice pointeur"
  for(p=tab ; p  < tab + NB ; p = p+1) {
    *p = 3.14 ;
  }

  printf("Voici les valeurs dans le tableau :\n");
  for(p=tab ; p  < tab + NB ; p++) {
    printf("%f", *p);
  }

  printf("Donnez les %d valeurs du tableau :\n"; NB);
  // un parcours par indice pointeur pour un scanf ? no pb !
  for(p=tab ; p  < tab + NB ; p++) {
    scanf("%f", p);
  }

  // affichons cette fois ci avec les numéros de cases, une case par ligne
  printf("Voici les valeurs dans le tableau :\n");
  for(p=tab ; p  < tab + NB ; p++) {
    printf("case %d vaut : %.1f\n",  p-tab, *p);
  }
  return EXIT_SUCCES;
}
                 

Avant de parler des indices pointeurs, remarquons que, plutôt que d'utiliser la valeur 5 dans le code pour désigner le nombre d'éléments voulu pour notre tableau, on a préféré déclarer une constante nommée avec NB avec :


#define NB 5
              

On utilise cette constante ensuite à chaque fois qu'on a besoin du nombre d'éléments.

Bien sûr, cette 1ère remarque n'a rien à voir avec la notion d'indice pointeur. C'est juste une bonne pratique quand on travaille avec des tableaux.

Maintenant, remarquez que ce code n'utilise aucun entier pour tous les parcours le tableau.

Tous les parcours utilisent un "indice pointeur" (le pointeur p).

Puisqu'on c'est un tableau de float auquel on a affaire, le pointeur déclaré pour écrire les parcours par indice pointeur doit être de type float *.

Voici l'état de la mémoire après les déclarations de variables :

Commencons nos parcours par indice pointeur.

Cette première boucle, que nous allons détailler, va affecter 3.14 à toutes les cases du tableau.

Notez que, comme un for est toujours équivalent à un while, on aurait pu aussi bien écrire cette boucle comme suit, en répartissant les trois instructions du for :


p=tab ;
while( p  < tab + NB) {
 *p = 3.14 ;
  p++;
}
                

Pour une boucle simple, on préfère en général le for plus concis que le while.

Considérons la première instruction de la boucle for (dite "initialisation") : p=tab ;

Elle affecte au pointer p la valeur de tab, c'est à dire l'adresse du début du tableau.

Autrement dit, au début de la boucle, p pointe la première case du tableau :

Considérons l'incrément du for (troisième instruction) : p = p+1

Puisque p est un pointeur - sur -float, affecter p+1 à p avance le pointeur p d'une case de type float (de 4 octets donc).

Ainsi, lorsque p est incrémenté à la fin de chaque tour de boucle, p va pointer la case suivante du tableau.

Enfin, considérons enfin la condition de sortie du for (seconde instruction) : p < tab + NB;

Puisque tab vaut l'adresse du début du tableau de NB cases, tab + NB est l'adresse qui suit immédiatement la dernière case du tableau.

p < tab + NB exprime donc "tant que p pointe une case mémoire située en mémoire avant la dernière case du tableau".

En conséquence, dès que p pointera au delà du tableau, la boucle s'arrêtera.

Cette boucle for exprime donc bien, globalement :

"Tant que p pointe dans le tableau, en avancant le pointeur d'une case à chaque tour".

Dans les slides qui suivent, déroulons pas à pas cette boucle for.

Au premier tour de boucle, p pointe la première case du tableau :

Au premier tour de boucle, l'instruction *p = 3.14; affecte donc 3.14 à la première case du tableau :

Au second tour de boucle , le pointeur p a été avancé d'une case. p pointe la deuxième case du tableau.

*p = 3.14; affecte donc 3.14 à la deuxième case du tableau.

Et ainsi de suite...

Au dernier tour de boucle , le pointeur p pointe la dernière case du tableau.

*p = 3.14; affecte donc 3.14 à la dernière case du tableau.

L'incrément p++ est ensuite exécuté une dernière fois : p pointe alors la case mémoire qui suit la fin du tableau.

La condition de sortie p < tab + NB devient alors fausse : la boucle s'arrête, et on passe à la suite.

Après la boucle, p pointe donc après le tableau.

Remarquons qu'il serait malvenu d'accéder à cette *p ici, bien sûr, puisqu'on est sorti des limites du tableau...

Dans cette seconde boucle, on utilise à nouveau un parcours par indice pointeur pour afficher toutes les valeurs dans le tableau.

Notez qu'on utilise bien l'opérateur d'indirection, *p pour passer en paramètre de la fonction printtf la valeur de la case pointée.

Notez aussi qu'on a écrit p++ au lieu de p = p+1 dans l'incrément du for. C'est exactement la même chose, et c'est plus court...

Cette fois ci, on utilise un parcours par indice pointeur pour lire au clavier toutes les valeurs dans le tableau.

Notez qu'on passe bien en paramètre de la fonction scanf l'adresse de la case du tableau, c'est à dire p (sans * !).

Enfin, cette dernière boucle utilise un nouveau parcours par indice pointeur pour afficher à la fois les nouvelles valeurs du tableau (choisies par l'utilisateur grâce au scanf précédents) et les indices des cases.

Comment peut-on afficher les numéros de case, alors qu'on a pas d'indice entier dans cette boucle ?

Eh bien on utilise tout simplement l'arithmétique de pointeur pour calculer l'indice de la case qu'on est en train d'afficher.

En effet, (p-tab), (adresse - adresse => un entier), est le nombre de cases (de type float) séparant la case pointée par p et le début tableau.

Ce code affiche donc, par exemple, suivant les valeurs données par l'utilisateur durant la boucle précédente :


Case 0 vaut : 1.5
Case 1 vaut : 3.0
Case 2 vaut : -8.5
Case 3 vaut : 1.5
Case 4 vaut : 19.9
                        

3. Mais à quoi ça sert un parcours par indice pointeur ?

(... à part gagner des points à l'examen ...)

Eh bien...

Tout d'abord, il s'agit d'une syntaxe très idiomatique en langage C (et C++), en ce qu'elle dénote au coeur du code la proximité permise par le C avec la mémoire de la machine.

Les développeurs C y ont donc souvent recours ; et il vous est demandé de savoir la manipuler.

Vous verrez, c'est du plus bel effet dans les discussions mondaines et c'est (presqu'aussi ?) efficace qu'un "Wind Surf" dernière génération pour frimer sur les plages.

Par ailleurs, pour écrire certains algorithmes, par exemple pour parcourir plusieurs tableau avec plusieurs valeurs d'indice en même temps, utiliser des indices pointeur peut se révéler beaucoup (... vraiment très beaucoup ... ) plus clair qu'utiliser des des indices entier. Vous aurez l'occasion d'avoir quelques exemples en TP.

Ceci étant dit, les compilateurs actuels parviennent à optimiser les parcours par indice entier plus efficacement que les parcours par indice pointeur.

*** TODO eh pourquoi donc ??? j'ai jamais pigé, mais j'ai constaté. sur plusieurs archis...

Ainsi, si on cherche à maximiser l'efficacité du code, il peut être préférable d'utiliser un bon vieux indice entier.