Page suivante Page précédente Table des matières

10. La bibliothèque standard sous Linux : première partie

Par James M. Rogers.

Le C est un très petit langage, c'est bien. Les programmeurs C utilisent presque tout le langage C quand ils écrivent un programme de taille raisonnable. La bibliothèque C standard étend les fonctions de C dans la mesure où l'on peut la trouver sur de nombreux systèmes. La bibliothèque fournit des outils comme scanf() et printf() qui rendent la lecture et les sorties formatées bien plus aisées qu'avec des blocs de caractères à l'aide des fonctions read() et write(). De plus, lorsque vous passez d'un environnement de programmation C à un autre, les fonctionnalités de printf() seront les mêmes. Vous n'avez pas à réapprendre comment effectuer des sorties formatées chaque fois que vous changez de machine.

Dans cette série d'articles, je traiterai des outils qui sont à la disposition du programmeur dans la bibliothèque standard C. À la fin de l'article, vous trouverez une bibliographie des livres et articles que j'ai utilisés comme base. Je me réfère à ces publications de manière quotidienne quand je programme. Si vous voulez programmer en C, je vous recommande d'acheter et de lire ces livres et articles.

Très souvent, les fonctions standards sont méprisées et réinventées par les programmeurs (dont je fais partie !) pour faire des choses comme savoir si un caractère est une lettre :


(c=>'a'|| c<='z' && c=>'A' || c<='Z') 

au lieu d'utiliser :


isalpha(c); 

La seconde forme est plus facile à lire et à comprendre. Il y a une autre raison d'utiliser celle-ci. Le premier exemple ne marche que pour les jeux de caractères ASCII, la seconde marchera sur toutes les machines. Si vous voulez écrire du code portable (du code qui peut être compilé et exécuté sur n'importe quelle machine avec très peu de changements), vous devez utiliser la bibliothèque C standard.

Plusieurs fois dans le passé, j'ai écrit du code qui m'as pris du temps à écrire, à déboguer et à interfacer avec le reste de mon programme pour seulement découvrir qu'il existait une fonction qui faisait ce que je voulais. Il y a quelques mois, j'ai écrit mon propre Donjon Multi-utilisateurs (Multi User Dimension) basé sur un article sur le client/serveur dans le Linux Journal et je devais traiter ce que l'utilisateur avait entré, un mot à la fois. J'ai donc écrit ma propre fonction de découpage de chaîne. En fait, j'aurais pu utiliser la fonction strtok() qui fait exactement la même chose. Et les autres sauront ce que la fonction strtok() fait sans avoir à déchiffrer mon code.

Rendez vous la vie plus facile, utilisez la bibliothèque C standard. Cela sera aussi une grande aide pour tous ceux qui essayeront de mettre à jour et de maintenir votre code dans le futur.

Le compilateur GNU, gcc, est fourni avec avec la bibliothèque C standard GNU. Ce compilateur est l'un des meilleurs au monde et la bibliothèque C standard GNU se conforme exactement à la norme. Aux endroits où la norme est imprécise, vous pouvez vous attendre à un comportement correct de la part de la bibliothèque et du compilateur. Je parlerai de la bibliothèque C standard GNU dans ces articles.

La bibliothèque <stdio.h> s'occupe des fonctions d'entrées/sorties standards pour les programmeurs C. C'est aussi, et de loin, la plus grosse bibliothèque. du fait de sa taille particulièrement importante. Je grouperai ces commandes dans les sections suivantes : opérations sur les fichiers, entrées et sorties.

Puisque nous parlons des fichiers, nous devons nous mettre d'accord sur les mots que nous allons utiliser. Sous Linux, un fichier ou un périphérique sont considérés comme des flux de données. Ce flux est associé au fichier ou au périphérique et est accédé par l'ouverture du fichier ou du périphérique par votre programme. Une fois le flux ouvert, vous pouvez y lire ou y écrire.

Trois flux sont ouverts automatiquement quand vous exécutez un programme. L'entrée standard (stdin), la sortie standard (stdout) et l'erreur standard (stderr). Ils peuvent tous être redirigés par votre shell quand vous exécutez le programme mais, normalement, stdin équivaut à votre clavier et stdout et stderr à votre moniteur.

Après vous être occupés des flux, vous devez dire au système d'exploitation de vider les tampons et de finir de sauver les données dans les périphériques. Vous faites cela en fermant le flux. Si vous ne fermez pas votre flux, il est possible que vous perdiez des données. Stdin, stdout et stderr sont tous fermés automatiquement, de la même manière qu'ils avaient été ouverts automatiquement.

Une des choses les plus importantes à se rappeler lorsque vous avez affaire à des périphériques et à des fichiers est que vous intervenez au niveau du monde réel. Ne faites pas comme si votre fonction allait fonctionner. Même une fonction telle que printf peut échouer. Les disques se remplissent, sont parfois défectueux, les utilisateurs entrent de mauvaises données, les processeurs sont surchargés, d'autres programmes verrouillent vos fichiers. La Loi de Murphy s'applique parfaitement aux systèmes informatiques. Toute fonction qui intervient sur le monde réel renvoie un code d'erreur en cas d'échec. Vérifiez toujours les valeurs de retour et prenez les dispositions appropriées quand vous êtes confrontés à une condition d'erreur. Les exceptions ne sont pas des erreurs à moins qu'elles soient mal traitées (William Kahan, au sujet du traitement des exceptions).

Le premier exemple vise à montrer comment ouvrir un fichier en lecture. Sa seule action est de renvoyer vers la sortie standard le contenu d'un fichier appelé test. Toutes les exceptions sont envoyées vers la sortie d'erreur standard, le programme étant alors stoppé avec un code d'erreur. Il produit une erreur si le fichier test n'est pas présent.


#include <stdio.h>    /* c'est une directive de compilation qui dit au
                         compilateur d'utiliser les fonctions de la 
                         bibliothèque standard des entrées/sorties
                      */ 
main (){ 

/* déclaration des variables */ 
    FILE *stream;            /* nécessite un pointeur vers FILE pour le flux */ 
    int buffer_character;    /* nécessite un int pour conserver un caractère */ 

/* ouvre le fichier test du répertoire courant en lecture */ 
    stream = fopen("test", "r"); 

/* Si le fichier n'est pas ouvert correctement, stream sera égal à NULL. 
   Il est maintenant courant de représenter NULL en transtypant la valeur 
   0 vers le type correct par vous-même plutôt que de laisser le compilateur 
   deviner lui-même du type de NULL à utiliser
*/ 
    if (stream == (FILE *)0) { 
        fprintf(stderr, "Erreur d'ouverture de fichier (envoyé sur l'erreur
        standard)\n"); 
        exit (1); 
        }  /* end if */ 

/*  lecture et écriture du fichier un caractère à la fois jusqu'à la fin du
    fichier, de lecture ou d'écriture. Si le EOF est atteint sur le 
    descripteur de fichier, on sort de la boucle while. Si l'EOF intervient 
    sur l'écriture vers la sortie standard on sort du programme avec une 
    condition d'erreur
*/ 
    while ((buffer_character=getc(stream))!=EOF) { 
        /* écriture le caractère vers la sortie standard et vérification 
           des erreurs */ 
        if((putc(buffer_character, stdout)) == EOF) { 
            fprintf(stderr,"Erreur d'écriture vers la sortie standard.
            (imprimé sur l'erreur standard)\n"); 
            fclose(stream); 
            exit(1); 
            }  /* end if */ 
        }  /* end while */ 

/* fermeture du fichier après en avoir fini; en cas d'échec de fermeture, la
   signaler et terminer
*/ 
    if ((fclose(stream)) == EOF) { 
        fprintf(stderr,"Erreur de fermeture du flux. (imprimé sur l'erreur
        standard)\n"); 
        exit(1); 
        }  /* end if */ 

/* rapport de succès vers l'environnement  */ 
    return 0; 

}  /* end main*/

Le simple programme ci-dessus est un exemple d'ouverture, de lecture puis d'écriture sur un fichier à l'aide de stdout et stderr. J'ai copié-collé le code dans une session vi, puis je l'ai sauvé, compilé et exécuté.

Ce qui suit est un résumé rapide des opérations sur les fichiers dans la bibliothèque <stdio.h>. Ce sont des opérations qui marchent directement sur les flux.

10.1 Ouverture de flux

Avant de pouvoir utiliser un flux, vous devez l'associer à un périphérique ou à un fichier. On appelle ça "ouvrir le flux". Votre programme demande à votre système d'exploitation de lire ou d'écrire vers un périphérique. Si vous avez les bons droits, si le fichier existe ou si vous pouvez créer le fichier et que personne n'a verrouillé le fichier, alors le système d'exploitation vous permet d'ouvrir le fichier et vous renvoie un objet, le flux. En utilisant cet objet, vous pouvez lire et écrire vers le flux et, quand vous avez fini, vous pouvez fermer celui-ci.

Je vais maintenant vous décrire le format des descriptions que vous verrez ici et dans les pages de man. La première entrée est le type qui est retourné par l'appel de fonction. La seconde est le nom de la fonction elle-même et la troisième est ma liste des types de variables que la fonction prend en arguments.

En regardant la première ligne ci-dessous, vous pouvez voir que la fonction fopen prend deux pointeurs sur des chaînes, le premier étant le chemin vers un fichier et le second étant le mode d'ouverture du programme. La fonction retourne un pointeur vers un type FILE qui est un objet composé défini dans <stdio.h>. De manière à recueillir le type de retour, vous devez avoir déclaré une variable de type "pointeur vers FILE" comme la variable de flux de l'exemple ci-dessus à la ligne 9. À la ligne 13, vous pouvez voir un appel à la fonction fopen avec le nom de fichier statique "test" et le mode d'ouverture "r" : la valeur de retour est envoyé dans l'objet flux.

Un flux peut être ouvert par n'importe laquelle de ces trois fonctions :


FILE *fopen( char *path, char *mode)
FILE *fdopen( int fildes, char *mode)
FILE *freopen( char *path, char *mode, FILE *stream)

char *path est un pointeur vers une chaîne avec un nom de fichier dedans. char *mode est le mode d'ouverture du fichier (voir la table ci-dessous). int fildes est un descripteur de fichier qui est déjà ouvert et dont le mode correspond.

Il est possible d'obtenir un descripteur de fichier grâce à la fonction système UNIX open. Veuillez noter qu'un descripteur de fichier n'est pas un pointeur vers FILE. Vous ne pouvez pas utiliser close(stream), vous devez utiliser fclose(stream). C'est une erreur très difficile à débusquer si votre compilateur ne vous en avertit pas. Si vous êtes intéressés par les appels système Linux, tapez man 2 intro pour une introduction aux fonctions et à ce qu'elles font.

FILE *stream est un flux déjà existant.

Ces fonctions retournent un pointeur vers un type FILE qui représente un flux e données ou un NULL de type (FILE *)0 pour toute condition d'erreur.

fopen est utilisée pour ouvrir le fichier donné avec le mode indiqué. C'est la fonction la plus utilisée pour ouvrir des fichiers.

fdopen est utilisée pour assigner un flux à un descripteur de fichier déjà ouvert. Le mode du descripteur de fichier et de fdopen doivent correspondre.

freopen est normalement utilisée pour rediriger stdin, stdout et stderr vers un fichier. Le flux qui est fourni sera fermé et un nouveau flux sera ouvert vers le chemin donné, avec le mode donné.

Cette table montre les modes et leurs résultats.

       ouverture pour   tronquer créer   position de
mode   lecture écriture fichier  fichier démarrage 
----   ----    -------  ------   ------  --------- 
"r"     y        n       n        n      début 
"r+"    y        y       n        n      début 
"w"     n        y       y        y      début 
"w+"    y        y       y        y      début 
"a"     n        y       n        y      fin de fichier 
"a+"    y        y       n        y      fin de fichier

la première ligne indique que "r" ouvrira un flux en lecture, le flux ne sera pas ouvert en écriture, ne tronquera pas le fichier à une taille de zéro, ne créera pas le fichier s'il n'existe pas déjà et sera positionné au début du fichier.

10.2 Vidage de flux

Il arrive parfois que vous vouliez vous assurer d'avoir écrit un fichier sur le disque et qu'il n'attende pas dans un tampon. De même, vous pouvez souhaitez vous débarrasser d'entrées utilisateurs pour en avoir de nouvelles, pour un jeu par exemple. Les deux fonctions suivantes sont utiles pour vider les tampons de flux. L'une se débarrasse des données alors que l'autre les stocke de manière sûre dans le flux.


int fflush(FILE *stream)
int fpurge(FILE *stream)

FILE *stream est un flux déjà existant.

Ces fonctions retournent 0 en cas de succès, EOF sinon.

fflush est utilisée pour vider en écrivant les tampons du flux vers un périphérique ou un fichier.

fpurge est utilisée pour vider les tampons des données non écrites ou non lues qui attendent dans un tampon. J'en parle comme d'une purge destructrice car elle vide les tampons d'écriture et de lecture en se débarrassant de leur contenu.

10.3 Fermeture de flux

Quand vous en avez fini avec un flux, vous devez tout nettoyer après votre programme. Quand vous fermez un flux, la commande assure que les tampons sont écrits avec succès et que le flux est vraiment fermé. Si vous quittez un programme sans fermer vos fichiers, il est plus que probable que les derniers octets que vous avez écrits seront là. Mais vous ne savez pas avant d'avoir vérifié. Il y a également une limite au nombre de flux qu'un processus peut ouvrir à la fois. Dès lors, si vous n'arrêtez pas d'ouvrir des flux sans fermer les vieux flux, vous allez gaspiller vos ressources système. Une seule commande est utilisée pour fermer les flux.

int fclose(FILE *stream)

FILE *stream est un flux déjà ouvert.

Retourne 0 sur un succès, EOF sinon.

fclose vide les tampons de flux donnés et les désassocie du pointeur vers FILE.

10.4 Renommage et effacement de fichiers

Ces deux commandes fonctionnent de la même manière que rm et mv mais sans les options. Elles ne sont pas récursives mais vos programmes peuvent l'être, donc faites attention de ne pas implémenter accidentellement votre propre version de rm -rf /. Par ailleurs, ne tapez pas cette commande, elle effacerait entièrement votre disque dur !!!


int remove(char *path)
int rename(char *oldpath, const char *newpath)

char *path, oldpath et newpath sont des pointeurs vers des fichiers existants.

Retourne 0 sur un succès, une valeur différente de 0 sinon.

remove fonctionne comme rm pour effacer le fichier dans la chaîne pointée par le chemin.

rename marche comme mv pour renommer un fichier de oldpath vers newpath, en changeant de répertoire si nécessaire.

10.5 Fichiers temporaires

Vous pouvez créer vos propres fichiers temporaires en utilisant les fonctions suivantes :

FILE *tmpfile(void)

Cette commande retourne un pointeur vers un FILE qui est un fichier temporaire qui disparaîtra magiquement quand votre programme finira son exécution. Vous ne saurez même jamais son nom. Si la fonction échoue, elle retourne un pointeur NULL de type (FILE *)0.

char *tmpnam(char *string)

Cette fonction retourne un nom de fichier dans le répertoire tmp qui est unique, ou NULL en cas d'erreur. Tout appel additionnel efface le nom précédent : il est donc nécessaire de déplacer le fichier autre part si vous devez en connaître le nom après l'ouverture du fichier.

10.6 Tampon de flux

Normalement, un flux est géré en tampon de bloc à moins d'être connecté à un terminal tel que stdin ou stdout. En mode de tampon de bloc, le flux lit un certain ensemble de données et vous donne de l'entrée ce que vous lui avez demandé quand vous lui demandez. Il arrive que vous vouliez un ensemble plus ou moins important pour améliorer les performances de certains programmes. Les quatre fonctions qui suivent peuvent être utilisées en positionnant le type de tampon et la taille des tampons. Les options par défaut correspondent normalement tout à fait à l'usage que l'on veut en faire, vous n'avez donc pas à trop vous inquiéter de cela.


int     setbuf( FILE *stream, char *buf); 
int     setbuffer( FILE *stream, char *buf, size_t size); 
int     setlinebuf( FILE *stream); 
int     setvbuf( FILE *stream, char *buf, int mode , size_t size); 

Où mode est l'un des suivants :

  • _IONBF, non tamponné, la sortie est envoyée dès réception.
  • _IOLBF, tamponné par ligne, la sortie est envoyée dès réception d'une fin de ligne.
  • _IOFBF, complètement tamponné, la sortie n'est pas envoyée avant réception d'un nombre size de caractères.

  • setbuf est un alias pour setvbuf(stream, buf, buf ? _IOFBF : _IONBF, BUFSIZ)
  • setbuffer est un alias pour setvbuf(stream, buf, buf ? _IOFBF : _IONBF, size)
  • setlinebuf est un alias pour setvbuf(stream, (char *)NULL, _IOLBF, 0)
  • setvbuf positionne un tampon vers un flux donné de taille size_t et de mode mode.

10.7 Positionnement dans un flux

Une fois un flux ouvert, vous êtes placés à une certaine position en fonction du mode d'ouverture du flux. Lors de toute écriture ou lecture, votre position augmente avec chaque caractère. Vous pouvez savoir où vous êtes dans le flux ainsi que sauter à n'importe quelle position dans le flux. Si vous écrivez un programme de base de données, vous ne voulez certainement pas lire et ignorer un million de caractères avant de trouver l'enregistrement que vous voulez ; ce que vous voulez, c'est sauter directement à l'enregistrement et commencer la lecture.

Notez que les terminaux ne peuvent pas voir leurs flux repositionnés, seuls les périphériques par blocs (comme les disques durs) le permettront.

Notez également que si vous ouvrez un fichier en écriture et que vous utilisez fseek pour vous déplacer de 10 000 octets, que vous écrivez un caractère et qu'ensuite vous fermez le fichier, vous n'aurez pas un fichier de 10 0001 octets. Le fichier sera bien plus petit. On appelle ça un fichier sparse. Si vous déplacez un fichier sparse en utilisant la commande mv, il ne changera pas de taille car mv est seulement un changement de la structure des répertoires, pas du fichier. Si vous faites un cp ou un tar sur un fichier sparse, il s'étendra à sa vraie taille.


int   fseek( FILE *stream, long offset, int whence); 
long  ftell( FILE *stream); 
void  rewind( FILE *stream); 
int   fgetpos( FILE *stream, fpos_t *pos); 
int   fsetpos( FILE *stream, fpos_t *pos); 

  • FILE *stream est un pointeur vers un fichier existant.
  • long offset est ajouté à la position indiqué par whence. int whence est soit SEEK_SET, SEEK_CUR, ou SEEK_END selon l'endroit où vous voulez appliquer l'offset : le début, la position courante ou la fin.
  • fpos_t *pos est un indicateur de position dans un fichier complexe.

Sur certains systèmes, vous devez utiliser ceci pour récupérer et positionner les positions dans le flux.

Si ces fonctions s'exécutent avec succès, fgetpos, fseek, fsetpos retournent 0, ftell retourne l'offset courant. Autrement, EOF est renvoyé. Il n'y a pas de valeur de retour pour rewind.

fseek affecte la position dans le fichier du flux à la valeur de l'offset plus la position indiquée par whence, que ce soit le début, la position courante ou la fin. Cela est utile pour la lecture linéaire, l'ajout à la fin d'un flux et revenir au point de lecture que vous avez quitté.

ftell retourne la position courante du flux.

rewind met la position courante au début du flux. Veuillez noter qu'aucun code d'erreur n'est retourné. Cette fonction est sensée toujours bien s'exécuter.

fgetpos est utilisé comme ftell pour retourner la position du flux. Cette position est retournée dans la variable pos qui est du type fpos_t.

fsetpos est utilisé comme fseek dans la mesure où il va mettre la position courante dans le flux à la valeur dans pos.

Sur certains systèmes, vous devez utiliser fgetpos et fsetpos pour positionner de manière fiable votre flux.

10.8 Codes d'erreur

Quand l'une des fonctions ci-dessus retourne une erreur, vous pouvez voir ce que l'erreur était et même obtenir un message d'erreur à afficher à l'utilisateur. Il existe un groupe de fonctions qui traitent les valeurs d'erreur. Il suffit, pour le moment, de reconnaître les erreurs et arrêter l'exécution.

Cependant, si vous écrivez un traitement de texte avec une belle interface graphique, vous ne souhaitez certainement pas qu'il s'arrête à chaque fois que vous ne pouvez pas ouvrir un fichier. Vous voulez plutôt afficher le message d'erreur à l'utilisateur et continuer. Dans un article futur, je parlerai des fonctions d'erreur ou alors quelqu'un peut les résumer pour nous et envoyer un article accompagné d'un code source commenté pour nous montrer comment cela fonctionne.

Si quelqu'un est intéressé, ces fonctions sont clearerr, feof, ferror et fileno.

10.9 Conclusion

Voilà, c'est tout pour ce mois-ci. J'ai beaucoup appris et j'espère que vous aussi. La plus grande part de ces informations est disponible à travers les pages de man mais les dates qui figurent sur celles-ci datent de 4 ans. Si quelqu'un dispose de mises à jour de celles ci, veuillez me les envoyer et je me corrigerai dans les articles futurs.

Le mois prochain, je parlerai des entrées/sorties. Je prendrai le programme listé plus haut et y ajouterais quelques fonctionnalités pour ajouter une colonne de nombres et sortir le résultat vers la sortie standard. Ce programme d'exemple pourra faire quelque chose d'utile.

10.10 Bibliographie :

  • The ANSI C Programming Language, Second Edition, Brian W. Kernighan, Dennis M. Ritchie, Printice Hall Software Series, 1988
  • The Standard C Library, P. J. Plauger, Printice Hall P T R, 1992
  • The Standard C Library, Parts 1, 2, and 3, Chuck Allison, C/C++ Users Journal, January, February, March 1995
  • STDIO(3), BSD MANPAGE, Linux Programmer's Manual, 29 November 1993
  • Unidentified File Objects, Lisa Lees, Sys Admin, July/August 1995
  • A Conversation With William Kahan, Jack Woehr, Dr Dobb's Journal, November 1997
  • Java and Client-Server, Joe Novosel, Linux Journal, January 1997


Copyright © 1998, James Rogers - Publié dans le Numéro 24 de la Linux Gazette

Adaptation française de Pierre Tane


Page suivante Page précédente Table des matières