Tableaux

Écrit le 11/11/2004 par Wikibooks
Dernière mise à jour : 02/02/2006

Il existe deux types de tableaux : les tableaux statiques, dont la taille est connue à la compilation, et les tableaux dynamiques, dont la taille est connue à l'exécution. Nous ne nous intéresserons pour l'instant qu'aux tableaux statiques, les tableaux dynamiques seront présentés avec les pointeurs.

Syntaxe

Déclaration

T tableau[N];

Déclare un tableau de N éléments, les éléments étant de type T. N doit être un nombre connu à la compilation, c'est à dire pas une variable. Pour avoir des tableaux dynamiques, il faut passer par l'allocation mémoire.

Accès aux éléments

tableau[i]
i[tableau]

Les deux syntaxes sont équivalentes et permettent d'accéder au i-ème élément du tableau. Un tableau est indicé de 0 à N - 1, cependant aucune vérification n'est faite sur les dépassements de capacité. À noter que la deuxième forme est très peu utilisée.

Exemples

int i;
int tableau[10];         /* déclare un tableau de 10 entiers */

for(i = 0; i < 10; i++)    /* boucle << classique >> pour le parcours d'un tableau */
{
    tableau[i] = i;  /* chaque case du tableau reçoit son indice comme valeur */
}
int tableau[1];          /* déclare un tableau d'un entier */

tableau[10] = 5;         /* accède à l'élément d'indice 10 (qui ne devrait pas exister) */

printf("%d\n", tableau[10]);

Ce deuxième exemple, non seulement compile (le compilateur ne détecte pas le dépassement de capacité) mais peut aussi s'exécuter et afficher le « bon » résultat.

Tableaux à plusieurs dimensions

Les tableaux vus pour l'instant étaient des tableaux à une dimension (ie : des tableaux à un seul indice), il est possible de déclarer des tableaux possédant un nombre aussi grand que l'on veut de dimensions. Par exemple pour déclarer un tableau d'entiers à deux dimensions :

int matrice[10][5];

Pour accèder aux éléments d'un tel tableau, on utilise une notation similaire à celle vue pour les tableaux à une dimension :

matrice[0][0] = 5;

affecte la valeur 5 à la case d'indice (0,0) du tableau.

Initialisation des tableaux

Il est possible d'initialiser directement les tableaux lors de leur déclaration :

int tableau[5] = { 1 , 5 , 45 , 3 , 9 };

initialise le tableau d'entiers avec les valeurs fournies entre accolades (tableau[0] = 1;, tableau[1] = 5;, etc.)

À noter que si on ne spécifie aucune taille entre les crochets du tableau, le compilateur la calculera automatiquement pour contenir tous les éléments. La déclaration ci-dessus aurait pu s'écrire plus simplement :

int tableau[] = { 1 , 5 , 45 , 3 , 9 };

Si on déclare un tableau de taille fixe, mais qu'on l'initialise avec moins d'éléments qu'il peut contenir, les éléments restant seront mis à zéro. On utilise cette astuce pour initialiser rapidement un tableau à zéro :

int tableau[512] = {0};

Cette technique est néanmoins à éviter, car dans ce cas le tableau sera stocké en entier dans le code du programme, faisant grossir artificiellement la taille du programme exécutable. Alors qu'en ne déclarant qu'une taille et l'initialisant à la main au début du programme, la plupart des formats d'exécutable sont capables d'optimiser en ne stockant que la taille du tableau dans le programme final.

Neanmoins, cette syntaxe est aussi utilisable pour les tableaux à plusieurs dimensions :

int matrice[2][3] = { { 1 , 2 , 3 } , { 4 , 5 , 6 } };

À noter que si on veut aussi utiliser l'adaptation dynamique, il faut quand même spécifier une dimension :

int identite[][3] = { { 1 , 0 , 0 } , { 0 , 1 , 0 } , { 0 , 0 , 1 } };
/* Ou plus "simplement" */
int identite[][3] = { { 1 } , { 0 , 1 } , { 0 , 0 , 1 } };

Ce qui peut aussi s'écrire :

int matrice[2][3] = { 1 , 2 , 3 , 4 , 5 , 6 };

Arithmétique des tableaux

Les tableaux, contrairement à tous les autres types du langage C, se manipulent par adresse. En fait, un tableau n'est ni plus ni moins qu'un pointeur spécial, un pointeur dont il est impossible de changer sa valeur. Considérez une déclaration telle que :

int tableau[10];

Ceci réservera quelque part en mémoire de la place pour 10 entiers de type int. tableau contient donc une référence sur le début de cet emplacement, on peut donc voir tableau comme un pointeur constant. Le terme constant est très important, car il faut bien comprendre qu'il n'y aura rien de plus que 10 entiers qui seront alloués, il n'y aura pas de variable supplémentaire pour acceuillir un pointeur, tableau n'étant qu'une référence connue à la compilation. C'est pourquoi les instructions suivantes n'ont aucun sens :

/* Ce code contient des erreurs grossières et volontaires */
int    tableau[10];
int *  pointeur;
int ** ppointeur;

tableau   = pointeur;    /* Incorrect. 'tableau' n'est pas une variable modifiable */
ppointeur = &tableau;    /* Incorrect. La référence 'tableau' n'occupe aucun espace dans la mémoire */
pointeur  = tableau;     /* Correct */

Il faut faire attention avec l'ambivalence entre les pointeurs et les tableaux, on utilise souvent la notation sous forme de tableaux pour déclarer un pointeur dans les prototypes de fonction, comme par exemple pour la fonction main :

int main( int nb, char * args[] );

Bien que dans ce cas uniquement (utilisation de la notation tableau dans un prototype de fonction pour une dimension du tableau), la déclaration est strictement équivalente à un tableau, dans tous les autres cas (déclaration de variables locales, globales, externes ou champ de structure/union), il ne s'agit pas tout à fait de la même notion. Considérez l'exemple qui suit :

Fichier1.c
int tableau[] = { 1, 2 };

void fonction( int * une_table )
{
    printf("une_table[0] = %d\n", une_table[0]);
}
Fichier2.c
/* Ce code contient une erreur grossière et volontaire */
extern int * tableau;
static int   un_autre_tableau[] = { 2, 3 };

int main( int nb, char * argv[] )
{
    printf("tableau[0] = %d\n", tableau[0]);
    fonction( un_autre_tableau );
}

Ce programme provoquera en fait une lecture d'un entier de type int à une adresse illégale, qui se traduira en général par son arrêt immédiat, sans même avoir eu le temps d'afficher quoi que ce soit.

L'erreur se situe bien-sûr dans la déclaration de la variable tableau dans le fichier Fichier2.c. Il aurait fallu écrire :

extern int tableau[];

En déclarant 'tableau' en tant que pointeur sur des entiers, on effectuera un déréférencement qui n'a pas lieu d'être et conduira le programme à effectuer un accès illégal à la mémoire. Erreur d'autant moins évidente à démasquer que pour la fonction externe dont on passe un tableau, on utilise la notation sous forme de pointeur dans le prototype.

Dans ce dernier cas, cela fonctionne parfaitement car la référence constante du tableau est copiée sur la pile, créant de ce fait effectivement une variable pointeur.

À noter qu'on ne spécifie pas la taille du tableau dans la déclaration externe, car de toute façon, le C ne vérifie pas les débordements.

C'est un piège assez classique, qui nécessite malheureusement d'avoir une compréhension assez bas niveau de la gestion des variables en C, ce qui n'arrange pas son cas face à ses détracteurs. On pourra rétorquer que les tableaux en C ne sont pas particulièrement flexible, et qu'un bon programmeur ferait bien d'utiliser une approche objet encapsulant les accès à ce tableau, plutôt que d'utiliser une référence externe directe.

Chaînes de caractères

Comme il a été dit tout au long de cet ouvrage, les chaînes de caractères sont des tableaux particuliers. En déclarant une chaîne de caractères on peut soit la manipuler en tant que pointeur soit en tant que tableau. Considérez les déclarations suivantes :

char * chaine1   = "Ceci est une chaine";
char   chaine2[] = "Ceci est une autre chaine";

Bien que se manipulant exactement de la même façon, les opérations permises sur les deux variables ne sont pas tout à fait les mêmes. Dans le premier cas on déclare un pointeur sur une chaîne de caractères statique, dans le second cas, un tableau (alloué soit sur la pile si la variable est déclarée dans une fonction ou soit dans le segment global si la variable est globale) de taille suffisante pour contenir tous les caractères de la chaîne affectée (incluant le caractère nul).

Le second cas est donc une notation abrégée pour char chaine2[] = { 'C', 'e', 'c', 'i', ' ', ..., 'e', 0 };. Dans tous les autres cas (ceux où une chaîne de caractères ne sert pas à initialiser un tableau), la chaîne déclarée est statique (les données sont persistantes entre les différents appels de fonctions), et sur certaines architectures, pour ne pas dire toutes, elle est même en lecture seule. En effet l'instruction chaine1[0] = 'c'; peut provoquer un accès illégal à la mémoire. En fait le compilateur peut optimiser la gestion des chaînes en regroupant celles qui sont identiques. C'est pourquoi il est préférable de classifier les pointeurs sur chaîne de caractères avec le mot clé const.

On comprends aisément que si chaine2 est alloué sur la pile (déclaré dans une fonction), la valeur ne pourra pas être utilisée comme valeur de retour. Tandis que la valeur de chaine1 pourra être retournée même si c'est une variable locale.

Cet article provient de Wikibooks et est sous licence GNU Free Documentation License. Il a été écrit par plusieurs personnes et est constamment mis à jour. Cet article est la version du 3 mai 2005 à 13:17. L'article d'origine se trouve à http://fr.wikibooks.org/wiki/Programmation_C_Tableaux.