Tableaux et pointeurs
Le rapport entre tableaux et pointeurs est confus pour la plupart des débutants en C. Le langage lui-même fait tout pour induire cette confusion et il est aidé en cela par les documents à destination des débutants qui présentent les choses avec des variations sur « les tableaux sont des pointeurs ».
Les tableaux ne sont pas des pointeurs, mais il est quasiment impossible de manipuler des tableaux sans se servir des pointeurs en C, et si la syntaxe rend la chose aisée c'est au risque de créer de la confusion.
Pointeurs
Les pointeurs, c'est une famille de types dont les valeurs possibles
sont les adresses des objets d'un type donné. Par exemple les pointeurs
vers int
ont pour valeur les adresses des objets de
type int
.
On référence l'objet (et on déréference le pointeur) en utilisant
l'opérateur unaire préfixe *
. La syntaxe des déclarations en C
mimiquant la syntaxe de l'utilisation, c'est aussi avec cette opérateur que
l'on déclare les variables de type pointeur. Par exemple int
*ptr;
déclare une variable ptr
de type pointeur
vers int
et dans la portée de cette
déclaration *ptr
désigne l'object de type int
pointé par ptr
.
On peut former un pointeur vers un objet en utilisant l'opérateur unaire
préfixe &
. Par exemple, dans le contexte de int
i;
, &i
est un pointeur vers int
dont
la valeur est l'adresse de i
.
On combine fréquemment le déréférencement d'un pointeur et l'accès à un
membre d'une structure. À cause de cette fréquence, il y a une syntaxe
spéciale: (*p).m
peut se noter p->m.
On peut additionner les pointeurs avec des entiers. On considère alors que le pointeur pointe vers une séquence d'objets contigus en mémoire et de même type. Le résultat de l'addition d'un pointeur avec n est un pointeur vers le nième élément de la séquence.
On utilise fréquemment le déréférencement du résultat de l'addition d'un
pointeur et d'un entier. À cause de cette fréquence, il y a une syntaxe
spéciale : *(p+n)
peut se
noter p[n]
[1].
Tableaux
Les tableaux, c'est une famille de types dont les valeurs possibles sont
les séquences non vides d'objets contigus en mémoire d'un type donné. Par
exemple, les tableaux de 4 int
ont pour valeurs les séquences
de 4 int
.
Les tableaux sont des types défavorisés en C. On peut déclarer des
variables. On peut construire un pointeur vers eux avec
l'opérateur &
. On peut avoir leur taille
avec sizeof
. C'est tout. On ne peut pas les copier, que ce
soit pour les assigner ou les passer en paramètre.
Pour faire à peu près n'importe quoi d'intéressant, il faut passer par les pointeurs et l'arithmétique sur les pointeurs. Mais pour ce faire, il faut avoir un pointeur vers un élément. On peut faire tellement peu de choses avec les tableaux et obtenir un pointeur vers le premier élément d'un tableau est une opération tellement courante qu'elle est implicite : les tableaux sont convertis en un pointeur vers leur premier élément dans la plupart des contextes.
Tableau unidimensionnel
Pour définir un tableau tab de n int
, on utilise
int tab[n];
où n doit être une expression constante entière[2]. Pour accéder à la taille en bytes du
tableau, on utilise sizeof tab
. Si on veut un pointeur vers le
tableau, il faut utiliser &tab
qui est de type pointeur
vers tableau de n int
. Dans tous les autres
contextes, tab
sera converti en un pointeur vers le
premier int
du tableau.
Si on veut la taille en nombre d'éléments, il faut diviser la taille en
bytes du tableau par la taille en bytes d'un élément, donc sizeof
tab/sizeof *tab
. Dans l'expression sizeof
*tab
, tab
est d'abord converti en un pointeur vers le
premier élément du tableau, ce pointeur est déréférencé et on prend la
taille de l'objet résultant, c'est donc bien la taille du premier élément
du tableau.
Pour accéder au iième élément du tableau, on
utilise tab[i]
. À nouveau tab
est converti en un
pointeur vers le premier élément du tableau, on ajoute i à ce
pointeur ce qui donne un pointeur vers le iième élément
du tableau et on déréférence ce pointeur.
On ne peut pas copier les tableaux, donc on ne peut pas les passer en
paramètre à une fonction. à la place, on peut passer un pointeur vers le
tableau. Pour passer un pointeur vers un tableau
de n int
à une fonction, la fonction doit avoir pour
prototype :
void f(int (*pTab)[n]);
Elle s'appelle avec f(&tab)
. Dans la fonction, on peut
récupérer la taille en bytes du tableau avec sizeof *pTab
, la
taille en nombre d'éléments avec sizeof *pTab/sizeof **pTab
(dans sizeof **pTab
, *pTab
est un tableau qui
n'est pas utilisé comme argument de sizeof ou de &
, donc
il est converti en un pointeur vers son premier élément, cette conversion
n'est pas réservée au tableau qui sont nommés, et l'exemple
de *pTab
montre que si on ne peut pas faire grand chose avec
les tableaux, il est cependant possible d'avoir des expressions de type
tableau plus compliquées qu'une variable). On peut accéder
au iième élément avec (*pTab)[i]
.
Tout cela n'est pas très agréable à utiliser, en particulier une fonction ne peut pas manipuler des tableaux de taille inconnue. Mais comme tout passe de toute manière par les pointeurs, en pratique quand on veut une fonction qui prend un tableau, on lui passe d'une part un pointeur vers son premier élément, et d'autre part la taille. Le prototype ressemble donc a :
void f(int *pTab, size_t n);
Elle s'appelle avec f(tab, sizeof tab/sizeof *tab)
. Dans la
fonction, on peut récupérer la taille en bytes du tableau
avec n*sizeof *pTab
, la taille en nombre d'éléments
avec n
. On peut accéder au iième élément
avec pTab[i]
. C'est quand même beaucoup plus simple à utiliser
même s'il faut passer la taille explicitement et ne pas utiliser dans la
fonction sizeof pTab
ou sizeof *pTab
pour obtenir
la taille du tableau.
De même, quand on veut allouer un tableau dynamiquement, on n'utilise généralement pas
int (*pTab)[n] = malloc(sizeof *pTab);
en conjonction avec (*pTab)[i]
, mais on fait
int *pTab = malloc(n * sizeof *pTab);
en conjonction avec pTab[i]
. Avec à nouveau l'avantage que
la taille peut ne pas être connue statiquement.
Tableau bidimensionnel
Il n'y a pas de tableaux multidimensionnels en C comme il peut y en
avoir dans d'autres langages. On utilise à la place des tableaux de
tableaux, ce qui revient quasiment au même. Un tableau de l tableaux
de c int
se déclare
int tab[l][c];
Pour accéder au jième élément de
la iième ligne, on
utilise tab[i][j]
. L'interprétation est que tab
(de type tableau de l tableaux de c int
) est
converti en un pointeur vers son premier élément (donc en un pointeur vers
un tableau de c int
). L'ajout de i suivit du
dérérérencement donne le iième tableau de
c int
. Celui-ci est converti à son tour en un pointeur
vers son premier élément (donc un pointeur vers int
; à
nouveau, la conversion a bien lieu pour tous les tableaux, pas uniquement
les variables) auquel on ajoute c et qu'on déréférence.
Pour passer un tableau bidimensionnel à une fonction, on peut déclarer la fonction
void f(int (*pTab)[l][c]);
l'appeler avec f(&tab)
et
utiliser (*pTab)[i][j]
. l et c devant être fixés
à la compilation. Tout comme dans le cas unidimensionnel, il est plus
commun de déclarer une fonction
void f(int (*pTab)[c], size_t l);
de l'appeler avec f(tab)
et
d'utiliser pTab[i][j]
. c doit toujours être fixé a la
compilation mais l est maintenant un paramètre et peut donc être
variable.
De même, quand on veut allouer un tableau dynamiquement, on n'utilise généralement pas
int (*pTab)[l][c] = malloc(sizeof *pTab);
en conjonction avec (*pTab)[i][j]
, mais on fait
int (*pTab)[c] = malloc(l * sizeof *pTab);
en conjonction avec pTab[i][j]
. Avec l'avantage que la
taille dans la première dimension peut ne pas être connue statiquement.
Si on veut un tableau bidimensionel dont la taille dans la deuxieme dimention est determinee dynamiquement, deux choix sont possibles. Le premier est d'allouer un tableau de la taille totale désirée :
int *pTab = malloc(l * c * sizeof *pTab);
et d'utiliser pTab[i * c + j]
.
Le deuxième est de faire un tableau dynamique de tableaux dynamiques[3] (avec l'avantage qu'on peut varier la taille de chacun des sous-tableaux individuellement si c'est utile, par exemple dans le cas de matrices triangulaires).
int **pTab = malloc(l * sizeof *pTab); for (i = 0; i < l; i++) pTab[i] = malloc(c * sizeof *pTab[i]);
et d'utiliser pTab[i][j]
.
On peut aussi faire l'allocation en une fois.
int **pTab = malloc(l * sizeof *pTab + l * c * sizeof **pTab); int *base = (int*)(pTab + l); for (i = 0; i < l; i++) pTab[i] = base + i*c;
Compléments
Copier des tableaux, impossible ?
Pour copier des tableaux, la syntaxe
t1 = t2;
n'est pas utilisable. Il faut utiliser memcpy
memcpy(t1, t2, sizeof t2);
Si on désire copier beaucoup de tableaux, et en particulier de pouvoir les passer par valeur à une fonction, une possibilité est d'encapsuler le tableau dans une struct. Si on a
struct Tableau { int m[10]; } t1, t2;
alors t1 = t2;
devient possible de même que le passage par
valeur à une fonction. Le prix à payer est la complication de la syntaxe
d'indexation : t1.m[i]
.
Pour ajouter à la confusion
Les déclarations de fonctions
void f(int *pTab, size_t l); void g(int (*pTab)[c], size_t l);
peuvent aussi s'écrire
void f(int pTab[], size_t l); void g(int pTab[][c], size_t l);
et même
void f(int pTab[42], size_t l); void g(int pTab[42][c], size_t l);
Dans le deuxième case, la taille (ici 42) est ignorée. Il est important de noter que cette utilisation d'une syntaxe associée aux tableaux pour declarer un pointeur n'est possible que pour la première dimension des paramètres de fonction.
En B, langage intermédiaire entre BCPL et C, la syntaxe
auto tab[10];
avait comme effet quelque chose qui s'écrirait en C
word _tab[11]; word* tab = _tab;
(ni l'absence de const ni le 11 ne sont des erreurs).
Il y a eu des étapes intermédiaire entre le B et le C ou les pointeurs
se déclaraient avec []
, la possibilité de le faire dans les
déclarations de fonction en est un reliquat.
[1] L'addition est commutative, et
l'opérateur []
aussi. On a donc la curiosité
que n[p]
est aussi valable et synonyme
de p[n]
. Ce n'est en pratique pas utilisé.
[2] C99 a introduit des tableaux de taille dynamique, ils n'en sera plus question par la suite dans ce document.
[3] Technique appelée vecteurs de Iliffe.