Les fichiers (1)

Jusqu’à présent, nous n’avons manipulé que des données en mémoire, ce qui nous empêchait de les stocker de manière permanente. Dans ces deux chapitres, nous allons voir comment conserver des informations de manière permanente à l’aide des fichiers.

Les fichiers

En informatique, un fichier est un ensemble d’informations stockées sur un support, réuni sous un même nom et manipulé comme une unité.

Extension de fichier

Le contenu de chaque fichier est lui-même organisé suivant un format qui dépend des données qu’il contient. Il en existe une pléthore pour chaque type de données :

  • audio : Ogg, MP3, MP4, FLAC, Wave, etc. ;
  • vidéo : Ogg, WebM, MP4, AVI, etc. ;
  • documents : ODT, DOC et DOCX, XLS et XLSX, PDF, Postscript, etc.

Afin d’aider l’utilisateur, ces formats sont le plus souvent indiqués à l’aide d’une extension qui se traduit par un suffixe ajouté au nom de fichier. Toutefois, cette extension est purement indicative et facultative, elle n’influence en rien le contenu du fichier. Son seul objectif est d’aider à déterminer le type de contenu d’un fichier.

Système de fichiers

Afin de faciliter leur localisation et leur gestion, les fichiers sont classés et organisés sur leur support suivant un système de fichiers. C’est lui qui permet à l’utilisateur de répartir ses fichiers dans une arborescence de dossiers et de localiser ces derniers à partir d’un chemin d’accès.

Le chemin d’accès

Le chemin d’accès d’un fichier est une suite de caractères décrivant la position de celui-ci dans le système de fichiers. Un chemin d’accès se compose au minimum du nom du fichier visé et au maximum de la suite de tous les dossiers qu’il est nécessaire de traverser pour l’atteindre depuis le répertoire racine. Ces éventuels dossiers à traverser sont séparés par un caractère spécifique qui est / sous Unixoïde et \ sous Windows.

La racine d’un système de fichier est le point de départ de l’arborescence des fichiers et dossiers. Sous Unixoïdes, il s’agit du répertoire / tandis que sous Windows, chaque lecteur est une racine (comme C: par exemple). Si un chemin d’accès commence par la racine, alors celui-ci est dit absolu car il permet d’atteindre le fichier depuis n’importe quelle position dans l’arborescence. Si le chemin d’accès ne commence pas par la racine, il est dit relatif et ne permet de parvenir au fichier que depuis un point précis dans la hiérarchie de fichiers.

Ainsi, si nous souhaitons accéder à un fichier nommé « texte.txt » situé dans le dossier « documents » lui-même situé dans le dossier « utilisateur » qui est à la racine, alors le chemin d’accès absolu vers ce fichier serait /utilisateur/documents/texte.txt sous Unixoïde et C:\utilisateur\documents\texte.txt sous Windows (à supposer qu’il soit sur le lecteur C:). Toutefois, si nous sommes déjà dans le dossier « utilisateur », alors nous pouvons y accéder à l’aide du chemin relatif documents/texte.txt ou documents\texte.txt. Néanmoins, ce chemin relatif n’est utilisable que si nous sommes dans le dossier « utilisateur ».

Métadonnées

Également, le système de fichier se charge le plus souvent de conserver un certain nombre de données concernant chaque fichier comme :

  • sa taille ;
  • ses droits d’accès (les utilisateurs autorisés à le manipuler) ;
  • la date de dernier accès ;
  • la date de dernière modification ;
  • etc.

Les flux : un peu de théorie

La bibliothèque standard vous fournit différentes fonctions pour manipuler les fichiers, toutes déclarées dans l’en-tête <stdio.h>. Toutefois, celles-ci manipulent non pas des fichiers, mais des flux de données en provenance ou à destination de fichiers. Ces flux peuvent être de deux types :

  • des flux de textes qui sont des suites de caractères terminées par un caractère de fin de ligne (\n) et formant ainsi des lignes ;
  • des flux binaires qui sont des suites de multiplets.
Pourquoi utiliser des flux ?

Pourquoi recourir à des flux plutôt que de manipuler directement des fichiers nous demanderez-vous ? Pour deux raisons : les disparités entre systèmes d’exploitation quant à la représentation des lignes et la lourdeur des opérations de lecture et d’écriture.

Disparités entre systèmes d’exploitation

Les différents systèmes d’exploitation ne représentent pas les lignes de la même manière :

  • sous Mac OS (avant Mac OS X), la fin d’une ligne était indiquée par le caractère \r ;
  • sous Windows, la fin de ligne est indiquée par la suite de caractères \r\n ;
  • sous Unixoïdes (GNU/Linux, *BSD, Mac OS X, Solaris, etc.), la fin de ligne est indiquée par le caractère \n.

Aussi, si nous manipulions directement des fichiers, nous devrions prendre en compte ces disparités, ce qui rendrait notre code nettement plus pénible à rédiger. En passant par les fonctions de la bibliothèque standard, nous évitons ce casse-tête car celle-ci remplace le ou les caractères de fin de ligne par un \n (ce que nous avons pu expérimenter avec les fonctions printf() et scanf()) et inversement.

Lourdeur des opérations de lecture et d’écriture

Lire depuis un fichier ou écrire dans un fichier signifie le plus souvent accéder au disque dur. Or, rappelez-vous, il s’agit de la mémoire la plus lente d’un ordinateur. Dès lors, si pour lire une ligne il était nécessaire de récupérer les caractères un à un depuis le disque dur, cela prendrait un temps fou.

Pour éviter ce problème, les flux de la bibliothèque standard recourent à un mécanisme appelé la temporisation ou mémorisation (buffering en anglais). La bibliothèque standard fournit deux types de temporisation :

  • la temporisation par blocs ;
  • et la temporisation par lignes.

Avec la temporisation par blocs, les données sont récupérées depuis le fichier et écrites dans le fichier sous forme de blocs d’une taille déterminée. L’idée est la suivante : plutôt que de lire les caractères un à un, nous allons demander un bloc de données d’une taille déterminée que nous conserverons en mémoire vive pour les accès suivants. Cette technique est également utilisée lors des opérations d’écritures : les données sont stockées en mémoire jusqu’à ce qu’elles atteignent la taille d’un bloc. Ainsi, le nombre d’accès au disque dur sera limité à la taille totale du fichier à lire (ou des données à écrire) divisée par la taille d’un bloc.

La temporisation par lignes, comme son nom l’indique, se contente de mémoriser une ligne. Techniquement, celle-ci est utilisée lorsque le flux est associé à un périphérique interactif comme un terminal. En effet, si nous appliquions la temporisation par blocs pour les entrées de l’utilisateur, ce dernier devrait entrer du texte jusqu’à atteindre la taille d’un bloc. Pareillement, nous devrions attendre d’avoir écrit une quantité de données égale à la taille d’un bloc pour que du texte soit affiché à l’écran. Cela serait assez gênant et peu interactif… À la place, c’est la temporisation par lignes qui est utilisée : les données sont stockées jusqu’à ce qu’un caractère de fin de ligne soit rencontré (\n, donc) ou jusqu’à ce qu’une taille maximale soit atteinte.

La bibliothèque standard vous permet également de ne pas temporiser les données en provenance ou à destination d’un flux.

stdin, stdout et stderr

Par défaut, trois flux de texte sont ouverts lors du démarrage d’un programme et sont déclarés dans l’en-tête <stdio.h> : stdin, stdout et stderr.

  • stdin correspond à l’entrée standard, c’est-à-dire le flux depuis lequel vous pouvez récupérer les informations fournies par l’utilisateur.
  • stdout correspond à la sortie standard, il s’agit du flux vous permettant de transmettre des informations à l’utilisateur qui seront le plus souvent affichées dans le terminal.
  • stderr correspond à la sortie d’erreur standard, c’est ce flux que vous devez privilégier lorsque vous souhaitez transmettre des messages d’erreurs ou des avertissements à l’attention de l’utilisateur (nous verrons comment l’utiliser un peu plus tard dans ce chapitre). Comme pour stdout, ces données sont le plus souvent affichées dans le terminal.

Les flux stdin et stdout sont temporisés par lignes (sauf s’ils sont associés à des fichiers au lieu de périphériques interactifs, auxquels cas ils seront temporisés par blocs) tandis que le flux stderr est au plus temporisé par lignes (ceci afin que les informations soient transmises le plus rapidement possible à l’utilisateur).

Ouverture et fermeture d'un flux

La fonction fopen
FILE *fopen(char *chemin, char *mode);

La fonction fopen() permet d’ouvrir un flux. Celle-ci attend deux arguments : un chemin d’accès vers un fichier qui sera associé au flux et un mode qui détermine le type de flux (texte ou binaire) et la nature des opérations qui seront réalisées sur le fichier via le flux (lecture, écriture ou les deux). Elle retourne un pointeur vers un flux en cas de succès et un pointeur nul en cas d’échec.

Le mode

Le mode est une chaîne de caractères composée d’une ou plusieurs lettres qui décrit le type du flux et la nature des opérations qu’il doit réaliser.

Cette chaîne commence obligatoirement par la seconde de ces informations. Il existe six possibilités reprises dans le tableau ci-dessous.

Mode

Type(s) d’opération(s)

Effets

r

Lecture

Néant

r+

Lecture et écriture

Néant

w

Écriture

Si le fichier n’existe pas, il est créé.

Si le fichier existe, son contenu est effacé.

w+

Lecture et écriture

Idem

a

Écriture

Si le fichier n’existe pas, il est créé.

Place les données à la fin du fichier

a+

Lecture et écriture

Idem

Par défaut, les flux sont des flux de textes. Pour obtenir un flux binaire, il suffit d’ajouter la lettre b à la fin de la chaîne décrivant le mode.

Exemple

Le code ci-dessous tente d’ouvrir un fichier nommé « texte.txt » en lecture seule dans le dossier courant. Notez que dans le cas où il n’existe pas, la fonction fopen() retournera un pointeur nul (seul le mode r permet de produire ce comportement).

#include <stdio.h>
#include <stdlib.h>


int main(void)
{
    FILE *fp = fopen("texte.txt", "r");

    if (fp == NULL)
    {
        printf("Le fichier texte.txt n'a pas pu être ouvert\n");
        return EXIT_FAILURE;
    }

    printf("Le fichier texte.txt existe\n");
    return 0;
}
La fonction fclose
int fclose(FILE *flux);

La fonction fclose() termine l’association entre un flux et un fichier. S’il reste des données temporisées, celles-ci sont écrites. La fonction retourne zéro en cas de succès et EOF en cas d’erreur.

EOF est une constante définie dans l’en-tête <stdio.h> et est utilisée par les fonctions déclarées dans ce dernier pour indiquer soit l’arrivée à la fin d’un fichier (nous allons y venir) soit la survenance d’une erreur. La valeur de cette constante est toujours un entier négatif.

Nous pouvons désormais compléter l’exemple précédent comme suit.

#include <stdio.h>
#include <stdlib.h>


int main(void)
{
    FILE *fp = fopen("texte.txt", "r");

    if (fp == NULL)
    {
        printf("Le fichier texte.txt n'a pas pu être ouvert\n");
        return EXIT_FAILURE;
    }

    printf("Le fichier texte.txt existe\n");

    if (fclose(fp) == EOF)
    {
        printf("Erreur lors de la fermeture du flux\n");
        return EXIT_FAILURE;        
    }

    return 0;
}

Veillez qu’à chaque appel à la fonction fopen() corresponde un appel à la fonction fclose().

Écriture vers un flux de texte

Écrire un caractère
int putc(int ch, FILE *flux);
int fputc(int ch, FILE *flux);
int putchar(int ch);

Les fonctions putc() et fputc() écrivent un caractère dans un flux. Il s’agit de l’opération d’écriture la plus basique sur laquelle reposent toutes les autres fonctions d’écriture. Ces deux fonctions retournent soit le caractère écrit, soit EOF si une erreur est rencontrée. La fonction putchar(), quant à elle, est identique aux fonctions putc() et fputc() si ce n’est qu’elle écrit dans le flux stdout.

Techniquement, putc() et fputc() sont identiques, si ce n’est que putc() est en fait le plus souvent une macrofonction. Étant donné que nous n’avons pas encore vu de quoi il s’agit, préférez utiliser la fonction fputc() pour l’instant.

L’exemple ci-dessous écrit le caractère « C » dans le fichier « texte.txt ». Étant donné que nous utilisons le mode w, le fichier est soit créé s’il n’existe pas, soit vidé de son contenu s’il existe (revoyez le tableau des modes à la section précédente si vous êtes perdus).

#include <stdio.h>
#include <stdlib.h>


int main(void)
{
    FILE *fp = fopen("texte.txt", "w");

    if (fp == NULL)
    {
        printf("Le fichier texte.txt n'a pas pu être ouvert\n");
        return EXIT_FAILURE;
    }
    if (fputc('C', fp) == EOF)
    {
        printf("Erreur lors de l'écriture d'un caractère\n");
        return EXIT_FAILURE;
    }
    if (fclose(fp) == EOF)
    {
        printf("Erreur lors de la fermeture du flux\n");
        return EXIT_FAILURE;        
    }

    return 0;
}
Écrire une ligne
int fputs(char *ligne, FILE *flux);
int puts(char *ligne);

La fonction fputs() écrit une ligne dans le flux flux. La fonction retourne un nombre positif ou nul en cas de succès et EOF en cas d’erreurs. La fonction puts() est identique si ce n’est qu’elle ajoute automatiquement un caractère de fin de ligne et qu’elle écrit sur le flux stdout.

Maintenant que nous savons comment écrire une ligne dans un flux précis, nous allons pouvoir diriger nos messages d’erreurs vers le flux stderr afin que ceux-ci soient affichés le plus rapidement possible.

L’exemple suivant écrit le mot « Bonjour » suivi d’un caractère de fin de ligne au sein du fichier « texte.txt ».

#include <stdio.h>
#include <stdlib.h>


int main(void)
{
    FILE *fp = fopen("texte.txt", "w");

    if (fp == NULL)
    {
        fputs("Le fichier texte.txt n'a pas pu être ouvert\n", stderr);
        return EXIT_FAILURE;
    }
    if (fputs("Bonjour\n", fp) == EOF)
    {
        fputs("Erreur lors de l'écriture d'une ligne\n", stderr);
        return EXIT_FAILURE;
    }
    if (fclose(fp) == EOF)
    {
        fputs("Erreur lors de la fermeture du flux\n", stderr);
        return EXIT_FAILURE;        
    }

    return 0;
}

La norme1 vous garantit qu’une ligne peut contenir jusqu’à 254 caractères (caractère de fin de ligne inclus). Aussi, veillez à ne pas écrire de ligne d’une taille supérieure à cette limite.

La fonction fprintf
int fprintf(FILE *flux, char *format, ...);

La fonction fprintf() est la même que la fonction printf() si ce n’est qu’il est possible de lui spécifier sur quel flux écrire (au lieu de stdout pour printf()). Elle retourne le nombre de caractères écrits ou une valeur négative en cas d’échec.


  1. ISO/IEC 9899:201x, doc. N1570, § 7.21.2, Streams, al. 9.

Lecture depuis un flux de texte

Récupérer un caractère
int getc(FILE *flux);
int fgetc(FILE *flux);
int getchar(void);

Les fonctions getc() et fgetc() sont les exacts miroirs des fonctions putc() et fputc() : elles récupèrent un caractère depuis le flux fourni en argument. Il s’agit de l’opération de lecture la plus basique sur laquelle reposent toutes les autres fonctions de lecture. Ces deux fonctions retournent soit le caractère lu, soit EOF si la fin de fichier est rencontrée ou si une erreur est rencontrée. La fonction getchar(), quant à elle, est identique à ces deux fonctions si ce n’est qu’elle récupère un caractère depuis le flux stdin.

Comme putc(), la fonction getc() est le plus souvent une macrofonction. Utilisez donc plutôt la fonction fgetc() pour le moment.

L’exemple ci-dessous lit un caractère provenant du fichier texte.txt.

#include <stdio.h>
#include <stdlib.h>


int main(void)
{
    FILE *fp = fopen("texte.txt", "r");

    if (fp == NULL)
    {
        fprintf(stderr, "Le fichier texte.txt n'a pas pu être ouvert\n");
        return EXIT_FAILURE;
    }

    int ch = fgetc(fp);

    if (ch != EOF)
        printf("%c\n", ch);
    if (fclose(fp) == EOF)
    {
        fprintf(stderr, "Erreur lors de la fermeture du flux\n");
        return EXIT_FAILURE;        
    }

    return 0;
}
Récupérer une ligne
char *fgets(char *tampon, int taille, FILE *flux);

La fonction fgets() lit une ligne depuis le flux flux et la stocke dans le tableau tampon. Cette dernière lit au plus un nombre de caractères égal à taille diminué de un afin de laisser la place pour le caractère nul, qui est automatiquement ajouté. Dans le cas où elle rencontre un caractère de fin de ligne : celui-ci est conservé au sein du tableau, un caractère nul est ajouté et la lecture s’arrête.

La fonction retourne l’adresse du tableau tampon en cas de succès et un pointeur nul si la fin du fichier est atteinte ou si une erreur est survenue.

L’exemple ci-dessous réalise donc la même opération que le code précédent, mais en utilisant la fonction fgets().

Étant donné que la norme nous garantit qu’une ligne peut contenir jusqu’à 254 caractères (caractère de fin de ligne inclus), nous utilisons un tableau de 255 caractères pour les contenir (puisqu’il est nécessaire de prévoir un espace pour le caractère nul).

#include <stdio.h>
#include <stdlib.h>


int main(void)
{
    char buf[255];
    FILE *fp = fopen("texte.txt", "r");

    if (fp == NULL)
    {
        fprintf(stderr, "Le fichier texte.txt n'a pas pu être ouvert\n");
        return EXIT_FAILURE;
    }
    if (fgets(buf, sizeof buf, fp) != NULL)
        printf("%s\n", buf);
    if (fclose(fp) == EOF)
    {
        fprintf(stderr, "Erreur lors de la fermeture du flux\n");
        return EXIT_FAILURE;        
    }

    return 0;
}

Toutefois, il y a un petit problème : la fonction fgets() conserve le caractère de fin de ligne qu’elle rencontre. Dès lors, nous affichons deux retours à la ligne : celui contenu dans la chaîne buf et celui affiché par printf(). Aussi, il serait préférable d’en supprimer un, de préférence celui de la chaîne de caractères. Pour ce faire, nous pouvons faire appel à une petite fonction (que nous appellerons chomp() en référence à la fonction éponyme du langage Perl) qui se chargera de remplacer le caractère de fin de ligne par un caractère nul.

void chomp(char *s)
{
    while (*s != '\n' && *s != '\0')
        ++s;

    *s = '\0';
}
La fonction fscanf
int fscanf(FILE *flux, char *format, ...);

La fonction fscanf() est identique à la fonction scanf() si ce n’est qu’elle récupère les données depuis le flux fourni en argument (au lieu de stdin pour scanf()).

Le flux stdin étant le plus souvent mémorisé par lignes, ceci vous explique pourquoi nous lisions les caractères restant après un appel à scanf() jusqu’à rencontrer un caractère de fin de ligne : pour vider le tampon du flux stdin.

La fonction fscanf() retourne le nombre de conversions réussies (voire zéro, si aucune n’est demandée ou n’a pu être réalisée) ou EOF si une erreur survient avant qu’une conversion n’ait eu lieu.

Écriture vers un flux binaire

Écrire un multiplet
int putc(int ch, FILE *flux);
int fputc(int ch, FILE *flux);

Comme pour les flux de texte, il vous est possible de recourir aux fonctions putc() et fputc(). Dans le cas d’un flux binaire, ces fonctions écrivent un multiplet (sous la forme d’un int converti en unsigned char) dans le flux spécifié.

Écrire une suite de multiplets
size_t fwrite(void *ptr, size_t taille, size_t nombre, FILE *flux);

La fonction fwrite() écrit le tableau référencé par ptr composé de nombre éléments de taille multiplets dans le flux flux. Elle retourne une valeur égale à nombre en cas de succès et une valeur inférieure en cas d’échec.

L’exemple suivant écrit le contenu du tableau tab dans le fichier binaire.bin. Dans le cas où un int fait 4 octets, 20 octets seront donc écrits.

#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>


int main(void)
{
    int tab[5] = { 1, 2, 3, 4, 5 };
    const size_t n = sizeof tab / sizeof tab[0];
    FILE *fp = fopen("binaire.bin", "wb");

    if (fp == NULL)
    {
        fprintf(stderr, "Le fichier binaire.bin n'a pas pu être ouvert\n");
        return EXIT_FAILURE;
    }
    if (fwrite(&tab, sizeof tab[0], n, fp) != n)
    {
            fprintf(stderr, "Erreur lors de l'écriture du tableau\n");
            return EXIT_FAILURE;
    }
    if (fclose(fp) == EOF)
    {
        fprintf(stderr, "Erreur lors de la fermeture du flux\n");
        return EXIT_FAILURE;        
    }

    return 0;
}

Lecture depuis un flux binaire

Lire un multiplet
int getc(FILE *flux);
int fgetc(FILE *flux);

Lors de la lecture depuis un flux binaire, les fonctions fgetc() et getc() permettent de récupérer un multiplet (sous la forme d’un unsigned char converti en int) depuis un flux.

Lire une suite de multiplets
size_t fread(void *ptr, size_t taille, size_t nombre, FILE *flux);

La fonction fread() est l’inverse de la fonction fwrite() : elle lit nombre éléments de taille multiplets depuis le flux flux et les stocke dans l’objet référencé par ptr. Elle retourne une valeur égale à nombre en cas de succès ou une valeur inférieure en cas d’échec.

Dans le cas où nous disposons du fichier binaire.bin produit par l’exemple de la section précédente, nous pouvons reconstituer le tableau tab.

#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>


int main(void)
{
    int tab[5] = { 0 };
    const size_t n = sizeof tab / sizeof tab[0];
    FILE *fp = fopen("binaire.bin", "rb");

    if (fp == NULL)
    {
        fprintf(stderr, "Le fichier binaire.bin n'a pas pu être ouvert\n");
        return EXIT_FAILURE;
    }
    if (fread(&tab, sizeof tab[0], n, fp) != n)
    {
            fprintf(stderr, "Erreur lors de la lecture du tableau\n");
            return EXIT_FAILURE;
    }
    if (fclose(fp) == EOF)
    {
        fprintf(stderr, "Erreur lors de la fermeture du flux\n");
        return EXIT_FAILURE;        
    }

    for (unsigned i = 0; i < n; ++i)
        printf("tab[%u] = %d\n", i, tab[i]);

    return 0;
}
Résultat
tab[0] = 1
tab[1] = 2
tab[2] = 3
tab[3] = 4
tab[4] = 5

Dans le chapitre suivant, nous continuerons notre découverte des fichiers en attaquant quelques notions plus avancées : la gestion d’erreur et de fin de fichier, le déplacement au sein d’un flux, la temporisation et les subtilités liées aux flux ouverts en lecture et écriture.

En résumé
  1. L’extension d’un fichier fait partie de son nom et est purement indicative ;
  2. Les fichiers sont manipulés à l’aide de flux afin de s’abstraire des disparités entre systèmes d’exploitation et de recourir à la temporisation ;
  3. Trois flux de texte sont disponibles par défaut : stdin, stdout et stderr ;
  4. La fonction fopen() permet d’associer un fichier à un flux, la fonction fclose() met fin à cette association ;
  5. Les fonctions fputc(), fputs() et fprintf() écrivent des données dans un flux de texte ;
  6. Les fonctions fgetc(), fgets() et fscanf() lisent des données depuis un flux de texte ;
  7. Les fonctions fputc() et fwrite() écrivent un ou plusieurs multiplets dans un flux binaire ;
  8. Les fonctions fgetc() et fread() lisent un ou plusieurs multiplets depuis un flux binaire.