Fichiers binaires et accès direct#
Les fichiers texte (.txt, .csv) sont parfaitement lisibles par un être humain mais présentent deux inconvénients majeurs en ingénierie logicielle :
1. Perte de performance : convertir des nombres en chaînes de caractères (et inversement avec sprintf ou fscanf) consomme du temps processeur.
2. Accès séquentiel obligatoire : pour lire la \(1\,000^{\text{e}}\) ligne d'un fichier texte, vous êtes contraint de lire et analyser toutes les 999 lignes précédentes.
Les fichiers binaires éliminent ces limitations. Ils permettent de transférer directement des blocs de mémoire vive vers le disque dur sans aucune conversion, et d'accéder instantanément à n'importe quel enregistrement grâce à l'accès direct (random access).
1. Fichier texte vs Fichier binaire#
Considérons l'entier int valeur = 12345678; :
| Critère | Fichier texte | Fichier binaire |
|---|---|---|
| Représentation sur disque | 8 caractères ASCII : '1', '2', '3', '4', '5', '6', '7', '8' |
4 octets bruts (codage binaire 32 bits Little-Endian) |
| Taille occupée | 8 octets | 4 octets (fixe quel que soit le nombre) |
| Vitesse d'écriture/lecture | Lente (conversion binaire \(\leftrightarrow\) ASCII) | Ultra-rapide (copie brute octet par octet) |
| Lisibilité | Lisible dans un éditeur (Notepad, VS Code) | Illisible directement (nécessite un éditeur hexadécimal) |
Pour ouvrir un fichier binaire, il suffit d'ajouter la lettre b au mode d'ouverture : "rb", "wb", "ab", "r+b".
2. Écriture binaire par blocs : fwrite()#
La fonction fwrite() copie directement une zone mémoire continue vers le flux binaire :
ptr: pointeur vers les données à enregistrer.taille_element: taille en octets d'une unité (souvent calculée avecsizeof(...)).nb_elements: nombre d'éléments à écrire.flux: pointeur vers le fichier ouvert en mode binaire ("wb"ou"ab").- Valeur de retour : le nombre d'éléments effectivement écrits (doit être égal à
nb_elementsen l'absence d'erreur).
// Exemple : Écriture d'un tableau d'entiers
int nombres[5] = {10, 20, 30, 40, 50};
FILE *f = fopen("donnees.bin", "wb");
if (f != NULL)
{
size_t ecrits = fwrite(nombres, sizeof(int), 5, f);
if (ecrits < 5)
{
perror("Erreur d'ecriture partielle");
}
fclose(f);
}
3. Lecture binaire par blocs : fread()#
Symétriquement, fread() lit des octets bruts depuis le fichier et les injecte directement dans une zone mémoire :
int copie[5];
FILE *f = fopen("donnees.bin", "rb");
if (f != NULL)
{
size_t lus = fread(copie, sizeof(int), 5, f);
printf("%zu entiers lus avec succes.\n", lus);
fclose(f);
}
4. Navigation et accès direct : fseek, ftell et rewind#
Contrairement à un fichier texte, un fichier binaire stockant des structures de taille fixe (sizeof(MaStructure)) permet de sauter directement à n'importe quel enregistrement sans lire ce qui précède !
flowchart LR
A[Enregistrement 0 : sizeof] --> B[Enregistrement 1 : sizeof]
B --> C[Enregistrement 2 : sizeof]
C --> D[Enregistrement 3 : sizeof]
Note over C: fseek saut direct vers i * sizeof
La fonction fseek()#
Déplace le curseur interne du fichier vers un décalage (offset) en octets :
L'argument origine accepte trois constantes symboliques :
- SEEK_SET : déplacement compté depuis le début du fichier.
- SEEK_CUR : déplacement compté depuis la position courante.
- SEEK_END : déplacement compté depuis la fin du fichier.
La fonction ftell()#
Renvoie la position courante du curseur en nombre d'octets depuis le début du fichier :
Déterminer la taille exacte d'un fichier sur disque#
En combinant fseek et ftell, vous pouvez calculer instantanément la taille de n'importe quel fichier :
long ObtenirTailleFichier(const char *chemin)
{
FILE *f = fopen(chemin, "rb");
if (f == NULL) return -1;
// Se déplacer tout à la fin
fseek(f, 0, SEEK_END);
long taille = ftell(f);
// Fermeture
fclose(f);
return taille;
}
5. Étude de cas complète : Mini-banque avec mise à jour in-place#
Imaginons une application bancaire où nous voulons mettre à jour le solde d'un compte précis sur disque sans avoir à réécrire l'ensemble de la base de données.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct
{
int numero_compte;
char titulaire[40];
double solde;
} CompteBancaire;
// Initialise le fichier binaire avec quelques comptes
void InitialiserComptes(const char *chemin)
{
CompteBancaire comptes[3] = {
{1001, "Alice Martin", 2500.00},
{1002, "Bob Diakite", 1450.50},
{1003, "Carla Rossi", 8900.25}
};
FILE *f = fopen(chemin, "wb");
if (!f) return;
fwrite(comptes, sizeof(CompteBancaire), 3, f);
fclose(f);
printf("Base binaire creee avec succes.\n");
}
// Affiche tous les enregistrements du fichier
void AfficherTousLesComptes(const char *chemin)
{
FILE *f = fopen(chemin, "rb");
if (!f) return;
CompteBancaire c;
int index = 0;
printf("\n--- ETAT DE LA BASE DE DONNEES ---\n");
while (fread(&c, sizeof(CompteBancaire), 1, f) == 1)
{
printf("Index [%d] | Compte #%d | %-15s | Solde : %10.2f EUR\n",
index++, c.numero_compte, c.titulaire, c.solde);
}
printf("----------------------------------\n\n");
fclose(f);
}
// Modifie directement le solde d'un compte sans recharger tout le fichier
int CrediterCompte(const char *chemin, int index, double montant)
{
// Mode "r+b" : lecture et écriture simultanées sans écraser le fichier
FILE *f = fopen(chemin, "r+b");
if (f == NULL)
{
perror("Impossible d'ouvrir le fichier en r+b");
return 0;
}
// 1. Calcul du décalage mémoire vers l'enregistrement ciblé
long offset = (long)index * sizeof(CompteBancaire);
if (fseek(f, offset, SEEK_SET) != 0)
{
fclose(f);
return 0;
}
// 2. Lecture de l'enregistrement actuel
CompteBancaire compte;
if (fread(&compte, sizeof(CompteBancaire), 1, f) != 1)
{
printf("Erreur : Compte a l'index %d inexistant.\n", index);
fclose(f);
return 0;
}
// 3. Modification en mémoire
compte.solde += montant;
// 4. Repositionnement du curseur car la lecture l'a avancé d'un cran !
fseek(f, offset, SEEK_SET);
// 5. Réécriture in-place de la structure modifiée
fwrite(&compte, sizeof(CompteBancaire), 1, f);
fclose(f);
printf(">> Compte #%d credite de %.2f EUR (Nouveau solde : %.2f EUR)\n",
compte.numero_compte, montant, compte.solde);
return 1;
}
int main(void)
{
const char *chemin = "banque.dat";
// Étape 1 : Création de la base
InitialiserComptes(chemin);
AfficherTousLesComptes(chemin);
// Étape 2 : Modification directe par fseek de l'index 1 (Bob Diakite)
CrediterCompte(chemin, 1, 500.00);
// Étape 3 : Vérification de la persistance
AfficherTousLesComptes(chemin);
return EXIT_SUCCESS;
}
6. Bonnes pratiques de la sérialisation binaire#
[!CAUTION] Ne sérialisez jamais directement des structures contenant des pointeurs ! Si votre structure contient un champ
char *adresse,fwrite(&structure, ...)enregistrera la valeur de l'adresse mémoire (ex:0x7ffee4bc) et non les caractères pointés. Lors d'un redémarrage ultérieur ou sur une autre machine, cette adresse pointera vers une zone mémoire invalide, provoquant un plantage garanti. Pour les données persistantes, utilisez toujours des tableaux de taille fixe (ex:char adresse[100];).
7. Défi d'application#
Développez une fonction RechercherParNumero(const char *chemin, int numero_compte) qui calcule le nombre total d'enregistrements à l'aide de ftell et réalise une recherche dichotomique ou séquentielle dans le fichier binaire pour afficher les détails du compte recherché.
Commentaires