cursus.

Cours 5 · Données structurées et fichiersLeçon 1 sur 1

Structures et fichiers

6 h de lecture8 sections Version PDF

À la fin de cette leçon, vous saurez

struct et typedef, tableaux de structures, structures imbriquées et pointeurs de structure ; fichiers texte et binaires, lecture, écriture, fin de fichier.

struct Mesure {    char   capteur;      /* 1 octet  */    int    valeur;       /* 4 octets */    char   unite;        /* 1 octet  */};printf("%zu\n", sizeof(struct Mesure));    /* affiche 12, pas 6 */

Six octets de données, douze octets occupés. La moitié de la structure est du vide — et il suffit de réordonner les champs pour retomber à huit. Ce chapitre explique pourquoi, puis donne aux données ce qui leur manque encore : la capacité de survivre à la fin du programme.

Se donner ses propres types

Une structure regroupe des champs de types différents sous un seul nom.

typedef struct {    char nom[32];    int  age;    double note;} Etudiant; Etudiant e = {"Ana", 20, 15.5};printf("%s a %d ans\n", e.nom, e.age);

Le typedef évite d'écrire struct Etudiant à chaque emploi. Il y a une exception à connaître : une structure qui se référence elle-même doit garder son étiquette, parce que le typedef n'existe pas encore au moment où le champ est déclaré.

typedef struct Cellule {    int valeur;    struct Cellule *suivant;     /* « struct Cellule », pas « Cellule » */} Cellule;

C'est la cellule du chapitre 8, et c'est la forme qu'il faut connaître par cœur : elle revient dans toutes les structures chaînées d'Algorithmique 2.

Un point qui distingue les structures des tableaux, et qui surprend : une structure est copiée. L'affectation a = b recopie tous les champs, et le passage en paramètre aussi — ce qui est le comportement du chapitre 4, sans l'exception du chapitre 5. Sur une structure volumineuse, on passe donc un pointeur, déclaré const si l'on ne modifie pas.

Attention toutefois : la copie est superficielle. Un champ char * est copié en tant qu'adresse, donc les deux structures désignent la même chaîne — et un free des deux côtés donne le double free du chapitre 8.

Alignement et bourrage

Voici l'explication des douze octets.

Le processeur lit la mémoire par mots, et il exige — ou préfère fortement — qu'une donnée de kk octets commence à une adresse multiple de kk. Le compilateur insère donc du bourrage (padding) entre les champs pour respecter cet alignement.

struct Mesure { char capteur; int valeur; char unite; }; octet :  0     1  2  3     4  5  6  7     8     9 10 11       ┌─────┬───────────┬─────────────┬─────┬──────────┐       │ cap │  bourrage │   valeur    │unite│ bourrage │       └─────┴───────────┴─────────────┴─────┴──────────┘         1        3            4          1       3        = 12 octets

Trois octets sont perdus avant valeur, pour que l'entier commence à l'adresse 4. Trois de plus à la fin, pour que la taille totale soit un multiple de l'alignement le plus contraignant — sans quoi le deuxième élément d'un tableau de Mesure serait mal aligné.

D'où la règle pratique : ranger les champs du plus grand au plus petit.

struct Mesure { int valeur; char capteur; char unite; };   /* 8 octets */

Un tiers d'économie, sans rien changer d'autre. Sur un tableau d'un million d'enregistrements, c'est quatre mégaoctets — et autant de défauts de cache en moins, au sens du chapitre 7 d'architecture.

Conséquence à retenir pour la suite du chapitre : la taille d'une structure n'est pas la somme de ses champs, et sa disposition exacte dépend du compilateur et de la machine. C'est précisément ce qui rend l'écriture binaire non portable.

Fichiers

Un fichier se manipule par un flux, désigné par un FILE *.

FILE *f = fopen("donnees.txt", "r");if (f == NULL) { perror("donnees.txt"); return 1; }/* … */fclose(f);

Le test de NULL n'est pas optionnel : le fichier peut être absent, ou les droits du chapitre 7 du cours de systèmes peuvent l'interdire. perror affiche le message correspondant à l'erreur réelle, ce qui évite de deviner.

ModeEffet
"r"lecture ; échoue si le fichier n'existe pas
"w"écriture ; crée ou VIDE le fichier
"a"ajout en fin ; crée si besoin
"r+", "w+"lecture et écriture
suffixe bmode binaire

Le mode "w" mérite un avertissement : il efface le contenu existant à l'ouverture, avant même la première écriture. Une faute de frappe dans un nom de fichier détruit son contenu.

Deux familles de lecture et d'écriture.

En texte : fprintf et fscanf — les mêmes formats qu'au chapitre 2 —, fgets pour lire une ligne entière avec une taille maximale, et fputs. Le fichier reste lisible et éditable, et il est portable.

En binaire : fwrite et fread recopient les octets tels quels. C'est compact et rapide — écrire un tableau de structures tient en un appel — mais non portable : la taille des types, l'ordre des octets et le bourrage varient d'une machine à l'autre. Un fichier binaire écrit sur une machine peut être illisible sur une autre.

Le piège de la fin de fichier

C'est la faute la plus fréquente du chapitre, et elle produit une ligne en trop.

while (!feof(f)) {                 /* FAUX */    fscanf(f, "%d", &n);    printf("%d\n", n);}

feof ne prédit pas la fin : elle indique qu'une lecture a déjà échoué en l'atteignant. Au dernier tour, fscanf lit la dernière valeur sans que feof bascule ; la condition reste vraie, on entre une fois de plus, fscanf échoue et laisse n inchangé — donc la dernière valeur est affichée deux fois.

La forme correcte teste le retour de la lecture, qui est la seule information fiable :

while (fscanf(f, "%d", &n) == 1) {    printf("%d\n", n);}

Même règle pour fgets, qui rend NULL en fin de fichier, et pour fread, qui rend le nombre d'éléments effectivement lus.

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

Pourquoi sizeof d'une structure { char ; int ; char } vaut-il 12 et non 6, et comment descendre à 8 ?

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

Une boucle while (!feof(f)) qui appelle fscanf puis affiche la valeur lue affiche la dernière valeur deux fois. Pourquoi ?

À vous

L'exercice a deux volets.

D'abord la disposition mémoire : vous écrivez le calcul du bourrage — alignement de chaque champ, puis alignement de la structure entière — et vous vérifiez sur trois structures que réordonner les champs change la taille. Vous retrouverez les 12 et les 8 du cours.

Ensuite un mini-format de fichier : écriture d'un tableau d'enregistrements en texte, relecture, et comparaison. Le squelette contient la boucle fautive avec feof ; vous constaterez la ligne dupliquée, puis vous écrirez la version correcte. Une dernière partie écrit les mêmes données en « binaire » — c'est-à-dire en recopiant la disposition mémoire calculée plus haut — et vous verrez ce qui se passe quand on les relit avec un autre ordre de champs : exactement ce qu'un changement de machine provoque.

Exercice · JavaScript · à vous de jouer

Calculez le bourrage d'une structure, puis faites tomber le piège de feof et la non-portabilité du binaire.

En attente
// ── 1. Disposition mémoire et bourrage ────────────────────────────────────
const TAILLE = { char: 1, short: 2, int: 4, double: 8, pointeur: 8 };

// Un champ de k octets doit commencer à une adresse multiple de k.
function disposer(champs) {
  let decalage = 0, alignementMax = 1;
  const plan = [];
  for (const [nom, type] of champs) {
    const k = TAILLE[type];
    alignementMax = Math.max(alignementMax, k);
    // ← à écrire : avancer decalage jusqu'au prochain multiple de k,
    //   en notant le bourrage inséré
    plan.push({ nom, type, decalage, taille: k, bourrageAvant: 0 });
    decalage += k;
  }
  // ← et à la fin : la TAILLE TOTALE doit être un multiple de alignementMax
  return { plan, taille: decalage, alignementMax };
}

function montrer(nom, champs) {
  const d = disposer(champs);
  console.log("   " + nom + " -> sizeof = " + d.taille +
              " (somme des champs : " + champs.reduce((s, [, t]) => s + TAILLE[t], 0) + ")");
  for (const c of d.plan) {
    console.log("      offset " + String(c.decalage).padStart(2) + "  " + c.nom.padEnd(8) +
      c.type.padEnd(9) + (c.bourrageAvant ? "  (+" + c.bourrageAvant + " de bourrage avant)" : ""));
  }
}

// ── 2. Un mini-fichier texte ──────────────────────────────────────────────
function creerFichier(lignes) {
  let position = 0;
  return {
    // Rend la valeur lue, ou null en fin de fichier — comme fscanf rend 1 ou 0.
    lire() { return position < lignes.length ? lignes[position++] : null; },
    finAtteinte: () => position >= lignes.length,
    rembobiner() { position = 0; },
  };
}

// La boucle fautive du cours.
function lireAvecFeof(f) {
  const sortie = [];
  let n = "?";
  while (!f.finAtteinte()) {
    const lu = f.lire();
    if (lu !== null) n = lu;      // en C, un fscanf en échec NE TOUCHE PAS n
    sortie.push(n);
  }
  return sortie;
}

function lireCorrectement(f) {
  const sortie = [];
  // ← à écrire : tester le RETOUR de la lecture, pas la fin de fichier
  return sortie;
}

// ── À VOUS ────────────────────────────────────────────────────────────────
// 1. Écrivez disposer() et retrouvez les 12 puis les 8 octets du cours.
// 2. Écrivez lireCorrectement() et comparez les deux sorties.
// 3. Écrivez un enregistrement « en binaire » avec une disposition, relisez-le
//    avec une AUTRE disposition : c'est ce qu'un changement de machine fait.

montrer("Mesure  { char, int, char }", [["capteur", "char"], ["valeur", "int"], ["unite", "char"]]);

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

En travaux pratiques

Travaux pratiques 9 · sur machine

Le fil rouge complet

Assembler tout le semestre : lire un fichier réel, le ranger dans des structures, produire un rapport, et sauvegarder en binaire — puis constater ce que le format binaire coûte en portabilité.

4 h
Avant de commencer
  • Les TP 1 à 8
  • Un fichier de journaux réel d'au moins 100 000 lignes
  1. 1. Définir la structure

    Définissez une structure représentant une ligne de journal : adresse, horodatage, méthode, chemin, code, taille. Affichez sa taille avec sizeof et comparez à la somme des champs.

  2. 2. Le remplissage

    Expliquez l'écart mesuré, puis réorganisez les champs du plus grand au plus petit et remesurez.

  3. 3. Lire et remplir

    Branchez le découpage du TP 6 pour remplir un tableau dynamique de structures à partir du fichier. Comptez les lignes rejetées.

  4. 4. Le rapport

    Produisez : nombre de requêtes, taux d'erreur, dix adresses les plus actives, répartition horaire. Comparez vos résultats à ceux obtenus au TP 2 de Systèmes avec des commandes shell.

  5. 5. Sauvegarder en binaire

    Écrivez le tableau de structures dans un fichier avec fwrite, puis relisez-le avec fread. Comparez la taille et le temps de lecture avec le fichier texte d'origine.

  6. 6. Le piège de la portabilité

    Relisez votre fichier binaire avec un programme compilé avec une déclaration de structure légèrement différente. Constatez, et dites comment un vrai format s'en protège.

  7. 7. Le pointeur dans la structure

    Remplacez un champ de taille fixe par un pointeur sur une chaîne allouée. Écrivez la structure en binaire, relisez-la dans un autre programme, et expliquez le désastre.

  8. 8. Finaliser

    Ajoutez les options de ligne de commande, les messages d'erreur sur la sortie d'erreur, les codes de retour, et le manuel d'usage. Passez valgrind une dernière fois.

C'est réussi quand
  • sizeof de votre structure est plus petit après réorganisation des champs
  • Vos chiffres coïncident exactement avec ceux de la chaîne shell du TP 2 de Systèmes
  • La lecture binaire est plusieurs fois plus rapide que l'analyse du texte
  • Vous savez expliquer pourquoi un pointeur écrit dans un fichier n'a aucun sens

Ce que la suite en fait

Le chapitre 10 clôt le cours par l'outillage, et les deux volets de ce chapitre y reviennent. gdb sait afficher une structure champ par champ — y compris le bourrage — ce qui est la façon la plus rapide de comprendre une disposition mémoire. Et les erreurs de fichier sont exactement le genre de faute que les assertions et un jeu de tests attrapent avant la mise en service.

À retenir

Flashcards · 1 / 5Toucher 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.