IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

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);

      /* Filtrage des caracteres (entier decimal)
       * Nota : la saisie s'arrete a la premiere erreur.
       * Ce qui est saisi avant est considere comme valide.
       *
       * "123a" -> "123" : ret = 1
       *
       * "a123" -> ""    : ret = 0
       */
      ret = sscanf (saisie, "%[0-9-]s", temp);
   }
   while (ret != 1);

   {
      long n = strtol (temp, NULL, 10);

      printf ("La chaine est '%s', soit %ld\n", temp, n);
   }
   return 0;
}
D'autres exemples dans le chapitre sur les fichiers


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 :

<enter>
x = 10 ('
')
On voit que le caractère extrait est '\n' (ici, LF, soit le code ASCII 10)

a<enter>
x = 97 ('a')
On voit que le caractère extrait est 'a' (ici, le code ASCII 97). Le <enter> ('\n') n'a pas été extrait. Si on appelait fgetc() une nouvelle fois, il n'y aurait pas de suspension.

a<backspace>b<enter>
x = 98 ('b')
On constate que, bien que le premier caractère saisi fut 'a', le caractère extrait est 'b' (ici, le code ASCII 98). En effet, la touche <backspace> a permis de corriger la dernière saisie.

abcd<enter>
x = 97 ('a')
On voit que le caractère extrait est 'a', bien que d'autres caractères aient été saisis après. C'est donc bien le plus ancien caractère qui est extrait. Les autres caractères sont en attente de lecture. Une boucle de fgetc() permettrait de les extraire.

#include <stdio.h>

int main (void)
{
   int x;

   do
   {
      x = fgetc(stdin);
      printf ("x = %d ('%c')\n", x, x);
   }
   while (1);

   return 0;
}
Je laisse au lecteur le soin de refaire les expériences précédentes et d'en tirer les conclusions qui s'imposent.


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.

 /* Definition d'un pointeur sur int */
   int *p_int;
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 '*'.

/* Definition de la variable 'a' valant 4 */
   int a = 4;

   /* Definition d'un pointeur 'p' initialise' avec l'adresse de la variable 'a' */
   int *p = &a;

   /* Definition de la variable 'b' non initialisee */
   int b;

   /* recuperation de la valeur de 'a' dans 'b' via le pointeur 'p' */
   b = *p;

   /* 'b' vaut maintenant 4 */

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 :

/* pointeur sur une fonction avec 2 parametres */
   int (*pf) (int, char **);

   /* prototype d'un fonction ayant un pointeur de fonction comme parametre */
   int fun (int (*pf) (int, char **));
Si on doit manipuler des fonctions qui retournent un pointeur de fonction, ou des tableaux de pointeurs de fonction, le code devient rapidement illisible. C'est pourquoi il est fortement conseillé, et ce dans tous les cas, d'utiliser un typedef pour créer un alias sur le type de la fonction.

/* definition d'un alias */
   typedef int fun_f (int, char **);

   /* definition d'un pointeur de fonction de ce type */
   fun_f *pf;

   /* prototype d'une fonction ayant un pointeur de fonction comme parametre */
   int fun (fun_f *pf);

   /* tableau de pointeurs de fonction */
   fun_f *pf[10];

   /* prototype d'une fonction retournant un pointeur de fonction */
   fun_f *getfunc (int);
La lecture et la maintenance du code s'en trouvent considérablement allégées.

Pour initialiser un pointeur de fonction, on peut soit :

  • 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.

/* definition d'un alias */
   typedef int fun_f (int);

   /* definition d'un pointeur de fonction de ce type */
   fun_f *pf;

   /* prototype d'une fonction du meme type */
   int function (int);

   /* NOTA : on peut aussi utiliser le type */
   fun_f function;

   /* initialisation du pointeur de fonction */
   pf = fonction;

   /* appel de la fonction via le pointeur de fonction */
   pf (123);

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.

/* Definition du pointeur. Il est initialise a l'etat invalide */
   int *p = NULL;

   /* le pointeur est initialise avec l'adresse d'un tableau
    * dynamique de 4 elements
    */
   p = malloc (4 * sizeof *p);

   /* en cas d'échec d'allocation, malloc() retourne NULL */
   if (p != NULL)
   {
      /* ... */

      /* apres utilisation, l'espace memoire est libere */
      free (p);

      /* le pointeur est force a l'etat invalide */
      p = NULL;
   }

IV-D. Passer un tableau à une fonction


IV-D-1. Introduction

info 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 :

void clear (int *p);
La fonction va donc utiliser le paramètre pointeur, dont la valeur est l'adresse du premier élément du tableau, pour accéder aux éléments du tableau. Par exemple, les mettre à 0 :

void clear (int *p)
{
   *(p + 0) = 0; /* premier element, */
   *(p + 1) = 0; /* deuxieme element, */
   *(p + 2) = 0; /* troisieme element, */
   *(p + 3) = 0; /* quatrieme element, */
   *(p + 4) = 0; /* dernier element (cinquieme) */
}
Afin d'alléger l'écriture, le langage C autorise l'utilisation de la syntaxe des tableaux pour accéder aux éléments :

void clear (int *p)
{
   p[0] = 0; /* premier element, */
   p[1] = 0; /* deuxieme element, */
   p[2] = 0; /* troisieme element, */
   p[3] = 0; /* quatrieme element, */
   p[4] = 0; /* dernier element (cinquieme) */
}
Cette implémentation est évidemment théorique, car dans la pratique, on utilisera une boucle et un paramètre supplémentaire (nombre d'éléments) afin d'écrire un code plus souple et auto adaptatif.

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.

     /* interdit */
   "hello"[2] = 'x';

   /* autorise' */
   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;

   /* une chaine litterale n'etant pas modifiable,
    * il est conseille de qualifier l'objet avec
    * 'const' (read-only)
    */
   char const *pf = "hello";
   char const *pg = "hello" + 2;
Un petit schéma pour modéliser :

Représentation graphique d'un objet 'c' de type char non initialisé :

char c;

   :---------:--------:
   : adresse : valeur :
   :         :        :
   :---------:--------:
   : &c      : ???    :
   :---------:--------:
Représentation graphique d'un objet 'c' de type char après initialisation :

c = 'A';

   :---------:--------:
   : adresse : valeur :
   :         :        :
   :---------:--------:
   : &c      : 'A'    :
   :---------:--------:
Représentation graphique d'un objet 'p' de type char * non initialisé :

char *p;

   :---------:--------:---------:
   : adresse : valeur : valeur  :
   :         :        : pointée :
   :---------:--------:---------:
   : &p      : ???    : ???     :
   :---------:--------:---------:
Représentation graphique d'un objet 'p' de type char * après initialisation (NULL) :

p = NULL;

   :---------:--------:---------:
   : adresse : valeur : valeur  :
   :         :        : pointée :
   :---------:--------:---------:
   : &p      : NULL   : ???     : --> NULL
   :---------:--------:---------:
Représentation graphique d'un objet 'p' de type char * après initialisation (adresse d'une variable) :

p = &c;

   :---------:--------:---------:     :---------:--------:
   : adresse : valeur : valeur  :     : adresse : valeur :
   :         :        : pointée :     :         :        :
   :---------:--------:---------:     :---------:--------:
   : &p      : &c     : 'A'     : --> : &c      : 'A'    :
   :---------:--------:---------:     :---------:--------:
Représentation graphique d'un objet 'p' de type char * après initialisation (adresse d'une chaine modifiable) :

char s[]="ab";
   p = s;

   :---------:---------:---------:     :---------:--------:
   : adresse : valeur  : valeur  :     : adresse : valeur :
   :         :         : pointée :     :         :        :
   :---------:---------:---------:     :---------:--------:
   : &p      : &s[0]   : 'a'     : --> : s+0     : 'a'    :
   :         : ou s+0  :         :     :---------:--------:
   :         : ou s    :         :     : s+1     : 'b'    :
   :---------:---------:---------:     :---------:--------:
                                       : s+2     : 0      :
                                       :---------:--------:
Le pointeur sur char est principalement utilisé pour les paramètres 'chaines de caractères' des fonctions et pour des manipulations de chaines de caractères.

info 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

/* fichier1.c */
#include <stdio.h>

int main (void)
{
   /* ouverture du fichier en mode texte */
   FILE *fp = fopen ("data.txt", "r");

   /* L'ouverture du fichier est-elle realisee ? */
   if (fp != NULL)
   {
      /* definition d'un tableau de char destine a recevoir la ligne
       * La taille est arbitraire. Elle doit etre cependant adaptee * aux besoins courants.
       * Pour les grandes tailles (disons > 256 char),
       * il est preferable d'utiliser une allocation dynamique.
       */
      char ligne[32];

      /* lecture de la premiere ligne */
      fgets (ligne, sizeof ligne, fp);

      /* Affichage de la premiere ligne */
      printf ("1: %s\n", ligne);

      /* lecture de la deuxieme ligne */
      fgets (ligne, sizeof ligne, fp);

      /* Affichage de la deuxieme ligne */
      printf ("2: %s\n", ligne);

      /* Fermeture du fichier */
      fclose (fp);
   }
   else
   {
      printf ("Erreur d'ouverture du fichier\n");
   }
   return 0;
}
   
On doit obtenir ceci sur la sortie standard (stdout):

1: Ceci est un simple fichier

2: texte de 2 lignes.

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))
   {
      /* la fin de fichier a ete detectee */
      puts ("EOF");
   }

   if (ferror(fp))
   {
      /* une erreur s'est produite */
      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];

   /* test d'erreur omis */
   fgets (ligne, sizeof ligne, fp);

   {
      /* chercher le '\n' */
      char *p = strchr(ligne, '\n');

      if (p != NULL)
      {
         /* si on l'a trouve, on l'elimine. */
         *p = 0;
      }
      else
      {
         /* Le traitement depend de l'application.
          * Par exemple, ici, on choisi d'ignorer
          * les autres caracteres.
          */

         /* sinon, on lit tous les caracteres restants */
         int c;

         while ((c = fgetc(fp)) != '\n' && c != EOF)
         {
         }
      }
   }
}
Il est clair que dans la pratique, l'ensemble de ce code devra être intégré dans une fonction unique de lecture d'une ligne à partir d'un flux.


IV-F-2-d-iv--6. Exemple amélioré avec détection de la fin de lecture

/* fichier2.c */
#include <stdio.h>

int main (void)
{
   FILE *fp = fopen ("data.txt", "r");

   if (fp != NULL)
   {
      char ligne[32];

      /* definition d'un compteur de lignes et initialisation */
      int cpt = 0;

      /* lecture des lignes */
      while (fgets (ligne, sizeof ligne, fp) != NULL)
      {
         /* Mise a jour du compteur */
         cpt++;

         /* Affichage des lignes */
         printf ("%d: %s\n", cpt, ligne);
      }

      /* On peut ajouter ici la detection de la cause
       * de l'erreur decrite ci-dessus
       */

      fclose (fp);
   }
   else
   {
      printf ("Erreur d'ouverture du fichier\n");
   }

   return 0;
}
Cet exemple met en 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>
...
{
   /* creation d'un tableau de 10 int */
   size_t n = 10;
   int *p = (int*) malloc (sizeof (int) * n);

   if (p != NULL)
   {
      ...
}
Ce code est correct et ne présente pas de comportement indéterminé. Cependant, il est inutilement compliqué et peut être amélioré de plusieurs façons.


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);
}
Quelques compléments sur malloc() et l'allocation dynamique en général


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>

<...>
{
   /* allocation d'un tableau de 10 int
    * Pour pouvoir gerer la taille, on utilise
    * une variable 'taille'. Une structure comprenant
    * l'adresse du tableau et sa taille est aussi envisageable.
    */

   size_t size = 10;
   int *p = malloc (size * sizeof *p);

<...>

   /* Agrandissement du tableau a 15 int */
   {
      /* reallocation. Le resultat est stocke'
       * dans une variable temporaire
       */
      size = 15;
      type_s *p_tmp = realloc (p, size * sizeof *p_tmp);

      if (p_tmp != NULL)
      {
         /* si la nouvelle valeur est valide,
          * le pointeur original est mis a jour.
          */
         p = p_tmp;
      }
      else
      {
         /* l'ancien bloc est valide, mais il n'a pas ete agrandi */
      }
   }
}
Il faut aussi garder à l'esprit que la partie nouvellement allouée n'est pas initialisée.


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__
/* The pesky "floating point formats not linked" killer hack : */
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).

/* data.c */

#include "data.h"

int G_x;

/* la taille du tableau est definie
 * dans la declaration.
 */
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

/* data.h */

typedef struct
{
   int a;
   char b[123];
}
data_s;

extern int G_x;

/* la defintion de la taille du tableau est unique. Elle est faite ici. */
extern double G_a[12];

extern data_s G_data;

#endif /* guard */
   

IV-M-5. Utilisation

Il est recommandé que le fichier qui utilise une variable globale inclue le fichier de déclaration (.h).

/* appli.c */

#include "data.h"

int main (void)
{
   G_x = 123;

   G_data.a = 456;

   G_a[3] = 123.456;

   return 0;
}
   

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; /* variable de largeur 1 bit */
   unsigned b:3; /* variable de largeur 3 bits */

   /* etc. */
}
data_s;
Il faut garder à l'esprit que l'implantation mémoire des bits n'est pas spécifiée par le langage C. (Et j'ai effectivement constaté sur le terrain des différences selon les implémentations, notamment concernant l'ordre des bits).

Autant une utilisation interne est possible et peut se justifier pour réduire la taille des objets (stockage en mémoire, notamment), extrait de 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
{
   /* 16 bits */
   int year;        /* -32767..+32767 */
   uint month:4;    /* 0-15 */
   uint day:5;      /* 0-31 */

   uint hour:5;     /* 0-31 */
   uint minute:6;   /* 0-63 */
   uint second:6;   /* 0-63 */
}
sDATE;
... autant il est illusoire d'utiliser les champs de bits pour créer une interface avec l'exterieur du programme, comme un flux 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

void main (void)
{
}
Qu'en est-il exactement ?

D'après la définition du langage C, dans un programme conforme, main() doit retourner int. D'ailleurs un compilateur comme Borland C 3.1 en mode ANSI refuse void main() (error). Dans les mêmes conditions, gcc qui émet un avertissement (warning).


IV-0-1. Historique

Dès l'apparition du langage C, une des formes canoniques de main() était

main()
{
   return 0;
}
l'autre étant la forme qui permet de récupérer les arguments de la ligne de commande.

Il faut bien comprendre qu'à cette époque, une fonction définie sans type de retour explicite, retournait un int (c'est toujours le cas en C90, mais plus en C99 où le type doit être explicite). Le mot clé 'void' n'existait pas.

info Il n'y avait donc aucune raison d'utiliser une forme void main().
Ensuite, est venue la normalisation du langage C. (1989 ANSI, 1990 ISO). Dans le texte, les deux formes canoniques sont décrites :

int main (void)
et

int main (int argc, char **argv)
Il est précisé en remarque (dans la partie non normative) qu'il existe d'autres formes sans autres précisions. Elles ne font donc pas partie de la norme, leur comportement est donc indéfini dans le cadre d'un programme respectueux de la norme (dit 'strictement conforme').


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:

int function ();
ou mieux, un prototype:

int function (int a, char *b);
Il est possible cependant de réduire la portée de la fonction à l'unité de compilation dans laquelle elle a été définie, en ajoutant le qualificateur static.

static int function (int a, char *b)
{
}
Cette pratique, lorsqu'elle est possible, apporte différents avantages :

  • 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)

/* objet de portee globale */
int x;

int f (void)
{
   /* la globale x est masque'e par une locale du meme nom */
   int x = 0;

   /* la globale x n'est pas modifie'e. */
   x++;
}

int main (void)
{
   /* la globale x est modifie'e */
   x = 2;

   f();

   /* la globale x vaut toujours 2 */

   return 0;
}

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);


   /* prochain Noel */
   {
      struct tm tm_xmas =
      {0};

      tm_xmas.tm_year = tm_now.tm_year;
      tm_xmas.tm_mon = 12 - 1;
      tm_xmas.tm_mday = 25;

      /* ajustement */
      {
         time_t xmas = mktime (&tm_xmas);

         strftime (s, sizeof s, "%d/%m/%Y", &tm_xmas);
         printf ("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

char s[4] = "ab";
le tableau est initialisé avec {'a', 'b', 0, 0} : Chaine valide

char s[4] = "abc";
le tableau est initialisé avec {'a', 'b', 'c', 0} : Chaine valide

char s[4] = "abcd";
le tableau est initialisé avec {'a', 'b', 'c', 'd'} : Chaine invalide /!\

char s[4] = "abcde";
Ne compile pas (trop d'initialisateurs)


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

int x;

<...>
{
   x = 2;
}
ou

{
   int x;
<...>
   x = 2;
}
Voici des exemples de déclarations de fonctions

int f();

extern int g(void);

static int h();

static int i(int a)
{
}

int j(char *b)
{
}

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:

/* permanente de portee illimitee */
int a;

/* permanente de portee limitee a l'unite de compilation */
static int b;

<...>
{
/* locale (de portee limitee au bloc) */
   int c;

/* permanente de portee limitee au bloc */
   static int d;

/* controlee de portee limitee a la validite du pointeur (non NULL) */
   int *p = malloc (sizeof *p * 3);


   free (p), p = NULL;
}

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':

modem> _
L'utilisateur peut alors passer une commande de numérotation

modem> ATD0123456789
_
et le modem répond

CONNECTED AT 9600 BAUDS
il passe alors en mode transmission de données, et il signale qu'il n'attend plus de commandes par l'absence de prompt.

De même, sur un PC sous Windows ou unixoide, il est possible d'ouvrir une console localement ou de se brancher localement ou à distance sur la machine via une console (gestion, application, debug...). L'application ne verra pas la différence et continuera à attendre ses commandes de stdin et à envoyer ses réponses à stdout et/ou stderr.

L'avantage d'un tel système est qu'il exige très peu de la console, qui peut se réduire à un simple terminal série, voire à une imprimante série s'il ne s 'agit que de traces sur stdout ou stderr (utile pour mettre au point une application graphique, par exemple).

La gestion plein écran est une autre façon de concevoir la relation entre l'homme et la machine (IHM). En effet, on peut reprocher à l'interface console 'interactive', un certain manque de convivialité (largement compensé par le développement de langages 'scripts' extrêmement puissants), rendant les applications difficiles d'accès à un public non informaticien (direction, comptables, secrétaires, achats, commerciaux etc.)

C'est pour cela qu'il a été développé la notion d'interface 'plein écran' qui permet d'améliorer l'ergonomie.

  • 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

warning 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

<...>

/* ---------------------------------------------------------------------
   VIDEO_putch()
   ---------------------------------------------------------------------
   ecriture d'un caractere a la position courante du curseur avec
   la couleur specifiee
   ---------------------------------------------------------------------
   E : caractere (0-255)
   E : couleur texte
   E : couleur fond
   S :
   --------------------------------------------------------------------- */
void VIDEO_putch (int c, eCOU ct, eCOU cf)
{
#if defined (__BORLANDC__)
   union REGS reg;

   /* caractere a écrire */
   reg.h.al = (uchar) c;

   /* putch avec attribut */
   reg.h.ah = 0x09;

   /* page 0 */
   reg.h.bh = 0;

   /* attributs 08h : clignotement */
   reg.h.bl = (uchar) (ct | ((cf & ~0x08) << 4));

   /* pas de repetitions */
   reg.x.cx = 1;

   /* Bios Video */
   int86 (0x10, &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 :

assert (i < n);
Avant l'accès en lecture au tableau.

info Pour être valide, la conception du piège doit se faire sans lire le code à tester, mais en se basant uniquement sur l'interface et le comportement présumé.

#include <stdio.h>
#include <assert.h>

static void afficher (int const t[], size_t n)
{
   size_t i;
   for (i = 0; i <= n; i++)
   {
      /* ajout du piege */
      assert (i < n);
      printf ("%4d", t[i]);
   }
   printf ("\n");
}

int main (void)
{
   int tab[] = {1, 2, 3, 4};

   afficher (tab, sizeof tab / sizeof *tab);
   return 0;
}
Ce qui provoque bien sûr :

1   2   3   4Assertion failed: i < n, file main.c, line 10

This application has requested the Runtime to terminate it in an unusual way.
Please contact the application's support team for more information.

Press ENTER to continue.
Ce qui signifie que i a dépassé la valeur maximale qu'autorise le langage C. La cause est évidemment le <= au lieu de < dans l'expression du for(), ce qui entraine une action corrective immédiate :

#include <stdio.h>
#include <assert.h>

static void afficher (int const t[], size_t n)
{
   size_t i;
   /* correction */
   for (i = 0; i < n; i++)
   {
      /* ajout du piege */
      assert (i < n);
      printf ("%4d", t[i]);
   }
   printf ("\n");
}

int main (void)
{
   int tab[] = {1, 2, 3, 4};

   afficher (tab, sizeof tab / sizeof *tab);
   return 0;
}
			
L'exécution et, à présent, conforme aux attentes :

1   2   3   4

Press ENTER to continue.

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); /* MODIF */

   printf ("Les valeurs vont de 0 a %d\n", RAND_MAX);
   for (i = 0; i < 10; i++)
   {
      int val = rand ();
      printf ("%d ", val);
   }
   printf ("\n");
   return 0;
}
On constate que les valeurs sont différentes du tirage précédent, mais que si on relance le programme, elles restent identiques :

Les valeurs vont de 0 a 32767
71 16899 3272 13694 13697 18296 6722 3012 11726 1899
Pour avoir une séquence différente à chaque lancement du programme, il faut donc trouver un moyen de passer une valeur 'changeante' à srand(), d'où l'idée d'utiliser la valeur retournée par time(), qui change une fois par seconde, indé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)); /* MODIF */

   printf ("Les valeurs vont de 0 a %d\n", RAND_MAX);
   for (i = 0; i < 10; i++)
   {
      int val = rand ();
      printf ("%d ", val);
   }
   printf ("\n");
   return 0;
}
Si on lance plusieurs fois le programme (au moins une seconde entre chaque lancement), on obtient maintenant des séquences différentes :

25985 16903 23861 29724 17917 23752 17039 25712 20507 30816
26197 27421 5384 20976 236 16846 22741 32047 12417 24408
26433 14874 13653 16830 21990 4658 17461 17892 21603 7731
etc. Je précise que pour que les valeurs changent dans le programme, il faut évidemment que srand() ne soit appelé qu'une seule fois au début du programme.

Après, il y a des astuces arithmétiques pour réduire la plage de valeurs... C'est du bête calcul entier... Détails dans la FAQ de f.c.l.c., notamment ici. D'autre part, ceci peut aider.


IV-AE. C'est quoi un prototype ?

Lorsqu'on défini une fonction,

int f (char *s)
{
  ...
}
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.

/* f.h */
int f (char *s);

/* f.c */
#include "f.h"
int f (char *s)
{
  ...
}

/* main.c */
#include "f.h"

int main (void)
{
   int x = f("hello");
}
Pour compléter, il est fortement recommandé de protéger les fichiers .h contre les inclusions multiples avec une garde (guard) :

#ifndef H_F
#define H_F
/* f.h */

int f (char *s);

#endif
Details ici


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);
      }
   }
}
Un article plus complet.


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 :

{
   int x = 123;
   f (x);
}
avec

void f (int a)
{
   a++;
}
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 :

{
   int x = 123;
   f (&x);
}
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)

"hello"
info Elle ne doit pas être confondue avec la liste de caractères servant à initialiser un tableau de char, par exemple :

char s[] = "hello";
(les détails sont indiqués ici.)

Une chaine littérale désigne en réalité l'adresse du premier élément d'un tableau de char anonyme non modifiable, situé en mémoire statique, initialisé avec la séquence de caractères mentionnés et terminé par un 0.

Tout se passe comme si on avait ceci :

static char const identificateur_connu_seulement_du_compilateur[] = {'h','e','l','l','o',0};
Si la chaine apparait dans un paramètre de fonction :

f ("hello");
c'est cette valeur (l'adresse) qui est passée à la fonction dans son paramètre :

void f (char const *s);
Si la chaine sert à initialiser un pointeur :

char const *p = "hello";

   p = "bye";
c'est cette valeur (l'adresse) qui est stockée dans le pointeur.

RAPPEL : le mot clé const sert à qualifier l'objet de non modifiable


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.

T *f();
NOTA : T représente le type d'un élément du tableau

Evidemment, cette adresse doit être valide après exécution de la fonction. Même si c'est techniquement possible, il est donc hors de question de retourner l'adresse d'un élément appartenant à un tableau local.

  • 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.

Ce document de référence définit un certain nombre d'éléments (obligations, interdictions).

Les autres éléments sont soit laissés à l'appréciation des implémenteurs (implementation defined ou défini par l'implémentation) qui doivent accompagner leur production (compilateur etc.) d'un document précisant les comportement de tel ou tels éléments, soit non définis du tout. Dans ce dernier cas, le comportement est dit indéfini ou indéterminé. (Undefined Behaviour ou UB)

Quelques exemples :

#include <stdio.h>

int main (void)
{
   int i = 0;

   printf ("i = %d\n", i);
   return 0;
}
Ce code est conforme à la spécification du langage, aucune zone n'a été laissée dans l'ombre. Le comportement est déterminé. Il est garanti d'écrire

i = 0
Par contre, voici 2 cas de comportement indéterminé :

  • 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 :

static int f(void)
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')

static int x;
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).

warning 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 :

int x = f();
ou

int x;
   x = f();
avec

int f(void);
  • Passer l'adresse de la variable dont on veut modifier la valeur à la fonction :

int x;
   f(&x);
avec

void f(int *);
Si l'objet est un pointeur, la règle est la même :

  • Retourner une valeur :

int *px = f();
avec

 int *f(void);
  • Passer l'adresse :

int *px;
   f(&px);
avec

void f(int **);
warning Il n'y a aucune chance de modifier l'entier en faisant ceci :

     int x;
   f(x);
warning De même, il n'y a aucune chance de modifier le pointeur en faisant cela :

 int *px;
   f(px);

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; /* acces en ecriture possible */
   y = 6; /* acces en ecriture interdit */
Le compilateur signale l'erreur.

On peut placer indifféremment le qualificateur const avant ou après le type.

int const a = 7;
   const int b = 8;
mais je conseille néanmoins la première forme, car elle est beaucoup plus claire (notamment avec les pointeurs).

Il est techniquement possible de définir un objet const non initialisé :

int const c;
évidemment, l'intérêt est limité, mais il a son application dans un contexte particulier : les paramètres de fonctions.


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++; /* interdit */
   *pa = 123; /* OK */
Mais un autre qualificateur const peut être utilisé pour préciser les droits du pointeur sur l'objet pointé.

Celui ci se place à la gauche de l'*, avant ou après le type :

int a = 123;
   int const * pa = &a;
   const int * pb = &a;
mais là encore, pour des question de clarté du code, je recommande la première forme. Ce qualificateur interdit la modification de l'objet via le pointeur (mais s'il n'est pas lui même qualifié const, l'objet reste modifiable directement, évidemment).

int a = 123;
   int const * pa = &a;

   *pa = 456; /* interdit */
   a = 456; /* OK */
warning Par contre, attention. Il est techniquement possible de définir un pointeur sur un objet qualifié const et de tenter de modifier l'objet. Cela produit un comportement indéfini qui n'est pas forcément signalé par le compilateur.

int const a = 123;
   int * pa = &a;

   *pa = 456; /* comportement indéfini */
Cependant, le plus souvent, le compilateur signale un problème au moment de l'affectation du pointeur.

int * pa = &a;
mais il convient de rester extrêmement prudent. Le C est un langage qui demande rigueur et maitrise.

NOTA : Bien évidemment, le typecast n'est pas la solution :

int const a = 123;
   int * pa = (int *) &a; /* NE PAS FAIRE CECI */
warning 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 :

char const *p = "hello";
Il est utile aussi pour les pointeurs passés en paramètre à des fonctions. Il permet en effet de restreindre l'accès à la variable pointée à un mode 'lecture seule', ce qui évite bien des erreurs de codage. (La modification d'une variable étant une opération lourde de conséquences si elle est faite au mauvais moment).

Une fonction qui affiche le contenu d'un tableau ou d'une structure, par exemple, n'a pas à la modifier. On fixe donc les règles du jeu dès la définition du prototype :

void display (T const *p)
Une utilisation astucieuse et intelligente du qualificateur const permet d'écrire du code plus sûr.


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.

struct mastructure;
Cette définition dite incomplète ne permet évidemment pas de créer un instanciation, puisque le compilateur ignore le contenu de la structure. Il n'a donc pas les moyens d'en déterminer la taille.

Par contre, il est possible de créer un pointeur de ce type :

struct mastructure *p;
Il devient alors possible de créer une fonction qui retourne un pointeur de ce type :

struct mastructure *fonction(void);
de passer ce pointeur en paramètre une fonction :

void fonction(struct mastructure *p);
d'en faire un élément de structure etc.

Evidemment, il faudra que la structure soit définie 'quelque part' afin qu'elle soit instanciable et que ses éléments soient manipulables.

On va donc créer un fichier source (.c) séparé d'implémentation contenant les fonctions permettant la création (instanciation) des données, et une interface (header ou .h) ne comportant que la définition incomplète de la fonction et, au minimum, les 2 fonctions permettant la création et la suppression d'une instance de la structure.

Soit la structure 'xxx'. On obtient :

/* xxx.h Interface */
#ifndef H_XXX
#define H_XXX

/* definition incomplete de structure */
struct xxx;

/* prototypes des fonctions */
struct xxx *xxx_create (void);
void xxx_delete (struct xxx *p);
/* etc. */

#endif

/* xxx.c Implementation */
#include "xxx.h"

/* definition de la structure (exemple) */
struct xxx
{
   int a;
   char b[10];
};

/* fonctions publiques */
struct xxx *xxx_create (void)
{
   /* a completer */
}

void xxx_delete (struct xxx *p)
{
   /* a completer */
}
Exemple d'utilisation :

/* test.c */
#include "xxx.h"

#include <stddef.h>

int main (void)
{
   /* instanciation */
   struct xxx *p = xxx_create ();

   /* tout appel de fonction peut echouer... */
   if (p != NULL)
   {
      /* utilisation de l'objet xxx
         (via de nouvelles fonctions a creer)

         ...
       */

      /* fin d'utilisation : destruction de l'objet */
      xxx_delete (p), p = NULL;
   }
   return 0;
}
Je laisse au lecteur le soin de proposer une ou des implémentations de xxx_create() et de xxx_delete(), sachant qu'on a pas forcément besoin d'un nombre illimité d'instanciations...


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é :

typedef struct xxx *xxx; /* /!\ */
Détails d'application dans l'article sur les TAD (ADT)

 

Valid XHTML 1.1!Valid CSS!

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.