Developpez.com

Plus de 14 000 cours et tutoriels en informatique professionnelle à consulter, à télécharger ou à visionner en vidéo.

Initiation au langage C


précédentsommairesuivant

V. Les entrées/sorties en langage C

V-A. Introduction

Les entrées/sorties (E/S) ne font pas vraiment partie du langage C car ces opérations sont dépendantes du système. Cela signifie que pour réaliser des opérations d'entrée/sortie en C, il faut en principe passer par les fonctionnalités offertes par le système. Néanmoins sa bibliothèque standard est fournie avec des fonctions permettant d'effectuer de telles opérations afin de faciliter l'écriture de code portable. Les fonctions et types de données liées aux entrées/sorties sont principalement déclarés dans le fichier stdio.h (standard input/output).

V-B. Les fichiers

Les entrées/sorties en langage C se font par l'intermédiaire d'entités logiques, appelés flux, qui représentent des objets externes au programme, appelés fichiers. Un fichier peut être ouvert en lecture, auquel cas il est censé nous fournir des données (c'est-à-dire être lu) ou ouvert en écriture, auquel cas il est destiné à recevoir des données provenant du programme. Un fichier peut être à la fois ouvert en lecture et en écriture. Une fois qu'un fichier est ouvert, un flux lui est associé. Un flux d'entrée est un flux associé à un fichier ouvert en lecture et un flux de sortie un flux associé à un fichier ouvert un écriture. Tous les fichiers ouverts doivent être fermés avant la fin du programme.

Lorsque les données échangées entre le programme et le fichier sont de type texte, la nécessité de définir ce qu'on appelle une ligne est primordiale. En langage C, une ligne est une suite de caractères terminée par le caractère de fin de ligne (inclus) : '\n'. Par exemple, lorsqu'on effectue des saisies au clavier, une ligne correspond à une suite de caractères terminée par ENTREE. Puisque la touche ENTREE termine une ligne, le caractère généré par l'appui de cette touche est donc, en C standard, le caractère de fin de ligne soit '\n'.

V-C. Les entrée et sortie standards

Lorsque le système exécute un programme, trois fichiers sont automatiquement ouverts :

  • l'entrée standard par défaut le clavier
  • la sortie standard, par défaut l'écran (ou la console)
  • et l'erreur standard, par défaut associé à l'écran (ou la console)

Respectivement associés aux flux stdin, stdout et stderr. Ils sont automatiquement fermés avant la fin du programme.

Chez la majorité des systèmes, dont Windows et UNIX, l'utilisateur peut rediriger les entrée et sortie standards vers un autre fichier à l'aide des symboles < et >. Par exemple, exécutez le programme qui affiche « Hello, world » à partir de l'interpréteur de commandes à l'aide de la commande suivante (nous supposons que nous sommes sous Windows et que le programme s'appelle hello.exe) :

 
Sélectionnez
hello > sortie.txt

et vous verrez que le message sera imprimé dans le fichier sortie.txt et non à l'écran. Si le fichier n'existe pas, il sera créé. S'il existe déjà, son ancien contenu sera effacé.

Un deuxième et dernier exemple : écrire un programme qui affiche Hello, world en écrivant explicitement sur stdout, autrement dit avec un printf beaucoup plus explicite. On utilisera alors la fonction fprintf qui permet d'écrire du texte sur un flux de sortie, dans notre cas : stdout, ce qui nous donne :

 
Sélectionnez
#include <stdio.h>

int main()
{
    fprintf(stdout, "Hello, world\n");
    return 0;
}

V-D. Exemple : lire un caractère, puis l'afficher

La macro getc permet de lire un caractère sur un flux d'entrée. La macro putc permet d'écrire un caractère sur un flux de sortie. Voici un programme simple qui montre comment utiliser les macros getc et putc :

 
Sélectionnez
#include <stdio.h>

int main()
{
    int c; /* le caractere */
    
    printf("Veuillez taper un caractere : ");
    c = getc(stdin);

    printf("Vous avez tape : ");
    putc(c, stdout);

    return 0;
}

Vous vous demandez certainement la raison pour laquelle on a utilisé int plutôt que char dans la déclaration de c. Et bien tout simplement parce que getc retourne un int (de même putc attend un argument de type int). Mais justement : Pourquoi ? Et bien parce que getc doit pouvoir non seulement retourner le caractère lu (un char) mais aussi une valeur qui ne doit pas être un char pour signaler qu'aucun caractère n'a pu être lu. Cette valeur est EOF. Elle est définie dans le fichier stdio.h. Dans ces conditions, il est clair qu'on peut utiliser tout sauf un char comme type de retour de getc.

Un des cas les plus fréquents où getc retourne EOF est lorsqu'on a rencontré la fin du fichier. La fin d'un fichier est un point situé au-delà du dernier caractère de ce fichier (si le fichier est vide, le début et la fin du fichier sont donc confondus). On dit qu'on a rencontré la fin d'un fichier après avoir encore tenté de lire dans ce fichier alors qu'on se trouve déjà à la fin, pas juste après avoir lu le dernier caractère. Lorsque stdin est associé au clavier, la notion de fin de fichier perd à priori son sens car l'utilisateur peut très bien taper n'importe quoi à n'importe quel moment. Cependant l'environnement d'exécution (le système d'exploitation) offre généralement un moyen de spécifier qu'on n'a plus aucun caractère à fournir (concrètement, pour nous, cela signifie que getc va retourner EOF). Sous Windows par exemple, il suffit de taper en début de ligne la combinaison de touches Ctrl + Z (héritée du DOS) puis de valider par ENTREE. Evidemment, tout recommence à zéro à la prochaine opération de lecture.

Les macros getchar et putchar s'utilisent comme getc et putc sauf qu'elles n'opèrent que sur stdin, respectivement stdout. Elles sont définies dans stdio.h comme suit :

 
Sélectionnez
#define getchar() getc(stdin)
#define putchar(c) putc(c, stdout)

Et enfin fgetc est une fonction qui fait la même chose que getc (qui peut être en fait une fonction ou une macro ...). De même fputc est une fonction qui fait la même chose que putc.

V-E. Saisir une chaîne de caractères

Il suffit de lire les caractères présents sur le flux d'entrée (dans notre cas : stdin) jusqu'à ce que l'on ait atteint la fin du fichier ou le caractère de fin de ligne. Nous devrons fournir en arguments de la fonction l'adresse du tampon destiné à contenir la chaîne de caractère saisie et la taille de ce tampon pour supprimer le risque de débordement de tampon.

 
Sélectionnez
#include <stdio.h>

char * saisir_chaine(char * lpBuffer, size_t nBufSize);

int main()
{
    char lpBuffer[20];

    printf("Entrez une chaine de caracteres : ");
    saisir_chaine(lpBuffer, sizeof(lpBuffer));
    
    printf("Vous avez tape : %s\n", lpBuffer);

    return 0;
}

char * saisir_chaine(char * lpBuffer, size_t nBufSize)
{
    size_t nbCar = 0;    
    int c;

    c = getchar();
    while (nbCar < nBufSize - 1 && c != EOF && c != '\n')
    {
        lpBuffer[nbCar] = (char)c;
        nbCar++;
        c = getchar();
    }

    lpBuffer[nbCar] = '\0';
    
    return lpBuffer;
}

La fonction scanf permet également de saisir une chaîne de caractères ne comportant aucun espace (espace, tabulation, etc.) grâce au spécificateur de format %s. Elle va donc arrêter la lecture à la rencontre d'un espace (mais avant d'effectuer la lecture, elle va d'abord avancer jusqu'au premier caractère qui n'est pas un espace). scanf ajoute enfin le caractère de fin de chaîne. Le gabarit permet d'indiquer le nombre maximum de caractères à lire (caractère de fin de chaîne non compris). Lorsqu'on utilise scanf avec le spécificateur %s (qui demande de lire une chaîne sans espace), il ne faut jamais oublier de spécifier également le nombre maximum de caractères à lire (à mettre juste devant le s) sinon le programme sera ouvert aux attaques par débordement de tampon. Voici un exemple qui montre l'utilisation de scanf avec le spécicateur de format %s :

 
Sélectionnez
#include <stdio.h>

int main()
{
    char lpBuffer[20];

    printf("Entrez une chaine de caracteres : ");
    scanf("%19s", lpBuffer);

    printf("Vous avez tape : %s\n", lpBuffer);

    return 0;
}

Et enfin, il existe également une fonction, gets, déclarée dans stdio.h, qui permet de lire une chaîne de caractères sur stdin. Cependant cette fonction est à proscrire car elle ne permet pas de spécifier la taille du tampon qui va recevoir la chaîne lue.

V-F. Lire une ligne avec fgets

La fonction fgets permet de lire une ligne (c'est-à-dire y compris le '\n') sur un flux d'entrée et de placer les caractères lus dans un buffer. Cette fonction ajoute ensuite le caractère '\0'. Exemple :

 
Sélectionnez
#include <stdio.h>

int main()
{
    char lpBuffer[20];

    printf("Entrez une chaine de caracteres : ");
    fgets(lpBuffer, sizeof(lpBuffer), stdin);

    printf("Vous avez tape : %s", lpBuffer);

    return 0;
}

Dans cet exemple, deux cas peuvent se présenter :

  • l'utilisateur entre une chaîne comportant 18 caractères tout au plus puis valide le tout par ENTREE, alors tous les caractères de la ligne, y compris le caractère de fin de ligne, sont copiés dans lpBuffer puis le caractère de fin de chaîne est ajouté

  • l'utilisateur entre une chaîne comportant plus de 18 caractères (c'est-à-dire >= 19) puis valide le tout par ENTREE, alors seuls les 19 premiers caractères sont copiés vers lpBuffer puis le caractère de fin de chaîne est ajouté

V-G. Mécanisme des entrées/sorties en langage C

V-G-1. Le tamponnage

V-G-1-a. Les tampons d'entrée/sortie

En langage C, les entrées/sorties sont par défaut bufférisées, c'est-à-dire que les données à lire (respectivement à écrire) ne sont pas directement lues (respectivement écrites) mais sont tout d'abord placées dans un tampon (buffer) associé au fichier. La preuve, vous avez certainement remarqué par exemple que lorsque vous entrez des données pour la première fois à l'aide du clavier, ces données ne seront lues qu'une fois que vous aurez appuyé sur la touche ENTREE. Ensuite, toutes les opérations de lecture qui suivent se feront immédiatement tant que le caractère '\n' est encore présent dans le tampon de lecture, c'est-à-dire tant qu'il n'a pas été encore lu. Lorsque le caractère '\n' n'est plus présent dans le buffer, vous devrez à nouveau appuyer sur ENTREE pour valider la saisie, et ainsi de suite.

Les opérations d'écriture sont moins compliquées, mais il y a quand même quelque chose dont il serait totalement injuste de ne pas en parler. Comme nous l'avons déjà dit plus haut, les entrées/sorties sont par défaut bufferisées c'est-à-dire passent par un tampon. Dans le cas d'une opération d'écriture, il peut arriver que l'on souhaite à un certain moment forcer l'écriture physique des données présentes dans le tampon sans attendre que le système se décide enfin de le faire. Dans ce cas, on utilisera la fonction fflush. Nous verrons dans le paragraphe suivant un exemple d'utilisation de cette fonction.

V-G-1-b. Les modes de tamponnage

Le langage C permet de spécifier le mode de tamponnage à utiliser avec un flux donné à l'aide de la fonction setvbuf. Elle doit être appelée avant toute utilisation du fichier.

Il existe 3 modes de tamponnage des entrées/sorties :

  • Pas de tamponnage (_IONBF (no buffering)), dans lequel le flux n'est associé à aucun tampon. Les données sont directement écrites sur ou lues depuis le fichier.

  • Tamponnage par lignes (_IOLBF (line buffering)), le mode par défaut (d'après la norme), dans lequel le flux est associé à un tampon vidé que lorsqu'il est plein, lorsque le caractère de fin de ligne a été envoyé, lorsqu'une opération de lecture sur un flux en mode "Pas de tampon" a été efectuée ou lorsqu'une opération de lecture sur un flux en mode "Tamponnage par lignes" nécessite son vidage. Par exemple, en pratique, lorsque stdout est dans ce mode, toute demande de lecture sur stdin provoquera l'écriture physique des caractères encore dans le tampon. Cela permet d'avoir les questions afichées à l'écran avant que l'utilisateur puisse entrer la réponse.

  • Tamponnage strict (_IOFBF (full buffering)), dans lequel le flux associé à un tampon vidé que lorsqu'il est plein.

Dans tous les cas, la fermeture d'un fichier ouvert en écriture entraîne également l'écriture des caractères qui sont encore dans le tampon, s'il y en a (ce qui a bien évidemment lieu avant sa fermeture).

Le prototype de la fonction setvbuf est le suivant :

 
Sélectionnez

int setvbuf(FILE * f, char * buf, int mode, size_t size);

L'argument mode indique évidemment le mode à utiliser (_IONBF, _IOLBF ou _IOFBF) et l'argument size la taille du tampon (buf) à associer au flux. Si buf vaut NULL, un tampon de taille size sera alloué et associé au fichier. Les arguments buf et size sont évidemment ignorés lorsque mode vaut _IONBF.

Voici un exemple d'utilisation de cette fonction :

 
Sélectionnez
#include <stdio.h>

#define N 1

void loop(unsigned long n);

int main()
{
    setvbuf(stdout, NULL, _IOFBF, 8);

    printf("a\n"); /* Contenu du buffer : [a\n]. */
    loop(N); /* Faire une quelconque longue boucle */

    /* Le buffer n'est pas encore plein, rien ne s'affichera donc sur la sortie standard. */

    printf("b\n"); /* Contenu du buffer : [a\nb\n]. */
    fflush(stdout); /* Vider le buffer. */

    /* Le buffer n'est pas encore plein mais fflush a ete appelee. On aura donc :   */
    /* - Sur la sortie standard : + [a\nb\n].                                       */
    /* - Dans le buffer : [] (rien).                                                */

    loop(N); /* Faire une quelconque longue boucle */

    printf("azertyuiop\n");

    /* On aura :                                          */
    /* - Dans le buffer : [azertyui].                     */
    /* Le buffer est plein, le vidage s'impose. On aura : */
    /* - Sur la sortie standard : + [azertui].            */
    /* - Dans le buffer : [] (rien).                      */
    /* Il reste encore les carateres [op\n]. On aura :    */
    /* - Dans le buffer : [op\n].                         */
    
    loop(N); /* Faire une quelconque longue boucle */

    return 0;

    /* Au dela de l'accolade : Fin du programme.                                           */
    /* Tous les fichiers encore ouverts (dont la sortie standard) seront fermes. On aura : */
    /* - Sur la sortie standard : + [op\n].                                                */
    /* - Dans le buffer : [] (rien).                                                       */
}

void loop(unsigned long n)
{
    unsigned long i, j, end = 100000000;

    for(i = 0; i < n; i++)
        for(j = 0; j < end; j++);
}

V-G-2. Lire de manière sûre des données sur l'entrée standard

Tout d'abord, analysons le tout petit programme suivant :

 
Sélectionnez
#include <stdio.h>

int main()
{
    char nom[12], prenom[12];

    printf("Entrez votre nom : ");
    fgets(nom, sizeof(nom), stdin);

    printf("Entrez votre prenom : ");
    fgets(prenom, sizeof(prenom), stdin);

    printf("Votre nom est : %s", nom);
    printf("Et votre prenom : %s", prenom);

    return 0;
}

Dans ce programme, si l'utilisateur entre un nom comportant moins de 10 caractères puis valide par ENTREE, alors tous les caractères rentrent dans nom et le programme se déroule bien comme prévu. Par contre si l'utilisateur entre un nom comportant plus de 10 caractères, seuls les 11 premiers caractères seront copiés dans nom et des caractères sont donc encore présents dans le buffer du clavier. Donc, à la lecture du prénom, les caractères encore présents dans le buffer seront immédiatement lus sans que l'utilisateur n'ait pu entrer quoi que ce soit. Voici un deuxième exemple :

 
Sélectionnez
#include <stdio.h>

int main()
{
    int n;
    char c;

    printf("Entrez un nombre (entier) : ");
    scanf("%d", &n);

    printf("Entrez un caractere : ");
    scanf("%c", &c);

    printf("Le nombre que vous ave entre est : %d\n", n);
    printf("Le caractere que vous ave entre est : %c\n", c);

    return 0;
}

Lorsqu'on demande à scanf de lire un nombre, elle va déplacer le pointeur jusqu'au premier caractère non blanc, lire tant qu'elle doit lire les caractères pouvant figurer dans l'expression d'un nombre, puis s'arrêter à la rencontre d'un caractère invalide (espace ou lettre par exemple).

Donc dans l'exemple ci-dessus, la lecture du caractère se fera sans l'intervention de l'utilisateur à cause de la présence du caractère '\n' (qui sera alors le caractère lu) due à la touche ENTREE frappée pendant la saisie du nombre.

Ces exemples nous montrent bien que d'une manière générale, il faut toujours vider le buffer du clavier après chaque saisie, sauf si celui-ci est déjà vide bien sûr. Pour vider le buffer du clavier, il suffit de manger tous les caractères présents dans le buffer jusqu'à ce qu'on ait rencontré le caractère de fin de ligne ou atteint la fin du fichier. A titre d'exemple, voici une version améliorée (avec vidage du tampon d'entrée après lecture) de notre fonction saisir_chaine :

 
Sélectionnez
char * saisir_chaine(char * lpBuffer, int nBufSize)
{
    char * ret = fgets(lpBuffer, nBufSize, stdin);
    
    if (ret != NULL)
    {
        char * p = lpBuffer + strlen(lpBuffer) - 1;
        if (*p == '\n')
            *p = '\0'; /* on ecrase le '\n' */
        else
        {
            /* On vide le tampon de lecture du flux stdin */
            int c;
        
            do
				c = getchar();
            while (c != EOF && c != '\n');
        }
    }
    
    return ret;
}

précédentsommairesuivant

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2008 Melem. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.