|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Voici un article sur les spécifications de la norme ANSI pour le langage C, avec comme plat de résistance les fonctions de la bibliothèque standard. ANSI est l'abréviation de American National Standards Institute, ce qui signifie en français et à peu de choses près, Institut National Américain de Standardisation (INAS). Mais bon, ANSI, c'est tout de même plus simple. Le but de ce vénérable établissement est tout simplement et comme son nom l'indique, de fournir au bon peuple américain des standards pour tout ce qu'il est possible de standardiser. Et Dieu sait que rien qu'en informatique, ces gens-là ont bien du boulot... On ne reviendra pas particulièrement sur le rôle et l'utilité d'un standard, qu'il s'agisse de machines à laver ou de contrôleurs de disques optiques réinscriptibles à triple faisceau laser orthogonalement entrelacé. Les travaux informatiques de l'ANSI sont mondialement connus, ne serait-ce que concernant les codes de contrôles des consoles d'ordinateurs, codes auxquels l'Amiga a d'ailleurs totalement adhéré. Ainsi, un fichier texte ASCII contenant quelques codes ANSI de mise en valeur (gras, italique, etc.) sera visualisé exactement de la même manière sur un Amiga que sur un PC équipé du pilote ANSI.SYS. Sauf que ce sera plus joli sur l'Amiga, car bon, quand même, hein, faut pas déconner. L'un des travaux les plus importants de l'ANSI a surtout été de fournir une standardisation pour presque tout langage informatique évolué, de façon à le rendre indépendant de la machine. C'est ainsi qu'il existe une norme ANSI pour le BASIC, pour le Pascal, le Lisp... Et bien entendu le C, qui nous intéresse plus particulièrement aujourd'hui. K&R vs ANSI Les créateurs du langage C, messieurs Kernighan et Ritchie, avaient déjà entrepris un effort de standardisation de leur bébé, que l'on a tout naturellement baptisé "norme K&R" et qu'ils ont exposé dans leur livre "Le Langage C". Seulement voilà, deux hommes seuls, et malgré toute la bonne volonté dont ils ont pu faire preuve, ne sauraient penser à tout. Aussi les ANSI-mens se sont-ils penchés sur le problème et ont étendu cette première norme, tout en essayant d'en rester le plus près possible (le problème étant bien entendu de rester compatible). Résultat, la norme ANSI, par ailleurs elle aussi décrite dans un excellent livre "Le Langage C, 2ème édition" signé... Kernighan et Ritchie. Par la suite, nous ne parlerons plus de la norme K&R, qui n'est finalement plus qu'un sous-ensemble de la norme ANSI. Pour la petite histoire, on peut signaler que la norme K&R date de 1978, cependant que la norme ANSI date, elle, de 1983, et qu'elle fut définitivement acceptée fin 1988. Principes Aujourd'hui, on trouve de çà et de là divers compilateurs C, dont beaucoup se réclament compatibles ANSI. L'utilisation d'un tel compilateur ANSI sur une machine donnée, garantira que le code source sera directement accepté par un autre compilateur ANSI sur une autre machine, disposant d'un autre système d'exploitation. Cela suppose donc qu'un certain nombre de fonctions, notamment d'entrées-sorties, soit effectivement disponibles dans les deux environnements. Comble de bonheur, ces fonctions sont toutes regroupées dans une bibliothèque (en anglais, library) qui se doit d'être standard. Peu importe donc pour le programmeur C la manière dont ces fonctions ont été effectivement implémentées sur telle ou telle machine, il est sûr de les retrouver quel que soit l'environnement et surtout, il est sûr que le nombre et la signification des paramètres de chacune sont identiques. C'est ça, la standardisation. Limites Pour être indépendante de la machine, la norme doit faire quelques sacrifices. Premier exemple, très parlant : l'ouverture d'une fenêtre sur Amiga s'effectue par un appel de la fonction OpenWindow() de l'intuition.library. Bien. MS-DOS ne disposant pas de système de fenêtrage, un programme Amiga utilisant OpenWindow() ne sera pas directement portable en MS-DOS. Quant aux Macintosh, Archimedes, Atari ST et autre NeXT, même s'ils disposent de fenêtres graphiques, ils n'ont pas d'intuition.library, donc pas d'OpenWindow(), donc notre programme ne sera pas non plus portable sur ces machines. Ce qui explique sans aucun doute possible que la fonction OpenWindow() ne fasse pas partie de la norme ANSI. Autre exemple, moins évident : supposons que l'on désire ouvrir un fichier à l'aide de la fonction ANSI appropriée. Assumons pour les besoins de l'exemple que cette fois-ci, la portabilité est totale, et que le source soit compilé sans aucune erreur particulière quelque soit le système. Des problèmes risquent encore de surgir à l'exécution du programme : AmigaDOS autorise des noms de fichiers d'une longueur de 30 caractères maximum, et le point décimal n'est pas considéré comme un séparateur particulier. Cette règle est également vraie sur Mac, mais pas en MS-DOS, où seuls huit caractères maximum sont autorisés, plus le point, plus trois autres caractères pour l'extension. Ce second exemple démontre clairement que toute la standardisation du monde ne saurait suffire à assurer une portabilité parfaite. Le programmeur doit également faire attention à ne pas utiliser, directement ou indirectement, telle ou telle particularité d'un système donné, sous peine de problèmes sérieux. En cas de conflit, comme dans l'exemple précédent, il devra se conformer aux exigences du système le plus contraignant (ici, MS-DOS) ou modifier explicitement chacune des versions de son oeuvre en conséquence - mais alors, à quoi donc sert une norme ? Prototypes Encore une petite digression avant de parler des fonctions elles-mêmes. L'un des principaux apports de la norme ANSI pour le langage C est la possibilité de déclarer des prototypes pour les fonctions que le programme emploie. Le raisonnement est simple : avant, la norme K&R indiquait qu'une déclaration de fonction ne spécifiait que le type de la fonction, c'est-à-dire et plus exactement, le type du résultat que cette fonction renvoyait. Par exemple :
Le nombre et le type des paramètres qu'elle exigeait n'était connu du compilateur qu'au moment-même de la compilation de la fonction. De plus, une fonction non-déclarée avant son utilisation était automatiquement considérée comme renvoyant un entier (int). Depuis la norme ANSI, une déclaration de fonction peut également indiquer le nombre et le type de ses paramètres. Ainsi, le compilateur peut vérifier dès l'appel de la fonction que le programmeur n'a pas commis d'erreur. Pour reprendre l'exemple ci-dessus, la déclaration de notre fonction Puissance() devient :
Il est même possible de donner également les noms des paramètres. Cela n'ajoute rien aux vérifications que le compilateur peut entreprendre, mais peut rendre le listing plus clair et plus facilement compréhensible par une tierce personne :
L'ancienne forme est toujours valable, et tout compilateur se doit de l'accepter. Cela dit, les avantages de la seconde forme sont évidents, et je vous encourage plus que vivement à l'utiliser chaque fois que vous le pourrez. On pourrait continuer à disserter pendant des heures sur toutes les modifications, bonnes et mauvaises, que la norme ANSI a apporté à la norme K&R, mais ce n'est pas vraiment là le but de cet article... La bibliothèque Découvrons à présent quelles fonctions l'on trouve dans la bibliothèque standard ANSI, ainsi que leur utilité. Note : pour le compilateur SAS/Lattice C 5.10, cette bibliothèque porte le nom évocateur de lc.lib, ou lcs.lib pour le modèle "small" (entiers 16 bits), ou lcr.lib pour la version réentrante, ou encore lcm.lib pour la version avec nombres en virgule flottante... Évidemment, seules les fonctions effectivement utilisées seront placées par l'éditeur de liens dans le programme exécutable final. En Manx/Aztec C, je ne sais pas, vérifiez avec votre documentation. La bibliothèque ne fait pas partie du langage C proprement dit. Cependant, tout compilateur C ANSI fournira les déclarations de fonctions, de macros et de types de cette bibliothèque. Elle se base sur plusieurs fichiers d'en-tête (header files), eux aussi standards :
Leur rôle est donc de définir les prototypes des fonctions de la bibliothèque (printf() et consoeurs), ainsi que de fournir au programmeur des constantes (la plus célèbre étant NULL), des macros (max() par exemple) et des types (size_t par autre exemple), utiles pour son travail de développement. Ils contiennent également des références aux variables globales de la bibliothèque, telle que errno pour ne citer qu'elle, qui contient le code d'erreur éventuel de la dernière fonction appelée. Encore une fois et au risque de me répéter, toutes ces fonctions, constantes et macros sont (censées être) disponibles sur tous les systèmes de France, de Navarre et des environs. La multiplicité des fichiers d'en-tête s'explique tout simplement par une envie de regrouper par thèmes toutes les déclarations. Ansi, <stdio.h> déclare les fonctions d'entrées-sorties, <string.h>, celles destinées à manipuler les chaînes de caractères, et <time.h> celles de gestion de l'heure système. C'est simple, pratique et économique en temps de compilation : si on n'a pas besoin de nombres flottants dans un programme, on n'inclut pas <math.h> et c'est toujours ça de gagné. Petite précision Tous les objets que nous allons voir dans ce qui suit, travaillent avec certains types de données, notamment les "int" (entiers) ou les "float" (flottants). D'autres travaillent avec des structures, composées d'objets de plusieurs types. Le programmeur n'a normalement pas à connaître la taille de ces objets. En d'autres termes, il n'a pas à se soucier de savoir si, sur telle ou telle machine, dans tel ou tel environnement, un int est codé sur deux, quatre ou même huit octets. Si une telle connaissance est souvent primordiale pour accéder aux ressources d'un système donné (par exemple pour l'Amiga, les structures définies par Intuition ou la graphics.library), il suffit de savoir que, dans le cas de la bibliothèque standard, telle ou telle fonction renvoie un int, un char, ou un pointeur quelconque. Si l'on a absolument besoin de connaître la taille d'un int (par exemple, pour une allocation mémoire), l'opérateur sizeof() doit être utilisé. Enfin, certains types définis particuliers seront rencontrés au cours de notre exploration de la bibliothèque. Nous les détaillerons à ce moment là. Les entrées-sorties : stdio.h Nous attaquons par le plus gros morceau... Les fonctions, macros et types d'entrées-sorties (Input-Output, ou plus simplement IO), représentent presque un tiers de la bibliothèque. On y trouve notamment la définition des flux : il ne s'agit ni plus ni moins que d'un flot de données associées à un périphérique quelconque (disque bien sûr, mais également clavier, écran, imprimante...). Les flux peuvent être gérés soit en mode texte, soit en mode binaire. Dans le premier mode, les données sont organisées en lignes, conclues par le caractère de fin de ligne, "\n". Ce caractère peut être, de manière interne, représenté soit par CR (ASCII 13), soit par LF (ASCII 10), soit par une combinaison des deux. C'est le système qui gère cela. Un flux binaire est constitué d'une suite d'octets non traités par le système, même s'il devait contenir un ou plusieurs caractères de fin de ligne. C'est le programmeur qui décide d'ouvrir un flux en mode texte ou au contraire en mode binaire. Au début de l'exécution du programme, le code de démarrage ouvre toujours les trois flux standard stdin (entrée), stdout (sortie) et stderr (sortie d'erreur, peut être différent de stdout). Les fichiers L'ouverture d'un fichier renvoie un objet de type FILE, contenant toutes les informations nécessaires au contrôle du flux. Le type FILE est défini ainsi : ![]() ![]()
"fopen()" tente d'ouvrir le fichier décrit par "filename", dans le mode défini par "mode", et retourne un flux en cas de succès, et NULL sinon. "mode" peut être :
Note : FILENAME_MAX n'est pas défini en Lattice/SAS C 5.10, et FOPEN MAX est défini sous le nom de _NFILES.
"freopen()" ouvre le fichier décrit par "filename" dans le mode défini par "mode", et lui associe le flux "stream". On utilise en général freopen() pour changer les fichiers associés à stdin, stdout et stderr.
"fflush()" provoque l'écriture anticipée des données encore présentes dans la mémoire tampon du flux "stream", qui doit avoir été ouvert en écriture. Son effet est indéfini pour un flux ouvert en lecture. Elle retourne EOF pour une erreur d'écriture, 0 si tout va bien.
"fclose()" force l'écriture des données encore présentes dans la mémoire tampon du flux "stream", libère tout tampon mémoire alloué automatiquement et ferme le flux, qui devient inutilisable. Elle retourne EOF en cas d'erreur, 0 sinon.
"remove()" détruit le fichier décrit par "filename". Elle retourne une valeur différente de 0 en cas d'échec (fichier absent, erreur disque...).
"rename()" change le nom du fichier "oldname" en "newname". Elle retourne une valeur différente de 0 en cas d'échec.
"tmpfile()" crée un fichier temporaire dans le mode "wb+", qui sera automatiquement détruit lors de sa fermeture ou lors de la fin normale du programme. Note : cette fonction n'est pas définie dans le stdio.h du Lattice/SAS C 5.10.
"setvbuf()" permet de contrôler la mise en mémoire tampon du flux "stream". Il faut l'appeler avant la première lecture ou écriture sur le flux. Le mode _IOFBF provoque un tamponnage complet, _IOLBF un tamponnage par ligne de texte et _IONBF ne provoque pas de tamponnage. Si "buf" est différent de NULL, il sera utilisé comme tampon, sinon il sera alloué. "size" détermine la taille du tampon. "setvbuf()" retourne une valeur différente de 0 en cas d'erreur. Le type "size_t" est défini comme "typedef unsigned int size_t".
"fread()" lit sur le flux "stream", "nobj" objets de taille "size" et les place dans le tampon mémoire "ptr". Elle retourne le nombre d'objets effectivement lus, qui peut être inférieur au nombre demandé.
"fwrite()" écrit dans le flux "stream, nobj" objets de taille "size" depuis le tampon mémoire "ptr". Elle retourne le nombre d'objets effectivement écrits, qui peut être inférieur au nombre demandé.
"fseek()" positionne le pointeur de fichier pour le flux "stream". Pour un fichier binaire, la nouvelle position est fixée à "offset" octets du début si "origin" vaut SEEK SET, de la fin si "origin" vaut SEEK END, ou de la position courante si "origin" vaut SEEK CUR. Pour un fichier texte, "offset" doit valoir 0 (ou une valeur retournée par "ftell()") et "origin" doit valoir SEEK SET. "fseek()" retourne une valeur non nulle en cas d'erreur.
"ftell()" retourne la position courante dans le fichier pour le flux "stream", ou -1L en cas d'erreur.
"rewind()" replace le pointeur de fichier au début du flux "stream" et réinitialise toute erreur détectée. "rewind(fp)" est l'équivalent de fseek(fp, 0, SEEK_SET); clearerr(fp);.
"fgetpos()" place dans "*ptr" la position courante dans "stream". Elle retourne une valeur non nulle en cas d'erreur. Le type "fpos_t" est défini comme "typedef unsigned long fpos_t".
"fsetpos()" déplace le pointeur de fichier de "stream" à la position mémorisée dans "*ptr" par fgetpos(). "fsetpos()" retourne une valeur non nulle en cas d'erreur.
"clearerr()" remet à 0 les indicateurs de fin de fichier et d'erreur du flux "stream".
"feof()" retourne une valeur non nulle si l'indicateur de fin de fichier (EOF) est positionné pour le flux "stream". Plusieurs types particuliers, généralement définis par un "typedef" bien placé et quelques fois par un "#define" de derrière les fagots, y figurent également. C'est notamment le cas de FILE, size_t et fpos_t, entrevus précédemment. D'autres types seront rencontrés, que nous expliciterons le moment venu.
"ferror()" retourne une valeur non nulle si l'indicateur d'erreur est positionné pour le flux "stream".
"perror(s)" imprime la chaîne "s" suivie d'un message d'erreur (dépendant du système) qui correspond à l'entier "errno". C'est l'équivalent de fprintf(stderr, "%s: %s\n", s, sys_errlist[errno]);.
"fgetc()" retourne le caractère suivant du flux "stream", converti en int. "fgetc()" retourne EOF si la fin de fichier est atteinte.
"fgets()" lit au plus n-1 caractères dans le flux "stream" et les place dans la chaîne "s", en s'arrêtant si elle rencontre un caractère de fin de ligne "\n". La chaîne est ensuite terminée par le caractère nul "\0". "fgets()" retourne "s" si tout s'est bien passé, et NULL en cas d'erreur.
"fputc()" écrit le caractère "c" dans le flux "stream". Elle retourne le caractère écrit, ou EOF en cas d'erreur.
"fputs()" écrit la chaîne "s" dans le flux "stream". Elle retourne EOF en cas d'erreur.
"getc()" équivaut à "fgetc()", mis à part que c'est une macro (qui peut donc évaluer stream plusieurs fois).
"getchar()" est une macro équivalant à "getc(stdin)".
"gets()" agit comme "fgets()", si ce n'est qu'elle utilise automatiquement "stdin" et qu'aucune vérification de longueur de la chaîne en entrée n'est effectuée. Elle retourne "s" si tout va bien, ou NULL en cas d'erreur.
"putc()" équivaut à "fputc()", mis à part que c'est une macro.
"putchar()" est une macro équivalant à "putc(c, stdout)".
"puts()" écrit la chaîne "s" et le caractère de fin de ligne "\n" sur stdout. Elle retour EOF en cas d'erreur.
"ungetc()" remet "c" dans le flux "stream", où il sera retrouvé à la prochaine lecture. Elle retourne le caractère remis, ou EOF en cas d'erreur.
"fprint()" convertit ses arguments (en nombre variable) d'après le format spécifié par la chaîne "format", et écrit le résultat dans le flux "stream". Elle retourne le nombre de caractères écrits, ou une valeur négative en cas d'erreur. La chaîne de formatage est décrite sous printf().
"fscanf()" lit les données depuis le flux "stream", d'après le format défini par la chaîne "format", et affecte les valeurs converties à chacun de ses arguments (en nombre variable), chacun de ceux-ci devant être un pointeur. Elle retourne le nombre d'objets convertis et affectés si tout va bien, et EOF en cas d'erreur. La chaîne de formatage est décrite sous scanf(). Les listes variables d'arguments La bibliothèque standard C met à la disposition du programmeur des fonctions dont le nombre et le type des arguments peut être variable. L'exemple le plus frappant en est printf() et ses consoeurs. Il n'y a bien entendu là-dedans aucun truc particulier (seulement une bonne gestion de la pile par le compilateur !) et le programmeur peut lui-même créer de telles fonctions. Bien que cela relève du fichier d'en-tête stdarg.h, il nous a paru opportun de faire un aparté sur le sujet. On déclare une telle fonction en spécifiant trois points de suspension comme son dernier argument :
Dans cet exemple, notre fonction accepte au minimum deux arguments, appelés "arg1" et "arg2", respectivement de type "char" et "int". D'autres peuvent suivre, de n'importe quel type. Pour implémenter cette fonction, on définit une liste d'arguments de type "va_list", que l'on initialise (une seule fois !) au moyen de la macro "va_start", à laquelle on fournit le nom du dernier argument obligatoire :
Par la suite, chaque appel à la macro "va_arg" retournera un objet avant le type et la valeur de l'argument suivant non nommé, et modifiera la liste "va" de telle sorte qu'elle pointe sur le prochain argument. Quand tous les arguments transmis auront été traités, il faut impérativement appeler la macro "va_end" avant de sortir de la fonction. Pour illustrer tout ceci, la fonction suivante concatène toutes les chaînes qui lui sont transmises en une seule, dans le tampon mémoire"buf". Elle retourne le nombre total de caractères placés dans "buf". ![]() La grande majorité des fonctions d'entrées-sorties de la bibliothèque standard du C version ANSI, est dérivée de printf(), dont la chaîne de formatage est un petit bijou de puissance... et de complexité. Rappelons en effet que toutes les fonctions de type printf() acceptent un nombre variable d'arguments de types quelconques. De ce fait, il fallait donner au programmeur un moyen pratique de contrôler le format de sortie des données, qui consiste à spécifier en premier argument de la fonction, le seul obligatoire d'ailleurs, une chaîne de formatage permettant non seulement ce contrôle de format, mais aussi et éventuellement des conversions de types. La chaîne de formatage contient donc deux types d'objets : des caractères "ordinaires", qui seront recopiés tels quels sur le flux de sortie (écran, fichier, imprimante...) et des spécifications optionnelles de conversion, dont chacune provoque une interprétation différente par la fonction de l'argument suivant à sortir. Ces spécifications commencent par le caractère spécial "%" (simplement pour les distinguer des caractères normaux !) et se terminent par un caractère de conversion. Entre les deux, on peut éventuellement placer : 1. Des drapeaux (flags) en nombre et en ordre quelconques, qui modifient la spécification :
3. Un second nombre, qui décrit la précision désirée. Pour les chaînes, la précision donne le nombre maximum de caractères à sortir, même si la chaîne est plus longue. Pour les entiers, elle donne le nombre minimum de chiffres à sortir, en complétant au besoin par des "0". Pour des réels, elle spécifie le nombre de chiffres après le point décimal. Si la précision est donnée, elle doit être séparée de la largeur du champ par un point. 4. Un suffixe qui modifie le type du nombre à sortir : "h" indique un short ou un unsigned short, "1" indique un long ou un unsigned long et "L" indique un long double. On peut également spécifier la largeur et/ou la précision par une étoile "*", qui indique que le nombre correspondant n'est pas inclus dans la chaîne de formatage, mais doit être lu depuis l'argument suivant de la fonction. Les caractères de conversion sont réunis dans le tableau ci-dessous.
Note : la bibliothèque exec.library dispose d'une fonction de formatage des chaînes de caractères dans un tampon mémoire, RawDoFmt(), fortement inspirée de printf(), mais aux performances toutefois restreintes. Reportez-vous à la documentation adéquate pour plus de renseignements. Quelques exemples de formats aideront certainement à éclaircir quelques points encore obscurs. ![]()
|