Bien programmer en langage C
Date de publication : 28 mai 2008
IV. Notes
IV-A. Saisie de données par un opérateur (stdin)
IV-A-1. Introduction
IV-A-2. fgetc(), getc(), getchar()
IV-A-3. gets()
IV-A-4. scanf()
IV-A-5. fgets()
IV-A-6. Ressources
IV-A-7. Comment fonctionne fgetc(stdin) alias getchar()
IV-A-7-a. Comportement visible.
IV-A-7-b. Comportement interne.
IV-A-7-c. Quelques expérimentations.
IV-B. Les pointeurs démythifiés !
IV-B-1. Introduction
IV-B-2. Définition
IV-B-2-a. Pointeur sur objet
IV-B-2-b. Pointeur de fonction
IV-B-2-b-i. Remarques importantes
IV-C. Du bon usage des pointeurs
IV-D. Passer un tableau à une fonction
IV-D-1. Introduction
IV-D-2. Tableau à une dimension
IV-D-3. Tableau à n dimensions
IV-E. char* char**
IV-E-1. Introduction
IV-E-2. Chaine de caractères
IV-E-3. Le type char *
IV-E-4. Le type char **
IV-F. Les fichiers
IV-F-1. Introduction
IV-F-2. Texte ou binaire ?
IV-F-2-a. Fichier texte
IV-F-2-b. Fichier binaire
IV-F-2-c. Modes d'ouverture d'un fichier
IV-F-2-d. Lecture d'un fichier
IV-F-2-d-i. fgetc(), getc()
IV-F-2-d-ii. fread()
IV-F-2-d-iii. fscanf()
IV-F-2-d-iv. fgets()
IV-F-2-d-iv--1. Exemple d'utilisation
IV-F-2-d-iv--2. Explication
IV-F-2-d-iv--3. Critique de cet exemple
IV-F-2-d-iv--4. Détection d'une erreur
IV-F-2-d-iv--5. Gestion des fins de ligne
IV-F-2-d-iv--6. Exemple amélioré avec détection de la fin de lecture
IV-F-2-d-iv--7. Explication
IV-F-2-e. Écriture dans un fichier
IV-F-2-e-i. fputc(), putc()
IV-F-2-e-ii. fwrite()
IV-F-2-e-iii. fprintf()
IV-F-2-e-iv. fputs()
IV-F-2-f. Bien utiliser les formats de données
IV-F-2-f-i. Format orienté texte
IV-F-2-f-ii. Format orienté binaire
IV-F-3. Supprimer un enregistrement dans un fichier binaire
IV-F-4. En guise de conclusion
IV-G. Se procurer la norme du langage C (C99)
IV-H. Bien utiliser malloc()
IV-H-1. Suppression du cast
IV-H-1-a. Compilateur non ISO
IV-H-1-b. Compilateur C++
IV-H-2. Déterminer la taille sans le type
IV-I. Bien utiliser realloc()
IV-J. Borland C : "floating point formats not linked"
IV-K. Production du code exécutable
IV-L. Bibliothèques de fonctions
IV-L-1. Bibliothèque statique
IV-L-2. Bibliothèque dynamique
IV-M. Variables globales
IV-M-1. Nommage
IV-M-2. Organisation
IV-M-3. Définition
IV-M-4. Déclaration
IV-M-5. Utilisation
IV-N. Champs de bits
IV-O. Le type retourné par main()
IV-0-1. Historique
IV-P. Pourquoi fflush (stdout) ?
IV-Q. Bien gérer la portée des objets et des fonctions
IV-Q-1. Fonctions
IV-Q-2. Objets
IV-Q-2-a. Définition hors d'un bloc
IV-Q-2-b. Définition dans un bloc
IV-Q-2-c. Masquage (Shadowing)
IV-R. <time.h> : bien utiliser difftime()
IV-S. C, UNIX, POSIX ?
IV-T. Initialisation des tableaux de caractères
IV-U. Déclarations ? Définitions ?
IV-U-1. Déclaration
IV-U-2. Définition
IV-V. Données
IV-V-1. Initialisation
IV-W. Le mode plein ecran
IV-W-1. Installation de ansi.sys sous Windows XP
IV-X. Les identificateurs réservés
IV-Y. Code standard ? Code portable ? Je suis perdu !
IV-Y-1. "standard" ?
IV-Y-2. "portable" ?
IV-Y-2-a. portabilité absolue
IV-Y-2-b. portabilité relative
IV-Y-3. Bon usage
IV-Z. Langage C ? Fonctions systemes ? Je suis perdu !
IV-Z-1. Domaine couvert par le langage C
IV-Z-2. Fonctions système
IV-Z-3. Bibliothèques tierces publiques
IV-Z-4. Bibliothèques tierces privées
IV-AA. size_t, c'est quoi ?
IV-AB. Un tableau n'est pas un pointeur ! Vrai ou faux ?
IV-AC. Du bon usage de assert()
IV-AC-1. Exemple d'utilisation
IV-AD. rand(), srand()... j'y comprends rien...
IV-AE. C'est quoi un prototype ?
IV-AF. C'est quoi l'algorithmique ?
IV-AG. pile, tas ? C'est quoi ?
IV-AH. Les pointeurs, ça sert à quoi ?
LIV-AH-1. A quoi ça sert?
IV-AH-1-a. Petit rappel.
IV-AH-1-b. Comment faire ?
IV-AI. Qu'est-ce qu'une chaine litterale ?
IV-AJ. Enregister une structure
IV-AJ-1. Le format binaire
IV-AJ-2. Le format texte
IV-AK. Retourner un tableau
IV-AL. Comportement indéfini
IV-AM. C'est quoi ce 'static' ?
IV-AN. Pourquoi ma fonction ne modifie pas ma variable ?
IV-AO. Comment créer un tableau dynamique à 2 dimensions ?
IV-AP. Bien utiliser const
IV-AP-1. Objets simples
IV-AP-2. Pointeurs
IV-AP-3. Usage
IV-AQ. Structures
IV-AQ-1. Structures visibles
IV-AQ-2. Structures opaques
IV-AQ-3. Pseudonymes (ou alias)
IV. Notes
IV-A. Saisie de données par un opérateur (stdin)
IV-A-1. Introduction
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.
-
fgetc()
-
getc()
-
getchar()
-
gets()
-
scanf()
-
fgets()
IV-A-2. fgetc(), getc(), getchar()
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.
IV-A-3. gets()
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.
IV-A-4. scanf()
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 formatté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.
IV-A-5. fgets()
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);
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
IV-A-6. Ressources
IV-A-7. Comment fonctionne fgetc(stdin) alias getchar()
Cette fonction d'apparence simple a en fait un comportement plus complexe qu'il n'y parait. En effet,
elle regoupe un certain nombre de comportements non triviaux qui sont rarement expliqués dans la
littérature C.
IV-A-7-a. Comportement visible.
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 programe) est marquée par la frappe de la touche <enter>.
IV-A-7-b. Comportement interne.
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.
IV-A-7-c. Quelques expérimentations.
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 :
On voit que le caractère extrait est '\n' (ici, LF, soit le code ASCII 10)
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.
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.
IV-B. Les pointeurs démythifiés !
IV-B-1. Introduction
Le langage C est indissociable de la notion de pointeur. Ce mot mythique en effraye 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...
IV-B-2. Définition
Un pointeur est une variable dont la valeur est une adresse.
On distingue 2 grandes familles de pointeurs :
-
Les pointeurs sur objet
-
Les pointeurs de fonction
IV-B-2-a. Pointeur sur objet
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 signe '*' et enfin l'identificateur, suivit 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.
Pour initialiser un pointeur, on peut soit :
-
lui donner la valeur 0 ou NULL, qui signifie "invalide, ne pas utiliser".
-
lui donner l'adresse d'une variable,
-
lui donner la valeur retournée par les fonctions malloc(), realloc(), calloc().
-
s'il est de type void ou FILE, lui donner la valeur retournée par fopen().
-
s'il est de même type, ou void, lui donner la valeur d'un autre pointeur réputé correctement initialisé.
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 '*'.
int a = 4 ;
int * p = & a;
int b;
b = * p;
|
IV-B-2-b. Pointeur de fonction
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 :
int (* pf) (int , char * * );
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.
typedef int fun_f (int , char * * );
fun_f * pf;
int fun (fun_f * pf);
fun_f * pf[10 ];
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 :
-
lui donner la valeur 0 ou NULL, qui signifie "invalide, ne pas utiliser".
-
lui donner l'adresse d'une fonction.
-
s'il est de même type, lui donner la valeur d'un autre pointeur réputé correctement initialisé.
Pour utiliser le pointeur, il suffit de l'invoquer comme une fonction.
typedef int fun_f (int );
fun_f * pf;
int function (int );
fun_f function;
pf = fonction;
pf (123 );
|
IV-B-2-b-i. Remarques importantes
-
void* n'est pas un type correct pour un pointeur de fonction.
-
Il n'existe pas de type générique pour les pointeurs de fonctions.
IV-C. Du bon usage des pointeurs
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.
int * p = NULL ;
p = malloc (4 * sizeof * p);
if (p ! = NULL )
{
free (p);
p = NULL ;
}
|
IV-D. Passer un tableau à une fonction
IV-D-1. Introduction
|
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 oeuvre l'arithmétique des pointeurs. Un paramètre 'pointeur' est
donc exactement ce qu'il faut.
IV-D-2. Tableau à une dimension
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 :
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 ;
* (p + 1 ) = 0 ;
* (p + 2 ) = 0 ;
* (p + 3 ) = 0 ;
* (p + 4 ) = 0 ;
}
|
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 ;
p[1 ] = 0 ;
p[2 ] = 0 ;
p[3 ] = 0 ;
p[4 ] = 0 ;
}
|
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.
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 ;
}
|
IV-D-3. Tableau à n dimensions
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'élements identiques. Le nombre d'éléments est donc
tout simplement le rapport entre la taille en bytes 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 ) )
|
IV-E. char* char**
IV-E-1. Introduction
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 ?
IV-E-2. Chaine de caractères
Une chaine de caractères est un tableau de char terminé par un 0. Une chaine littérale n'est pas modifiable.
" hello " [2 ] = ' x ' ;
char s[] = " hello " ;
s[2 ] = ' x ' ;
|
IV-E-3. Le type char *
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 ;
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'.
|
IV-E-4. Le type char **
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. Un petit schéma pour modéliser :
IV-F. Les fichiers
IV-F-1. Introduction
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/ecriture).
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.
IV-F-2. Texte ou binaire ?
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'autre 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.
IV-F-2-a. Fichier texte
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.
IV-F-2-b. Fichier binaire
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'.
IV-F-2-c. Modes d'ouverture d'un fichier
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)
|
IV-F-2-d. Lecture d'un fichier
Le langage C offre plusieurs fonctions permettant de lire les données d'un fichier.
-
fgetc()
-
getc()
-
fread()
-
fscanf()
-
fgets()
IV-F-2-d-i. fgetc(), getc()
Ces fonctions sont identiques. Elle permettent de lire un caractère.
IV-F-2-d-ii. fread()
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).
IV-F-2-d-iii. fscanf()
Cette fonction permet de lire des données 'texte' formattées. Cette fonction est d'une utilisation complexe
et son usage est peu recommandé.
IV-F-2-d-iv. fgets()
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.
IV-F-2-d-iv--1. Exemple d'utilisation
Soit le fichier texte :
Ceci est un simple fichier
texte de 2 lignes.
|
et un petit programme permettant de lire ces 2 lignes
# include <stdio.h>
int main (void )
{
FILE * fp = fopen (" data.txt " , " r " );
if (fp ! = NULL )
{
char ligne[32 ];
fgets (ligne, sizeof ligne, fp);
printf (" 1: %s\n " , ligne);
fgets (ligne, sizeof ligne, fp);
printf (" 2: %s\n " , ligne);
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.
|
IV-F-2-d-iv--2. Explication
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, suivit de ': ', la ligne (avec son '\n') et un '\n' en plus, ce
qui explique la présence de lignes "vides".
IV-F-2-d-iv--3. Critique de cet exemple
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.
IV-F-2-d-iv--4. Détection d'une erreur
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))
{
puts (" EOF " );
}
if (ferror (fp))
{
perror (NOM_DU_FICHIER);
}
fclose (fp);
|
IV-F-2-d-iv--5. Gestion des fins de ligne
On constate que lorsque fgets() lit une ligne entière, un '\n' se retouve à 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 ];
fgets (ligne, sizeof ligne, fp);
{
char * p = strchr (ligne, ' \n ' );
if (p ! = NULL )
{
* p = 0 ;
}
else
{
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.
IV-F-2-d-iv--6. Exemple amélioré avec détection de la fin de lecture
# include <stdio.h>
int main (void )
{
FILE * fp = fopen (" data.txt " , " r " );
if (fp ! = NULL )
{
char ligne[32 ];
int cpt = 0 ;
while (fgets (ligne, sizeof ligne, fp) ! = NULL )
{
cpt+ + ;
printf (" %d: %s\n " , cpt, ligne);
}
fclose (fp);
}
else
{
printf (" Erreur d'ouverture du fichier\n " );
}
return 0 ;
}
|
Cet exemple met en oeuvre 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.
|
IV-F-2-d-iv--7. Explication
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'.
IV-F-2-e. Écriture dans un fichier
Le langage C offre plusieurs fonctions permettant d'écrire des données dans un fichier.
-
fputc()
-
putc()
-
fwrite()
-
fprintf()
-
fputs()
IV-F-2-e-i. fputc(), putc()
Ces fonctions sont identiques. Elle permettent d'écrire un caractère.
IV-F-2-e-ii. fwrite()
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).
IV-F-2-e-iii. fprintf()
Cette fonction permet d'écrire des données 'texte' formattées. Elle comporte de nombreuses possibilité
de conversion de valeurs numériques en texte. (Entiers, flottants etc.)
IV-F-2-e-iv. fputs()
Cette fonction permet d'écrire une chaine de caractères.
IV-F-2-f. Bien utiliser les formats de données
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.
IV-F-2-f-i. Format orienté texte
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 formatté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é.
IV-F-2-f-ii. Format orienté binaire
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).
IV-F-3. Supprimer un enregistrement dans un fichier binaire
Pour supprimer un enregistrement, le plus simple est de procéder ainsi:
-
Le fichier original est ouvert en lecture. Un nouveau fichier est ouvert en écriture. L'original est
lu enregistrement par enregistrement (fread()), et recopié dans le nouveau fichier (fwrite())
en omettant l'enregistrement à supprimer (if ...).
-
Par un jeu subtil de suppression et de renommage (remove(), rename()), on se retrouve avec une copie
de l'original (genre .old ou .bak) et le nouveau fichier qui a maintenant le nom de l'ancien.
L'opération reste simple, et a l'avantage de permettre l'annulation (par renommage de l'ancien
fichier).
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.
IV-F-4. En guise de conclusion
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).
IV-G. Se procurer la norme du langage C (C99)
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 (N1124) ici : (
C99).
Ce document est complet et integre C99, TC1 et TC2 (Technical Corrigendum 1 et 2). 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.
IV-H. Bien utiliser malloc()
Il est fréquent de rencontrer ce genre de code :
# include <stdlib.h>
...
{
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.
IV-H-1. Suppression du cast
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.
-
Le compilateur n'est pas conforme à ISO C-90 ou ISO C-99
-
Le compilateur n'est pas C mais par exemple pré-C++98
IV-H-1-a. Compilateur non ISO
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.
IV-H-1-b. Compilateur C++
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écification 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).
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<...>.
IV-H-2. Déterminer la taille sans le type
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);
}
|
IV-I. Bien utiliser realloc()
La fonction realloc(), bien que souvent décriée pour sa lenteur, offre une alternative interessante 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>
< ...>
{
size_t size = 10 ;
int * p = malloc (size * sizeof * p);
< ...>
{
size = 15 ;
type_s * p_tmp = realloc (p, size * sizeof * p_tmp);
if (p_tmp ! = NULL )
{
p = p_tmp;
}
else
{
}
}
}
|
Il faut aussi garder à l'esprit que la partie nouvellement allouée n'est pas initialisée.
IV-J. Borland C : "floating point formats not linked"
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 utilise pas de fonction 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__
extern unsigned _floatconvert;
# pragma extref _floatconvert
# endif
|
IV-K. Production du code exécutable
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
:
-
Production de modules de code machine intermédiaires par compilation individuelle des codes sources.
Il manque les références externes.
-
Production du code machine exécutable par résolution des références externes (liens) entre les modules
intermédiaires, et l'éventuelle ajout de bibliothèques de fonctions selon les besoins.
Pour cela, on utilise successivement 2 outils :
-
Le compilateur (compiler), autant de fois que nécessaire, selon le nombre de fichiers sources
à traiter.
-
L'éditeur de lien (linker), une fois pour produire l'exécutable.
IV-L. Bibliothèques de fonctions
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).
IV-L-1. Bibliothèque statique
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).
IV-L-2. Bibliothèque dynamique
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.
IV-M. Variables globales
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 initalisé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 :
-
On ne sait ni comment ni quand ni qui accède à cette variable. Ca rend le code intestable et incompréhensible.
On a aucune certitude sur le code.
-
Cela crée une dépendance, ce qui le code rend non modulaire et non réutilisable.
-
L'instance étant unique, ça rend le code impropre à la récursion et à l'utilisation dans des threads
et même au simple appel imbriqué.
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.
IV-M-1. Nommage
Il est recommandé d'utiliser le préfixe g_ ou G_ pour signifier qu'une variable est globale.
IV-M-2. Organisation
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.
IV-M-3. Définition
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).
# include "data.h"
int G_x;
double G_a[];
data_s G_data;
|
IV-M-4. Déclaration
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
typedef struct
{
int a;
char b[123 ];
}
data_s;
extern int G_x;
extern double G_a[12 ];
extern data_s G_data;
# endif
|
IV-M-5. Utilisation
Il est recommandé que le fichier qui utilise une variable globale inclue le fichier de déclaration (.h).
# include "data.h"
int main (void )
{
G_x = 123 ;
G_data.a = 456 ;
G_a[3 ] = 123 .456 ;
return 0 ;
}
|
IV-N. Champs de bits
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 ;
unsigned b:3 ;
}
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
http://mapage.noos.fr/emdel/clib.htm
Module DATE (date.h) (Les tailles indiquées en commentaire sont les tailles minimales garanties)
...
typedef unsigned int uint;
< ...>
typedef struct
{
int year;
uint month:4 ;
uint day:5 ;
uint hour:5 ;
uint minute:6 ;
uint second:6 ;
}
sDATE;
|
... autant il est illusoire d'utiliser les champs de bits pour créer une interface avec l'exterieur du
programme, comme un flux de bytes 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 (&,
|, ~, <<, >>, ^)
IV-O. Le type retourné par main()
Bien que main() soit censé retourner un int, on voit quelquefois écrit
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).
IV-0-1. Historique
Dès l'apparition du langage C, une des formes canoniques de main() était
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 :
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').
IV-P. Pourquoi fflush (stdout) ?
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 envoit 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 :
-
Le tampon d'émission est plein (incontrôlable)
-
Un '\n' a été placé dans le tampon[1]
-
La commande de forçage a été activée
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.
IV-Q. Bien gérer la portée des objets et des fonctions
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).
IV-Q-1. Fonctions
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:
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 :
-
Une économie d'identificateurs. La portée de celui-ci étant limitée à une unité de compilation, il est
possible de le réutiliser pour une autre fonction qualifiée 'static' dans une autre unité de compilation.
-
Une meilleure optimisation. Certains compilateurs sont capables d'"inliner" une telle fonction dans certaines
conditions, ce qui diminue le temps d'exécution au prix d'une augmentation de la taille (le code
de la fonction est recopié autant de fois que nécessaire).
-
Une meilleure organisation du code. Etant donné que ces fonctions sont forcément appelées par une fonction
de l'unité de compilation dans laquelle elles ont été définies, le code se trouve naturellement
organisé en blocs fonctionnels cohérents. De plus, comme à priori, ces fonctions n'ont pas besoin
de prototypes séparés, celà favorise une organisation du fichier source selon le principe 'Définir
avant d'utiliser' (Top-down)
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 |
+ - - - - - - - - - - - - - - - - - - - - +
|
IV-Q-2. Objets
La portée d'un objet est régie selon plusieurs critères.
IV-Q-2-a. Définition hors d'un bloc
La portée par défaut est globale. Elle peut être réduite à l'unité de compilation en ajoutant le qualificateur
static.
IV-Q-2-b. Définition dans un bloc
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 evite 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.
IV-Q-2-c. Masquage (Shadowing)
int x;
int f (void )
{
int x = 0 ;
x+ + ;
}
int main (void )
{
x = 2 ;
f ();
return 0 ;
}
|
IV-R. <time.h> : bien utiliser difftime()
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);
{
struct tm tm_xmas =
{ 0 } ;
tm_xmas.tm_year = tm_now.tm_year;
tm_xmas.tm_mon = 12 - 1 ;
tm_xmas.tm_mday = 25 ;
{
time_t xmas = mktime (& tm_xmas);
strftime (s, sizeof s, " %d/%m/%Y " , & tm_xmas);
printf (" Noel : %s\n " , s);
{
time_t diff = difftime (xmas, now);
struct tm tm_diff = * gmtime (& diff);
printf (" Plus que %d jours avant Noel\n " , tm_diff.tm_yday);
}
}
}
return 0 ;
}
|
IV-S. C, UNIX, POSIX ?
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 :
-
Le mode commande (shell) pour l'utilisateur de la machine.
-
Une API utile aux programmeurs d'applications.
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
-
La gestion des répertoires
-
La gestion des processus (process)
-
La gestion des tâches (threads)
-
La gestion des réseau (sockets)
-
etc.
IV-T. Initialisation des tableaux de caractères
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 intialisés.
Exemples
le tableau est initialisé avec {'a', 'b', 0, 0} : Chaine valide
le tableau est initialisé avec {'a', 'b', 'c', 0} : Chaine valide
le tableau est initialisé avec {'a', 'b', 'c', 'd'} : Chaine invalide /!\
Ne compile pas (trop d'initialisateurs)
IV-U. Déclarations ? Définitions ?
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.
IV-U-1. Déclaration
Une déclaration permet l'utilisation d'un objet ou d'un fonction. Elle doit être préalable à l'utilisation.
Exemples avec un objet x de type int :
extern int x;
< ...>
{
x = 2 ;
}
|
ou
ou
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)
{
}
|
IV-U-2. Définition
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 sur 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
IV-V. Données
Le langage C utilise 3 zones mémoire pour implémenter les données.
-
La mémoire statique, qui contient les variables permanentes (modifiables ou non)
-
La mémoire automatique qui contient les variables locales et les paramètres des fonctions
-
La mémoire allouée qui contient des variables dynamiques gérées à l'exécution par le programme (malloc()
/ free().
Les données en C sont caractérisées par
-
Leur portée
-
Leur durée de vie.
La portée peut être
-
locale à un bloc
-
limitée à une unité de compilation
-
illimitée
-
contrôlée par l'application.
La durée de vie peut être
-
limitée à un bloc
-
permanente
-
contrôlée par l'application.
Exemples:
int a;
static int b;
< ...>
{
int c;
static int d;
int * p = malloc (sizeof * p * 3 );
free (p), p = NULL ;
}
|
IV-V-1. Initialisation
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.
IV-W. Le mode plein ecran
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 :
-
stdin : entrée standard
-
stdout : sortie standard
-
stderr : sortie erreur
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':
L'utilisateur peut alors passer une commande de numérotation
et le modem répond
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.
-
Effacement de l'ecran
-
Placement absolu du curseur
-
Contrôle de la couleur du texte et du fond
Malheureusement, il n'a jamais été prévu de norme internationale regissant cette gestion d'écran (probalement
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.
-
Basées sur les ressources de la machine :
-
PC/MS-DOS : Borland et sa bibliothèque conio (Turbo Pascal, C) pour la console intégrée. Windows bénéficie
d'un portage réduit de conio pour MinGW (PDF).
-
Unixoides : curses et ncurses. (consoles locales)
-
Basées sur des ressources console
-
Commandes ANSI (VT-100)
-
Vidéotex (Minitel)
-
HTML (Browser)
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
IV-W-1. Installation de ansi.sys sous Windows XP
|
A ma connaissance, il n'y a pas de possibilité d'installer un interpréteur de séquences ANSI pour cmd.exe.
|
IV-X. Les identificateurs réservés
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 identificarteurs 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 | | Exemple | Exemple |
| commençant par | suivi de | valide | Reservé |
+ - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - +
| " _ " | " _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 |
+ - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - +
|
IV-Y. Code standard ? Code portable ? Je suis perdu !
On entend parler de code standard, de code portable ? Qu'est-ce que ça signifie ? Ca sert à quoi ?
IV-Y-1. "standard" ?
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 est défini par la norme
-
Ce qui est défini par la cible[1]
-
Ce qui n'est pas défini
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)
IV-Y-2. "portable" ?
C'est la capacité qu'a un code source à produire un comportement identique sur différentes plateformes.
On distingue la portabilté absolue (pour n'importe quelle plateforme) de la portabilité relative
(limitée à un cetain nombre de plateformes bien définies).
IV-Y-2-a. portabilité absolue
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'.
IV-Y-2-b. portabilité relative
C'est un code portable 'absolu' auquel s'ajoute des extensions (généralement, des bibliothèques) tierces
concues 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éseau (sockets). Mais ils 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.
IV-Y-3. Bon usage
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 confome 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.
IV-Z. Langage C ? Fonctions systemes ? Je suis perdu !
Sur un forum je pose une question sur le langage C et on me réponds "va voir sur un forum consacré à
ton système". M'enfin, je programme en C, c'est quoi ce cirque ?
IV-Z-1. Domaine couvert par le langage C
Le langage C, tel qu'il est défini par la norme, est un ensemble de regles d'écriture (syntaxe, sémantique)
définissant les éléments du langage, et un ensemble de fonctions regoupées sous le terme générique
de 'bibliothèque d'exécution du [langage] C'.
Les domaines couverts par la bibliothèque sont
-
Les flux d'entrée/sorties
-
Les traitements de chaines
-
Les conversions chaine/binaires
-
Les fonctions mathématiques
-
Les algorithmes génériques (tri, recherche)
-
La gestion du temps
-
(d'autres qui me reviendront plus tard...)
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.
+ - - - - - - - - + + - - - - - - - - - - - - + + - - - - - - - - +
| entrée | - - > | traitement | - - > | sortie |
+ - - - - - - - - + + - - - - - - - - - - - - + + - - - - - - - - +
|
Exemples typiques
-
compilateur
-
générateur de code
-
convertisseur wav -> mp3
-
etc.
Si on cherche d'autres domaines d'applications comme
-
Gestion de l'écran en mode texte
-
Lecture du clavier sans attente
-
Impression
-
Port série
-
Réseau
-
Ecran graphique
-
Programmation évènementielle
-
Programmation multi-tâche
-
Interface graphique
-
Souris
-
etc.
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 ecrit en C, elles
ne font pas partie du langage C.
On distingue principalement 3 types de ressources
-
Les fonctions fournies par le système
-
Les fonctions des bibliothèques publiques (gratuites, payante)
-
Les fonctions des bibliothèques privées (personnelles, entreprises)
IV-Z-2. Fonctions système
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èvements. 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 :
-
identificateur
-
paramètres
-
retour
-
comportement
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 'Video':
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
< ...>
void VIDEO_putch (int c, eCOU ct, eCOU cf)
{
# if defined ( __BORLANDC__ )
union REGS reg;
reg.h.al = (uchar) c;
reg.h.ah = 0x09 ;
reg.h.bh = 0 ;
reg.h.bl = (uchar) (ct | ((cf & ~ 0x08 ) < < 4 ));
reg.x.cx = 1 ;
int86 (0x10 , & reg, & reg);
# 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 du-dit 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.
IV-Z-3. Bibliothèques tierces publiques
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)
-
GTK+ (GUI)
-
SDL (Graphisme 2D simple)
-
Fmod (Multimédia)
-
MySQL (SGBD)
-
etc.
IV-Z-4. Bibliothèques tierces privées
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 interesser d'autres
personnes et si la licence le permet.
IV-AA. size_t, c'est quoi ?
size_t est le type retourné par l'opérateur sizeof. C'est un entier non signé. Il est suffisament 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.)
IV-AB. Un tableau n'est pas un pointeur ! Vrai ou faux ?
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.
-
Un tableau est une séquence d'élements de types identiques.
-
Le nom du tableau est invariant. Il a la valeur et le type de l'adresse du premier élément du tableau.
C'est donc une adresse typée (encore appelée pointeur constant[1]). Etant de la même nature qu'un
pointeur, les même regles d'adressage s'appliquent, à savoir que le premier élément est en tab,
soit tab + 0 et que son contenu est donc *(tab + 0). De même le contenu du deuxième élément est
*(tab + 1) etc.
-
Cette syntaxe étant un peu lourde, le langage C définit une simplification qui est tab[0], tab[1] etc.
Le nombre entre les crochets est appelé indice. Son domaine de définition pour un tableau de taille
N est 0 à N-1.
représentation graphique d'un tableau de 4 éléments :
tab : |---------------|
tab[0] : |---|
tab[1] : |---|
tab[2] : |---|
tab[3] : |---|
|
[1] C'est là que ce 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.
IV-AC. Du bon usage de assert()
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 corrects.
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.
IV-AC-1. Exemple d'utilisation
# 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.
-
il doit être >=0 (toujours vrai, vu le type size_t)
-
il doit être < n
Je vais donc ajouter un piège :
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+ + )
{
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;
for (i = 0 ; i < n; i+ + )
{
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.
|
IV-AD. rand(), srand()... j'y comprends rien...
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 );
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épendemment 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 ));
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.
D'autre part,
ceci peut aider.
IV-AE. C'est quoi un prototype ?
Lorsqu'on défini une fonction,
on définit en même temps son interface, à savoir :
-
son nom (ici , f)
-
son type de retour (ici, int)
-
ses paramètres (ici, s, de type pointeur sur char)
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.
# include "f.h"
int f (char * s)
{
...
}
|
# 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
int f (char * s);
# endif
|
IV-AF. C'est quoi l'algorithmique ?
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);
}
}
}
|
IV-AG. pile, tas ? C'est quoi ?
Le langage C définit 3 zones de stockage pour les objets (aussi appellés variables)
-
La mémoire statique
-
La mémoire allouée
-
La mémoire automatique
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.
IV-AH. Les pointeurs, ça sert à quoi ?
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.
LIV-AH-1. A quoi ça sert?
IV-AH-1-a. Petit rappel.
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 :
avec
Déroulement des opérations :
-
x prend la valeur 123
-
appel de la fonction f() avec en paramètre la valeur de x (soit 123)
-
Dans f(), la variable locale a prend la valeur 123
-
la valeur de a est augmentée de 1 et devient 124
-
la fonction se termine
-
la valeur de x n'a pas changé, elle vaut toujours 123.
Si on avait voulu modifier x en appelant f(), c'est raté.
IV-AH-1-b. Comment faire ?
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
-
x prend la valeur 123
-
p prend l'adresse de x
-
x est incrémenté via p et l'opérateur de déréférencement. Il vaut maintenant 124.
sauf que les opérations vont être réparties entre l'appelant et l'appelé comme ceci :
avec
void f (int * p)
{
(* p)+ + ;
}
|
-
x prend la valeur 123
-
l'adresse de x est passée à la fonction
-
dans f(), la variable locale p est initialisée avec l'adresse de x
-
la valeur de x est incrémentée de 1 via p et l'opérateur de déréférencement. Elle vaut maintenant 124.
-
la fonction se termine
-
la valeur de x a changé, elle vaut maintenant 124.
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.
IV-AI. Qu'est-ce qu'une chaine litterale ?
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)
|
Elle ne doit pas être confondue avec la liste de caractères servant à initialiser un tableau de char,
par exemple :
|
(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 :
c'est cette valeur (l'adresse) qui est passée à la fonction dans son paramètre :
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
IV-AJ. Enregister une structure
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 :
-
La représentation interne des données en C peut changer d'une implémentation à l'autre, en terme de largeur
(nombre de bits), de 'boutisme' (position du byte de poids fort) et de codage (entiers négatifs,
nombres réel, jeu de caractères).
-
L'alignement requis. En effet, certaines architectures imposent que les éléments de la structure soient
alignés sur une adresse multiple de 2, 4 ou autre, ce qui rend impossible de prévoir de façon portable,
la signification des bytes dans le fichier.
Pour résoudre ce problème, il y a 2 grandes familles de solutions :
-
Le format binaire
-
Le format texte
IV-AJ-1. Le format binaire
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().
IV-AJ-2. Le format texte
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 :
-
Du jeu de caractères utilisé. En dehors d'EBCDIC utilisé sur les gros systèmes IBM (Mainframes),
c'est généralement ASCII (0-127), mais ça peut ne pas suffire à encoder tous les caractères, notamment
les caractères accentués et autres signes mathématiques ou pseudo-graphiques. Or il existe plusieurs
façons de coder ces caractères (ASCII étendu, IBM-PC8, UTF-8, Unicode etc.). Là encore, une définition
indépendante peut aider...
-
De la façon de coder les fins de ligne (CR, LF, CRLF etc.)
Certaines corrections doivent donc parfois être effectuées à l'aide de tables de codages et autres astuces
algorithmiques.
IV-AK. Retourner un tableau
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.
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.
-
Soit on passe à la fonction l'adresse du premier élément d'un tableau existant, et elle peut retourner
l'adresse de ce tableau.
-
Soit la fonction fait une allocation dynamique et retourne l'adresse du bloc alloué.
-
On peut aussi retourner l'adresse du premier élément d'une chaine littérale. Celle-ci est statique (attention
accès en lecture seule, qualificateur 'const' recommandé).
-
Enfin, il est techniquement possible de retourner l'adresse du premier élément d'un tableau statique,
mais cette pratique est déconseillée, car elle rend la fonction non-réentrante, donc impropre à
plusieurs utilisations comme les appels imbriqués ou les threads... Plusieurs fonctions du C sont
malheureusement victimes de ce défaut (ctime() asctime(), strtok() etc.)
IV-AL. Comportement indéfini
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.
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
Par contre, voici 2 cas de comportement indéterminé :
-
Absence de prototype pour printf()
int main (void )
{
int i = 0 ;
printf (" i = %d\n " , i);
return 0 ;
}
|
-
Lecture d'une valeur non initialisée
# 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'oeil 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 ;
}
|
IV-AM. C'est quoi ce 'static' ?
static est un qualificateur qui a plusieurs significations selon le contexte.
-
Devant une déclaration de fonction :
Limite la portée de la fonction à l'unité de compilation courante.
-
Devant une variable définie hors de tout bloc (dite 'de portée globale')
Limite la portée de la variable à l'unité de compilation courante.
-
Devant une variable définie dans une bloc (dite 'de portée locale')
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).
|
IV-AN. Pourquoi ma fonction ne modifie pas ma variable ?
Il y a deux façon de modifier la valeur d'un objet (aka variable) avec une fonction :
-
Récupérer la valeur retournée par la fonction :
ou
avec
-
Passer l'adresse de la variable dont on veut modifier la valeur à la fonction :
avec
Si l'objet est un pointeur, la règle est la même :
avec
avec
|
Il n'y a aucune chance de modifier l'entier en faisant ceci :
|
|
De même, il n'y a aucune chance de modifier le pointeur en faisant cela :
|
IV-AO. Comment créer un tableau dynamique à 2 dimensions ?
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 ;
|
IV-AP. Bien utiliser const
Voici à quoi sert le qualificateur const et comment l'utiliser correctement.
IV-AP-1. Objets simples
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 ;
y = 6 ;
|
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é :
évidemment, l'intérêt est limité, mais il a son application dans un contexte particulier : les paramètres
de fonctions.
IV-AP-2. Pointeurs
Le cas des pointeurs est un peu plus complexe, puis qu'il y a en quelque sorte 2 objets pour le prix
d'un !
-
Le pointeur lui même qui peut être qualifié const
-
L'objet pointé qui peut lui même être qualifié const
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+ + ;
* pa = 123 ;
|
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 ;
a = 456 ;
|
|
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 ;
|
Cependant, le plus souvent, le compilateur signale un problème au moment de l'affectation du pointeur.
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;
|
|
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à.
|
IV-AP-3. Usage
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 :
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.
IV-AQ. Structures
IV-AQ-1. Structures visibles
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'.
IV-AQ-2. Structures opaques
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.
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 :
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 :
# ifndef H_XXX
# define H_XXX
struct xxx;
struct xxx * xxx_create (void );
void xxx_delete (struct xxx * p);
# endif
|
# include "xxx.h"
struct xxx
{
int a;
char b[10 ];
} ;
struct xxx * xxx_create (void )
{
}
void xxx_delete (struct xxx * p)
{
}
|
Exemple d'utilisation :
# include "xxx.h"
# include <stddef.h>
int main (void )
{
struct xxx * p = xxx_create ();
if (p ! = NULL )
{
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...
IV-AQ-3. Pseudonymes (ou alias)
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é :
Détails d'application dans l'article sur les
TAD (ADT)
Copyright © 2008 Emmanuel Delahaye.
Aucune reproduction, même partielle, ne peut être faite
de ce site ni de l'ensemble de son contenu : textes, documents, images, etc.
sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à
trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.