Les fichiers (2)

Dans ce chapitre, nous allons poursuivre notre lancée et vous présenter plusieurs points un peu plus avancés en rapport avec les fichiers et les flux.

Détection d'erreurs et fin de fichier

Lors de la présentation des fonctions fgetc(), getc() et fgets() vous avez peut-être remarqué que ces fonctions utilisent un même retour pour indiquer soit la rencontre de la fin du fichier, soit la survenance d’une erreur. Du coup, comment faire pour distinguer l’un et l’autre cas ?

Il existe deux fonctions pour clarifier une telle situation : feof() et ferror().

La fonction feof
int feof(FILE *flux);

La fonction feof() retourne une valeur non nulle dans le cas où la fin du fichier associé au flux spécifié est atteinte.

La fonction ferror
int ferror(FILE *flux);

La fonction ferror() retourne une valeur non nulle dans le cas où une erreur s’est produite lors d’une opération sur le flux visé.

Exemple

L’exemple ci-dessous utilise ces deux fonctions pour déterminer le type de problème rencontré par la fonction fgets().

#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);
    else if (ferror(fp))
    {
        fprintf(stderr, "Erreur lors de la lecture\n");
        return EXIT_FAILURE;
    }
    else
    {
        fprintf(stderr, "Fin de fichier rencontrée\n");
        return EXIT_FAILURE;
    }
    if (fclose(fp) == EOF)
    {
        fprintf(stderr, "Erreur lors de la fermeture du flux\n");
        return EXIT_FAILURE;        
    }

    return 0;
}

Position au sein d'un flux

De manière imagée, un fichier peut être vu comme une longue bande magnétique depuis laquelle des données sont lues ou sur laquelle des informations sont écrites. Ainsi, au fur et à mesure que les données sont lues ou écrites, nous avançons le long de cette bande.

Toutefois, il est parfois nécessaire de se rendre directement à un point précis de cette bande (par exemple à la fin) afin d’éviter des lectures inutiles pour parvenir à un endroit souhaité. À cet effet, la bibliothèque standard fournit plusieurs fonctions.

La fonction ftell
long ftell(FILE *flux);

La fonction ftell() retourne la position actuelle au sein du fichier sous forme d’un long. Elle retourne un nombre négatif en cas d’erreur. Dans le cas des flux binaires, cette valeur correspond au nombre de multiplets séparant la position actuelle du début du fichier.

Lors de l’ouverture d’un flux, la position courante correspond au début du fichier, sauf pour les modes a et a+ pour lesquels la position initiale est soit le début, soit la fin du fichier.

La fonction fseek
int fseek(FILE *flux, long distance, int repere);

La fonction fseek() permet d’effectuer un déplacement d’une distance fournie en argument depuis un repère donné. Elle retourne zéro en cas de succès et une autre valeur en cas d’échec. Il existe trois repères possibles :

  • SEEK_SET qui correspond au début du fichier ;
  • SEEK_CUR qui correspond à la position courante ;
  • SEEK_END qui correspond à la fin du fichier.

Cette fonction s’utilise différemment suivant qu’elle opère sur un flux de texte ou sur un flux binaire.

Les flux de texte

Dans le cas d’un flux de texte, il y a deux possibilités :

  • la distance fournie est nulle ;
  • la distance est une valeur fournie par un précédent appel à la fonction ftell() et le repère est SEEK_SET.
Les flux binaires

Dans le cas d’un flux binaire, seuls les repères SEEK_SET et SEEK_CUR peuvent être utilisés. La distance correspond à un nombre de multiplets à passer depuis le repère fourni.

Dans le cas des modes a et a+, un déplacement a lieu à la fin du fichier avant chaque opération d’écriture et ce, qu’il y ait eu déplacement auparavant ou non.

Un fichier peut-être vu comme une bande magnétique : si vous rembobinez pour ensuite écrire, vous remplacez les données existantes par celles que vous écrivez.

La fonction fgetpos
int fgetpos(FILE *flux, fpos_t *position);

La fonction fgetpos() écrit la position courante dans un objet de type fpos_t référencé par position. Elle retourne zéro en cas de succès et un nombre non nul dans le cas contraire.

La fonction fsetpos
int fsetpos(FILE *flux, fpos_t *position);

La fonction fsetpos() effectue un déplacement à la position précédemment écrite dans un objet de type fpos_t référencé par position. En cas de succès, elle retourne zéro, sinon un nombre entier non nul.

La fonction rewind
void rewind(FILE *flux);

La fonction rewind() vous ramène au début du fichier (autrement dit, elle rembobine la bande).

Si vous utilisez un flux ouvert en lecture et écriture vous devez appeler une fonction de déplacement entre deux opérations de natures différentes ou utiliser la fonction fflush() (présentée dans l’extrait suivant) entre une opération d’écriture et de lecture. Si vous ne souhaitez pas vous déplacer, vous pouvez utiliser l’appel fseek(flux, 0, SEEK_CUR) afin de respecter cette condition sans réellement effectuer un déplacement.

La temporisation

Dans le chapitre précédent, nous vous avons précisé que les flux étaient le plus souvent temporisés afin d’optimiser les opérations de lecture et d’écriture sous-jacentes. Dans cette section, nous allons nous pencher un peu plus sur cette notion.

Introduction

Nous vous avons dit auparavant que deux types de temporisations existaient : la temporisation par lignes et celle par blocs. Une des conséquences logiques de cette temporisation est que les fonctions de lecture/écriture récupèrent les données et les inscrivent dans ces tampons. Ceci peut paraître évident, mais peut avoir des conséquences parfois surprenantes si ce fait est oublié ou inconnu.

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


int main(void)
{
	char nom[64];
	char prenom[64];

	printf("Quel est votre nom ? ");

	if (scanf("%63s", nom) != 1)
	{
		fprintf(stderr, "Erreur lors de la saisie\n");
		return EXIT_FAILURE;
	}

	printf("Quel est votre prénom ? ");

	if (scanf("%63s", prenom) != 1)
	{
		fprintf(stderr, "Erreur lors de la saisie\n");
		return EXIT_FAILURE;
	}

	printf("Votre nom est %s\n", nom);
	printf("Votre prénom est %s\n", prenom);
	return 0;
}
Résultat
Quel est votre nom ? Charles Henri
Quel est votre prénom ? Votre nom est Charles
Votre prénom est Henri

Comme vous le voyez, le programme ci-dessus réalise deux saisies, mais si l’utilisateur entre par exemple « Charles Henri », il n’aura l’occasion d’entrer des données qu’une seule fois. Ceci est dû au fait que l’indicateur s récupère une suite de caractères exempte d’espaces (ce qui fait qu’il s’arrête à « Charles ») et que la suite « Henri » demeure dans le tampon du flux stdin. Ainsi, lors de la deuxième saisie, il n’est pas nécessaire de récupérer de nouvelles données depuis le terminal puisqu’il y en a déjà en attente dans le tampon, d’où le résultat obtenu.

Le même problème peut se poser si par exemple les données fournies ont une taille supérieure par rapport à l’objet qui doit les accueillir.

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


int main(void)
{
    char chaine1[16];
    char chaine2[16];

    printf("Un morceau : ");

    if (fgets(chaine1, sizeof chaine1, stdin) == NULL)
    {
        printf("Erreur lors de la saisie\n");
        return EXIT_FAILURE;
    }

    printf("Un autre morceau : ");

    if (fgets(chaine2, sizeof chaine2, stdin) == NULL)
    {
        printf("Erreur lors de la saisie\n");
        return EXIT_FAILURE;
    }

    printf("%s ; %s\n", chaine1, chaine2);
    return 0;
}
Résultat
Un morceau : Une chaîne de caractères, vraiment, mais alors vraiment trop longue
Un autre morceau : Une chaîne de  ; caractères, vr

Ici, la chaîne entrée est trop importante pour être contenue dans chaine1, les données non lues sont alors conservées dans le tampon du flux stdin et lues lors de la seconde saisie (qui ne lit par l’entièreté non plus).

Interagir avec la temporisation
Vider un tampon

Si la temporisation nous évite des coûts en terme d’opérations de lecture/écriture, il nous est parfois nécessaire de passer outre cette mécanique pour vider manuellement le tampon d’un flux.

Opération de lecture

Si le tampon contient des données qui proviennent d’une opération de lecture, celles-ci peuvent être abandonnées soit en appelant une fonction de positionnement soit en lisant les données, tout simplement. Il y a toutefois un bémol avec la première solution : les fonctions de positionnement ne fonctionnent pas dans le cas où le flux ciblé est lié à un périphérique « interactif », c’est-à-dire le plus souvent un terminal.

Autrement dit, pour que l’exemple précédent recoure bien à deux saisies, il nous est nécessaire de vérifier que la fonction fgets() a bien lu un caractère \n (qui signifie que la fin de ligne est atteinte et donc celle du tampon s’il s’agit du flux stdin). Si ce n’est pas le cas, alors il nous faut lire les caractères restants jusqu’au \n final (ou la fin du fichier).

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


int vider_tampon(FILE *fp)
{
    int c;

    do
        c = fgetc(fp);
    while (c != '\n' && c != EOF);

    return ferror(fp) ? 0 : 1;
}


int main(void)
{
    char chaine1[16];
    char chaine2[16];

    printf("Un morceau : ");

    if (fgets(chaine1, sizeof chaine1, stdin) == NULL)
    {
        printf("Erreur lors de la saisie\n");
        return EXIT_FAILURE;
    }
    if (strchr(chaine1, '\n') == NULL)
        if (!vider_tampon(stdin))
        {
            fprintf(stderr, "Erreur lors de la vidange du tampon.\n");
            return EXIT_FAILURE;
        }

    printf("Un autre morceau : ");

    if (fgets(chaine2, sizeof chaine2, stdin) == NULL)
    {
        printf("Erreur lors de la saisie\n");
        return EXIT_FAILURE;
    }
    if (strchr(chaine2, '\n') == NULL)
        if (!vider_tampon(stdin))
        {
            fprintf(stderr, "Erreur lors de la vidange du tampon.\n");
            return EXIT_FAILURE;
        }

    printf("%s ; %s\n", chaine1, chaine2);
    return 0;
}
Résultat
Un morceau : Une chaîne de caractères vraiment, mais alors vraiment longue
Un autre morceau : Une autre chaîne de caractères
Une chaîne de  ; Une autre chaî
Opération d’écriture

Si, en revanche, le tampon comprend des données en attente d’écriture, il est possible de forcer celle-ci soit à l’aide d’une fonction de positionnement, soit à l’aide de la fonction fflush().

int fflush(FILE *flux);

Celle-ci vide le tampon du flux spécifié et retourne zéro en cas de succès ou EOF en cas d’erreur.

Notez bien que cette fonction ne peut être employée que pour vider un tampon comprenant des données en attente d'écriture.

Modifier un tampon

Techniquement, il ne vous est pas possible de modifier directement le contenu d’un tampon, ceci est réalisé par les fonctions de la bibliothèque standard au gré de vos opérations de lecture et/ou d’écriture. Il y a toutefois une exception à cette règle : la fonction ungetc().

int ungetc(int ch, FILE *flux);

Cette fonction est un peu particulière : elle ajoute un caractère ou un multiplet ch au début du tampon du flux flux. Celui-ci pourra être lu lors d’un appel ultérieur à une fonction de lecture. Elle retourne le caractère ou le multiplet ajouté en cas de succès et EOF en cas d’échec.

Cette fonction est très utile dans le cas où les actions d’un programme dépendent du contenu d’un flux. Imaginez par exemple que votre programme doit déterminer si l’entrée standard contient une suite de caractères ou un nombre et doit ensuite afficher celui-ci. Vous pourriez utiliser getchar() pour récupérer le premier caractère et déterminer s’il s’agit d’un chiffre. Toutefois, le premier caractère du flux est alors lu et cela complique votre tâche pour la suite… La fonction ungetc() vous permet de résoudre ce problème en replaçant ce caractère dans le tampon du flux stdin.

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


static void afficher_nombre(void);
static void afficher_chaine(void);


static void afficher_nombre(void)
{
    double f;

    if (scanf("%lf", &f) == 1)
        printf("Vous avez entré le nombre : %f\n", f);
    else
        fprintf(stderr, "Erreur lors de la saisie\n");
}


static void afficher_chaine(void)
{
    char chaine[255];

    if (scanf("%254s", chaine) == 1)
        printf("Vous avez entré la chaine : %s\n", chaine);
    else
        fprintf(stderr, "Erreur lors de la saisie\n");
}


int main(void)
{
    int ch = getchar();

    if (ungetc(ch, stdin) == EOF)
    {
        fprintf(stderr, "Impossible de replacer un caractère\n");
        return EXIT_FAILURE;
    }

    switch (ch)
    {
    case '0':
    case '1':
    case '2':
    case '3':
    case '4':
    case '5':
    case '6':
    case '7':
    case '8':
    case '9':
        afficher_nombre();
        break;

    default:
        afficher_chaine();
        break;
    }
    
    return 0;
}
Résultat
Test
Vous avez entré la chaine : Test

52.5
Vous avez entré le nombre : 52.500000

La fonction ungetc() ne vous permet de replacer qu’un seul caractère ou multiplet avant une opération de lecture.

Flux ouverts en lecture et écriture

Pour clore ce chapitre, un petit mot sur le cas des flux ouverts en lecture et en écriture (soit à l’aide des modes r+, w+, a+ et leurs équivalents binaires).

Si vous utilisez un tel flux, vous devez appeler une fonction de déplacement entre deux opérations de natures différentes ou utiliser la fonction fflush() entre une opération d’écriture et de lecture. Si vous ne souhaitez pas vous déplacer, vous pouvez utiliser l’appel fseek(flux, 0L, SEEK_CUR) afin de respecter cette condition sans réellement effectuer un déplacement.

Vous l’aurez sans doute compris : cette règle impose en fait de vider le tampon du flux entre deux opérations de natures différentes.

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


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

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

    /* Retour au début : la condition est donc remplie. */

    if (fseek(fp, 0L, SEEK_SET) != 0)
    {
        fprintf(stderr, "Impossible de revenir au début du fichier.\n");
        return EXIT_FAILURE;
    }
    if (fgets(chaine, sizeof chaine, fp) == NULL)
    {
        fprintf(stderr, "Erreur lors de la lecture.\n");
        return EXIT_FAILURE;
    }

    printf("%s\n", chaine);

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

    return 0;
}
Résultat
Une phrase.

En résumé
  1. La fonction feof() permet de savoir si la fin du fichier associé à un flux a été atteinte ;
  2. La fonction ferror() permet de savoir si une erreur est survenue lors d’une opération sur un flux ;
  3. La fonction ftell() retourne la position actuelle au sein d’un flux ;
  4. La fonction fseek() effectue un déplacement au sein d’un fichier depuis un point donné ;
  5. La fonction rewind() réalise un déplacement jusqu’au début d’un fichier ;
  6. Pour vider un tampon contenant des données en attente de lecture, il est nécessaire de lire ces dernières ;
  7. Afin de vider un tampon contenant des données en attente d’écriture, il est possible d’employer soit une fonction de positionnement, soit la fonction fflush() ;
  8. La fonction ungetc() permet de placer un caractère ou un multiplet dans un tampon en vue d’une lecture ;
  9. Lorsqu’un flux est ouvert en lecture et écriture, il est nécessaire d’appeler une fonction de déplacement entre deux opérations de natures différentes (ou d’appeler la fonction fflush() entre une opération d’écriture et de lecture).