Programmation C/Tableaux
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, N ne peut pas être une variable. Pour avoir des tableaux dynamiques, il faut passer par l'allocation mémoire.
Cette dernière restriction a été levée par la norme C99 qui a apporté le type tableau à longueur variable appelé Variable Length Array (VLA). il est maintenant possible de déclarer un tableau de type VLA par :
T tableau[expr];
où "expr" est une expression entière, calculée à l'exécution du programme. Ce type de tableau ne peut pas appartenir à la classe de stockage static ou extern. Sa portée est limitée au bloc dans lequel il est défini. Par rapport à une allocation dynamique, les risques de fuite mémoire sont supprimés.
Accès aux éléments.
L'exemple précédent a permis de déclarer un tableau à N éléments. En C, ces éléments sont indexés de 0 à N-1, et le i élément peut être accédé de la manière suivante (où codice_1 est supposé déclaré comme une variable entière ayant une valeur entre 0 et N-1):
tableau[i]
Note: La syntaxe suivante est équivalente à la précédente:
i[tableau]
Bien qu'autorisée par le C, elle va à l'encontre de ce à quoi nombre de programmeurs sont habitués dans d'autres langages, c'est pourquoi elle est très peu utilisée. Comme, de plus, elle n'apporte aucun réel avantage par rapport à la première syntaxe, elle est simplement à éviter, au profit de la précédente.
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 peut compiler (le compilateur peut ne pas détecter le dépassement de capacité), mais peut aussi s'exécuter et afficher le « bon » résultat. Le langage C n'impose pas à une implémentation de vérifier les accès, en écriture comme en lecture, hors des limites d'un tableau ; il précise explicitement qu'un tel code a un "comportement indéfini", donc que n'importe quoi peut se passer. En l’occurrence, ce code peut très bien marcher comme on pourrait l'attendre, i.e. afficher codice_2, ou causer un arrêt du programme avec erreur (si la zone qui correspondrait au 11ème élément du tableau est hors de la mémoire allouée au processus, le système d'exploitation peut détecter une tentative d'accès invalide à une zone mémoire, ce qui peut se traduire par une « erreur de segmentation » qui termine le programme), ou encore corrompre une autre partie de la mémoire du processus (dans le cas où le pseudo-11ème élément correspondrait à une partie de la mémoire du processus), ce qui peut modifier son comportement ultérieur de manière très difficile à prévoir.
Ce code est donc un exemple d'un type de bogue très courant en langage C. S'il est facile à détecter dans ce code très court, il peut être très difficile à identifier dans du code plus complexe, où on peut par exemple essayer d'accéder à un indice codice_1 d'un tableau, la valeur de codice_1 pouvant varier suivant un flot d'exécution complexe, ou alors essayer de copier le contenu d'un tableau dans un autre, sans vérifier la taille du tableau de destination (ce dernier cas est connu sous le nom de "débordement de tampon", "dépassement de capacité", ou encore "buffer overflow" en anglais). Il est donc très important de s'assurer que tous les accès au contenu de tableaux, en lecture comme en écriture, se font dans les limites de ses bornes.
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.
Avec le type VLA (Variable Length Array), introduit par C99, il est possible de définir un tableau à plusieurs dimensions dont les bornes ne sont connues qu'à l'exécution et non à la compilation :
static void vlaDemo(int taille1, int taille2)
 // Declaration du tableau VLA
 int table[taille1][taille2];
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 (codice_5, codice_6, 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.
Néanmoins, cette syntaxe est aussi utilisable pour les tableaux à plusieurs dimensions :
int matrice[2][3] = { { 1 , 2 , 3 } , { 4 , 5 , 6 } };
Ce qui peut aussi s'écrire :
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 } };
Conversion des noms de tableaux en pointeurs.
À quelques exceptions près (c.f. ci-dessous), un nom de tableau apparaissant dans une expression C sera automatiquement converti en un pointeur vers son premier élément lors de l'évaluation de cette expression : si codice_7 est le nom d'un tableau d'entiers, le "nom de tableau" codice_7 sera converti dans le type pointeur vers codice_9 dans "l'expression" </code>tab</code>, et la valeur de cette expression sera l'adresse du début de la zone mémoire allouée pour le stockage des éléments du tableau, i.e. l'adresse de son premier élément. Le code suivant est par exemple valide :
int tab[10];
int *p, *q;
p = tab; 
q = &(tab[0]);
La déclaration codice_10 réserve une zone mémoire suffisante pour stocker 10 variables de type codice_9, désignées par codice_12, codice_13..., codice_14. Dans l'instruction codice_15, l'expression codice_7 est de type pointeur vers codice_9, et la valeur affectée à codice_18 est l'adresse de la première case du tableau. Cette adresse est aussi l'adresse de la variable codice_12 : dans la seconde instruction, l'adresse de codice_12 est affectée à codice_21 via l'opérateur de prise d'adresse codice_22. Après la seconde instruction, codice_18 et codice_21 ont la même valeur, et les instructions codice_25, codice_26 ou codice_27 deviennent opératoirement équivalentes.
Exceptions à la règle de conversion.
Noter que dans la déclaration codice_28, le nom codice_7 ne désigne pas en lui-même un pointeur mais bien un "tableau", de type "tableau d'entiers à 10 éléments", même si ce nom est converti en pointeur dans la plupart des contextes. Les exceptions à la règle de conversion interviennent : 
Dans chaque cas, le nom du tableau gardera le sens d'une variable de type tableau, c'est-à-dire conservera son type initial :
int tab[10]; /* tableau d'entiers à 10 éléments */
int (*p)[10]; /* pointeur vers les tableaux d'entiers à 10 éléments */
p = &tab;
Le code suivant est en revanche incorrect :
int tab[10];
int *p;
p = &tab; /* incorrect. '&tab' n'est pas du bon type */
Le cas où codice_43 est membre gauche d'une affectation montre que les valeurs de type tableau ne sont jamais manipulables de manière directe : il est par exemple impossible d'affecter un tableau à un autre tableau - l'instruction codice_52 est toujours incompilable, à cause de la conversion en pointeur de codice_53 - ou de comparer structurellement deux tableaux par une comparaison simple - dans codice_54, les deux noms seront convertis en pointeur, et la comparaison effectuée sera celle des adresses.
Cas des paramètres de fonctions.
Tout paramètre de fonction déclaré comme étant de type "tableau d’éléments de type T" est automatiquement converti en un pointeur vers T à la compilation. Les deux écritures suivantes sont équivalentes :
Dans la première version de codice_55 et codice_56, les paramètres codice_57 et codice_58 sont bien des pointeurs, conformément à l'équivalence avec la seconde version, et sont donc réaffectables dans le corps de ces fonctions. La taille en la première dimension de codice_57 et de codice_58, même si elle est spécifiée dans le code (e.g. codice_61), sera de toute manière effacée à la compilation : il ne s'agit alors que d'un simple commentaire pour le programmeur. 
Cette équivalence est cohérente avec la règle de conversion des noms de tableaux : lors d'un appel de la forme codice_62 où codice_43 est un tableau d'entiers, le nom codice_43 sera converti en pointeur vers codice_65, qui est bien le type du paramètre de codice_55 dans la seconde version.
Cas des tableaux externes.
Dans le cas des déclarations externes, et contrairement au cas des paramètres de fonctions, il n'y a "pas" équivalence entre la notation par tableaux et la notation par pointeurs. Le programme suivant est en général compilable, mais incorrect : 
Les valeurs affichées pour codice_43 dans codice_68 et dans codice_55 seront en général distinctes.
L'erreur se situe dans la déclaration externe de codice_7 située dans le fichier codice_71. Il aurait fallu écrire la déclaration suivante, spécifiant correctement le type de codice_43 dans codice_73 comme étant celui d'un tableau et non d'un pointeur (mais ne spécifiant pas sa taille, le compilateur n'effectuant de toute manière aucun test de dépassement) :
extern int tab[];
L'absence de message d'erreur dans la compilation de la première version de ce programme n'est due qu'à l'insuffisance de la vérification de la cohérence globale du typage lors de la phase de liaison : il est plus généralement possible de déclarer une variable externe de n'importe quel autre type que son type réel, sans que l'incohérence globale du typage soit nécessairement signalée par le compilateur. Aucune norme ne spécifiant le comportement du programme résultant, ce comportement est, de fait, indéfini.
Tableaux VLA passés à une fonction.
En C99, il est possible de passer à une fonction un tableau de type VLA (Variable Length Array).
Il faut passer les tailles des dimensions en premier.
Exemple de fonction recevant en argument un tableau de type VLA à deux dimensions :
static void vlaPrint(int taille1, int taille2, int table[taille1][taille2])
 // Ecriture du tableau sur stdout
 for (int i=0; i< taille1; i++)
 for (int j=0; j< taille2; j++)
 (void)printf("table[%d][%d] = %d\n", i, j, table[i][j]);
Le prototype de cette fonction peut s'écrire de la sorte
static void vlaPrint(int, int, int[*][*])
Notez l'emploi des * pour les dimensions variables.
Chaînes de caractères.
Comme il fut 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 codice_74. 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 codice_75 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é codice_76.
On comprend aisément que si codice_77 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 codice_78 pourra être retournée même si c'est une variable locale.