Aller au contenu principal

Les tableaux

Ce que ce chapitre apporte 7 points
  • Déclarer un tableau, l'initialiser, et accéder à ses éléments par leur indice.
  • Calculer l'adresse d'un élément, et expliquer ce que devient un accès hors des bornes.
  • Parcourir un tableau sans jamais sortir de ses bornes.
  • Calculer le nombre d'éléments d'un tableau avec sizeof, et savoir où ce calcul cesse de fonctionner.
  • Piloter plusieurs sorties, et décrire une séquence entière, à partir de tableaux.
  • Calculer une somme, une moyenne et un maximum en un seul parcours, et classer une mesure dans une zone.
  • Estimer la mémoire occupée par un tableau, à une ou à deux dimensions.

Une LED, c'est une variable. Huit LEDs, c'est huit variables, huit pinMode, huit digitalWrite recopiés, et autant d'occasions de se tromper de numéro. Le tableau range ces valeurs sous un seul nom et les rend parcourables par une boucle. Il apporte aussi un risque que le pseudo-code ne connaissait pas : en C, sortir d'un tableau ne provoque aucune erreur, seulement des résultats faux.

Un nom pour plusieurs valeurs

Définition

Un tableau est une suite de valeurs du même type, rangées côte à côte en mémoire sous un seul nom. Chaque élément est désigné par son indice, qui commence à 0.

Ce décalage d'une unité par rapport au pseudo-code, où les indices commencent à 1, est la source d'erreur la plus banale du passage à un vrai langage. Le chapitre d'algorithmique sur les tableaux et leur parcours traite des parcours eux-mêmes, indépendamment du langage, et le chapitre d'architecture sur les tableaux et les adresses montre pourquoi un indice n'est rien d'autre qu'un décalage ajouté à une adresse.

type nom[taille];                     // gabarit de déclaration
type nom[] = {valeur, valeur, ...};   // la taille est déduite du nombre de valeurs
Arduino
int mesures[5]; // cinq entiers ; déclaré hors de toute fonction, il vaut cinq zéros
int seuils[] = {100, 250, 600}; // trois entiers : seuils[0] vaut 100, seuils[2] vaut 600
void setup() {
mesures[0] = seuils[1]; // le premier élément reçoit 250
}
void loop() {}

Un tableau de n éléments a des indices de 0 à n − 1. Le dernier élément de seuils est seuils[2], pas seuils[3].

Vérification rapideon peut se reprendre

Quels sont les indices valides d'un tableau de 8 éléments ?

Où se trouve t[i]

« Côte à côte en mémoire » est à prendre au pied de la lettre. Les éléments occupent des octets consécutifs, et l'emplacement de chacun se calcule à partir de trois nombres :

adresse de t[i]=adresse de t[0]+i×taille d’un eˊleˊment\text{adresse de } t[i] = \text{adresse de } t[0] + i \times \text{taille d'un élément}

Sur l'Uno, un int occupe 2 octets. Si mesures[0] commence à l'adresse 0x100, mesures[1] commence à 0x102, mesures[2] à 0x104, et ainsi de suite. C'est ce calcul, et rien d'autre, que fait la carte à chaque accès mesures[i] : elle ne vérifie pas que i est dans les bornes, elle calcule une adresse et va la lire.

Ce calcul a un nom en C : l'arithmétique des pointeurs. t[i] n'est qu'une autre écriture de « l'adresse de t, décalée de i éléments », et le chapitre Les pointeurs du parcours C la manipule directement.

Sortir des bornes

En Python, lire un indice qui n'existe pas lève une erreur. En C, rien ne se produit : le programme lit ou écrit la case mémoire située juste après le tableau, qui appartient à une autre variable. Le parcours C en fait l'objet d'une section entière de son chapitre sur les tableaux, sur ordinateur, où la même faute produit les mêmes effets.

La figure range deux variables globales à la suite, comme le fait souvent le compilateur : un tableau mesures de cinq int, puis un int nommé seuil. Chaque carré est un octet, avec son adresse en dessous.

MÉMOIRE VIVE : 12 octets sur les 2 048 de l'Uno, un carré par octet0x1000x1010x1020x1030x1040x1050x1060x1070x1080x1090x10A0x10B?0x10C?0x10D10mesures[0]20mesures[1]30mesures[2]40mesures[3]50mesures[4]600seuil↑ mesures[0]
mesures[0]

mesures[0] → 0x100 + 0 × 2 = 0x100 : dans le tableau, qui va de l'indice 0 à l'indice 4.

Cinq int côte à côte, puis la variable seuil. Le curseur calcule l'adresse de mesures[i] ; au-delà de l'indice 4, cette adresse tombe sur les octets de seuil, et la carte y accède sans aucune protestation.
À manipuler
Faire avancer l'indice de 0 à 4 en lisant le calcul d'adresse sous la figure, puis passer à 5. Le cadre quitte le tableau et se pose exactement sur seuil. Cliquer alors « écrire mesures[5] = 0 » : seuil vaut désormais 0, alors qu'aucune ligne du programme ne l'a modifié. Revenir, puis recommencer à l'indice 6, qui tombe sur des octets qui n'appartiennent à aucune des deux variables.
L'erreur la plus coûteuse du chapitre
Lire mesures[5] dans un tableau de cinq éléments rend une valeur quelconque, celle qui se trouve là. Écrire mesures[5] = 0 est pire : cela modifie silencieusement une autre variable du programme, et le défaut apparaît ailleurs, sans rapport visible avec la ligne fautive. Dans la figure, c'est seuil qui passe à 0 : une alarme réglée sur ce seuil se déclencherait en permanence, et personne ne chercherait la cause dans la boucle de calcul d'une moyenne.
La cause est presque toujours la même : une boucle écrite avec <= au lieu de <.

La forme sûre d'un parcours est toujours celle-ci : partir de 0, s'arrêter strictement avant le nombre d'éléments.

Arduino
int mesures[] = {10, 20, 30, 40, 50};
const int NB_MESURES = sizeof(mesures) / sizeof(mesures[0]); // 5
void setup() {
Serial.begin(9600);
for (int i = 0; i < NB_MESURES; i++) { // i vaut 0, 1, 2, 3, 4
Serial.println(mesures[i]);
}
}
void loop() {}

Compter les éléments avec sizeof

sizeof(mesures) rend la place occupée par le tableau entier, en octets ; sizeof(mesures[0]) celle d'un élément. Leur quotient est le nombre d'éléments, et il se met à jour tout seul si l'on ajoute une valeur dans les accolades. Sur la figure : 10 octets divisés par 2 octets, soit 5 éléments.

sizeof ne fonctionne que là où le tableau est déclaré
Un tableau passé à une fonction n'y arrive pas entier : la fonction ne reçoit que l'adresse de son premier élément. Dans la fonction, sizeof mesure cette adresse, et le quotient est faux. C'est pourquoi une fonction qui traite un tableau reçoit toujours sa taille en second paramètre, comme le montre le chapitre suivant.
Vérification rapideon peut se reprendre

1.Que se passe-t-il en C quand on lit un indice situé hors du tableau ?

2.Un tableau d'int commence à l'adresse 0x200. À quelle adresse commence t[3] ?

3.Que calcule sizeof(t) / sizeof(t[0]) là où le tableau t est déclaré ?

4.Pourquoi une fonction qui traite un tableau reçoit-elle sa taille en paramètre ?

Un tableau de broches

Le tableau prend tout son intérêt quand il contient des numéros de broches. Le programme ne dépend plus du nombre de LEDs : il suffit d'ajouter un numéro entre les accolades.

Arduino
const int broches[] = {2, 3, 4, 5};
const int NB = sizeof(broches) / sizeof(broches[0]);
void setup() {
for (int i = 0; i < NB; i++) {
pinMode(broches[i], OUTPUT);
}
}
void loop() {
for (int i = 0; i < NB; i++) { // une LED après l'autre : un chenillard
digitalWrite(broches[i], HIGH);
delay(250);
digitalWrite(broches[i], LOW);
}
}
250 ms par bit4 bauds8 bits, 2 s en tout
broche 210001000broche 301000100broche 400100010broche 50001000102 s
Le chenillard sur deux secondes. Une seule LED allumée à la fois, chacune pendant un quart de seconde. Un cycle complet dure le nombre de LEDs multiplié par 250 ms, soit une seconde.

Décrire une séquence au lieu de la recopier

Un feu tricolore enchaîne trois phases, chacune avec sa LED et sa durée. Recopier trois fois digitalWrite et delay fonctionne, mais le comportement du feu se retrouve dispersé dans le code. Deux tableaux parallèles le rassemblent : à l'indice i, phases[i] dit quelle LED allumer, et durees[i] combien de temps.

Arduino
const int VERT = 11, ORANGE = 10, ROUGE = 9;
const int phases[] = {VERT, ORANGE, ROUGE};
const unsigned long durees[] = {3000, 1000, 3000};
const int NB_PHASES = sizeof(phases) / sizeof(phases[0]);
void setup() {
for (int i = 0; i < NB_PHASES; i++) {
pinMode(phases[i], OUTPUT);
}
}
void loop() {
for (int i = 0; i < NB_PHASES; i++) {
digitalWrite(phases[i], HIGH); // la LED de cette phase
delay(durees[i]); // pendant la durée de cette phase
digitalWrite(phases[i], LOW);
}
}
1 s par bit1 bauds14 bits, 14 s en tout
vert11100001110000orange00010000001000rouge0000111000011108 s
Le feu décrit par les deux tableaux, sur deux cycles de sept secondes. Chaque colonne des tableaux devient un palier du chronogramme : trois secondes de vert, une d'orange, trois de rouge.

Le programme ne contient plus aucune durée ni aucune broche dans loop() : il joue une séquence décrite ailleurs. Ajouter une phase clignotante, ou raccourcir l'orange, ne touche que les tableaux. C'est la même idée que le tableau de broches, poussée un cran plus loin : les données décrivent le comportement, le code se contente de le dérouler.

Deux tableaux parallèles doivent avoir la même taille
Si durees compte une valeur de moins que phases, la dernière phase lit une durée hors du tableau, donc une valeur quelconque. Rien ne le signale. Déclarer les deux tableaux l'un sous l'autre, avec une valeur par colonne, rend l'oubli visible à la relecture.

Résumer une série en un parcours

Somme, moyenne et maximum se calculent tous en parcourant le tableau une seule fois.

Arduino
int releves[] = {-3, -7, -1, -12};
const int NB = sizeof(releves) / sizeof(releves[0]);
void setup() {
Serial.begin(9600);
long somme = 0;
int maximum = releves[0]; // partir d'un élément réel du tableau
for (int i = 0; i < NB; i++) {
somme += releves[i];
if (releves[i] > maximum) {
maximum = releves[i];
}
}
Serial.println((float)somme / NB); // conversion avant division : la moyenne garde ses décimales
Serial.println(maximum); // -1
}
void loop() {}
Le maximum qui part de zéro
Initialiser maximum à 0 paraît naturel, et fonctionne tant que toutes les valeurs sont positives. Sur des relevés tous négatifs, aucun ne dépasse 0, et le programme annonce un maximum de 0, une valeur qui n'est pas dans le tableau. Partir du premier élément règle le problème dans tous les cas.

La somme est déclarée long, et ce n'est pas un excès de prudence : 100 lectures analogiques de 1023 totalisent 102 300, bien au-delà des 32 767 d'un int. Le chapitre sur le langage a montré ce qui arrive alors : aucune erreur, et une moyenne fausse.

Classer une mesure dans une zone

Un tableau de seuils croissants découpe une plage de mesures en zones : sous 300, la luminosité est « sombre » ; de 300 à 699, « moyenne » ; à partir de 700, « forte ». Classer une mesure revient à chercher le premier seuil qu'elle n'atteint pas.

Arduino
const int seuils[] = {300, 700}; // deux seuils, donc trois zones
const int NB_SEUILS = sizeof(seuils) / sizeof(seuils[0]);
const char* NOMS[] = {"sombre", "moyenne", "forte"}; // trois textes : NOMS[z] est le nom de la zone z
void setup() {
Serial.begin(9600);
}
void loop() {
int mesure = analogRead(A0); // de 0 à 1023
int z = 0;
while (z < NB_SEUILS && mesure >= seuils[z]) { // tant qu'un seuil est franchi
z++;
}
Serial.println(NOMS[z]); // z vaut 0, 1 ou 2
delay(500);
}

Pour une mesure de 120, aucun seuil n'est franchi et z reste à 0 : « sombre ». Pour 450, le premier seuil est franchi, pas le second : z vaut 1, « moyenne ». Pour 950, les deux sont franchis : z vaut 2, « forte ». Le tableau NOMS a une case de plus que seuils, puisque deux seuils découpent trois zones. Ses éléments sont des textes, d'où le type const char* : l'étoile sera expliquée avec les adresses, au chapitre suivant ; ici, il suffit de lire NOMS[z] comme « le nom de la zone z ».

L'ordre des deux conditions du while n'est pas indifférent. z < NB_SEUILS est testé en premier ; quand il est faux, le C n'évalue pas la seconde condition, et seuils[z] n'est jamais lu hors du tableau. Écrites dans l'autre ordre, les deux conditions liraient seuils[2], qui n'existe pas, pour toute mesure à partir de 700.

Deux dimensions

Un tableau peut contenir des tableaux. Un motif pour quatre LEDs est une ligne de quatre valeurs ; une animation est une suite de motifs, donc un tableau à deux dimensions : une ligne par étape, une colonne par LED.

Arduino
const int broches[] = {2, 3, 4, 5};
const byte motifs[4][4] = {
{1, 0, 0, 1}, // étape 0 : les deux LEDs extérieures
{0, 1, 1, 0}, // étape 1 : les deux LEDs intérieures
{1, 1, 1, 1}, // étape 2 : toutes
{0, 0, 0, 0}, // étape 3 : aucune
};
void setup() {
for (int j = 0; j < 4; j++) {
pinMode(broches[j], OUTPUT);
}
}
void loop() {
for (int etape = 0; etape < 4; etape++) {
for (int j = 0; j < 4; j++) {
digitalWrite(broches[j], motifs[etape][j]); // ligne etape, colonne j
}
delay(400);
}
}
400 ms par bit3 bauds8 bits, 3,2 s en tout
broche 210101010broche 301100110broche 401100110broche 51010101003,2 s
L'animation des quatre motifs, jouée deux fois. Chaque colonne du chronogramme est une ligne du tableau motifs, lue de haut en bas : le tableau à deux dimensions est déjà le dessin du signal.

motifs[etape][j] se lit « ligne etape, colonne j ». En mémoire, les lignes sont rangées l'une après l'autre : l'élément motifs[2][1] se trouve au rang 2×4+1=92 \times 4 + 1 = 9 à partir du début. La place occupée vaut lignes × colonnes × taille d'un élément, ici 4×4×1=164 \times 4 \times 1 = 16 octets.

Vérification rapideon peut se reprendre

1.Un tableau byte m[3][5] est déclaré. Combien d'octets occupe-t-il ?

2.Dans while (z < NB && mesure >= seuils[z]), pourquoi le test z < NB vient-il en premier ?

Ce qu'un tableau coûte en mémoire

Un tableau occupe le nombre d'éléments multiplié par la taille d'un élément. Sur une carte qui dispose de 2 Ko de mémoire vive, le calcul se fait avant d'écrire la déclaration.

DéclarationPlace occupéePart des 2 Ko
int lectures[200];400 octetsenviron 20 %
byte niveaux[200];200 octetsenviron 10 %
float temperatures[200];800 octetsenviron 40 %
byte image[16][16];256 octets12,5 %

Une lecture analogique, de 0 à 1023, ne tient pas dans un byte. Un niveau de PWM, de 0 à 255, y tient : le ranger dans un int gaspille la moitié de la place.

const ne fait pas économiser la mémoire vive
Sur l'Uno, un tableau déclaré const est tout de même recopié en mémoire vive au démarrage : le mot-clé interdit de le modifier, il ne le déplace pas. Pour une grande table de valeurs fixes, une mélodie ou une police de caractères, le mot-clé PROGMEM la laisse dans la mémoire flash, au prix d'une lecture un peu plus lourde. Pour quelques dizaines de valeurs, ce n'est pas la peine.

Mettre en pratique

Adresses, tailles et durées

  • 1.

    Un tableau long t[10] commence à l'adresse 0x100, soit 256. À quelle adresse, en décimal, commence t[7] ?

  • 2.

    Combien d'octets occupe float mesures[60] ?

  • 3.

    Quelle part des 2 048 octets de mémoire vive cela représente-t-il, en pour cent ?

  • 4.

    Un feu est décrit par durees[] = {3000, 1000, 3000, 500}. Combien de secondes dure un cycle complet ?

  • 5.

    Dans byte motifs[4][4], à quel rang, compté depuis 0, se trouve motifs[3][2] ?

Un programme calcule la moyenne de int lectures[5] = {10, 20, 30, 40, 50} avec for (int i = 0; i <= 5; i++). Qu'affiche-t-il ?

On ne peut pas le prévoir. La boucle fait six tours et additionne lectures[5], une case qui n'appartient pas au tableau. La somme contient donc une valeur quelconque, et la moyenne est fausse.

La correction : for (int i = 0; i < 5; i++), et une division qui garde les décimales, (float)somme / 5. Le programme affiche alors 30.00.

Sur les relevés {-3, -7, -1, -12}, un calcul de maximum initialisé à 0 affiche 0. Quelle est la bonne réponse, et comment l'obtenir ?

Le maximum est -1. Aucun relevé ne dépasse 0, si bien que la valeur initiale n'est jamais remplacée.

Initialiser le maximum avec le premier élément, int maximum = releves[0];, garantit que le résultat est toujours une valeur présente dans le tableau.

Un enregistreur doit conserver les 200 dernières lectures de analogRead. Quel type choisir, et quelle part de la mémoire vive de l'Uno cela représente-t-il ?

analogRead rend une valeur de 0 à 1023, qui ne tient pas dans un byte : il faut un int. Deux cents int occupent 400 octets, soit environ 20 % des 2 Ko de mémoire vive.

Si seules des valeurs ramenées de 0 à 255 étaient nécessaires, un tableau de byte diviserait cette place par deux.

Un chenillard pilote les broches {2, 3, 4, 5, 6, 7}, chaque LED restant allumée 150 ms. Combien dure un cycle complet ?

Six LEDs de 150 ms chacune : 6×150=9006 \times 150 = 900 ms.

L'intérêt du tableau apparaît ici : passer de quatre à six LEDs n'a demandé que d'ajouter deux numéros entre les accolades. NB, calculé par sizeof, s'est mis à jour tout seul, et aucune boucle n'a été modifiée.

Depuis l'ajout d'une boucle de moyenne, un seuil d'alarme déclaré juste après le tableau des mesures change tout seul de valeur. Quelle est la cause la plus probable ?

Une écriture hors des bornes. La boucle écrit un élément de trop, par exemple avec i <= NB au lieu de i < NB, et l'adresse de cet élément fictif tombe sur les octets de la variable suivante, le seuil.

Le symptôme est typique : la variable corrompue n'apparaît dans aucune ligne fautive. La recherche commence donc par les boucles qui écrivent dans un tableau déclaré juste avant elle, et par chaque <= de leurs conditions.

Modifier le feu tricolore pour que l'orange clignote deux fois avant le rouge, sans toucher à loop().

Il suffit d'allonger les deux tableaux, une colonne par palier. La pause entre les deux éclairs est une phase comme les autres, jouée sur une broche où rien n'est branché :

Arduino
const int VERT = 11, ORANGE = 10, ROUGE = 9;
const int AUCUNE = 12; // broche libre : l'allumer n'éclaire rien
const int phases[] = {VERT, ORANGE, AUCUNE, ORANGE, ROUGE};
const unsigned long durees[] = {3000, 300, 300, 300, 3000};
const int NB_PHASES = sizeof(phases) / sizeof(phases[0]);
void setup() {
for (int i = 0; i < NB_PHASES; i++) {
pinMode(phases[i], OUTPUT);
}
}
void loop() {
for (int i = 0; i < NB_PHASES; i++) {
digitalWrite(phases[i], HIGH);
delay(durees[i]);
digitalWrite(phases[i], LOW);
}
}

loop() n'a pas changé d'une ligne, et c'est tout l'intérêt de la description par tableaux. Une pause de 0 ms n'aurait pas suffi : les deux éclairs d'orange se seraient fondus en un seul de 600 ms. L'astuce de la broche libre a ses limites, et un vrai feu préférerait un tableau d'états qui dit, pour chaque phase, quelles LEDs sont allumées ; le projet du carrefour le construit.

Chapitre révisé en octobre 2026 · Une erreur dans ce chapitre ?

Dans la bibliothèque d'exercices