Accès rapide :
Les trois flux standards
Ouvrir et fermer un fichier
Lire un fichier texte
Ecrire dans un fichier
Texte ou binaire
Ecrire des données binaires
Relire des données binaires
Enregistrer des structures
Accès direct dans un fichier binaire
La librairie <stdio.h> fournit une abstraction importante : le flux. Un flux peut représenter un fichier,
mais aussi l'entrée standard, la sortie standard ou la sortie d'erreur de votre programme. En C, on manipule ces flux au
travers du type FILE *.
Au démarrage, un programme possède trois flux déjà ouverts : stdin pour les entrées, stdout pour les
sorties normales et stderr pour les messages d'erreur. Cette séparation permet, par exemple, de rediriger les résultats
dans un fichier tout en conservant les erreurs à l'écran.
Pour lire ou écrire dans un fichier, il faut d'abord l'ouvrir avec fopen.
Cette fonction peut échouer : chemin incorrect, droits insuffisants, fichier absent, etc. Le pointeur retourné doit donc être
testé avant toute utilisation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <stdio.h> #include <stdlib.h> int main() { FILE * inputFile = fopen( "data.txt", "r" ); if ( inputFile == NULL ) { perror( "fopen" ); return EXIT_FAILURE; } fclose( inputFile ); return EXIT_SUCCESS; } |
Pour une première approche, fgets reste une fonction très pratique : elle lit
au plus un certain nombre de caractères et protège donc votre buffer contre les dépassements. Sur un système POSIX, la fonction
getline permet d'aller plus loin en ajustant automatiquement le buffer.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#include <stdio.h> #include <stdlib.h> int main() { FILE * inputFile = fopen( "data.txt", "r" ); if ( inputFile == NULL ) { perror( "fopen" ); return EXIT_FAILURE; } char line[128]; while ( fgets( line, sizeof line, inputFile ) != NULL ) { printf( "%s", line ); } if ( ferror( inputFile ) ) { perror( "fgets" ); } fclose( inputFile ); return EXIT_SUCCESS; } |
Pour écrire du texte formaté dans un fichier, vous pouvez utiliser fprintf.
Le principe est le même que pour printf, à ceci près que le premier paramètre indique le flux de destination.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <stdio.h> #include <stdlib.h> int main() { FILE * outputFile = fopen( "result.txt", "w" ); if ( outputFile == NULL ) { perror( "fopen" ); return EXIT_FAILURE; } fprintf( outputFile, "pi vaut environ %.2f\n", 3.14159 ); fclose( outputFile ); return EXIT_SUCCESS; } |
Les exemples précédents manipulent des fichiers texte : les données sont écrites sous une forme lisible par un humain, et certaines conversions peuvent être effectuées par la librairie C. En mode binaire, les octets sont lus et écrits tels quels. C'est souvent plus compact et plus rapide, mais ce n'est pas pour autant un format d'échange universel : la taille des types, l'alignement des structures ou l'ordre des octets peuvent varier d'une plateforme à l'autre.
Pour un fichier binaire, on ajoute la lettre b au mode d'ouverture de fopen.
Sous Unix, la différence avec le mode texte est généralement faible. Sous Windows, elle est importante, car le mode texte peut
transformer certaines fins de ligne.
| Mode | Utilisation |
|---|---|
"rb" |
ouvrir un fichier binaire existant en lecture ; |
"wb" |
créer ou remplacer un fichier binaire en écriture ; |
"ab" |
ajouter des données binaires à la fin du fichier ; |
"r+b" |
ouvrir un fichier binaire existant en lecture et écriture ; |
"w+b" |
créer ou remplacer un fichier binaire en lecture et écriture. |
La fonction fwrite écrit un bloc mémoire dans un flux. Elle reçoit l'adresse
du premier octet à écrire, la taille d'un élément et le nombre d'éléments à transférer. Sa valeur de retour indique le nombre
d'éléments réellement écrits : il faut donc la tester.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include <stdio.h> #include <stdlib.h> int main() { int values[] = { 10, 20, 30, 40, 50 }; size_t count = sizeof values / sizeof values[0]; FILE * outputFile = fopen( "values.bin", "wb" ); if ( outputFile == NULL ) { perror( "fopen" ); return EXIT_FAILURE; } size_t written = fwrite( values, sizeof values[0], count, outputFile ); if ( written != count ) { perror( "fwrite" ); fclose( outputFile ); return EXIT_FAILURE; } fclose( outputFile ); return EXIT_SUCCESS; } |
Dans cet exemple, le fichier ne contient pas les caractères 1, 0, puis un séparateur : il contient la
représentation binaire des entiers. Un éditeur de texte ne permettra donc pas de lire correctement le fichier produit.
La lecture utilise fread, sur le même principe. Une lecture courte peut signaler
une fin de fichier normale ou une erreur d'entrée / sortie. Pour distinguer les deux cas, on peut tester
ferror après l'appel.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
#include <stdio.h> #include <stdlib.h> int main() { int values[5]; size_t count = sizeof values / sizeof values[0]; FILE * inputFile = fopen( "values.bin", "rb" ); if ( inputFile == NULL ) { perror( "fopen" ); return EXIT_FAILURE; } size_t read = fread( values, sizeof values[0], count, inputFile ); if ( read != count ) { if ( ferror( inputFile ) ) { perror( "fread" ); } else { fprintf( stderr, "Le fichier ne contient pas assez de valeurs.\n" ); } fclose( inputFile ); return EXIT_FAILURE; } for ( size_t index = 0; index < count; ++index ) { printf( "values[%zu] == %d\n", index, values[index] ); } fclose( inputFile ); return EXIT_SUCCESS; } |
On peut aussi écrire des structures complètes. C'est pratique pour un fichier interne à votre programme, mais il faut rester prudent : une structure C peut contenir des octets d'alignement, et sa représentation exacte n'est pas forcément identique sur deux compilateurs ou deux machines différentes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
#include <stdio.h> #include <stdlib.h> typedef struct { int id; double balance; } Account; int main() { Account accounts[] = { { 1, 1200.50 }, { 2, 850.00 }, { 3, 42.75 } }; size_t count = sizeof accounts / sizeof accounts[0]; FILE * outputFile = fopen( "accounts.bin", "wb" ); if ( outputFile == NULL ) { perror( "fopen" ); return EXIT_FAILURE; } if ( fwrite( accounts, sizeof accounts[0], count, outputFile ) != count ) { perror( "fwrite" ); fclose( outputFile ); return EXIT_FAILURE; } fclose( outputFile ); return EXIT_SUCCESS; } |
Un fichier binaire contenant des enregistrements de taille fixe permet d'accéder directement à une donnée. Pour cela, on se
positionne avec fseek. La position courante peut être récupérée avec
ftell, et rewind permet de revenir
au début du fichier.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
#include <stdio.h> #include <stdlib.h> typedef struct { int id; double balance; } Account; int main() { FILE * inputFile = fopen( "accounts.bin", "rb" ); if ( inputFile == NULL ) { perror( "fopen" ); return EXIT_FAILURE; } long recordIndex = 2; /* Troisième enregistrement : index 2. */ long offset = recordIndex * (long) sizeof( Account ); if ( fseek( inputFile, offset, SEEK_SET ) != 0 ) { perror( "fseek" ); fclose( inputFile ); return EXIT_FAILURE; } Account account; if ( fread( &account, sizeof account, 1, inputFile ) != 1 ) { if ( ferror( inputFile ) ) { perror( "fread" ); } else { fprintf( stderr, "Enregistrement absent.\n" ); } fclose( inputFile ); return EXIT_FAILURE; } printf( "Compte %d : %.2f\n", account.id, account.balance ); fclose( inputFile ); return EXIT_SUCCESS; } |
Le même principe permet de modifier un enregistrement en place. Il faut ouvrir le fichier en lecture / écriture avec
"r+b", lire l'enregistrement, revenir au même emplacement, puis réécrire la structure modifiée.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
#include <stdio.h> #include <stdlib.h> typedef struct { int id; double balance; } Account; int main() { FILE * dataFile = fopen( "accounts.bin", "r+b" ); if ( dataFile == NULL ) { perror( "fopen" ); return EXIT_FAILURE; } long recordIndex = 1; long offset = recordIndex * (long) sizeof( Account ); if ( fseek( dataFile, offset, SEEK_SET ) != 0 ) { perror( "fseek" ); fclose( dataFile ); return EXIT_FAILURE; } Account account; if ( fread( &account, sizeof account, 1, dataFile ) != 1 ) { if ( ferror( dataFile ) ) { perror( "fread" ); } else { fprintf( stderr, "Enregistrement absent.\n" ); } fclose( dataFile ); return EXIT_FAILURE; } account.balance += 100.0; if ( fseek( dataFile, offset, SEEK_SET ) != 0 ) { perror( "fseek" ); fclose( dataFile ); return EXIT_FAILURE; } if ( fwrite( &account, sizeof account, 1, dataFile ) != 1 ) { perror( "fwrite" ); fclose( dataFile ); return EXIT_FAILURE; } fclose( dataFile ); return EXIT_SUCCESS; } |
Dès que le fichier doit être échangé avec d'autres programmes, il est préférable de définir un format explicite : écrire les champs un par un, documenter les tailles, fixer l'ordre des octets, ou utiliser un format existant. L'écriture directe d'une structure est surtout intéressante pour des fichiers temporaires ou très liés à une version précise de votre programme.
Améliorations / Corrections
Vous avez des améliorations (ou des corrections) à proposer pour ce document : je vous remerçie par avance de m'en faire part, cela m'aide à améliorer le site.
Emplacement :
Description des améliorations :