Aller au contenu principal

Les tableaux

Ce que ce chapitre apporte

  • Déclarer un tableau, l'initialiser, et accéder à ses éléments par leur indice.
  • 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 à partir d'un tableau de numéros de broches.
  • Calculer une somme, une moyenne et un maximum en un seul parcours.
  • Estimer la mémoire occupée par un tableau.

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.

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, valeurs initiales indéterminées
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].

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.

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.
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.

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.

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.

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.

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 %

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.

Exercices type

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.

Vérification

Vérification rapideon peut se reprendre

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

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

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 ?

5.Un maximum initialisé à 0 donne 0 sur une série de relevés. Quelle peut en être la cause ?

La méthode

  1. Calculer la taille avec sizeof(t) / sizeof(t[0]), là où le tableau est déclaré, et la ranger dans une constante.
  2. Écrire tout parcours sous la forme for (int i = 0; i < NB; i++), et se méfier de chaque <=.
  3. Initialiser un maximum ou un minimum avec le premier élément, jamais avec une valeur arbitraire.
  4. Convertir en float avant de diviser quand une moyenne doit garder ses décimales.
  5. Chiffrer la mémoire occupée avant de déclarer un grand tableau, et choisir le plus petit type qui contient toutes les valeurs.
  6. Passer la taille en paramètre à toute fonction qui reçoit un tableau.

Synthèse

  • Un tableau range des valeurs du même type sous un nom ; ses indices vont de 0 à n − 1.
  • Sortir des bornes ne provoque aucune erreur en C : la lecture est quelconque, l'écriture corrompt une autre variable.
  • for (int i = 0; i < NB; i++) est la forme sûre de parcours.
  • sizeof(t) / sizeof(t[0]) donne le nombre d'éléments, uniquement là où le tableau est déclaré.
  • Un tableau de broches rend le programme indépendant du nombre de composants.
  • Un maximum part du premier élément ; une moyenne se calcule après conversion en float.
  • La place occupée vaut le nombre d'éléments multiplié par la taille du type : sur l'Uno, elle se compte en pourcentage de 2 Ko.

Mettre en pratique