Structures et fichiersDans le dialogue d’impression, choisissez « Enregistrer au format PDF » comme destination.
Retour

Programmation en C · C5 Données structurées et fichiers · Chapitre 1 · 6 h

Structures et fichiers

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 · 1 question

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

  • Parce que le compilateur ajoute un en-tête de 6 octets à chaque structureen-tête
  • À cause du BOURRAGE d'alignement : un int doit commencer à une adresse multiple de 4, et la taille totale doit être un multiple de l'alignement le plus contraignant. En rangeant les champs du plus grand au plus petit, on tombe à 8bourrage d'alignement
  • Parce que chaque char occupe en réalité 4 octets dans une structuretaille des char

Réponse : Le processeur exige — ou préfère fortement — qu'une donnée de k octets commence à une adresse multiple de k. Le compilateur insère donc du bourrage : trois octets perdus après le premier char pour que l'int démarre à l'adresse 4, puis trois de plus à la fin pour que la TAILLE TOTALE soit un multiple de 4, sans quoi le deuxième élément d'un tableau de ces structures serait mal aligné. Ranger les champs du plus grand au plus petit — int, puis les deux char — supprime le premier bourrage et ne laisse que deux octets de queue : 8 au lieu de 12, un tiers d'économie sans rien changer d'autre. Sur un million d'enregistrements, cela fait quatre mégaoctets et autant de défauts de cache en moins. C'est aussi pourquoi l'écriture binaire n'est pas portable : la disposition dépend du compilateur et de la machine.

Quiz · 1 question

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

  • Parce que fscanf lit toujours une valeur de trop, quelle que soit la conditionfscanf lit trop
  • Parce que feof n'indique pas qu'on VA atteindre la fin mais qu'une lecture l'a DÉJÀ atteinte : après la dernière valeur, feof est encore faux, on entre une fois de plus, fscanf échoue et laisse n inchangéfeof réagit trop tard
  • Parce que le fichier contient un retour à la ligne final qui compte comme une valeurretour à la ligne

Réponse : feof est un CONSTAT, pas une prédiction : l'indicateur ne se lève que lorsqu'une lecture a effectivement buté sur la fin. Déroulé : la dernière valeur est lue avec succès, feof reste donc faux, la condition est vraie, on entre dans le corps une fois de trop. Là, fscanf échoue — elle rend 0 au lieu de 1 — et ne touche pas à n, qui garde la valeur précédente : elle est affichée une seconde fois. La forme correcte teste le RETOUR DE LA LECTURE, qui est la seule information fiable : while (fscanf(f, ...) == 1). Même règle partout : fgets rend NULL en fin de fichier, fread rend le nombre d'éléments réellement lus. La règle générale : on ne demande jamais « suis-je à la fin ? », on tente la lecture et on regarde si elle a réussi.

À 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 de code

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

Point de départ

// ── 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"]]);

Solution

const TAILLE = { char: 1, short: 2, int: 4, double: 8, pointeur: 8 };

function alignerSur(x, k) { return Math.ceil(x / k) * 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);
    // Le champ doit commencer à un multiple de sa taille : on avance
    // jusque-là, et l'écart est du bourrage perdu.
    const aligne = alignerSur(decalage, k);
    plan.push({ nom, type, decalage: aligne, taille: k, bourrageAvant: aligne - decalage });
    decalage = aligne + k;
  }
  // La taille totale doit être un multiple de l'alignement le plus
  // contraignant, sans quoi le DEUXIÈME élément d'un tableau serait mal aligné.
  const taille = alignerSur(decalage, alignementMax);
  return { plan, taille, alignementMax, bourrageFinal: taille - decalage };
}

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

console.log("— 1. bourrage —");
montrer("{ char, int, char }", [["capteur", "char"], ["valeur", "int"], ["unite", "char"]]);
montrer("{ int, char, char }", [["valeur", "int"], ["capteur", "char"], ["unite", "char"]]);
montrer("{ char, double, int }", [["a", "char"], ["b", "double"], ["c", "int"]]);
montrer("{ double, int, char }", [["b", "double"], ["c", "int"], ["a", "char"]]);
console.log("   Mêmes champs, même contenu : seul l'ORDRE change.");

console.log("");
console.log("— 2. le piège de feof —");
function creerFichier(lignes) {
  let position = 0;
  return {
    lire() { return position < lignes.length ? lignes[position++] : null; },
    finAtteinte: () => position >= lignes.length,
    rembobiner() { position = 0; },
  };
}

function lireAvecFeof(f) {
  const sortie = [];
  let n = "?";
  while (!f.finAtteinte()) {
    const lu = f.lire();
    if (lu !== null) n = lu;
    sortie.push(n);
  }
  return sortie;
}

function lireCorrectement(f) {
  const sortie = [];
  // On tente la lecture, et on regarde si elle a réussi. On ne demande
  // jamais « suis-je à la fin ? » — la seule information fiable est le
  // retour de la lecture elle-même.
  let n;
  while ((n = f.lire()) !== null) sortie.push(n);
  return sortie;
}

const f = creerFichier([10, 20, 30]);
console.log("   avec feof        : " + lireAvecFeof(f).join(" "));
f.rembobiner();
console.log("   en testant la lecture : " + lireCorrectement(f).join(" "));
console.log("   Le fichier contient trois valeurs. La première boucle en produit");
console.log("   quatre — pas parce qu'elle lit une valeur de trop, mais parce que");
console.log("   la lecture ratée laisse la variable INCHANGÉE.");

console.log("");
console.log("— 3. le binaire n'est pas portable —");
// « Écrire » un enregistrement : on pose chaque champ à son décalage.
function ecrireBinaire(valeurs, champs) {
  const d = disposer(champs);
  const octets = new Array(d.taille).fill(0);
  d.plan.forEach((c, i) => { octets[c.decalage] = valeurs[i]; });
  return { octets, plan: d.plan };
}
function relireBinaire(octets, champs) {
  const d = disposer(champs);
  return d.plan.map((c) => octets[c.decalage]);
}

const CHAMPS_A = [["capteur", "char"], ["valeur", "int"], ["unite", "char"]];
const CHAMPS_B = [["valeur", "int"], ["capteur", "char"], ["unite", "char"]];
const ecrit = ecrireBinaire([7, 1234, 3], CHAMPS_A);
console.log("   écrit avec { char, int, char } : [" + ecrit.octets.join(",") + "]");
console.log("   relu   avec { char, int, char } : [" + relireBinaire(ecrit.octets, CHAMPS_A).join(",") + "]   correct");
console.log("   relu   avec { int, char, char } : [" + relireBinaire(ecrit.octets, CHAMPS_B).join(",") + "]   INCOHÉRENT");
console.log("   Un compilateur, une option, une architecture différente suffisent");
console.log("   à produire cet écart. Le texte, lui, se relit partout.");

En travaux pratiques

Travaux pratiques 9 · 4 h

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

Avant de commencer

  • Les TP 1 à 8
  • Un fichier de journaux réel d'au moins 100 000 lignes

Énoncé

  1. Définir la structureDé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. Le remplissageExpliquez l'écart mesuré, puis réorganisez les champs du plus grand au plus petit et remesurez. Indice : Un entier de 4 octets ne peut pas commencer à n'importe quelle adresse.
  3. Lire et remplirBranchez le découpage du TP 6 pour remplir un tableau dynamique de structures à partir du fichier. Comptez les lignes rejetées.
  4. Le rapportProduisez : 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. 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. 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. Le pointeur dans la structureRemplacez 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. FinaliserAjoutez 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

Correction

Le remplissage
struct Ligne {          taille des champs   ordre en mémoire
  char  code;            1                   0
  long  taille;          8                   8   ← 7 octets perdus
  char  methode;         1                  16
  int   heure;           4                  20   ← 3 octets perdus
};                                      sizeof = 32, somme = 14

/* réorganisé, du plus grand au plus petit */
struct Ligne { long taille; int heure; char code, methode; };
                                      sizeof = 16

Le processeur lit la mémoire par mots alignés : un long doit commencer à une adresse multiple de 8. Le compilateur insère donc du remplissage. Ordonner les champs du plus grand au plus petit réduit la structure de moitié — sur un million de lignes, 16 Mo économisés, et surtout deux fois moins de lignes de cache, ce qui rejoint directement le TP 7 d'Architecture.

Le noyau du fil rougeanalyse.c
typedef struct { long taille; int heure, code; 
               char adresse[46], chemin[256]; } Ligne;

int analyser_fichier(const char *chemin, Ligne **out, size_t *n) {
  FILE *f = fopen(chemin, "r");
  if (!f) { perror(chemin); return -1; }

  Ligne *t = NULL; size_t cap = 0; *n = 0;
  char ligne[1024];
  size_t rejetees = 0;

  while (fgets(ligne, sizeof ligne, f)) {
      Ligne l;
      if (decouper_ligne(ligne, &l) < 0) { rejetees++; continue; }
      if (*n == cap) { /* doublement, TP 8 */ }
      t[(*n)++] = l;
  }
  fclose(f);
  if (rejetees) fprintf(stderr, "%zu lignes ignorées\n", rejetees);
  *out = t;
  return 0;
}

Les lignes rejetées sont comptées et signalées, jamais tues : un analyseur qui ignore silencieusement 30 % de son entrée produit un rapport faux dont personne ne se méfie. Notez aussi la copie de structure — l'affectation copie tous les champs, ce qui est licite et lisible, mais ne copie PAS ce que pointeraient d'éventuels pointeurs.

Texte contre binaire
acces.log       : 128 Mo, analyse en 2,4 s
acces.bin       :  17 Mo, lecture en 0,08 s   (×30)

fwrite(t, sizeof(Ligne), n, f);       /* écriture d'un bloc */
fread (t, sizeof(Ligne), n, f);       /* relecture */

Trente fois plus rapide, parce qu'il n'y a plus rien à analyser : les octets du fichier sont l'image exacte de la mémoire. C'est tout l'intérêt du binaire, et toute sa fragilité — le fichier ne se lit qu'avec la même structure, le même compilateur et la même architecture.

Pourquoi le binaire brut ne se partage pas
programme A : struct { long taille; int heure; }   sizeof 16
programme B : struct { int heure; long taille; }   sizeof 16
            → mêmes octets, champs INTERVERTIS, aucune erreur signalée

autres divergences : boutisme (petit/gros), taille de long
(4 sur Windows 64, 8 sur Linux 64), alignement selon l'ABI

/* ce que fait un vrai format */
- un nombre magique et un numéro de VERSION en en-tête
- des tailles explicites (int32_t, int64_t)
- un boutisme fixé, converti à la lecture
- ou un format autodescriptif : JSON, Protobuf, CBOR

Le binaire brut est parfait pour un cache local que l'on peut regénérer, et inutilisable pour un échange. La ligne de partage est simple : si le fichier peut être relu par un autre programme, une autre machine ou une version ultérieure, il lui faut un format versionné.

Le pointeur écrit dans un fichier
typedef struct { char *chemin; int code; } Ligne;   /* pointeur ! */

fwrite(&l, sizeof l, 1, f);   /* écrit l'ADRESSE 0x55d3e8a04010 */

à la relecture, dans un autre processus :
l.chemin vaut 0x55d3e8a04010, adresse qui n'a AUCUN sens ici
→ texte aberrant, ou Segmentation fault

Une adresse n'a de sens que dans l'espace d'adressage qui l'a produite — c'est exactement le TP 6 de Systèmes. Sérialiser une structure contenant des pointeurs demande de les remplacer par le CONTENU pointé, précédé de sa longueur. C'est ce que fait toute bibliothèque de sérialisation, et c'est pourquoi cela ne peut pas être automatique en C : le langage ne sait pas ce qu'un pointeur désigne, ni combien d'éléments.

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 · 5 cartes

Quand faut-il garder l'étiquette d'une structure malgré le typedef ?
Quand la structure SE RÉFÉRENCE ELLE-MÊME : le typedef n'existe pas encore au moment où le champ est déclaré, donc on écrit « struct Cellule *suivant » et non « Cellule *suivant ». C'est la forme de toutes les structures chaînées d'Algorithmique 2, et elle est à connaître par cœur.
Une structure est-elle copiée quand on l'affecte ou qu'on la passe en paramètre ?
OUI — contrairement à un tableau. L'affectation recopie tous les champs, et le passage en paramètre aussi : c'est le comportement du chapitre 4, sans l'exception du chapitre 5. Sur une grosse structure on passe donc un POINTEUR, déclaré const si l'on ne modifie pas. Attention : 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 un double free.
Pourquoi la taille d'une structure n'est-elle pas la somme de ses champs ?
À cause du BOURRAGE d'alignement : une donnée de k octets doit commencer à une adresse multiple de k, et la taille totale doit être un multiple de l'alignement le plus contraignant, pour que le deuxième élément d'un tableau soit aligné. Règle pratique : ranger les champs du PLUS GRAND AU PLUS PETIT — { char, int, char } fait 12 octets, { int, char, char } en fait 8. Conséquence : la disposition dépend du compilateur et de la machine, d'où la non-portabilité du binaire.
Quelle est la différence entre écriture texte et écriture binaire d'un fichier ?
TEXTE (fprintf, fscanf, fgets) : le fichier reste lisible, éditable et PORTABLE, au prix d'une conversion à chaque valeur. BINAIRE (fwrite, fread) : les octets sont recopiés tels quels, donc compact et rapide — un tableau de structures en un appel — mais NON PORTABLE, la taille des types, l'ordre des octets et le bourrage variant d'une machine à l'autre. Et le mode « w » VIDE le fichier à l'ouverture, avant toute écriture.
Pourquoi while (!feof(f)) est-il faux, et que faut-il écrire ?
Parce que feof est un CONSTAT et non une prédiction : l'indicateur ne se lève qu'après qu'une lecture a buté sur la fin. Après la dernière valeur, feof est encore faux : on entre une fois de trop, la lecture échoue sans toucher à la variable, et la dernière valeur est traitée deux fois. Il faut tester le RETOUR DE LA LECTURE : while (fscanf(f, ...) == 1). Même règle pour fgets, qui rend NULL, et fread, qui rend le nombre d'éléments lus.