Menu

Les tableaux en C : déclaration, initialisation et parcours

Comment déclarer et initialiser des tableaux en C, les indexer à partir de zéro, calculer la longueur avec sizeof, parcourir les éléments, et pourquoi lire au-delà de la fin est un comportement indéfini plutôt qu'un message d'erreur.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

Un tableau est un bloc de taille fixe d'éléments du même type, disposés les uns après les autres en mémoire. Cette disposition contiguë explique tout : c'est pourquoi l'indexation est instantanée, pourquoi tableaux et pointeurs sont si étroitement liés, et pourquoi le C peut vous donner l'adresse de l'élément zéro et vous laisser retrouver le reste par arithmétique.

Déclarer un tableau

Type d'élément, nom et taille entre crochets :

int    scores[5];       // 5 int
double prices[100];     // 100 double
char   initials[3];     // 3 char

La taille est le nombre d'éléments, et elle doit être connue là où le tableau est déclaré - généralement un littéral ou une constante #define. La mémoire est réservée immédiatement ; rien n'est alloué plus tard.

Un tableau local fraîchement déclaré contient du garbage, pas des zéros :

La seconde ligne peut afficher des zéros, des nombres énormes, et différer d'une exécution à l'autre ou entre une compilation de débogage et une de production. Lire ces valeurs est un comportement indéfini ; initialisez avant de lire.

Initialiser

Plusieurs formes, toutes utiles :

L'idiome {0} est celui à retenir : tous les éléments que vous ne listez pas sont initialisés à zéro, donc un seul zéro met tout le tableau à zéro. Les initialiseurs désignés ([4] = 99) permettent de fixer des positions précises et de laisser le reste à zéro, ce qui est pratique pour des tables de correspondance majoritairement vides.

Une chose que vous ne pouvez pas faire, c'est affecter un tableau après coup :

int a[3] = {1, 2, 3};
int b[3];
b = a;                    // erreur : un tableau n'est pas affectable
memcpy(b, a, sizeof a);   // voici comment on en copie un

L'indexation commence à zéro

Le premier élément est arr[0] et le dernier arr[n-1]. Il n'y a pas de arr[n].

Exécutez cette dernière boucle et regardez les adresses : chacune est exactement sizeof(int) octets après la précédente. Les éléments sont réellement disposés bout à bout :

 indice     0         1         2         3         4
         +--------+--------+--------+--------+--------+
 valeur  |   88   |   95   |   75   |   60   |  100   |
         +--------+--------+--------+--------+--------+
 offset   +0       +4       +8       +12      +16      octets
          ^
          &scores[0], soit aussi ce en quoi `scores` se degrade

L'indexation à partir de zéro n'est pas un choix arbitraire - elle fait de l'indice un décalage depuis le début, exactement ce dont l'arithmétique d'adresses a besoin. scores[i] est défini comme *(scores + i) ; voir pointeurs et tableaux.

Trouver la longueur

Le C ne stocke la longueur d'un tableau nulle part à l'exécution, mais le compilateur la connaît et sizeof vous y donne accès :

sizeof(arr) / sizeof(arr[0]) est l'idiome. Diviser par arr[0] plutôt que par un sizeof(double) écrit en dur fait que la ligne continue de fonctionner si vous changez le type des éléments.

Le piège : cela ne fonctionne que là où le tableau a été déclaré. Passez le tableau à une fonction et le paramètre est un pointeur, donc sizeof mesure le pointeur - typiquement 8 octets, donnant une « longueur » de 1 ou 2. C'est pourquoi toute fonction C prenant un tableau prend aussi un compte :

Parcourir un tableau

La forme standard est une boucle for partant de 0 tant que i < n :

Notez i < n, et non i <= n. Avec n éléments, les indices valides vont de 0 à n - 1, donc <= fait un passage de trop et lit au-delà de la fin. Initialiser hottest et coldest à partir de temps[0] plutôt qu'à 0 est l'autre habitude à copier - démarrer un maximum à zéro casse silencieusement sur des données entièrement négatives.

Le dépassement de bornes est un comportement indéfini

C'est la partie du C qui surprend les gens venant d'autres langages. Il n'y a aucune vérification de bornes. Aucune à la compilation, aucune à l'exécution. arr[10] sur un tableau de cinq éléments n'est pas une erreur - c'est un calcul d'adresse que le compilateur effectue sans commentaire.

int arr[5] = {1, 2, 3, 4, 5};
arr[7] = 99;     // ecrit 8 octets au-dela de la fin du tableau
int x = arr[-1]; // lit avant le debut

Ce qui se passe ensuite est un comportement indéfini, et ses symptômes sont variés d'une façon peu utile :

  • cela semble fonctionner, parce que la mémoire touchée n'était pas utilisée ;
  • une autre variable change sans raison visible ;
  • le programme plante avec une erreur de segmentation, possiblement bien plus tard ;
  • le comportement diffère entre -O0 et -O2.

« Cela semble fonctionner » est le cas dangereux, car cela signifie que le bug part en production. Trois défenses :

  1. Bouclez avec i < n. La plupart des débordements sont des erreurs de décalage d'un dans une condition de boucle.
  2. Validez les indices qui viennent d'une entrée.
  1. Compilez avec un sanitizer pendant le développement. gcc -fsanitize=address -g prog.c transforme la plupart des débordements en un rapport immédiat nommant le fichier, la ligne et le tableau concerné.

Tableaux d'autres types

La même syntaxe fonctionne pour n'importe quel type d'élément, y compris les structures :

La ligne char word[6] = "hello"; mérite une pause : une chaîne en C n'est qu'un tableau de char dont le dernier octet significatif est '\0'. Ce terminateur explique pourquoi le tableau a besoin de six cases pour cinq lettres.

Les tableaux de taille variable, brièvement

C99 autorise un tableau dont la taille est une valeur calculée à l'exécution :

int n = get_count();
int buffer[n];        // un tableau de taille variable (VLA)

Deux mises en garde. La taille est figée une fois le tableau créé, donc un VLA n'est pas une liste extensible. Et la mémoire vient de la pile, donc un n grand ou contrôlé par un attaquant peut la faire déborder et faire planter le programme - c'est pourquoi de nombreux projets, dont le noyau Linux, interdisent purement et simplement les VLA. Leur prise en charge est de plus optionnelle pour les implémentations C11.

Quand la taille est réellement inconnue jusqu'à l'exécution, allouez plutôt :

Remarquez que arr[i] se lit à l'identique que arr soit un tableau ou un pointeur vers de la mémoire allouée - la syntaxe d'indexation s'en moque. Tous les détails dans mémoire dynamique.

Questions fréquentes

Comment déclare-t-on un tableau en C ?

Donnez le type des éléments, un nom et une taille entre crochets : int scores[5]; réserve la place pour cinq entiers. La taille doit être une constante connue à l'endroit où le tableau est déclaré (hors tableaux de taille variable), et les éléments ne sont pas initialisés tant que vous ne fournissez pas de valeurs.

Comment trouver la longueur d'un tableau en C ?

sizeof(arr) / sizeof(arr[0]) - la taille totale en octets divisée par la taille d'un élément. Cela ne fonctionne que dans la portée où le tableau a été déclaré : une fois passé à une fonction, le tableau s'est dégradé en pointeur et sizeof mesure le pointeur.

Que se passe-t-il si on dépasse la fin d'un tableau en C ?

Rien ne vous en empêche. Le C ne vérifie aucune borne, donc arr[10] sur un tableau de 5 éléments lit ou écrit la mémoire qui se trouve là. C'est un comportement indéfini : cela peut afficher n'importe quoi, corrompre une autre variable ou planter - et cela peut sembler fonctionner jusqu'au jour où non.

Comment initialiser tous les éléments d'un tableau à zéro en C ?

int arr[100] = {0}; - tous les éléments que vous ne listez pas sont initialisés à zéro, donc un seul zéro met tout le tableau à zéro. int arr[100] = {}; fonctionne aussi en C23. Sans initialiseur, le contenu d'un tableau local est du garbage indéterminé.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER