Comme nous l'avons déjà constaté au chapitre 7, le nom d'un tableau représente l'adresse de son premier élément. En d'autre termes:
En simplifiant, nous pouvons retenir que le nom d'un tableau est un pointeur constant sur le premier élément du tableau.
Exemple
En déclarant un tableau A de type int et un pointeur P sur int,
int A[10]; int *P;l'instruction:
P = A; est équivalente à P = &A[0];
P+i |
pointe sur la i-ième composante derrière P et |
||
P-i |
pointe sur la i-ième composante devant P. |
Ainsi, après l'instruction,
P = A;
le pointeur P pointe sur A[0], et
*(P+1) |
désigne le contenu de A[1] |
*(P+2) |
désigne le contenu de A[2] |
... |
... |
*(P+i) |
désigne le contenu de A[i] |
Remarque
Au premier coup d'oeil, il est bien surprenant que P+i n'adresse pas le i-ième octet derrière P, mais la i-ième composante derrière P ...
Ceci s'explique par la stratégie de programmation 'défensive' des créateurs du langage C:
Si on travaille avec des pointeurs, les erreurs les plus perfides sont causées par des pointeurs malplacés et des adresses mal calculées. En C, le compilateur peut calculer automatiquement l'adresse de l'élément P+i en ajoutant à P la grandeur d'une composante multipliée par i. Ceci est possible, parce que:
- chaque pointeur est limité à un seul type de données, et
- le compilateur connaît le nombre d'octets des différents types.
Exemple
Soit A un tableau contenant des éléments du type float et P un pointeur sur float:
float A[20], X; float *P;Après les instructions,
P = A; X = *(P+9);X contient la valeur du 10-ième élément de A, (c.-à-d. celle de A[9]). Une donnée du type float ayant besoin de 4 octets, le compilateur obtient l'adresse P+9 en ajoutant 9 * 4 = 36 octets à l'adresse dans P.
Rassemblons les constatations ci dessus :
Comme A représente l'adresse de A[0],
*(A+1) |
désigne le contenu de A[1] |
||
*(A+2) |
désigne le contenu de A[2] |
||
... |
|||
*(A+i) |
désigne le contenu de A[i] |
Attention !
Il existe toujours une différence essentielle entre un pointeur et le nom d'un tableau:
- Un pointeur est une variable,
donc des opérations comme
P = A ou P++ sont permises.
- Le nom d'un tableau est une constante,
donc des
opérations comme A = P ou A++ sont
impossibles.
Ceci nous permet de jeter un petit coup d'oeil derrière les rideaux:
Lors de la première phase de la compilation, toutes les expressions de la forme A[i] sont traduites en *(A+i). En multipliant l'indice i par la grandeur d'une composante, on obtient un indice en octets:
Cet indice est ajouté à l'adresse du premier élément du tableau pour obtenir l'adresse de la composante i du tableau. Pour le calcul d'une adresse donnée par une adresse plus un indice en octets, on utilise un mode d'adressage spécial connu sous le nom 'adressage indexé':
Presque tous les processeurs disposent de plusieurs registres spéciaux (registres index) à l'aide desquels on peut effectuer l'adressage indexé de façon très efficace.
Résumons Soit un tableau A d'un type quelconque et i un indice pour les composantes de A, alors
A |
désigne l'adresse de |
A[0] |
|
A+i |
désigne l'adresse de |
A[i] |
|
*(A+i) |
désigne le contenu de |
A[i] |
Si P = A, alors
P |
pointe sur l'élément |
A[0] |
|
P+i |
pointe sur l'élément |
A[i] |
|
*(P+i) |
désigne le contenu de |
A[i] |
Formalisme tableau et formalisme pointeur
A l'aide de ce bagage, il nous est facile de 'traduire' un programme écrit à l'aide du 'formalisme tableau' dans un programme employant le 'formalisme pointeur'.
Exemple
Les deux programmes suivants copient les éléments positifs d'un tableau T dans un deuxième tableau POS.
Formalisme tableau
main() { int T[10] = {-3, 4, 0, -7, 3, 8, 0, -1, 4, -9}; int POS[10]; int I,J; /* indices courants dans T et POS */ for (J=0,I=0 ; I<10 ; I++) if (T[I]>0) { POS[J] = T[I]; J++; } return 0; }
Nous pouvons remplacer systématiquement la notation tableau[I] par *(tableau + I), ce qui conduit à ce programme:
Formalisme pointeur
main() { int T[10] = {-3, 4, 0, -7, 3, 8, 0, -1, 4, -9}; int POS[10]; int I,J; /* indices courants dans T et POS */ for (J=0,I=0 ; I<10 ; I++) if (*(T+I)>0) { *(POS+J) = *(T+I); J++; } return 0; }
Sources d'erreurs
Un bon nombre d'erreurs lors de l'utilisation de C provient de la confusion entre soit contenu et adresse, soit pointeur et variable. Revoyons donc les trois types de déclarations que nous connaissons jusqu'ici et résumons les possibilités d'accès aux données qui se présentent.
Les variables et leur utilisation
int A;
déclare une variable simple du
type int
A |
désigne le contenu de A |
|
&A |
désigne l'adresse de A |
int B[];
déclare un tableau
d'éléments du type int
B |
désigne l'adresse de la première composante de B. |
|
(Cette adresse est toujours constante) |
||
B[i] |
désigne le contenu de la composante i du tableau |
|
&B[i] |
désigne l'adresse de la composante i du tableau |
en utilisant le formalisme pointeur:
B+i |
désigne l'adresse de la composante i du tableau |
|
*(B+i) |
désigne le contenu de la composante i du tableau |
int *P;
déclare un pointeur sur des
éléments du type int.
P peut pointer |
sur des variables simples du type int ou |
|
sur les composantes d'un tableau du type int. |
||
P |
désigne l'adresse contenue dans P |
|
(Cette adresse est variable) |
||
*P |
désigne le contenu de l'adresse dans P |
Si P pointe dans un tableau, alors
P |
désigne l'adresse de la première composante |
|
P+i |
désigne l'adresse de la i-ième composante derrière P |
|
*(P+i) |
désigne le contenu de la i-ième composante derrière P |