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

 
Copyright © 2008 Emmanuel Delahaye.
Aucune reproduction, même partielle, ne peut être faite
de ce site ni de l'ensemble de son contenu : textes, documents, images, etc.
sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à
trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.