• Aucun résultat trouvé

Substitution des tableaux

9 Classes de mémorisation

10.3 Pointeurs et tableaux

10.3.2 Substitution des tableaux

Puisqu’on peut utiliser les tableaux et les pointeurs quasiment de la même façon, avec des contraintes supplémentaires pour les tableaux, il est tentant de supposer qu’un tableau est juste une sorte particulière de pointeur. Comme on sait que l’identificateur d’un tableau ne peut pas être une left-value (cf. section 6), et qu’on ne peut donc pas modifier l’adresse d’un tableau, les tableaux sont parfois qualifiés de pointeurs constants. Cependant, cette observation est fausse, et il suffit de dessiner l’occupation mémoire d’un tableau et d’un pointeur pour s’en apercevoir :

short tab[3]={12,4,8},*p; p = &tab[0];

Le pointeur p est une variable qui occupe ici 2 octets en mémoire (rappelons que la taille d’un pointeur varie d’un système à l’autre, et que la valeur 2 n’est ici qu’un exemple). Son type est short* et sa valeur est l’adresse 12345.

Le tableau tab est une variable qui occupe 6 octets (soit 3 valeurs de type short). Son type est short[3] et il contient trois valeurs distinctes 12, 4 et 8. Notez que l’adresse 12346 n’apparait jamais explicitement, à la différence de ce qu’on avait observé pour p. La caractéristique principale d’un pointeur étant de contenir une adresse, on peut en conclure que tab n’est pas un pointeur, puisqu’il ne contient pas d’adresse.

Considérons les instructions suivantes : tab[2] = 5;

p[2] = 5;

Leur résultat est le même : on place la valeur 5 dans l’élément numéro 2 du tableau. Mais leur évaluation est différente. Pour le tableau (1ère ligne), on obtient directement l’adresse de tab (comme pour n’importe quelle variable) et on se rend dans son élément numéro 2 que l’on modifie. Pour le pointeur (2ème ligne), on obtient l’adresse de p, va consulter sa valeur, qui est l’adresse de tab. On va ensuite à l’adresse de tab, puis on se déplace dans son élément numéro 2 pour finalement le modifier. On a donc une étape supplémentaire.

La confusion décrite plus haut vient du fait que, comme on l’a déjà mentionné, quand l’identificateur d’un tableau de type xxxx[] apparait dans une expression, il est remplacé par un pointeur de type xxxx* pointant sur son premier élément. Ce pointeur est géré automatiquement et de façon transparente par le compilateur. De plus, il est non-modifiable, et on a donc effectivement un pointeur constant.

Mais cette substitution n’a pas toujours lieu : il y a plusieurs exceptions, correspondant aux situations où l’identificateur du tableau est l’opérande de certains opérateurs particuliers. On peut citer entre autres sizeof et & (opérateur de référençage). Soit l’instruction suivante, appliquée au tableau de l’exemple précédent :

printf("Taille du tableau tab: %d\n", sizeof(tab));

Le résultat affiché sera 6, soit 2 × 3 octets, ce qui correspond bien à l’espace mémoire total occupé par le tableau. Si la substitution avait lieu pour sizeof, il renverrait la taille d’un pointeur, ce qui n’est pas le cas ici. Pour &, considérons l’expression suivante : &tab. Elle est de type int (*)[10], c’est-à-dire : pointeur sur un tableau de 10 entiers. Là encore, il n’y a donc pas de substitution, sinon on aurait simplement int**.

Cette fameuse substitution a aussi lieu lorsqu’on passe un tableau en paramètre à une fonction : lors de l’appel, l’expression correspondant au nom du tableau est convertie implicitement en un pointeur, qui est lui-même passé à la fonction.

Cela signifie qu’une fonction reçoit toujours un pointeur, jamais un tableau. Lorsque le paramètre formel est déclaré en tant que tableau, il est lui aussi implicitement converti en un pointeur. Comme on a vu qu’il était possible de traiter un tableau à peu près comme un pointeur, cela ne pose pas de problème dans la fonction. Mais on pourrait tout aussi bien déclarer un pointeur en tant que paramètre formel, tout en passant un tableau en tant que paramètre effectif :

? 12345 1234 6 1234 7 1234 8 1234 9 123 50 123 51 123 52 123 53 ? 123 54 12346 12 4 8 ? 1234 4 ? 123 55 ? 123 56 ? 123 57

cela reviendrait au même, puisque le tableau (paramètre effectif) sera transformé en pointeur lors de l’appel.

exemple :

void ma_fonction(short tab[4]) { ...

}

int main(int argc, char** argv) { short t[4];

...

ma_fonction(t); }

Ici, on déclare un tableau de 4 entiers short dans la fonction main. Lors de l’appel, on l’expression t qui doit être évaluée. Comme aucun des opérateurs mentionnés précédemment (& et sizeof) n’est utilisé, le tableau t est remplacé par un pointeur sur son premier élément. On passe donc du type short[4] au type short*. La fonction est supposée recevoir un tableau de 4 entiers short, mais en réalité le compilateur a changé cette déclaration en un paramètre de même nom mais de type short*. Comme la fonction reçoit un short* lors de l’appel de la fonction main, ces types sont consistants. Tout se passe comme si on avait

directement déclaré dans notre code source une fonction d’en-tête void

ma_fonction(short* tab).

10.3.3 Tableaux multidimensionnels

Considérons maintenant des tableaux à plusieurs dimensions. Pour eux, la substitution ne concerne que la première dimension. Soit la matrice 𝑀 × 𝑁 suivante :

short m[M][N];

Le type de m est évidemment short[M][N]. Mais si m apparait dans une expression (sans & ni sizeof), alors elle sera remplacée par un pointeur de type short (*)[N], autrement dit : pointeur sur un tableau de 𝑁 entiers courts. L’expression &m aura pour type short (*)[M][N], i.e. pointeur sur une matrice de 𝑀 × 𝑁 entiers courts.

exemple : on met à jour l’exemple précédent de façon à utiliser une matrice : void ma_fonction(short tab[4][5])

{ ... }

int main(int argc, char** argv) { short t[4][5];

...

ma_fonction(t); }

Alors l’en tête de la fonction pourrait tout aussi bien ne pas préciser la première dimension, qui est superflue :

void ma_fonction(short tab[][5])

Ou même directement déclarer un pointeur sur un tableau : void ma_fonction(short (*tab)[5])

Remarque : notez bien les parenthèses autour de *tab. Sans elles, on obtient pas un pointeur sur un tableau de short, mais plutôt un tableau de pointeurs sur des shorts (cf. section 10.3.4).

Considérons les instructions suivantes : printf("%p\n",m)

On sait que la variable m est remplacée par une adresse de type short (*)[N]. Cette adresse est celle de la matrice, donc de sa première ligne m[0], donc du premier élément de cette ligne. Au final, on obtient l’adresse du premier élément du tableau.

printf("%p\n",*m)

L’application de l’opérateur * à m permet d’obtenir une expression de type short[N]. On sait qu’un type tableau est remplacé par un type pointeur sur les mêmes données, donc ici il sera remplacé par un pointeur de type short*. Donc on va afficher l’adresse de la première ligne du tableau, i.e. celle de son premier élément, i.e. la même que tout à l’heure. Par contre, le type est différent : short* au lieu de short(*)[N]. Ceci confirme que short[][] n’est pas équivalent à short**, comme on pourrait le croire a priori : si c’était le cas, les adresses obtenues avec ces deux expressions ne pourraient pas être les mêmes (sauf à pointer sur sa propre adresse).

printf("%p\n",**m)

La deuxième application de * transforme le short* en short, et on obtient finalement la valeur du tout premier élément du tableau. L’expression **m envoie la valeur de cet élément non pas parce qu’une matrice est un pointeur de pointeur, mais bien à cause du système de substitution transformant les tableaux en pointeurs lors de l’évaluation d’expressions.