Notes
Merci à Marc Baudy, Toupi, hiveNzin0 et Ram0000 pour la relecture. Dernière version : 12/07/2018 08:28:11
Un forum web "Bien programmer en C" permet de poser des questions sur les articles, le C en général, la programmation etc.
Il est courant en C standard d'utiliser le flux stdin pour acquérir des données en provenance d'un opérateur. (Mode conversationnel). On admettra pour la suite que stdin est connecté à la partie 'clavier' d'un périphérique console.
Le langage C offre plusieurs fonctions permettant de lire des données sur un flux en général et sur stdin en particulier.
Ces trois fonctions extraient un caractère du flux entrant (pour getchar(), ce flux est stdin). C'est insuffisant pour saisir autre chose qu'un simple <ENTER>. Ces fonctions ne sont absolument pas adaptées à la saisie d'un caractère comme un choix de menu par exemple.
Par contre, ces fonctions peuvent être utilisées pour construire des fonctions d'entrées de plus haut niveau plus ou moins spécialisées.
Détails de fonctionnement de fgetc()
Pour des raisons évidentes de sécurité (pas de limitation du nombre de caractères saisis), la fonction gets() ne devrait pas être utilisée. Bien que, à ma connaissance, cette fonction ne soit pas officiellement dépréciée pour des raisons de compatibilité avec le code existant, il est fortement conseillé de ne pas l'utiliser pour de nouveaux développements.
Malgré ce que l'on constate dans l'abondante littérature consacrée à l'initiation au langage C, l'utilisation de scanf() n'est pas adaptée.
En effet, le 'f' de scanf() est là pour nous rappeler que l'entrée doit être formatée (formatted), ce qui n'est évidemment pas le cas avec un opérateur humain qui peut entrer n'importe quoi. D'autre part, scanf() gère difficilement le '\n', ce qui entraine des comportements aberrants dans les saisies si on ne prend pas certaines précautions d'usage.
L'utilisation correcte et sûre de scanf() est complexe, et n'est pas à la porté d'un débutant (ni même à celle de la plupart des programmeurs expérimentés). Néanmoins, il est possible d'utiliser correctement scanf() si on se forme correctement.
Cette fonction est parfaitement adaptée à la saisie d'une ligne, (même de 1 caractère). Son usage est recommandé.
S'il faut saisir une valeur numérique, celle-ci sera d'abord saisie sous forme de ligne, puis traduite par la fonction appropriée (strtol(), strtoul(), strtod()) ou sscanf()) avec le filtre approprié :
#include <stdio.h> #include <stdlib.h> int main(void) { int ret; char temp[20]; do { char saisie[20]; printf("Entrez un nombre : "); fflush (stdout); fgets (saisie, sizeof saisie, stdin); /* Filtrage des caracteres (entier decimal) * Nota : la saisie s'arrete a la premiere erreur. * Ce qui est saisi avant est considere comme valide. * * "123a" -> "123" : ret = 1 * * "a123" -> "" : ret = 0 */ ret = sscanf (saisie, "%[0-9-]s", temp); } while (ret != 1); { long n = strtol (temp, NULL, 10); printf ("La chaine est '%s', soit %ld\n", temp, n); } return 0; }
D'autres exemples dans le chapitre sur les fichiers
Comment se fabriquer des entrées solides en C
Cette fonction d'apparence simple a en fait un comportement plus complexe qu'il n'y parait. En effet, elle regroupe un certain nombre de comportements non triviaux qui sont rarement expliqués dans la littérature C.
L'appel de cette fonction provoque une suspension de l'exécution du programme. Durant cette suspension, il est possible de rentrer des caractères (par exemple à l'aide du clavier) et même éventuellement de supprimer le ou les derniers caractères saisis à l'aide de la touche 'BackSpace'. La fin de saisie (et la reprise de l'exécution du programme) est marquée par la frappe de la touche <enter>.
Les caractères saisis sont stockés dans le flux stdin. Lorsque l'on frappe la touche <enter>, le caractère '\n' est aussi placé dans stdin, et l'exécution reprend. Le caractère le plus ancien est alors extrait du flux et il est retourné. En cas d'erreur de lecture ou d'entrée d'un caractère spécial dit 'de fin de fichier' (Ctrl-D, Ctrl-Z etc. selon le système), la valeur EOF (int < 0) est retournée.
Ensuite, si on rappelle fgetc(), deux cas sont possibles. Soit le flux est vide, soit il ne l'est pas. Si le flux est vide, la fonction fgetc() suspend l'exécution, et on retrouve le comportement précédent. S'il n'est pas vide, l'exécution n'est pas suspendue, et le caractère le plus ancien est extrait et retourné.
Dans la grande majorité des cas la lecture du '\n' signifie que la ligne saisie a été complètement lue.
A l'aide de simples programmes, il est possible de vérifier un certain nombre de comportements décrits précédemment :
#include <stdio.h> int main (void) { int x = fgetc(stdin); printf ("x = %d ('%c')\n", x, x); return 0; }
Quelques essais de saisie :
<enter> x = 10 (' ')
On voit que le caractère extrait est '\n' (ici, LF, soit le code ASCII 10)
a<enter> x = 97 ('a')
On voit que le caractère extrait est 'a' (ici, le code ASCII 97). Le <enter> ('\n') n'a pas été extrait. Si on appelait fgetc() une nouvelle fois, il n'y aurait pas de suspension.
a<backspace>b<enter> x = 98 ('b')
On constate que, bien que le premier caractère saisi fut 'a', le caractère extrait est 'b' (ici, le code ASCII 98). En effet, la touche <backspace> a permis de corriger la dernière saisie.
abcd<enter> x = 97 ('a')
On voit que le caractère extrait est 'a', bien que d'autres caractères aient été saisis après. C'est donc bien le plus ancien caractère qui est extrait. Les autres caractères sont en attente de lecture. Une boucle de fgetc() permettrait de les extraire.
#include <stdio.h> int main (void) { int x; do { x = fgetc(stdin); printf ("x = %d ('%c')\n", x, x); } while (1); return 0; }
Je laisse au lecteur le soin de refaire les expériences précédentes et d'en tirer les conclusions qui s'imposent.
Le langage C est indissociable de la notion de pointeur. Ce mot mythique en effraie plus d'un, et il est bon de démythifier enfin les pointeurs, sources de tant d'erreurs de codage, de discussions sans fin et de contre-vérités...
Un pointeur est une variable dont la valeur est une adresse.
Une adresse est une valeur unique dans le système désignant un objet en mémoire. Dans le domaine de la programmation en général, on parle aussi de référence, mais ce mot n'est pas utilisé dans le cadre du langage C.
Sur les systèmes avancés utilisant une MMU (Memory Management Unit), la mémoire n'est pas la mémoire physique. La valeur de l'adresse ne correspond donc pas à un emplacement physique précis. C'est la MMU qui fait le lien entre l'adresse vue de l'application et l'adresse physique. L'application ne connait pas l'adresse physique. Seules certaines parties du système (driver) ont l'autorisation d'accéder aux adresses physiques.
On distingue 2 grandes familles de pointeurs :
En C, un objet est essentiellement une variable mémoire (par opposition à registre ou constante), modifiable ou non, mais munie d'une adresse.
Un pointeur sur objet (aussi appelé communément 'pointeur') est une variable qui peut contenir l'adresse d'un objet.
Pour définir un pointeur, on utilise un type, puis le symbole '*' et enfin l'identificateur, suivi de ';' si on ne désire pas l'initialiser à la déclaration (peu recommandé). Sinon, on utilise l'opérateur '=', suivit de la valeur d'initialisation. Si le pointeur est déclaré dans un bloc, on peut utiliser la valeur retournée par une fonction.
/* Definition d'un pointeur sur int */ int *p_int;
Pour initialiser un pointeur, on peut soit :
Pour accéder à la valeur pointée, le pointeur doit être typé (donc différent de void). Dans ce cas, on peut obtenir la valeur en utilisant l'opérateur de déréférencement '*'.
/* Definition de la variable 'a' valant 4 */ int a = 4; /* Definition d'un pointeur 'p' initialise' avec l'adresse de la variable 'a' */ int *p = &a; /* Definition de la variable 'b' non initialisee */ int b; /* recuperation de la valeur de 'a' dans 'b' via le pointeur 'p' */ b = *p; /* 'b' vaut maintenant 4 */
Une fonction a une adresse qui est le nom de cette fonction. Un pointeur de fonction peut recevoir cette adresse. Il est possible, via un pointeur de fonction correctement initialisé, d'appeler une fonction.
Cette capacité du langage C lui confère une puissance rarement égalée, qui permet d'écrire du code flexible, dont les adresses des fonctions peuvent être définies à l'exécution. Cela permet de 'personnaliser' des fonctions génériques selon les besoins.
La définition des pointeurs de fonctions est un peu complexe, et a tendance à alourdir le code :
/* pointeur sur une fonction avec 2 parametres */ int (*pf) (int, char **); /* prototype d'un fonction ayant un pointeur de fonction comme parametre */ int fun (int (*pf) (int, char **));
Si on doit manipuler des fonctions qui retournent un pointeur de fonction, ou des tableaux de pointeurs de fonction, le code devient rapidement illisible. C'est pourquoi il est fortement conseillé, et ce dans tous les cas, d'utiliser un typedef pour créer un alias sur le type de la fonction.
/* definition d'un alias */ typedef int fun_f (int, char **); /* definition d'un pointeur de fonction de ce type */ fun_f *pf; /* prototype d'une fonction ayant un pointeur de fonction comme parametre */ int fun (fun_f *pf); /* tableau de pointeurs de fonction */ fun_f *pf[10]; /* prototype d'une fonction retournant un pointeur de fonction */ fun_f *getfunc (int);
La lecture et la maintenance du code s'en trouvent considérablement allégées.
Pour initialiser un pointeur de fonction, on peut soit :
Pour utiliser le pointeur, il suffit de l'invoquer comme une fonction.
/* definition d'un alias */ typedef int fun_f (int); /* definition d'un pointeur de fonction de ce type */ fun_f *pf; /* prototype d'une fonction du meme type */ int function (int); /* NOTA : on peut aussi utiliser le type */ fun_f function; /* initialisation du pointeur de fonction */ pf = fonction; /* appel de la fonction via le pointeur de fonction */ pf (123);
Un pointeur est avant tout une variable. Comme toutes les variables, elle doit être initialisée avant d'être utilisée.
Pour un pointeur en général, l'utilisation signifie le passage de sa valeur à une fonction. Pour un pointeur sur objet, c'est le déréférencement par l'opérateur '*'. Pour un pointeur de fonction, c'est l'appel de cette fonction via le pointeur.
Il est recommandé de donner une valeur significative à un pointeur. Soit il est invalide, et on lui donne la valeur 0 ou NULL, soit il est valide, et dans ce cas sa valeur est celle de l'adresse d'un objet ou d'une fonction valide. Si le bloc mémoire ou la fonction deviennent invalides, il est recommandé de donner au pointeur la valeur 0 ou NULL.
/* Definition du pointeur. Il est initialise a l'etat invalide */ int *p = NULL; /* le pointeur est initialise avec l'adresse d'un tableau * dynamique de 4 elements */ p = malloc (4 * sizeof *p); /* en cas d'échec d'allocation, malloc() retourne NULL */ if (p != NULL) { /* ... */ /* apres utilisation, l'espace memoire est libere */ free (p); /* le pointeur est force a l'etat invalide */ p = NULL; }
Rappel : En langage C, les passages de paramètres se font exclusivement par valeur.
Le langage C n'autorise pas le passage d'un tableau en paramètre à une fonction. La raison est probablement une recherche d'efficacité, afin d'éviter des copies inutiles.
Le but de 'passer un tableau' à une fonction est en fait de permettre à celle-ci d'accéder aux éléments du tableau en lecture ou en écriture. Pour se faire, l'adresse du début du tableau et le type des éléments suffisent à mettre en œuvre l'arithmétique des pointeurs. Un paramètre 'pointeur' est donc exactement ce qu'il faut.
Soit l'appelant :
int main (void) { int tab[5]; clear (tab); return 0; }
Le prototype de la fonction appelée doit comporter un pointeur du même type que les éléments du tableau, pour recevoir l'adresse du premier élément de celui-ci, soit :
void clear (int *p);
La fonction va donc utiliser le paramètre pointeur, dont la valeur est l'adresse du premier élément du tableau, pour accéder aux éléments du tableau. Par exemple, les mettre à 0 :
void clear (int *p) { *(p + 0) = 0; /* premier element, */ *(p + 1) = 0; /* deuxieme element, */ *(p + 2) = 0; /* troisieme element, */ *(p + 3) = 0; /* quatrieme element, */ *(p + 4) = 0; /* dernier element (cinquieme) */ }
Afin d'alléger l'écriture, le langage C autorise l'utilisation de la syntaxe des tableaux pour accéder aux éléments :
void clear (int *p) { p[0] = 0; /* premier element, */ p[1] = 0; /* deuxieme element, */ p[2] = 0; /* troisieme element, */ p[3] = 0; /* quatrieme element, */ p[4] = 0; /* dernier element (cinquieme) */ }
Cette implémentation est évidemment théorique, car dans la pratique, on utilisera une boucle et un paramètre supplémentaire (nombre d'éléments) afin d'écrire un code plus souple et auto adaptatif.
NOTA : Dans le contexte des paramètres de fonctions, il y a 2 façons de noter le pointeur :
T *p
ou T p[]
. Elles sont techniquement équivalentes. Pour des raisons évidentes de clarté,
je recommande quand même d'utiliser la forme T *p quand il s'agit de l'adresse d'un objet
unique, et la forme T p[] quand c'est l'adresse d'un élément de tableau.
void clear (int p[], size_t nb) { size_t i; for (i = 0; i < nb; i++) { p[i] = 0; } } int main (void) { int tab[5]; clear (tab, 5); return 0; }
Rappelons que lorsqu'on définit un paramètre, les syntaxes type *param
et type param[]
sont sémantiquement équivalentes.
Pour définir un paramètre de type pointeur sur un tableau à 2 dimensions,
on serait tenté d'écrire type p[][]
, ce qui serait une erreur
de syntaxe. En effet, la notation []
est une notation abrégée de
[TAILLE]
dans les cas où cette taille est ignorée par le
compilateur, c'est à dire lorsque la dimension concernée est la plus à
gauche. Les syntaxes suivantes sont légales :
type_retour fonction (int p[]) type_retour fonction (int p[12]) type_retour fonction (int p[][34]) type_retour fonction (int p[56][78]) etc.
NOTA : Pour déterminer le nombre d'éléments d'un tableau, on peut utiliser les propriétés des tableaux. En effet, un tableau est une suite contiguë d'éléments identiques. Le nombre d'éléments est donc tout simplement le rapport entre la taille en bytes (ou multiplets) du tableau (sizeof tab) et la taille d'un élément en bytes (sizeof tab[0] ou sizeof *tab), que l'on généralise sous la forme d'une macro bien connue :
#define NB_ELEM(a) (sizeof (a) / sizeof *(a))
En langage C, beaucoup de problèmes de codage et d'exécution proviennent d'une confusion entre tableaux et pointeurs. C'est particulièrement vrai avec les chaines de caractères, au point d'utiliser le terme 'Char étoile' à la place du terme 'Chaine de caractères' ou 'Char étoile étoile' à la place de 'Tableau de chaines'. Qu'en est-il exactement ?
Une chaine de caractères est un tableau de char terminé par un 0. Une chaine littérale n'est pas modifiable.
/* interdit */ "hello"[2] = 'x'; /* autorise' */ char s[] = "hello"; s[2] = 'x';
Un pointeur sur char est une variable qui peut contenir NULL, l'adresse d'un char ou celle d'un élément d'un tableau de char. Si c'est un tableau, on n'a aucune information sur le nombre d'éléments du tableau pointé. Néanmoins, si c'est une chaine valide, elle est terminée par un 0 qui sert de balise de fin.
char c; char *pa = &c; char *pb = 0; char *pc = NULL; char s[] = "hello"; char *pd = s; char *pe = s + 3; /* une chaine litterale n'etant pas modifiable, * il est conseille de qualifier l'objet avec * 'const' (read-only) */ char const *pf = "hello"; char const *pg = "hello" + 2;
Un petit schéma pour modéliser :
Représentation graphique d'un objet 'c' de type char non initialisé :
char c; :---------:--------: : adresse : valeur : : : : :---------:--------: : &c : ??? : :---------:--------:
Représentation graphique d'un objet 'c' de type char après initialisation :
c = 'A'; :---------:--------: : adresse : valeur : : : : :---------:--------: : &c : 'A' : :---------:--------:
Représentation graphique d'un objet 'p' de type char * non initialisé :
char *p; :---------:--------:---------: : adresse : valeur : valeur : : : : pointée : :---------:--------:---------: : &p : ??? : ??? : :---------:--------:---------:
Représentation graphique d'un objet 'p' de type char * après initialisation (NULL) :
p = NULL; :---------:--------:---------: : adresse : valeur : valeur : : : : pointée : :---------:--------:---------: : &p : NULL : ??? : --> NULL :---------:--------:---------:
Représentation graphique d'un objet 'p' de type char * après initialisation (adresse d'une variable) :
p = &c; :---------:--------:---------: :---------:--------: : adresse : valeur : valeur : : adresse : valeur : : : : pointée : : : : :---------:--------:---------: :---------:--------: : &p : &c : 'A' : --> : &c : 'A' : :---------:--------:---------: :---------:--------:
Représentation graphique d'un objet 'p' de type char * après initialisation (adresse d'une chaine modifiable) :
char s[]="ab"; p = s; :---------:---------:---------: :---------:--------: : adresse : valeur : valeur : : adresse : valeur : : : : pointée : : : : :---------:---------:---------: :---------:--------: : &p : &s[0] : 'a' : --> : s+0 : 'a' : : : ou s+0 : : :---------:--------: : : ou s : : : s+1 : 'b' : :---------:---------:---------: :---------:--------: : s+2 : 0 : :---------:--------:
Le pointeur sur char est principalement utilisé pour les paramètres 'chaines de caractères' des fonctions et pour des manipulations de chaines de caractères.
Mais cela n'autorise pas à utiliser le terme 'char étoile' à la place de 'chaine de caractères'.
Un pointeur sur pointeur de char est une variable qui peut contenir NULL, l'adresse d'un pointeur de char ou celle d'un élément d'un tableau de pointeurs de char. Si c'est un tableau, on a aucune information sur le nombre d'éléments du tableau pointé. On peut parfois ajouter un élément de valeur NULL pour délimiter le tableau de pointeurs.
{ char const *as[] = {"hello", "world"}; char const **pp = as;
pp -> as[0] -> "hello" as[1] -> "world"
Le langage C n'offre pas, à proprement parler, de gestion de fichiers. Il définit plutôt des flux d'entrées / sorties (I/O streams) sur lesquels il peut agir (ouverture/fermeture, lecture/écriture). L'unité d'information gérée par un flux est le byte.
Certains de ces flux sont connectés à des périphériques permettant par exemple de réaliser une interface entre la machine et l'utilisateur (IHM) en mode texte. Mais la plupart du temps, le nom associé au flux est en fait un 'fichier', c'est-à-dire une sorte de mémoire (disque, flash) accessible en écriture et en lecture par l'intermédiaire du système. L'avantage évident est que les données sont permanentes, même après mise hors tension de la machine.
En conséquence, dans la pratique, les termes flux et fichiers sont souvent confondus.
Le langage C fait la distinction entre les fichiers binaires et les fichiers textes. Cette distinction est historique. Elle dépend en fait du système utilisé. Sur certains systèmes, il n'existe aucune différence physique entre les fichiers textes et les fichiers binaires. Sur d'autres systèmes, il existe une différence. Par souci de portabilité, il est recommandé de respecter cette distinction.
Le choix entre fichier texte ou binaire provient du contenu de ce fichier.
On appelle fichier texte un fichier qui contient des informations de type texte, c'est à dire des séquences de lignes.
Une ligne est une séquence de caractères imprimables terminée par une marque de fin de ligne.
Selon le système, la marque de fin de ligne est composée de un ou plusieurs caractères de contrôle (par exemple, CR, LF, ou une séquence de ces caractères)
:----------------:--------------:----------------: : Système : Fin de ligne : Fin de fichier : :----------------:--------------:----------------: : Unix : : : : Mac X : 0x0A LF : Sans objet : : Linux : : : :----------------:--------------:----------------: : Mac (non unix) : 0x0D CR : Sans objet : :----------------:--------------:----------------: : MS-DOS : 0x0D CR : 0x1A : : Windows : 0x0A LF : ^Z : : Windows NT : : : :----------------:--------------:----------------: : VMS STREAM_CR : 0x0D CR : Sans objet : :----------------:--------------:----------------: : VMS STREAM_LF : 0x0A LF : Sans objet : :----------------:--------------:----------------: : VMS STREAM_CRLF: 0x0D CR : Sans objet : : : 0x0A LF : : :----------------:--------------:----------------:
L'ensemble des valeurs numériques des caractères (charset) dépend du système. La plupart du temps, il s'agit du codage ASCII (0-127) avec des extensions plus ou moins standards au delà de 127. Il existe d'autres codes, comme EBCDIC utilisé sur certains mainframes IBM.
Pour écrire une fin de ligne dans un fichier texte, il suffit d'écrire le caractère '\n'. Celui-ci sera alors automatiquement traduit en marqueur de fin de ligne.
De même, lors de la lecture d'un fichier texte, le marqueur de fin de ligne est automatiquement traduit en '\n', quel qu'il soit.
Nota : Certains systèmes marquent la fin des fichiers textes d'un caractère spécial. Par exemple MS-DOS ajoute un code 26 (^Z). Cela signifie que, pour ce système, la lecture d'un fichier texte s'arrête dès la rencontre de ce caractère.
N'importe quel fichier, y compris un fichier texte, peut être considéré comme binaire. Dans ce cas, l'écriture et la lecture des caractères se fait sans interprétation.
Par exemple, sur une plateforme utilisant le jeu de caractères ASCII, CR vaut 13 ou 0x0D ou '\r'. De même, LF vaut 10 ou 0x0A ou '\n'.
La fonction d'ouverture de fichier est fopen(). Comme pour les autres fonctions de gestion des fichiers, le fichier d'interface est <stdio.h>.
FILE *fopen (char const *filename, char const *mode);
Le mode d'ouverture est déterminé par une chaine de caractère. Voici les chaînes correspondant aux principaux modes :
"r" : mode texte en lecture "w" : mode texte en écriture (création) "a" : mode texte en écriture (ajout) "rb" : mode binaire en lecture "wb" : mode binaire en écriture (création) "ab" : mode binaire en écriture (ajout)
Le langage C offre plusieurs fonctions permettant de lire les données d'un fichier.
Ces fonctions sont identiques. Elles permettent de lire un caractère.
Cette fonction permet de lire un bloc de caractères d'une longueur donnée. Elle est tout à fait adaptée à la lecture des données binaires brutes (non interprétées).
Cette fonction permet de lire des données 'texte' formatées. Cette fonction est d'une utilisation complexe et son usage est peu recommandé.
Cette fonction permet de lire une ligne de texte. Elle est tout à fait adaptée à la lecture d'un fichier texte ligne par ligne.
Sa simplicité d'utilisation et sa robustesse en font la fonction préférée des programmeurs qui doivent analyser des fichiers textes.
Soit le fichier texte :
Ceci est un simple fichier texte de 2 lignes.
et un petit programme permettant de lire ces 2 lignes
/* fichier1.c */ #include <stdio.h> int main (void) { /* ouverture du fichier en mode texte */ FILE *fp = fopen ("data.txt", "r"); /* L'ouverture du fichier est-elle realisee ? */ if (fp != NULL) { /* definition d'un tableau de char destine a recevoir la ligne * La taille est arbitraire. Elle doit etre cependant adaptee * aux besoins courants. * Pour les grandes tailles (disons > 256 char), * il est preferable d'utiliser une allocation dynamique. */ char ligne[32]; /* lecture de la premiere ligne */ fgets (ligne, sizeof ligne, fp); /* Affichage de la premiere ligne */ printf ("1: %s\n", ligne); /* lecture de la deuxieme ligne */ fgets (ligne, sizeof ligne, fp); /* Affichage de la deuxieme ligne */ printf ("2: %s\n", ligne); /* Fermeture du fichier */ fclose (fp); } else { printf ("Erreur d'ouverture du fichier\n"); } return 0; }
On doit obtenir ceci sur la sortie standard (stdout):
1: Ceci est un simple fichier 2: texte de 2 lignes.
La ligne lue est stockée dans la variable ligne, y compris le '\n'. La fonction d'affichage printf() affiche le numéro de ligne, suivi de ': ', la ligne (avec son '\n') et un '\n' en plus, ce qui explique la présence de lignes "vides".
Cet exemple de codage 'naïf' souffre d'un défaut majeur : Il fait l'hypothèse que le fichier fait 2 lignes, et il continue à lire le fichier même si une erreur de lecture s'est produite. En fait, tout simplement, il ne gère pas les erreurs de lecture.
Il est facile de gérer les erreurs de lecture. Toutes les fonctions de lecture retournent une valeur. Celle-ci peut prendre une valeur particulière qui signifie 'Arrêt de la lecture'. La cause n'est pas précisée. Ça peut être à cause d'une erreur (support en panne, données corrompu, fichier inexistant etc.) ou tout simplement par ce que la fin de fichier a été atteinte.
La fonction fgets() retourne une valeur de type char *. Si la lecture a réussi, la valeur retournée est l'adresse du tableau de char passé en paramètre. En cas d'échec, la valeur NULL est retournée. Il suffit donc de surveiller cette valeur pour savoir si on peut continuer ou non. Comme une des causes d'échec est la "fin de fichier atteinte", on peut donc parfaitement intégrer ce test dans une boucle de lecture "ligne par ligne".
Une fois l'échec de la lecture constaté, il est possible d'en identifier la cause. Le langage C met à disposition les deux fonctions feof() et ferror() qu'il faut appeler après la boucle de lecture, mais avant la fermeture du fichier.
while (fonction_de_lecture(fp) != ERREUR) { ... } if (feof(fp)) { /* la fin de fichier a ete detectee */ puts ("EOF"); } if (ferror(fp)) { /* une erreur s'est produite */ perror (NOM_DU_FICHIER); } fclose (fp);
On constate que lorsque fgets() lit une ligne entière, un '\n' se retrouve à la fin de la chaine saisie. La présence de '\n' est gênante ou non selon l'application.
Ceci dit, dans tous les cas, il est conseillé d'en détecter la présence. En effet, sa présence indique que la ligne a été lue entièrement, alors que son absence indique que la ligne a été tronquée, et que d'autres caractères (au minimum un '\n') attendent pour être lus. Il est donc conseillé d'écrire ces quelques lignes après un fgets() pour clarifier la situation :
#include <stdio.h> #include <string.h> ... { char ligne[123]; /* test d'erreur omis */ fgets (ligne, sizeof ligne, fp); { /* chercher le '\n' */ char *p = strchr(ligne, '\n'); if (p != NULL) { /* si on l'a trouve, on l'elimine. */ *p = 0; } else { /* Le traitement depend de l'application. * Par exemple, ici, on choisi d'ignorer * les autres caracteres. */ /* sinon, on lit tous les caracteres restants */ int c; while ((c = fgetc(fp)) != '\n' && c != EOF) { } } } }
Il est clair que dans la pratique, l'ensemble de ce code devra être intégré dans une fonction unique de lecture d'une ligne à partir d'un flux.
/* fichier2.c */ #include <stdio.h> int main (void) { FILE *fp = fopen ("data.txt", "r"); if (fp != NULL) { char ligne[32]; /* definition d'un compteur de lignes et initialisation */ int cpt = 0; /* lecture des lignes */ while (fgets (ligne, sizeof ligne, fp) != NULL) { /* Mise a jour du compteur */ cpt++; /* Affichage des lignes */ printf ("%d: %s\n", cpt, ligne); } /* On peut ajouter ici la detection de la cause * de l'erreur decrite ci-dessus */ fclose (fp); } else { printf ("Erreur d'ouverture du fichier\n"); } return 0; }
Cet exemple met en œuvre un mécanisme qui s'adapte automatiquement au nombre de lignes du fichier. Cependant, attention, le fonctionnement, bien qu'il reste sûr, risque d'être surprenant si la longueur de la ligne est supérieure à celle du tableau 'ligne'.
Par exemple, si on diminue la taille de 'ligne' à 16 au lieu de 32,
<...> char ligne[16]; <...>
on obtient :
1: Ceci est un sim 2: ple fichier 3: texte de 2 lign 4: es.
Rappelons que la taille du tableau de char a été transmise à la fonction fgets().
Celle-ci tente de lire la ligne, mais celle-ci est trop longue pour tenir dans le variable 'ligne'. fgets(), qui connaît la taille de la variable 'ligne', applique alors une stratégie d'adaptation qui consiste à stocker ce qui est possible dans la variable, en laissant une place pour le 0 final. En effet, fgets() a pour obligation de produire une chaine de caractères valide dans tous les cas.
C'est pourquoi la première ligne est partiellement lue ainsi :
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 : Indice 'C' 'e' 'c' 'i' ' ' 'e' 's' 't' ' ' 'u' 'n' ' ' 's' 'i' 'm' 0 : Données
Mais les caractères manquants ne sont pas perdus, et ils sont lus par l'appel suivant:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 : Indice 'p' 'l' 'e' ' ' 'f' 'i' 'c' 'h' 'i' 'e' 'r' '\n' 0 : Données
Cette fois, la place est suffisante, et l'ensemble de la chaine est lue, y compris le '\n'.
Le langage C offre plusieurs fonctions permettant d'écrire des données dans un fichier.
Ces fonctions sont identiques. Elles permettent d'écrire un caractère.
Cette fonction permet d'écrire un bloc de caractères d'une longueur donnée. Elle est tout à fait adaptée à l'écriture de données binaires brutes (non interprétées).
Cette fonction permet d'écrire des données 'texte' formatées. Elle comporte de nombreuses possibilités de conversion de valeurs numériques en texte. (Entiers, flottants etc.)
Cette fonction permet d'écrire une chaine de caractères.
Il n'est pas rare que des données enregistrées dans un fichier par une machine soient lues par une autre machine, ou par autre programme ou par le même programme mais compilé avec des options différentes. Pour pouvoir récupérer les données, il faut qu'en aucun cas, le format des données enregistrées ne dépende de l'implémentation.
Le format texte est un bon choix, car il utilise une séquence de caractères simple et évidente (chronologique) et un codage très répandu (ASCII). Il peut y avoir quelques problèmes de transcodage pour les valeurs de 128 à 255 (ANSI, OEM etc.), mais rien qui ne soit insurmontable. D'autre part, la conversion ASCII/EBCDIC est triviale.
Il subsiste le problème des fins de ligne qui sont différentes d'un système à l'autre. Il existe des utilitaires bien connus (dos2unix, unix2dos etc.) généralement fournis avec ces systèmes qui font les conversions. Rappelons que la fonction system() permet d'appeler une commande extérieure. Si néanmoins, cet utilitaire n'existait pas, il serait facile de le faire soi-même. Bien sûr, il faudrait travailler en mode binaire de façon à contrôler les données du fichier de manière 'brute' (raw).
Les chaînes et les valeurs numériques sont encodées et éventuellement formatées avec fprintf(). Une organisation en ligne est souhaitable. Elles sont ensuite lues ligne par ligne avec fgets() et analysées soit par strtol(), strtoul() ou strtod() pour les cas les plus simples (valeurs numériques pures), soit par sscanf() pour les cas plus complexes, à condition que le formatage soit clairement défini. Il est souhaitable d'utiliser des formats simples à analyser et surtout sans ambiguïté quant aux séparateurs. Le format CSV est recommandé.
Une mauvaise utilisation des formats binaires (raw) peut apporter des problèmes de portabilité. Il est recommandé d'utiliser des formats indépendants comme XDR (RFC 1832).
Pour supprimer un enregistrement, le plus simple est de procéder ainsi:
Toute autre opération basée sur l'écriture/lecture dans le même fichier est dangereuse, non portable et se traduit souvent par la destruction du fichier original sans recours possible.
Il ne faut pas se tromper d'outil. Les flux du C sur disque sont très pratiques pour enregistrer quelques données statiques dans un fichier texte. En binaire, c'est déjà plus risqué à moins de passer par un format indépendant comme XDR. Pour gérer des enregistrements, les fichiers C sont trop rustiques. Il faut une véritable base de données (comme SQLite ou MySql par exemple).
La norme du C est définie par l'ISO. On trouve l'original sur le site de l'ISO, mais elle est très chère. Elle est aussi reprise par l' ANSI à l'identique, pour la somme raisonnable de 20 USD.
Il y a aussi le site de Dinkumware qui fournit un excellent résumé des fonctions (C99).
Il est possible de télécharger gratuitement le dernier draft de la norme (n1256) ici : (C99). Ce document est complet et integre C99, TC1, TC2 et TC3 (Technical Corrigendum 1, 2 et 3). C'est probablement le dernier draft du document définitif de l'ultime norme du C qui ne devrait plus évoluer, le comité étant en sommeil.
En fait, le comité continue de travailler, et une nouvelle version du langage C (C1x) est en cours d'élaboration. En voici le dernier draft : n1336.
Il existe un document quasiment officiel qui commente la norme : Le 'Rationale'. Très utile pour trouver des explications complémentaires.
Il est fréquent de rencontrer ce genre de code :
#include <stdlib.h> ... { /* creation d'un tableau de 10 int */ size_t n = 10; int *p = (int*) malloc (sizeof (int) * n); if (p != NULL) { ... }
Ce code est correct et ne présente pas de comportement indéterminé. Cependant, il est inutilement compliqué et peut être amélioré de plusieurs façons.
Il est d'usage d'éviter les casts en C. Certains sont indispensables, d'autres non. Ici, par exemple, et contrairement aux idées reçues, le cast est inutile, et on peut parfaitement écrire :
{ int *p = malloc (sizeof (int) * n); }
Il est cependant des cas rares où le cast est indispensable.
Il est rare de nos jours d'utiliser un compilateur datant d'avant la normalisation du langage C (1989 aux USA, 1990 au niveau international). En effet, ces compilateurs ne supportent pas les prototypes, ce qui les rend impropre à produire du code cohérent, à moins d'utiliser un outil de vérification indépendant comme PCLint.
Le cas peut cependant se produire, s'il s'agit de maintenir du code ancien avec une chaine de compilation ancienne. Dans ce cas, effectivement, le cast est indispensable si le type du pointeur et différent de char*.
L'opportunité de conserver une telle pratique est donc laissée à l'appréciation du programmeur. Il semble cependant assez évident que dans les nouveaux développements utilisant un compilateur ISO, il est inutile d'ajouter le cast.
Il est techniquement possible, à de rares exceptions syntaxiques près, de faire compiler du code C par un compilateur C++. Néanmoins, cette pratique est rarement justifiée et est largement déconseillée.
En effet, en dehors des points syntaxiques évidents (comme par exemple la conversion de type explicite void* <-> type* qui justement oblige à utiliser le cast) plusieurs points de sémantique diffèrent entre les deux langages. En l'état actuel des normes, les spécifications C++98 et C99 ont même plutôt tendance à diverger (cette situation pourrait changer en 2005 avec une nouvelle révision de C++ intégrant les nouveautés de C99).
Différences entre C et C++ (anglais) .
On peut aussi se demander pourquoi on utiliserait malloc() et free() en C++, alors que ce langage dispose des opérateurs new et delete . D'autre part, en C++98, un cast se fait avec static_cast<...>.
Il est courant de déterminer la taille d'un objet en utilisant son type
{ int *p = malloc (sizeof (int)); }
Si le type change, on est obligé de modifier 2 fois le code:
{ long *p = malloc (sizeof (long)); }
Lorsqu'il s'agit d'un pointeur typé, il existe une technique alternative qui consiste à utiliser la taille d'un élément pointé par ce pointeur :
{ int *p = malloc (sizeof *p); }
Le changement de type se trouve largement simplifié :
{ long *p = malloc (sizeof *p); }
Quelques compléments sur malloc() et l'allocation dynamique en général
La fonction realloc(), bien que souvent décriée pour sa lenteur, offre une alternative intéressante pour gérer des tableaux de taille variable. Bien sûr il ne faut pas allouer les objets un par un, mais par blocs (doublage, par exemple).
Pour utiliser correctement realloc(), quelques précautions doivent être prise. Par exemple :
#include <stdlib.h> <...> { /* allocation d'un tableau de 10 int * Pour pouvoir gerer la taille, on utilise * une variable 'taille'. Une structure comprenant * l'adresse du tableau et sa taille est aussi envisageable. */ size_t size = 10; int *p = malloc (size * sizeof *p); <...> /* Agrandissement du tableau a 15 int */ { /* reallocation. Le resultat est stocke' * dans une variable temporaire */ size = 15; type_s *p_tmp = realloc (p, size * sizeof *p_tmp); if (p_tmp != NULL) { /* si la nouvelle valeur est valide, * le pointeur original est mis a jour. */ p = p_tmp; } else { /* l'ancien bloc est valide, mais il n'a pas ete agrandi */ } } }
Il faut aussi garder à l'esprit que la partie nouvellement allouée n'est pas initialisée.
Un programme généré avec l'IDE Borland C++ 3.1 signale parfois ce message à l'exécution:
scanf : floating point formats not linked Abnormal program termination
Il s'agit en fait d'un bug connu de certains compilateurs Borland. Il se produit lorsqu'on utilise un format 'flottant' avec *printf() ou *scanf(), et qu'on n'utilise pas de fonctions de la bibliothèque mathématique.
La parade est simple. Il suffit d'ajouter ces quelques lignes dans le code source contenant le main(), par exemple.
#ifdef __BORLANDC__ /* The pesky "floating point formats not linked" killer hack : */ extern unsigned _floatconvert; #pragma extref _floatconvert #endif
Il existe de nombreuses implémentations du langage C. Certaines sont des interpréteurs, mais la plupart sont des compilateurs.
Compiler un programme consiste à vérifier le code source, puis à le traduire en langage machine de façon à en faire un fichier exécutable qui pourra ensuite être exécuté par la machine.
Les détails de production du code dépendent de l'implémentation, c'est à dire de la machine, du système, des outils utilisés etc. Cependant, il existe une procédure générale commune à toutes ces implémentations :
Pour cela, on utilise successivement 2 outils :
Une bibliothèque (library) est une collection de fonctions mise à la disposition des programmeurs. Elle se compose d'une interface matérialisée par un ou plusieurs fichiers d'entêtes (.h), et d'un fichier d'implémentation qui contient le corps des fonctions (.lib, .a, .dll, .so etc.) sous forme exécutable.
Il est aussi possible à un programmeur de 'capitaliser' son travail en réalisant des fonctions réutilisables qu'il peut ensuite organiser en bibliothèques. Cette pratique est courante et encouragée. La création physique d'une bibliothèque est assez simple si on respecte quelques règles de conception, comme l'absence de globales, la souplesse et l'autonomie. Elle nécessite un outil spécialisé (librarian) généralement livré avec toute implémentation du langage C.
Une bibliothèque peut être statique ou dynamique (partagée).
Une bibliothèque à édition de lien statique (.lib, .a etc.) est liée à l'application pour ne former qu'un seul exécutable. La taille de celui-ci peut être importante, mais il a l'avantage d'être autonome. Cette pratique a l'avantage de la simplicité, et elle ne requiert aucune action particulière de la part d'un éventuel système lors de l'exécution du programme. Elle est très utilisée en programmation embarquée (embedded).
Une bibliothèque à édition de lien dynamique, est un fichier séparé (.dll, .so, ...) qui doit être livré avec l'exécutable. L'intérêt est que plusieurs applications peuvent se partager la même bibliothèque, ce qui est intéressant, surtout si sa taille est importante. Dans ce cas, les exécutables sont plus petits. Autre avantage, les défauts de la bibliothèque peuvent être corrigés indépendamment des applications. Ensuite, la correction est répercutée immédiatement sur toutes les applications concernées par simple mise à jour de la bibliothèque dynamique.
Une application qui utilise une bibliothèque dynamique doit réaliser le lien avec la bibliothèque à l'exécution. Pour cela, elle utilise des appels à des fonctions système spécifiques dans sa phase d'initialisation. Les détails dépendent de la plate-forme.
Une variable globale est une variable définie en dehors d'une fonction et de portée globale. Sa durée de vie est égale à celle du programme. Elle est initialisée par défaut (0) ou explicitement avant l'exécution de main(). Sa valeur est persistante.
Un usage abusif des variables globales est fortement déconseillé pour diverses raisons :
Ceci dit, il est des cas rares (ou plus fréquents s'il s'agit de lecture seule) où les variables globales sont utiles, voire indispensables. Dans le cadre d'une application professionnelle, ces cas doivent être justifiés. Voici comment les définir correctement dans le cadre d'une application composée d'unités de compilations séparées.
Il est recommandé d'utiliser le préfixe g_ ou G_ pour signifier qu'une variable est globale.
Il est préférable pour éviter la dispersion, d'utiliser une ou des structures de variables globales regroupées par fonction, plutôt qu'une multitude de variables.
Il est recommandé que la définition d'une variable globale soit faite exclusivement dans un fichier source (.c). Ce fichier doit inclure le fichier de déclaration (en-tête).
/* data.c */ #include "data.h" int G_x; /* la taille du tableau est definie * dans la declaration. */ double G_a[]; data_s G_data;
Il est recommandé que la déclaration d'une variable globale soit faite exclusivement dans un fichier d'entête (.h). Ce fichier doit être inclus dans le fichier de définition et dans tous les fichiers d'utilisation (.c). Comme tous les fichiers d'en-têtes, celui-ci dispose de protections contre les inclusions multiples.
#ifndef H_DATA #define H_DATA /* data.h */ typedef struct { int a; char b[123]; } data_s; extern int G_x; /* la defintion de la taille du tableau est unique. Elle est faite ici. */ extern double G_a[12]; extern data_s G_data; #endif /* guard */
Il est recommandé que le fichier qui utilise une variable globale inclue le fichier de déclaration (.h).
/* appli.c */ #include "data.h" int main (void) { G_x = 123; G_data.a = 456; G_a[3] = 123.456; return 0; }
Afin de réduire la taille des objets, il est possible de définir un champ de bits. La définition doit se faire dans une structure. Le type de l'objet unitaire doit être int ou unsigned int (recommandé) ou _Bool (bool) en C99.
typedef struct { unsigned a:1; /* variable de largeur 1 bit */ unsigned b:3; /* variable de largeur 3 bits */ /* etc. */ } data_s;
Il faut garder à l'esprit que l'implantation mémoire des bits n'est pas spécifiée par le langage C. (Et j'ai effectivement constaté sur le terrain des différences selon les implémentations, notamment concernant l'ordre des bits).
Autant une utilisation interne est possible et peut se justifier pour réduire la taille des objets (stockage en mémoire, notamment), extrait de clib.htm Module DATE (date.h) (Les tailles indiquées en commentaire sont les tailles minimales garanties) ...
typedef unsigned int uint; <...> typedef struct { /* 16 bits */ int year; /* -32767..+32767 */ uint month:4; /* 0-15 */ uint day:5; /* 0-31 */ uint hour:5; /* 0-31 */ uint minute:6; /* 0-63 */ uint second:6; /* 0-63 */ } sDATE;
... autant il est illusoire d'utiliser les champs de bits pour créer une interface avec l'exterieur du programme, comme un flux d'octets ou un périphérique en accès direct (mémoire, bus I/O etc.).
Autre pratique non portable, faire une union entre un champ de bits et une variable en s'imaginant pouvoir accéder à la variable, soit d'un bloc, soit bit à bit.
La solution portable pour accéder aux bits d'une variable est d'utiliser les opérateurs binaires (&, |, ~, <<, >>, ^)
Bien que main() soit censé retourner un int, on voit quelquefois écrit
void main (void) { }
Qu'en est-il exactement ?
D'après la définition du langage C, dans un programme conforme, main() doit retourner int. D'ailleurs un compilateur comme Borland C 3.1 en mode ANSI refuse void main() (error). Dans les mêmes conditions, gcc qui émet un avertissement (warning).
Dès l'apparition du langage C, une des formes canoniques de main() était
main() { return 0; }
l'autre étant la forme qui permet de récupérer les arguments de la ligne de commande.
Il faut bien comprendre qu'à cette époque, une fonction définie sans type de retour explicite, retournait un int (c'est toujours le cas en C90, mais plus en C99 où le type doit être explicite). Le mot clé 'void' n'existait pas.
Il n'y avait donc aucune raison d'utiliser une forme void main().
Ensuite, est venue la normalisation du langage C. (1989 ANSI, 1990 ISO). Dans le texte, les deux formes canoniques sont décrites :
int main (void)
et
int main (int argc, char **argv)
Il est précisé en remarque (dans la partie non normative) qu'il existe d'autres formes sans autres précisions. Elles ne font donc pas partie de la norme, leur comportement est donc indéfini dans le cadre d'un programme respectueux de la norme (dit 'strictement conforme').
Il arrive parfois de rencontrer ce genre de code ...
printf("Entrez un nombre : "); fflush (stdout);
... et on se demande alors à quoi peut bien servir ce fflush (stdout).
Le printf() précédent envoie une chaine de caractères à stdout. Or cette chaine n'est pas terminée par un '\n'.
Il faut savoir que stdout est souvent un flux "bufferisé'", ce qui signifie, en bon français, que les caractères sont placés dans un tampon (buffer) de sortie avant d'être réellement émis.
Il y a trois critères qui déclenchent l'émission réelle des caractères :
La commande de forçage est activée par l'appel de la fonction fflush (stdout), ce qui explique sa présence dans le code mentionné.
[1] sauf en cas de redirection dudit flux vers un fichier.
Le langage C offre par nature un contrôle assez fin de la portée des objets et des fonctions. Cette caractéristique est souvent mal connue, pourtant elle apporte un bénéfice certain, notamment sur le plan de l'organisation du code (conception détaillée).
Par défaut, la portée d'une fonction est globale.
int function (int a, char *b) { }
Elle est visible d'un autre module une simple déclaration:
int function ();
ou mieux, un prototype:
int function (int a, char *b);
Il est possible cependant de réduire la portée de la fonction à l'unité de compilation dans laquelle elle a été définie, en ajoutant le qualificateur static.
static int function (int a, char *b) { }
Cette pratique, lorsqu'elle est possible, apporte différents avantages :
On évitera cependant de multiplier les codes identiques, et les principes de factorisation du code restent en vigueur.
+--------------------+ +--------------------+ | Bloc fonctionnel A | | Bloc Fonctionnel B | +--------------------+ +--------------------+ | | v v +--------------------+ | Outils | +--------------------+
La portée d'un objet est régie selon plusieurs critères.
La portée par défaut est globale. Elle peut être réduite à l'unité de compilation en ajoutant le qualificateur static.
Si deux objets ont le même nom, l'objet de portée inférieure masque les objets de portée supérieure. Pour cette raison, qui entraine un comportement confus, on évite de donner le même nom à des objets dont les portées sont imbriquées.
La portée est celle du bloc et des blocs inclus.
/* objet de portee globale */ int x; int f (void) { /* la globale x est masque'e par une locale du meme nom */ int x = 0; /* la globale x n'est pas modifie'e. */ x++; } int main (void) { /* la globale x est modifie'e */ x = 2; f(); /* la globale x vaut toujours 2 */ return 0; }
Les fonctions de <time.h> offrent une interface assez complexe. Voici un exemple qui rassemble l'usage de la plupart de ces fonctions.
#include <stdlib.h> #include <stdio.h> #include <time.h> int main (void) { time_t now = time (NULL); struct tm tm_now = *localtime (&now); char s[64]; strftime (s, sizeof s, "%d/%m/%Y", &tm_now); printf ("Aujourd'hui : %s\n", s); /* prochain Nœl */ { struct tm tm_xmas = {0}; tm_xmas.tm_year = tm_now.tm_year; tm_xmas.tm_mon = 12 - 1; tm_xmas.tm_mday = 25; /* ajustement */ { time_t xmas = mktime (&tm_xmas); strftime (s, sizeof s, "%d/%m/%Y", &tm_xmas); printf ("Nœl : %s\n", s); { time_t diff = difftime (xmas, now); struct tm tm_diff = *gmtime (&diff); printf ("Plus que %d jours avant Nœl\n", tm_diff.tm_yday); } } } return 0; }
Le langage C n'a à voir ni avec UNIX ni avec POSIX.
UNIX est une norme de spécification de système (Actuellement UNIX 03) gérée par l'Open Group qui définit un système indépendamment de la machine sur lequel il tourne. Elle s'appuie, entre autre, sur la spécification des fonctions systèmes définie par POSIX.1 sous l'égide de l'IEEE.
Cette norme (POSIX.1) comprend à la fois la description d'un certain nombre de fonctions systèmes, et d'au moins deux types d'interfaces :
L'API est décrite sous la forme d'interfaces de fonctions appelables directement en langage C (C99 pour les dernières specs de POSIX.1). Les fonctions standards du C sont reprises telles quelles par POSIX.1.
NOTA : Ce sont ces nombreux emprunts au langage C qui ont créés la confusion entre langage C et POSIX ...
Cette API est implémentée par une bibliothèque (.a, .so, .lib, .dll, ...) et des fichiers d'entête (.h) livrés avec les systèmes compatibles POSIX.1 (la plupart des unixoïdes, mais aussi certains Windows).
La spécification complète de POSIX.1 est disponible ici (s'inscrire, c'est gratuit et sans danger).
Les constructeurs de systèmes et de compilateurs s'efforcent de suivre tout ou partie de ces normes, ce qui permet une portabilité importante dans des domaines qui ne sont pas couverts par la norme du langage C comme
Il est possible d'initialiser un tableau de char au moment de sa définition avec une constante qui ressemble a une chaine de caractères. Il faut cependant faire attention, car il n'est pas garanti que le tableau ainsi initialisé forme une chaine C valide (c'est à dire terminée par un 0).
En effet, la liste de caractères placée entre double quotes et servant à initialiser le tableau de char n'est en aucun cas une chaine de caractères. C'est une simple liste de caractères. Les zéros que l'on voit dans le tableau sont le résultat du comportement standard du C qui complète à 0 les tableaux partiellement initialisés.
Exemples
char s[4] = "ab";
le tableau est initialisé avec {'a', 'b', 0, 0} : Chaine valide
char s[4] = "abc";
le tableau est initialisé avec {'a', 'b', 'c', 0} : Chaine valide
char s[4] = "abcd";
le tableau est initialisé avec {'a', 'b', 'c', 'd'} : Chaine invalide
char s[4] = "abcde";
Ne compile pas (trop d'initialisateurs)
Il y a beaucoup de confusions sur les termes définition et déclaration en C. Voici un petit article qui va s'efforcer de mettre les choses au point.
Une déclaration permet l'utilisation d'un objet ou d'une fonction. Elle doit être préalable à l'utilisation.
Exemples avec un objet x de type int :
extern int x; <...> { x = 2; }
ou
int x; <...> { x = 2; }
ou
{ int x; <...> x = 2; }
Voici des exemples de déclarations de fonctions
int f (); extern int g (void); static int h (); static int i (int a) { } int j (char *b) { }
Une définition est l'endroit où l'objet ou la fonction sont réellement définis. De l'espace mémoire est alloué à l'objet, les instructions sont fournies à la fonction (entre des {}). Notons que par conséquent, une définition est aussi une déclaration implicite. Il convient donc d'être prudent lors de l'emploi des termes. Voici des définitions d'objets :
int a; static float b[12]; { struct xxx c; static long d; }
ou de fonction
int f () { } int g (void) { }
La dernière forme est une définition avec déclaration implicite sous forme de prototype
Le langage C utilise 3 zones mémoire pour implémenter les données.
Les données en C sont caractérisées par
La portée peut être
La durée de vie peut être
Exemples:
/* permanente de portee illimitee */ int a; /* permanente de portee limitee a l'unite de compilation */ static int b; <...> { /* locale (de portee limitee au bloc) */ int c; /* permanente de portee limitee au bloc */ static int d; /* controlee de portee limitee a la validite du pointeur (non NULL) */ int *p = malloc (sizeof *p * 3); free (p), p = NULL; }
Seules les données statiques sont initialisées avant le lancement de main(). Les autres ont une valeur indéterminée. Il est donc nécessaire de les initialiser avant utilisation (lecture). Sauf indication explicite contraire, l'initialisation par défaut des données statiques est 0.
En C standard, la notion d'écran (et plus généralement de matériel) n'existe pas. Une application écrite en C est censée tourner sur une machine abstraite dont les interfaces matérielles sont vues comme des flux d'entrée ou de sortie. Ceci dans un souci de portabilité, qui est une des raisons d'être du langage C.
Parmi ces flux, il en existe 3 qui sont ouverts par défaut :
Une application écrite en C peut être recompilée pour une multitude de plateformes (qui diffèrent selon le processeur, l'architectures, le système, etc.) pour lesquelles stdin, stdout et stderr seront implémentés de manières différentes.
Sur un modem, par exemple, ces flux seront connectés à un port série permettant de brancher une console par laquelle on pourra passer, par exemple, des commandes AT.
Le but est d'instaurer un dialogue 'interactif' dans lequel l'application montre qu'elle attend une commande de l'opérateur à l'aide d'un 'prompt' ou 'invite de commande':
modem> _
L'utilisateur peut alors passer une commande de numérotation
modem> ATD0123456789 _
et le modem répond
CONNECTED AT 9600 BAUDS _
il passe alors en mode transmission de données, et il signale qu'il n'attend plus de commandes par l'absence de prompt.
De même, sur un PC sous Windows ou unixoide, il est possible d'ouvrir une console localement ou de se brancher localement ou à distance sur la machine via une console (gestion, application, debug...). L'application ne verra pas la différence et continuera à attendre ses commandes de stdin et à envoyer ses réponses à stdout et/ou stderr.
L'avantage d'un tel système est qu'il exige très peu de la console, qui peut se réduire à un simple terminal série, voire à une imprimante série s'il ne s 'agit que de traces sur stdout ou stderr (utile pour mettre au point une application graphique, par exemple).
La gestion plein écran est une autre façon de concevoir la relation entre l'homme et la machine (IHM). En effet, on peut reprocher à l'interface console 'interactive', un certain manque de convivialité (largement compensé par le développement de langages 'scripts' extrêmement puissants), rendant les applications difficiles d'accès à un public non informaticien (direction, comptables, secrétaires, achats, commerciaux etc.)
C'est pour cela qu'il a été développé la notion d'interface 'plein écran' qui permet d'améliorer l'ergonomie.
Malheureusement, il n'a jamais été prévu de norme internationale régissant cette gestion d'écran (probablement parce que faisant trop partie du domaine applicatif, chaque constructeur voulant tirer parti des performances de son matériel et de son logiciel).
Certaines pratiques et normes de fait se sont toutefois imposées.
Je recommande la bibliothèque PDCurses qui est une version portable et multi-plateforme de [n]curses et qui permet facilement de gérer les entrées/sorties directes à la console, et ce soit d'une manière basique à-la-conio, ou d'une manière plus avancée. Il existe de nombreux tutoriels sur le net.
Mention spéciale pour 'termcaps' qui est une bibliothèque multi-plateforme censée s'adapter à quasiment tous les terminaux supportant des séquences d'échappement. Exemple d'implémentation : vv_termcaps
A ma connaissance, il n'y a pas de possibilité d'installer un interpréteur de séquences ANSI pour cmd.exe.
Le document de définition du langage C a réservé un certain nombre d'identificateurs réservés. Ils peuvent être utilisés par les implémenteurs (ceux qui écrivent les compilateurs) ou pour des extensions du langage.
Ces identificateurs peuvent apparaître dans les interfaces publiques ou pour réaliser certaines parties des fichiers d'entête (macros, paramètres...). Ils sont choisis de façon à ne pas interférer avec les identificateurs des utilisateurs, sous réserve, bien sûr, que ceux-ci ne les utilisent pas, d'où l'intérêt de cet article.
Les identificateurs réservés sont :
Les identificateurs commençant par | suivis de | Exemples valides | Exemples réservés |
"_" | "_A-Z" | _123 | _ABC |
"is" | "a-z" | is_abc | isabc |
"mem" | "a-z" | ||
"str" | "a-z" | ||
"to" | "a-z | ||
"wcs" | "a-z" | ||
"E" | "A-Z" ou "0-9" | Eabc | E123 EABC |
"LC_" | "A-Z" | LC_abc LC_123 LCABC | LC_ABC |
"SIG" | "_A-Z" | SIGabc | SIGABC SIG_ABC |
On entend parler de code standard, de code portable ? Qu'est-ce que ça signifie ? Ca sert à quoi ?
Le terme 'standard' est erroné. C'est un anglicisme de 'standard' qui signifie 'norme' (subst.) ou 'normalisé' (adj.).
Le langage C est normalisé. Cela signifie en clair qu'il est défini par un document spécifié et publié sous la responsabilité de l'ISO (International Standard Organisation ou Organisme de normalisation international). Ce document décrit la syntaxe et la sémantique du langage ainsi que l'interface et le comportement des fonctions de la bibliothèque d'exécution (RTL ou Run-Time Library).
Cette spécification s'applique aux compilateurs réputés 'conformes à la norme' et par conséquent aux programmeurs qui les utilise. La spécification définit en gros trois domaines :
Ce qui n'est pas défini par la norme peut l'être par une implémentation du C qui comporte des extensions spécifiques à une cible ou à un système. (Mots clés, fonctions, bibliothèques)
Par exemple system() est une fonction normalisée, dont le paramètre est une chaine de caractères. Cependant, la sémantique du texte porté par cette chaine de caractères peut varier d'un système à l'autre, voire ne pas être reconnue du tout par le système.
[1] (on dit aussi implémentation (anglicisme), implantation ou plateforme)
C'est la capacité qu'a un code source à produire un comportement identique sur différentes plateformes. On distingue la portabilité absolue (pour n'importe quelle plateforme) de la portabilité relative (limitée à un certain nombre de plateformes bien définies).
C'est lorsqu'un code source ne contient que des éléments normalisés du langage dont la définition et l'utilisation ne dépendent pas de la plateforme. Certaines pratiques additionnelles peuvent cependant rendre portable du code standard, comme ajouter fflush(stdout) après un printf() qui ne se termine pas par un '\n'.
C'est un code portable 'absolu' auquel s'ajoutent des extensions (généralement, des bibliothèques) tierces conçues pour fonctionner sur un certain nombre de plateformes bien définies.
La norme POSIX.1 en définit un certain nombre, notamment en matière de gestion des répertoires, processus légers (threads) et réseaux (sockets). Mais il existe des initiatives indépendantes comme GTK+ qui définit une interface de programmation graphique pour utilisateur (GUI) commune à Windows, X (Unix/Linux), Apple/Mac et même BeOS.
Le fait de bien connaître les domaines couverts par le langage C constitue une aide considérable pour l'écriture du code. En effet, la portabilité n'est possible que si le code est portable et donc, dans sa grande majorité normalisé, ou tout au moins confomes aux définitions de telle ou telle bibliothèque d'abstraction.
Il est donc impératif de séparer le code normalisé (qui doit représenter la majorité de celui-ci) du code spécifique à telle ou telle plateforme, et qui n'aurait pas trouvé sa place dans les bibliothèques d'abstraction.
Sur un forum je pose une question sur le langage C et on me répond : "va voir sur un forum consacré à ton système". M'enfin, je programme en C, c'est quoi ce cirque ?
Le langage C, tel qu'il est défini par la norme, est un ensemble de règles d'écriture (syntaxe, sémantique) définissant les éléments du langage, et un ensemble de fonctions regroupées sous le terme générique de 'bibliothèque d'exécution du [langage] C'.
Les domaines couverts par la bibliothèque sont
On constate donc qu'un programme C standard permet d'entrer des données à partir de la ligne de commande (paramètres de main()) ou d'un flux entrant (stdin, fichier en lecture), de les traiter 'silencieusement', ou avec une trace vers un flux sortant (stdout, stderr ou un fichier en écriture) et de sortir des données vers une un flux sortant selon le schéma bien connu.
Exemples typiques
Si on cherche d'autres domaines d'applications comme
il va falloir utiliser des ressources externes au langage C. Bien que ces ressources disposent (entre autres), d'une interface leur permettant d'être appelées par un programme écrit en C, elles ne font pas partie du langage C.
On distingue principalement 3 types de ressources
Rappelons qu'un système est un logiciel lancé par la machine au démarrage et qui prend en charge la gestion des ressources matérielles, ainsi que la surveillance de différents évènements. D'autre part, il fourni un certain nombre de fonctions utilisables dans les applications, ainsi que le moyen de charger et lancer (exécuter) une ou des applications.
L'ensemble des interfaces des fonctions système utilisables pour développer des applications est décrit dans un document appelé API (Application Programming Interface). Ce document précise pour chaque fonction :
Pour des raisons propres à chaque architecture, l'interface est souvent matérialisée par un 'TRAP' ou une interruption logicielle que l'on ne peut appeler qu'en assembleur. Par exemple en sur un PC/x86, l'interruption BIOS 'Vidéo':
MOV AH, 09h MOV AL, character MOV BH, 00h MOV BL, attributes MOV CX, 01h INT 10h
ou avec des extensions de très bas niveau comme par exemple ceci en Borland C:
#if defined (__BORLANDC__) #include <dos.h> #else #error Undefined for this platform #endif <...> /* --------------------------------------------------------------------- VIDEO_putch() --------------------------------------------------------------------- ecriture d'un caractere a la position courante du curseur avec la couleur specifiee --------------------------------------------------------------------- E : caractere (0-255) E : couleur texte E : couleur fond S : --------------------------------------------------------------------- */ void VIDEO_putch (int c, eCOU ct, eCOU cf) { #if defined (__BORLANDC__) union REGS reg; /* caractere a écrire */ reg.h.al = (uchar) c; /* putch avec attribut */ reg.h.ah = 0x09; /* page 0 */ reg.h.bh = 0; /* attributs 08h : clignotement */ reg.h.bl = (uchar) (ct | ((cf & ~0x08) << 4)); /* pas de repetitions */ reg.x.cx = 1; /* Bios Video */ int86 (0x10, ®, ®); #endif }
Afin de faciliter l'appel à partir de différents langages de développement, chaque implémenteur de compilateur (C, C++, Pascal, Ada etc.) ou d'interpréteur (BASIC, Python, Ruby etc.) fournit une interface dans son langage. Les fonctions systèmes apparaissent donc comme une extension dudit langage.
Chaque système dispose de sa propre API. Mais certains systèmes offrent des API définies selon la norme POSIX, ce qui tend à normaliser au moins une partie des API.
Il est aussi possible d'utiliser des fonctions fournies par des bibliothèques publiques gratuites ou payantes selon les besoins et les licences requises. Ces bibliothèques sont le plus souvent indépendantes de la plateforme. On peut citer (interface C)
Chaque développeur peut, pour lui-même, ou au sein de son entreprise, développer une bibliothèque de fonctions 'métier' qui lui facilitent la réalisation d'applications spécialisées. Il est courant que ces bibliothèques soient partagées dans l'entreprise.
Certaines de ces bibliothèques peuvent ensuite devenir publiques si elles peuvent intéresser d'autres personnes et si la licence le permet.
size_t est le type retourné par l'opérateur sizeof. C'est un entier non signé. Il est suffisamment grand pour contenir la valeur représentant, en nombre de bytes (ou char), la taille du plus grand objet possible d'une implémentation donnée.
Il convient pour les tailles, les dimensions de tableau, les index croissants et non négatifs...
Ce type est défini dans <stddef.h> qui est inclus dans la plupart des headers standards courants (<stdio.h>, <stdlib.h> <string.h> etc.)
Le tableau est probablement le concept le plus difficile à définir correctement en C. Il y a en effet beaucoup de confusion sur les termes : tableau, adresse, pointeur, indices... Ce petit article essaye de tirer les choses au clair.
représentation graphique d'un tableau de 4 éléments :
tab : |--------------- | tab[0] : |--- | tab[1] : |--- | tab[2] : |--- | tab[3] : |--- |
-------------------------------------------
[1] C'est là que se situe la difficulté. Le langage C parle de non-modifiable L-value
ce qui signifie que c'est un objet (il a une adresse), non modifiable. On ne peut pas changer sa valeur.
On ne peut changer que la valeur de ses éléments.
assert() est une macro qui permet de 'poser un piège'. On s'en sert en phase de mise au point pour vérifier si la conception et la réalisation sont correctes.
Le paramètre est une expression. Si elle retourne 0 (expression fausse), le programme s'arrête et un message indiquant le lieu et la cause est affiché.
Il est d'usage qu'en mode production (release), la macro globale NDEBUG soit définie, ce qui fait que les macros assert(), bien que toujours présentes dans le source, ne génèrent plus aucun code de vérification. En conséquence, cette macro ne doit évidemment pas être utilisée pour détecter des erreurs d'utilisation ou de système.
#include <stdio.h> static void afficher (int const t[], size_t n) { size_t i; for (i = 0; i <= n; i++) { printf ("%4d", t[i]); } printf ("\n"); } int main (void) { int tab[] = {1, 2, 3, 4}; afficher (tab, sizeof tab / sizeof *tab); return 0; }
Ce code parait correct, mais à l'exécution, on constate :
1 2 3 4 2 Press ENTER to continue.
Pour vérifier le comportement, je pose un piège qui vérifie la validité de l'index.
Je vais donc ajouter un piège :
assert (i < n);
Avant l'accès en lecture au tableau.
Pour être valide, la conception du piège doit se faire sans lire le code à tester, mais en se basant uniquement sur l'interface et le comportement présumé.
#include <stdio.h> #include <assert.h> static void afficher (int const t[], size_t n) { size_t i; for (i = 0; i <= n; i++) { /* ajout du piege */ assert (i < n); printf ("%4d", t[i]); } printf ("\n"); } int main (void) { int tab[] = {1, 2, 3, 4}; afficher (tab, sizeof tab / sizeof *tab); return 0; }
Ce qui provoque bien sûr :
1 2 3 4Assertion failed: i < n, file main.c, line 10 This application has requested the Runtime to terminate it in an unusual way. Please contact the application's support team for more information. Press ENTER to continue.
Ce qui signifie que i a dépassé la valeur maximale qu'autorise le langage C. La cause est évidemment le <= au lieu de < dans l'expression du for(), ce qui entraine une action corrective immédiate :
#include <stdio.h> #include <assert.h> static void afficher (int const t[], size_t n) { size_t i; /* correction */ for (i = 0; i < n; i++) { /* ajout du piege */ assert (i < n); printf ("%4d", t[i]); } printf ("\n"); } int main (void) { int tab[] = {1, 2, 3, 4}; afficher (tab, sizeof tab / sizeof *tab); return 0; }
L'exécution et, à présent, conforme aux attentes :
1 2 3 4 Press ENTER to continue.
Un générateur pseudo-aléatoire est une machine qui génère une séquence de nombres déterminée, cyclique, mais difficile à prévoir pour un humain. De plus, la répartition des valeurs (histogramme) est supposée être équilibrée.
La génération n'est pas 'spontanée', mais 'à la demande' (par appel de la fonction rand()). Les valeurs produites sont comprises entre 0 et RAND_MAX inclus.
A chaque fois que l'on appelle rand(), une nouvelle valeur sort :
#include <stdio.h> #include <stdlib.h> int main (void) { int i; printf ("Les valeurs vont de 0 a %d\n", RAND_MAX); for (i = 0; i < 10; i++) { int val = rand (); printf ("%d ", val); } printf ("\n"); return 0; }
Par exemple :
Les valeurs vont de 0 a 32767 41 18467 6334 26500 19169 15724 11478 29358 26962 24464
Mais on constate que si on lance le programme plusieurs fois, la séquence est toujours la même.
On peut modifier l'origine de la séquence avec srand(), en passant une valeur comprise entre 0 et RAND_MAX. Par exemple 10 :
#include <stdio.h> #include <stdlib.h> int main (void) { int i; srand(10); /* MODIF */ printf ("Les valeurs vont de 0 a %d\n", RAND_MAX); for (i = 0; i < 10; i++) { int val = rand (); printf ("%d ", val); } printf ("\n"); return 0; }
On constate que les valeurs sont différentes du tirage précédent, mais que si on relance le programme, elles restent identiques :
Les valeurs vont de 0 a 32767 71 16899 3272 13694 13697 18296 6722 3012 11726 1899
Pour avoir une séquence différente à chaque lancement du programme, il faut donc trouver un moyen de passer une valeur 'changeante' à srand(), d'où l'idée d'utiliser la valeur retournée par time(), qui change une fois par seconde, indépendamment du programme (c'est une valeur gérée par le système).
Une seconde, c'est long, mais ça suffit pour les besoins courants.
#include <stdio.h> #include <stdlib.h> #include <time.h> int main (void) { int i; srand((unsigned) time(NULL)); /* MODIF */ printf ("Les valeurs vont de 0 a %d\n", RAND_MAX); for (i = 0; i < 10; i++) { int val = rand (); printf ("%d ", val); } printf ("\n"); return 0; }
Si on lance plusieurs fois le programme (au moins une seconde entre chaque lancement), on obtient maintenant des séquences différentes :
25985 16903 23861 29724 17917 23752 17039 25712 20507 30816 26197 27421 5384 20976 236 16846 22741 32047 12417 24408 26433 14874 13653 16830 21990 4658 17461 17892 21603 7731
etc. Je précise que pour que les valeurs changent dans le programme, il faut évidemment que srand() ne soit appelé qu'une seule fois au début du programme.
Après, il y a des astuces arithmétiques pour réduire la plage de valeurs... C'est du bête calcul entier... Détails dans la FAQ de f.c.l.c., notamment ici (analyse détailée là). D'autre part, ceci peut aider.
Lorsqu'on défini une fonction,
int f (char *s) { ... }
on définit en même temps son interface, à savoir :
Cet interface est aussi appelé prototype intégré. Il permet une utilisation de la fonction si l'appel est placé après la définition (il n'y a pas de raison de faire autrement, sauf cas exceptionnels et souvent douteux...) :
int f (char *s) { ... } int main (void) { int x = f("hello"); }
Maintenant, dans le cas de compilation séparée, on doit 'détacher' le prototype et le placer dans un fichier qui sera inclus à la fois dans le fichier d'implémentation de la fonction et dans le ou les fichiers qui l'utilisent.
/* f.h */ int f (char *s);
/* f.c */ #include "f.h" int f (char *s) { ... }
/* main.c */ #include "f.h" int main (void) { int x = f("hello"); }
Pour compléter, il est fortement recommandé de protéger les fichiers .h contre les inclusions multiples avec une garde (guard) :
#ifndef H_F #define H_F /* f.h */ int f (char *s); #endif
Détails ici
C'est l'art de traduire un comportement en phrases simples et claires.
Surveiller la température.
Si elle dépasse la consigne, actionner une alarme.
Ces phrases sont ensuite traduites en 'pseudo-code' qui est une sorte de langage de description des comportements (ressemble au Pascal) à base d'actions et de structures de code comme IF-ELSE-ENDIF, SELECT-CASE, REPEAT-UNTIL, WHILE etc.
DO temperature := read_temperature() IF temperature > threshold alarm (ON) ENDIF FOREVER
Ce pseudo-code est ensuite traduit facilement en langage d'implémentation. (Par exemple en C)
{ for (;;) { int temperature = read_temperature(); if (temperature > threshold) { alarm (ON); } } }
Un article plus complet.
Le langage C définit 3 zones de stockage pour les objets (aussi appellés variables)
1 - Les objets définis hors des fonctions et les objets définis avec le mot clé static sont placés en mémoire statique. Leur durée de vie est celle de l'exécution du programme. Ils existent et sont initialisés (à 0 par défaut) avant même le lancement de main().
2 - Les objets définis avec les fonctions malloc(), calloc() et realloc() sont placés en mémoire allouée (appelée heap ou tas sur certaines implémentations). Leur durée de vie est contrôlée par le programme. Ils existent dès que l'appel de *alloc() retourne une valeur différente de NULL et cessent d'exister dès que free() est appelé avec la valeur retournée par *alloc().
3 - Les objets définis dans un bloc, sans qualificateur 'static', sont placés en mémoire automatique (appelée stack ou pile sur certaines implémentations). Leur durée de vie est celle du bloc dans lequel ils sont définis.
Les pointeurs sont un peu l'essence même du C et de tous les autres langages. Sauf que dans beaucoup de langages, cette notion est considérée comme honteuse (ou trop technique), et des astuces sont utilisées pour 'cacher' les pointeurs.
En C, on n'a pas honte des pointeurs et on les affiche ostensiblement.
Un paramètre de fonction est une variable locale de la fonction dont la valeur est donnée par l'appelant. Si on modifie cette valeur, on ne fait que modifier une variable locale. Exemple :
{ int x = 123; f (x); }
avec
void f (int a) { a++; }
Déroulement des opérations :
Si on avait voulu modifier x en appelant f(), c'est raté.
Tout simplement en donnant un moyen à la fonction qui lui permettre de modifier la variable x. Ce moyen est simple. Il suffit de lui donner l'adresse de x et elle pourra alors modifier x gràce à un pointeur et à l'opérateur de déréférencement (*).
En fait, on va faire ceci :
{ int x = 123; int *p = &x; (*p)++; }
C'est à dire
sauf que les opérations vont être réparties entre l'appelant et l'appelé comme ceci :
{ int x = 123; f (&x); }
avec
void f (int *p) { (*p)++; }
Voilà donc un exemple d'utilisation des pointeurs. Pour un tableau, par exemple, il n'y a pas le choix. La seule façon de faire est de passer l'adresse du premier élément du tableau via un pointeur sur le même type que l'élément.
Ensuite, on a accès à tous les éléments du tableau. Non seulement le [0] en *p , mais aussi aux autres, grâce aux propriétés de l'arithmétique des pointeurs.
En effet, le type étant connu, l'adresse des autres éléments est tout simplement p+1, p+2 etc.
L'élément lui même se trouve donc en *(p + 1), *(p + 2) etc. Le langage C définit cette écriture comme strictement équivalente à p[1], p[2] etc. On dit alors que la 'notation tableau' peut s'appliquer aux pointeurs. Mais cela ne signifie pas qu'un pointeur soit un tableau ni inversement comme on le lit parfois.
Ce principe est massivement utilisé avec les chaines de caractères, qui, rappelons le, sont des tableaux de char initialisés avec des valeurs de caractères et terminés par un 0.
Une chaine littérale, telle qu'elle apparait dans un source C, est une séquence de caractères entourée de guillemets (double quotes)
"hello"
Elle ne doit pas être confondue avec la liste de caractères servant à initialiser un tableau de char, par exemple :
char s[] = "hello";
(les détails sont indiqués ici.)
Une chaine littérale désigne en réalité l'adresse du premier élément d'un tableau de char anonyme non modifiable, situé en mémoire statique, initialisé avec la séquence de caractères mentionnés et terminé par un 0.
Tout se passe comme si on avait ceci :
static char const identificateur_connu_seulement_du_compilateur[] = {'h','e','l','l','o',0};
Si la chaine apparait dans un paramètre de fonction :
f ("hello");
c'est cette valeur (l'adresse) qui est passée à la fonction dans son paramètre :
void f (char const *s);
Si la chaine sert à initialiser un pointeur :
char const *p = "hello"; p = "bye";
c'est cette valeur (l'adresse) qui est stockée dans le pointeur.
RAPPEL : le mot clé const sert à qualifier l'objet de non modifiable
L'enregistrement d'une structure dans un fichier est une opération plus complexe qu'il n'y parait. En effet, le code naif suivant :
#include <stdio.h> struct data { char nom[32]; int age; }; int main (void) { #define FNAME "data.txt" struct data data = { "Emmanuel", 50 }; FILE *fp = fopen (FNAME, "wb"); if (fp != NULL) { fwrite (&data, sizeof data, 1, fp); fclose (fp), fp = NULL; } else { perror (FNAME); } return 0; }
est certes simple et efficace, mais est malheureusement non portable, et ce pour plusieurs raisons :
Pour résoudre ce problème, il y a 2 grandes familles de solutions :
C'est le plus portable, mais aussi le plus complexe. Il consiste à définir un format de données indépendant de toute implémentation. Il nécessite une conversion à l'écriture (host->file) et une conversion à la lecture (file->host), et ce dans le strict respect du format 'fichier' spécifié. Une méthode simple est TLV (Type, Longueur, Valeur ou Type, Length, Value).
(à venir : exemple de spécification TLV)
Il existe des solutions normalisées comme BER (Basic Encoding Rules) spécifié par les recommandations ITU-T X.209 et X.690 ou XDR (eXternal Data Representation) spécifié par la RFC 1832 plus ou moins basées sur TLV. Ces solutions sont complexes et sont plutôt utilisées avec l'aide d'une bibliothèque tierce comme BER sous Linux.
Dans tous les cas, le fichier est traité en mode binaire ("wb", "rb", "ab"), et les fonctions les plus utilisées sont fgetc(), fputc(), fread() et fwrite().
Il est un peu moins portable, mais moins complexe. Il consiste à définir un format de données indépendant sous forme de texte. Il nécessite une conversion à l'écriture (host->file) et une conversion à la lecture (file->host), et ce dans le strict respect du format 'fichier' spécifié. Une méthode simple est CSV (Comma Separated Values).
On peut trouver les spécifications sur Wotsit , le site de référence des formats de fichiers.
Le fichier est traité le plus souvent en mode texte ("w", "r", "a"), mais aussi parfois en mode binaire pour régler les problèmes de fins de ligne hétérogènes. (voir plus loin). Les fonctions les plus utilisées sont fgetc(), fputc(), fgets(), fputs() et fprintf().
Les principaux problèmes de portabilité proviennent :
Certaines corrections doivent donc parfois être effectuées à l'aide de tables de codages et autres astuces algorithmiques.
En C une fonction ne sait pas 'retourner un tableau'.
Ce qu'elle sait faire, c'est retourner une valeur. La pratique courante est de retourner l'adresse du premier élément du tableau. Pour cela, on définit le type retourné comme un pointeur sur le type d'un élément du tableau.
T *f();
NOTA : T représente le type d'un élément du tableau
Evidemment, cette adresse doit être valide après exécution de la fonction. Même si c'est techniquement possible, il est donc hors de question de retourner l'adresse d'un élément appartenant à un tableau local.
Le langage C est défini par un document unique et reconnu sur le plan international (ISO) par tous les intervenants, que ce soit les développeurs de compilateurs (les 'implémenteurs') les développeurs d'applications (les 'utilisateurs') ou les différents formateurs.
Ce document de référence définit un certain nombre d'éléments (obligations, interdictions).
Les autres éléments sont soit laissés à l'appréciation des implémenteurs (implementation defined ou défini par l'implémentation) qui doivent accompagner leur production (compilateur etc.) d'un document précisant les comportement de tel ou tels éléments, soit non définis du tout. Dans ce dernier cas, le comportement est dit indéfini ou indéterminé. (Undefined Behaviour ou UB)
Quelques exemples :
#include <stdio.h> int main (void) { int i = 0; printf ("i = %d\n", i); return 0; }
Ce code est conforme à la spécification du langage, aucune zone n'a été laissée dans l'ombre. Le comportement est déterminé. Il est garanti d'écrire
i = 0
Par contre, voici 2 cas de comportement indéterminé :
int main (void) { int i = 0; printf ("i = %d\n", i); return 0; }
#include <stdio.h> int main (void) { int i; printf ("i = %d\n", i); return 0; }
Les conséquences d'un UB ne sont pas prévisibles. En effet, ça va du crash au comportement d'apparence conforme. Il est donc impossible de compter sur la simple vérification du comportement pour garantir qu'un code est correct. Il faut avant tout qu'il soit exempt de tout UB.
Le compilateur et ses warnings (ou un outil d'analyse spécialisé comme Lint) peut nous aider à débusquer certains UB. Ici, il est probable qu'une 'utilisation de variable non initialisée' ou qu'un 'appel de fonction sans prototypes' soient detectés (mais ça dépend du compilateur et de ses reglages). Mais il est des cas où le compilateur ne voit rien. Le seul recours est alors l'œil exercé du programmeur expérimenté.
La chasse aux UB est donc ouverte en permanence. C'est la principale source de bugs dans un programme C. Il convient donc, d'une part, de bien connaitre le langage et ses limites de définition et, d'autre part, d'être extrêmement vigilant lors de l'écriture et de la relecture du code. Lorsqu'elle est possible, la relecture croisée est une bonne méthode de détection des UB.
Exercice : trouver le UB :
#include <stdio.h> int main (void) { int num = 12; char num_text[] = ""; sprintf (num_text, "%d", num); printf ("Voici num_text : %s\n", num_text); return 0; }
static est un qualificateur qui a plusieurs significations selon le contexte.
static int f(void)Limite la portée de la fonction à l'unité de compilation courante.
static int x;Limite la portée de la variable à l'unité de compilation courante.
int counter(void) { static int x; x++; return x; }Place la variable dans la mémoire statique, la rendant persistante. Usage rarissime (quick'n dirty). A éviter, surtout pour du code réutilisable (bibliothèque).
Il y a deux façon de modifier la valeur d'un objet (aka variable) avec une fonction :
int x = f();ou
int x; x = f();avec
int f(void);
int x; f(&x);avec
void f(int *);
Si l'objet est un pointeur, la règle est la même :
int *px = f();avec
int *f(void);
int *px; f(&px);avec
void f(int **);
Il n'y a aucune chance de modifier l'entier en faisant ceci :
int x; f(x);
De même, il n'y a aucune chance de modifier le pointeur en faisant cela :
int *px; f(px);
Il y a plusieurs façons de créer un tableau dynamique à deux dimensions de type T. La plus courante consiste à créer un tableau de N lignes contenant les adresses des N tableaux de M colonnes. L'avantage de cette méthode est qu'elle permet un usage habituel du tableau avec la notation [i][j].
Comme le tableau de N lignes contient des adresses, ses éléments sont donc des pointeurs sur T. Il se définit ainsi :
T* *pp = malloc (sizeof (T*) * N);
NOTA : T représente le type d'un élément du tableau
Ensuite, chaque élément reçoit l'adresse du premier élément d'un tableau alloué de M colonnes. Chaque élément est donc de type T :
size_t i; for (i = 0; i < N; i++) { pp[i] = malloc (sizeof (T) * M); }
Bien sûr, pour une utilisation correcte, il faut en plus tenir compte du fait que malloc() peut échouer et qu'il faut libérer les blocs alloués après usage.
D'autre part, je rappelle que les valeurs d'un bloc fraichement alloué sont indéfinies.
Enfin, selon les principes énoncés ici, on peut simplifier le codage comme ceci :
T **pp = malloc (sizeof *pp * N);
size_t i; for (i = 0; i < N; i++) { pp[i] = malloc (sizeof *pp[i] * M); }
ce qui facilite la maintenance et évite bien des erreurs de type, le choix étant confié au compilateur.
Il va sans dire qu'il faut ensuite libérer le tableau alloué selon le procédé inverse :
size_t i; for (i = 0; i < N; i++) { free(pp[i]), pp[i] = NULL; } free(pp), pp = NULL;
Voici à quoi sert le qualificateur const et comment l'utiliser correctement.
Le mot clé 'const' est un qualificateur (qualifier) d'objet. Il lui fait perdre sa qualité par défaut qui est 'accessible en lecture ou en écriture' pour le modifier en 'accessible en lecture seule'. Par exemple :
int x = 3; int const y = 4; x = 5; /* acces en ecriture possible */ y = 6; /* acces en ecriture interdit */
Le compilateur signale l'erreur.
On peut placer indifféremment le qualificateur const avant ou après le type.
int const a = 7; const int b = 8;
mais je conseille néanmoins la première forme, car elle est beaucoup plus claire (notamment avec les pointeurs).
Il est techniquement possible de définir un objet const non initialisé :
int const c;
évidemment, l'intérêt est limité, mais il a son application dans un contexte particulier : les paramètres de fonctions.
Le cas des pointeurs est un peu plus complexe, puisqu'il y a en quelque sorte 2 objets pour le prix d'un !
Pour le pointeur, celui-ci étant un objet comme autre, la même règle s'applique, sachant que const doit être placé juste avant l'identificateur, c'est à dire après le dernier * :
int a; int * const pa = &a; pa++; /* interdit */ *pa = 123; /* OK */
Mais un autre qualificateur const peut être utilisé pour préciser les droits du pointeur sur l'objet pointé.
Celui ci se place à la gauche de l'*, avant ou après le type :
int a = 123; int const * pa = &a; const int * pb = &a;
mais là encore, pour des question de clarté du code, je recommande la première forme. Ce qualificateur interdit la modification de l'objet via le pointeur (mais s'il n'est pas lui même qualifié const, l'objet reste modifiable directement, évidemment).
int a = 123; int const * pa = &a; *pa = 456; /* interdit */ a = 456; /* OK */
Par contre, attention. Il est techniquement possible de définir un pointeur sur un objet qualifié const et de tenter de modifier l'objet. Cela produit un comportement indéfini qui n'est pas forcément signalé par le compilateur.
int const a = 123; int * pa = &a; *pa = 456; /* comportement indéfini */
Cependant, le plus souvent, le compilateur signale un problème au moment de l'affectation du pointeur.
int * pa = &a;
mais il convient de rester extrêmement prudent. Le C est un langage qui demande rigueur et maitrise.
NOTA : Bien évidemment, le typecast n'est pas la solution :
int const a = 123; int * pa = (int *) &a; /* NE PAS FAIRE CECI */
il ne fait éventuellement que masquer le problème au compilateur ("je sais ce que fais"), mais il ne résout rien et le comportement indéfini est toujours là.
Le rôle du qualificateur const est particulièrement utile avec les pointeurs, notamment sur des chaines de caractères, qui, rappelons le, ne sont pas modifiables. Il est fortement recommandé de définir tout pointeur sur une chaine de caractères avec le qualificateur const :
char const *p = "hello";
Il est utile aussi pour les pointeurs passés en paramètre à des fonctions. Il permet en effet de restreindre l'accès à la variable pointée à un mode 'lecture seule', ce qui évite bien des erreurs de codage. (La modification d'une variable étant une opération lourde de conséquences si elle est faite au mauvais moment).
Une fonction qui affiche le contenu d'un tableau ou d'une structure, par exemple, n'a pas à la modifier. On fixe donc les règles du jeu dès la définition du prototype :
void display (T const *p)
Une utilisation astucieuse et intelligente du qualificateur const permet d'écrire du code plus sûr.
On appelle structure visible une structure dont les éléments sont visibles de l'utilisateur.
Une 'définition de structure' est le moyen par lequel le programmeur indique au compilateur comment est constitué une structure. Cette opération ne réserve aucune mémoire.
struct mastructure { type_1 element_a; type_2 element_b; };
Ensuite, une structure peut être instanciée, c'est à dire qu'une instance de cette structure est définie en mémoire.
struct mastructure mastructure;
Nota : il est autorisé d'utiliser le même nom, même si ce n'est probablement pas le meilleur des choix possible.
Ces informations suffisent à définir et instancier n'importe quelle structure 'visible'.
On appelle structure opaque une structure dont les éléments ne sont pas visibles de l'utilisateur.
Pour cela, on utilise une définition réduite (ou incomplète) qui consiste à définir le nom de la structure sans en préciser le contenu.
struct mastructure;
Cette définition dite incomplète ne permet évidemment pas de créer un instanciation, puisque le compilateur ignore le contenu de la structure. Il n'a donc pas les moyens d'en déterminer la taille.
Par contre, il est possible de créer un pointeur de ce type :
struct mastructure *p;
Il devient alors possible de créer une fonction qui retourne un pointeur de ce type :
struct mastructure *fonction(void);
de passer ce pointeur en paramètre une fonction :
void fonction(struct mastructure *p);
d'en faire un élément de structure etc.
Evidemment, il faudra que la structure soit définie 'quelque part' afin qu'elle soit instanciable et que ses éléments soient manipulables.
On va donc créer un fichier source (.c) séparé d'implémentation contenant les fonctions permettant la création (instanciation) des données, et une interface (header ou .h) ne comportant que la définition incomplète de la fonction et, au minimum, les 2 fonctions permettant la création et la suppression d'une instance de la structure.
Soit la structure 'xxx'. On obtient :
/* xxx.h Interface */ #ifndef H_XXX #define H_XXX /* definition incomplete de structure */ struct xxx; /* prototypes des fonctions */ struct xxx *xxx_create (void); void xxx_delete (struct xxx *p); /* etc. */ #endif
/* xxx.c Implementation */ #include "xxx.h" /* definition de la structure (exemple) */ struct xxx { int a; char b[10]; }; /* fonctions publiques */ struct xxx *xxx_create (void) { /* a completer */ } void xxx_delete (struct xxx *p) { /* a completer */ }
Exemple d'utilisation :
/* test.c */ #include "xxx.h" #include <stddef.h> int main (void) { /* instanciation */ struct xxx *p = xxx_create (); /* tout appel de fonction peut echouer... */ if (p != NULL) { /* utilisation de l'objet xxx (via de nouvelles fonctions a creer) ... */ /* fin d'utilisation : destruction de l'objet */ xxx_delete (p), p = NULL; } return 0; }
Je laisse au lecteur le soin de proposer une ou des implémentations de xxx_create() et de xxx_delete(), sachant qu'on a pas forcément besoin d'un nombre illimité d'instanciations...
Il est possible, afin de simplifier l'écriture (notamment pour les interfaces publiques), de remplacer le nom de la structure par un nom différent (alias ou pseudonyme) généralement plus court. Il est recommandé de ne pas abuser de l'abstraction, car les possibilités sont réduite en C et il est bon que le programmeur garde en tête qu'il manipule des pointeurs.
Ceci est possible :
typedef struct xxx xxx_s;
mais ceci est déconseillé :
typedef struct xxx *xxx; /* /!\ */
Détails d'application dans l'article sur les TAD (ADT)
© Emmanuel Delahaye 2004-2024 | emmanuel dot delahaye at gmail dot com |
Home |
Forum |
Livre d'or