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
.
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.
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 avecNB
avec :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 typefloat *
.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 à unwhile
, on aurait pu aussi bien écrire cette boucle comme suit, en répartissant les trois instructions dufor
:Pour une boucle simple, on préfère en général le
for
plus concis que lewhile
.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 unpointeur - sur -float
, affecterp+1
àp
avance le pointeurp
d'une case de typefloat
(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 deNB
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 donc3.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 donc3.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 donc3.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 fonctionprinttf
la valeur de la case pointée.Notez aussi qu'on a écrit
p++
au lieu dep = p+1
dans l'incrément dufor
. 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 à direp
(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 typefloat
) séparant la case pointée parp
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 :