cursus.

Cours 3 · Tableaux et chaînesLeçon 1 sur 2

Tableaux

4 h de lecture8 sections Version PDF

À la fin de cette leçon, vous saurez

Déclaration, indices et parcours, tableaux à deux dimensions, tableau passé en paramètre, et l'absence totale de vérification des bornes en C.

int T[5];T[10] = 42;

Ce programme compile sans avertissement et s'exécute sans erreur. Il écrit 42 vingt octets plus loin que la fin du tableau, sur ce qui s'y trouvait — une autre variable, l'adresse de retour de la fonction, n'importe quoi. Le programme continue, avec un état corrompu, et plantera peut-être mille lignes plus loin.

Le C ne vérifie jamais les bornes d'un tableau. Ce n'est pas un oubli de la norme, c'est une décision : vérifier coûterait une comparaison à chaque accès, et le C a été conçu pour écrire des systèmes d'exploitation. Le prix est une classe entière de vulnérabilités, et une bonne partie du chapitre 10.

Déclarer et parcourir

int notes[5] = {12, 15, 8, 17, 10};int vide[100] = {0};             /* toutes les cases à zéro */int deduit[] = {1, 2, 3};        /* taille déduite : 3 */

Trois faits à poser fermement.

Les indices vont de 0 à n−1. notes[0] est le premier, notes[4] le dernier, notes[5] n'existe pas. L'idiome for (int i = 0; i < n; i++) du chapitre 3 donne exactement les indices valides, et c'est la raison de l'employer systématiquement.

Les éléments sont contigus. Un int T[5] occupe vingt octets consécutifs, et l'adresse de T[i] vaut celle de T[0] plus i×4i \times 4. C'est l'accès en O(1)O(1) d'Algorithmique 1, et c'est aussi la localité spatiale du chapitre 7 d'architecture — parcourir un tableau dans l'ordre est ce qu'une machine fait le plus vite.

La taille est fixée à la compilation et ne peut pas changer. Un tableau dont la taille dépend de l'exécution demande l'allocation dynamique du chapitre 8.

Enfin, {0} initialise toutes les cases à zéro, alors qu'un tableau non initialisé contient n'importe quoi — même règle qu'au chapitre 2 pour les variables.

L'absence de vérification

Voici ce qui se passe réellement lors d'un débordement, et pourquoi c'est pire qu'une erreur.

L'expression T[i] est traduite en une adresse : début du tableau plus ii fois la taille d'un élément. Le compilateur émet ce calcul sans se demander si ii est valide — il ne le sait pas toujours, et le vérifier coûterait un test à chaque accès.

pile (adresses croissantes)┌──────────┬──────────┬──────────┬──────────┬──────────┬───────────┐│  T[0]    │  T[1]    │  T[2]    │  T[3]    │  T[4]    │  secret   │└──────────┴──────────┴──────────┴──────────┴──────────┴───────────┘                                              T[5] écrit ICI

Trois issues possibles, et la troisième est la plus dangereuse. L'adresse touchée peut être dans une page interdite : erreur de segmentation, le programme meurt, et c'est le meilleur cas — l'erreur est immédiate. Elle peut appartenir à une autre variable : corruption silencieuse, le programme continue avec des données fausses. Elle peut enfin être l'adresse de retour de la fonction, et un attaquant qui contrôle ce qui est écrit contrôle alors où le programme va sauter : c'est le débordement de tampon, sujet du chapitre 8 de cybersécurité.

La règle pratique est donc : la taille voyage avec le tableau. Toute fonction qui reçoit un tableau reçoit aussi son nombre d'éléments, et vérifie ses indices elle-même. Le langage ne le fera pas.

Tableaux à deux dimensions

int grille[3][4];                /* 3 lignes de 4 colonnes */grille[1][2] = 7;

La mémoire reste linéaire : le tableau est rangé ligne par ligne, et l'adresse de grille[i][j] se calcule comme

deˊbut+(i×4+j)×sizeof(int)\text{début} + (i \times 4 + j) \times \text{sizeof(int)}

Ce détail a une conséquence mesurable, celle de l'exercice du chapitre 7 d'architecture : parcourir par lignes suit l'ordre mémoire et exploite le cache ; parcourir par colonnes saute d'une ligne à l'autre à chaque accès et peut être plusieurs fois plus lent, pour exactement le même nombre d'opérations.

Un tableau en paramètre n'est pas copié

C'est l'exception à la règle du chapitre 4, et elle est majeure.

void modifier(int T[], int n) {    T[0] = 99;                   /* modifie le tableau de l'APPELANT */}

Un paramètre déclaré int T[] est en réalité un int * : ce qui est copié n'est pas le tableau mais l'adresse de son premier élément. La fonction travaille donc sur l'original.

Ce n'est pas une entorse au passage par valeur — l'adresse, elle, est bien copiée — mais une conséquence de la conversion tableau-pointeur que le chapitre 7 expliquera. Deux conséquences immédiates.

Passer un grand tableau ne coûte rien : huit octets, quelle que soit sa taille. C'est efficace, et c'est aussi pourquoi le C n'offre aucun moyen simple de passer un tableau en lecture seule — on écrit const int T[] pour l'exprimer.

sizeof ne fonctionne plus. Dans la fonction, sizeof(T) rend la taille d'un pointeur — 8 — et non celle du tableau. L'astuce sizeof(T)/sizeof(T[0]) fonctionne uniquement là où le tableau a été déclaré, et devient silencieusement fausse dans toute fonction. C'est précisément pourquoi il faut passer la taille en second paramètre.

Quiz · vérifiez votre compréhension Sans réponse

Une fonction reçoit int T[] et calcule sizeof(T)/sizeof(T[0]) pour connaître le nombre d'éléments. Que vaut ce calcul ?

Quiz · vérifiez votre compréhension Sans réponse

Pourquoi int T[5]; T[10] = 42; ne provoque-t-il ni erreur de compilation ni erreur d'exécution ?

À vous

L'exercice modélise un cadre de pile — un tableau et ses variables voisines dans une même zone mémoire — puis vous fait écrire hors bornes pour observer ce qui est écrasé.

Vous verrez les trois issues : la corruption d'une variable voisine, l'écrasement d'une valeur sensible, et l'accès à une adresse interdite. Vous écrirez ensuite la version défensive, où la fonction reçoit la taille et vérifie ses indices — la seule protection dont on dispose en C.

La dernière partie mesure le parcours d'une matrice par lignes et par colonnes, comme au chapitre 7 d'architecture, mais cette fois sur la représentation linéaire du C : c'est le même tableau, et le calcul d'adresse rend l'écart évident.

Exercice · JavaScript · à vous de jouer

Écrivez hors bornes et observez ce qui est écrasé, puis écrivez la version défensive.

En attente
// ── Un cadre de pile, avec ses variables voisines ─────────────────────────
// La zone est un tableau d'octets ; chaque variable occupe une plage.
function creerCadre() {
  const OCTETS = 40;
  const memoire = new Array(OCTETS).fill(0);
  const PLAN = {
    "T[0..4]":      { debut: 0,  taille: 20 },   // int T[5], 4 octets chacun
    "secret":       { debut: 20, taille: 4 },
    "adresseRetour":{ debut: 24, taille: 4 },
  };
  const INTERDIT = 36;   // au-delà : page non allouée

  function nomDe(octet) {
    for (const [nom, p] of Object.entries(PLAN)) {
      if (octet >= p.debut && octet < p.debut + p.taille) return nom;
    }
    return "zone non allouée";
  }

  return {
    plan: PLAN,
    // Écrit un int de 4 octets à l'indice i du tableau T. AUCUN contrôle.
    ecrireT(i, valeur) {
      const octet = 0 + i * 4;
      if (octet >= INTERDIT) return { erreur: "Segmentation fault (adresse " + octet + ")" };
      memoire[octet] = valeur;
      return { touche: nomDe(octet), octet };
    },
    lire(nom) {
      const p = PLAN[nom];
      return memoire[p.debut];
    },
    poser(nom, v) { memoire[PLAN[nom].debut] = v; },
  };
}

// ── Version défensive ─────────────────────────────────────────────────────
// La seule protection en C : la taille voyage avec le tableau.
function ecrireSur(cadre, i, n, valeur) {
  // ← à écrire : refuser si i est hors de [0, n[, sinon écrire
  return cadre.ecrireT(i, valeur);
}

// ── À VOUS ────────────────────────────────────────────────────────────────
// 1. Écrivez T[5], T[6] et T[12]. Que touche chacun ?
// 2. Écrivez ecrireSur() et vérifiez qu'elle refuse proprement.
// 3. Comparez le parcours d'une matrice 4x4 par lignes et par colonnes :
//    même nombre d'accès, quelle différence sur les adresses visitées ?

const c = creerCadre();
c.poser("secret", 1234);
c.poser("adresseRetour", 9999);
console.log("T[2] :", JSON.stringify(c.ecrireT(2, 42)));

Console de sortie
Le résultat s'affiche dans la console

En travaux pratiques

Travaux pratiques 5 · sur machine

Sortir du tableau, exprès

Constater que le C ne vérifie aucun indice, mesurer ce que cela permet, et adopter les deux outils qui rendent l'erreur visible.

3 h
Avant de commencer
  • Les TP 1 à 4
  • gcc avec -fsanitize=address
  1. 1. Écrire à côté

    Déclarez un tableau de cinq entiers et écrivez à l'indice 5, 6, puis 100. Compilez avec -Wall et exécutez. Notez ce qui plante et ce qui ne plante pas.

  2. 2. Écraser une variable choisie

    Déclarez une variable juste après le tableau, affichez son adresse et celle du tableau, puis modifiez-la en écrivant hors du tableau. Vous venez de faire un débordement dirigé.

  3. 3. L'outil qui voit

    Recompilez avec le détecteur d'adresses et relancez. Lisez le rapport : il donne le tableau, l'indice, et la ligne. Comparez à votre diagnostic sans outil.

  4. 4. sizeof, et où il ment

    Affichez la taille d'un tableau dans la fonction qui le déclare, puis dans une fonction à laquelle vous le passez. Expliquez l'écart.

  5. 5. Tableau à deux dimensions

    Créez une matrice, remplissez-la par lignes puis par colonnes, et chronométrez. Retrouvez le résultat du TP 7 d'Architecture, cette fois dans votre propre code.

  6. 6. Au fil rouge

    Ajoutez à journal un histogramme du nombre de lignes par heure, sur 24 cases. Faites-le d'abord sans contrôle d'indice, testez avec une heure invalide, puis protégez.

  7. 7. La bonne façon de passer un tableau

    Réécrivez toutes vos fonctions prenant un tableau pour qu'elles prennent aussi sa taille. Ajoutez const là où c'est possible, et vérifiez que le compilateur refuse une modification.

C'est réussi quand
  • Vous provoquez un débordement qui ne plante PAS, et vous savez pourquoi c'est le cas dangereux
  • Le détecteur d'adresses vous donne la ligne exacte que vous cherchiez à la main
  • Toutes vos fonctions reçoivent la taille du tableau qu'elles parcourent

Ce que la suite en fait

Le chapitre 6 applique tout cela au cas particulier le plus répandu : une chaîne de caractères est un tableau de char, avec une convention supplémentaire — un marqueur de fin. Les pièges de ce chapitre s'y aggravent, parce que la longueur n'est plus une donnée mais un résultat à calculer.

Le chapitre 7 expliquera enfin pourquoi un tableau passé en paramètre devient un pointeur, et le chapitre 8 lèvera la dernière limite : allouer un tableau dont la taille n'est connue qu'à l'exécution.

À retenir

Flashcards · 1 / 4Toucher pour retourner
Fin de la leçon

Vous avez parcouru les 8 sections.

Marquez-la terminée pour faire avancer votre parcours, ou revenez sur un point avant de passer à la suite.