Cours 5 · Données structurées et fichiersLeçon 1 sur 1
Structures et fichiers
6 h de lecture8 sections Version PDF
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 octets commence à une adresse multiple de . 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 octetsTrois 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.
| Mode | Effet |
|---|---|
"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 b | mode 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.
Pourquoi sizeof d'une structure { char ; int ; char } vaut-il 12 et non 6, et comment descendre à 8 ?
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.
Calculez le bourrage d'une structure, puis faites tomber le piège de feof et la non-portabilité du binaire.
// ── 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"]]);
En travaux pratiques
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é.
- Les TP 1 à 8
- Un fichier de journaux réel d'au moins 100 000 lignes
- 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. Le remplissage
Expliquez l'écart mesuré, puis réorganisez les champs du plus grand au plus petit et remesurez.
- 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. 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. 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 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. 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.
- 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
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.