Bienvenue dans le blog de PAGAL CHEF

COURS DE PROGRAMMATION ET COMPILATION

Universit´e du Luxembourg 2005–2006
Cours de programmation avanc´ee.
Le langage C
S´ebastien Varrette <Sebastien.Varrette@imag.fr> Version : 0.4
Nicolas Bernard <n.bernard@lafraze.net>
2
Table des mati`eres
1 Les bases 2
1.1 Un langage compil´e . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.1.1 G´en´eralit´es sur les langages de programmation . . . . . . 2
1.1.2 Le C comme langage compil´e . . . . . . . . . . . . . . . . 3
1.1.3 Notes sur la normalisation du langage C . . . . . . . . . . 4
1.1.4 C en pratique : compilation et debuggage . . . . . . . . . 4
1.2 Les mots-cl´es . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.3 Les commentaires . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.4 Structure g´en´erale d’un programme C . . . . . . . . . . . . . . . 7
1.5 Notion d’identificateur . . . . . . . . . . . . . . . . . . . . . . . . 9
1.6 Conventions d’´ecritures d’un programme C . . . . . . . . . . . . 9
1.7 Les types de base . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.7.1 Les caract`eres . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.7.2 Les entiers . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1.7.3 Les flottants . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.7.4 Le type void . . . . . . . . . . . . . . . . . . . . . . . . . 14
2 La syntaxe du langage 15
2.1 Expressions et Op´erateurs . . . . . . . . . . . . . . . . . . . . . . 15
2.1.1 Op´erateurs arithm´etiques . . . . . . . . . . . . . . . . . . 15
2.1.2 Op´erateurs d’affectation . . . . . . . . . . . . . . . . . . . 16
2.1.3 Op´erateurs relationnels . . . . . . . . . . . . . . . . . . . 16
2.1.4 Op´erateurs logiques . . . . . . . . . . . . . . . . . . . . . 17
2.1.5 Op´erateurs bit `a bit . . . . . . . . . . . . . . . . . . . . . 18
2.1.6 Op´erateurs d’acc`es `a la m´emoire . . . . . . . . . . . . . . 18
2.1.7 Autres op´erateurs . . . . . . . . . . . . . . . . . . . . . . 19
2.2 Les structures de contrˆole . . . . . . . . . . . . . . . . . . . . . . 19
2.2.1 Instruction if...else . . . . . . . . . . . . . . . . . . . . . 20
2.2.2 Instruction for . . . . . . . . . . . . . . . . . . . . . . . . 20
2.2.3 Instruction while . . . . . . . . . . . . . . . . . . . . . . 20
2.2.4 Instruction do...while . . . . . . . . . . . . . . . . . . . 21
2.2.5 Instruction switch . . . . . . . . . . . . . . . . . . . . . . 22
2.2.6 Instruction goto . . . . . . . . . . . . . . . . . . . . . . . 23
2.2.7 Instruction break . . . . . . . . . . . . . . . . . . . . . . 23
2.2.8 Instruction continue . . . . . . . . . . . . . . . . . . . . 24
2.3 La r´ecursivit´e . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
3
2.4 Les conversions de types . . . . . . . . . . . . . . . . . . . . . . . 25
2.4.1 Les situations de la conversion de type . . . . . . . . . . . 25
2.4.2 La r`egle de ”promotion des entiers” . . . . . . . . . . . . . 26
2.4.3 Les conversions arithm´etiques habituelles . . . . . . . . . 26
2.4.4 Les surprises de la conversion de type . . . . . . . . . . . 27
2.5 Principales fonctions d’entr´ees-sorties standard . . . . . . . . . . 27
2.5.1 La fonction getchar . . . . . . . . . . . . . . . . . . . . . 28
2.5.2 La fonction putchar . . . . . . . . . . . . . . . . . . . . . 28
2.5.3 La fonction puts . . . . . . . . . . . . . . . . . . . . . . . 28
2.5.4 La fonction d’´ecriture `a l’´ecran formatt´ee printf . . . . . 29
2.5.5 La fonction de saisie scanf . . . . . . . . . . . . . . . . . 31
3 Les pointeurs 33
3.1 D´eclaration d’un pointeur . . . . . . . . . . . . . . . . . . . . . . 34
3.2 Op´erateurs de manipulation des pointeurs . . . . . . . . . . . . . 34
3.2.1 L’op´erateur ’adresse de’ & . . . . . . . . . . . . . . . . . . 34
3.2.2 L’op´erateur ’contenu de’ : * . . . . . . . . . . . . . . . . . 35
3.3 Initialisation d’un pointeur . . . . . . . . . . . . . . . . . . . . . 36
3.4 Arithm´etique des pointeurs . . . . . . . . . . . . . . . . . . . . . 37
3.5 Allocation dynamique de m´emoire . . . . . . . . . . . . . . . . . 38
3.6 Lib´eration dynamique avec la fonction free . . . . . . . . . . . . 40
4 Les types d´eriv´es 41
4.1 Les ´enum´erations . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
4.2 Les tableaux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
4.2.1 Initialisation d’un tableau . . . . . . . . . . . . . . . . . . 42
4.2.2 Tableaux multidimensionnels . . . . . . . . . . . . . . . . 44
4.2.3 Passage de tableau en param`etre . . . . . . . . . . . . . . 45
4.2.4 Relation entre tableaux et pointeurs . . . . . . . . . . . . 47
4.2.5 Cas des tableaux de chaˆınes de caract`eres . . . . . . . . . 49
4.2.6 Gestion des arguments de la ligne de commande . . . . . 50
4.3 Les structures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
4.3.1 Initialisation et affectation d’une structure . . . . . . . . . 52
4.3.2 Comparaison de structures . . . . . . . . . . . . . . . . . 52
4.3.3 Tableau de structures . . . . . . . . . . . . . . . . . . . . 52
4.3.4 Pointeur vers une structure . . . . . . . . . . . . . . . . . 53
4.3.5 Structures auto-r´ef´er´ees . . . . . . . . . . . . . . . . . . . 53
4.4 Les unions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
4.4.1 D´eclaration d’une union . . . . . . . . . . . . . . . . . . . 55
4.4.2 Utilisation pratique des unions . . . . . . . . . . . . . . . 56
4.4.3 Une m´ethode pour all´eger l’acc`es aux membres . . . . . . 57
4.5 Les champs de bits . . . . . . . . . . . . . . . . . . . . . . . . . . 57
4.6 D´efinition de synonymes de types avec typedef . . . . . . . . . . 58
4
5 Retour sur les fonctions 59
5.1 D´eclaration et d´efinition d’une fonction . . . . . . . . . . . . . . 59
5.2 Appel d’une fonction . . . . . . . . . . . . . . . . . . . . . . . . . 61
5.3 Dur´ee de vie des identificateurs . . . . . . . . . . . . . . . . . . . 61
5.4 Port´ee des variables . . . . . . . . . . . . . . . . . . . . . . . . . 62
5.4.1 Variables globales . . . . . . . . . . . . . . . . . . . . . . 62
5.4.2 Variables locales . . . . . . . . . . . . . . . . . . . . . . . 63
5.5 R´ecursivit´e . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
5.6 Passage de param`etres `a une fonction . . . . . . . . . . . . . . . 64
5.6.1 G´en´eralit´es . . . . . . . . . . . . . . . . . . . . . . . . . . 64
5.6.2 Passage des param`etres par valeur . . . . . . . . . . . . . 65
5.6.3 Passage des param`etres par adresse . . . . . . . . . . . . . 65
5.6.4 Passage de tableau en param`etre . . . . . . . . . . . . . . 66
5.7 Fonction `a nombre variable de param`etres . . . . . . . . . . . . . 67
5.8 Pointeurs sur une fonction . . . . . . . . . . . . . . . . . . . . . . 69
6 Gestion des fichiers 71
6.1 Ouverture et fermeture de fichiers . . . . . . . . . . . . . . . . . . 71
6.1.1 Ouverture de fichiers : la fonction fopen . . . . . . . . . . 71
6.1.2 Fermeture de fichiers : la fonction fclose . . . . . . . . . 73
6.2 Les entr´ees-sorties format´ees . . . . . . . . . . . . . . . . . . . . . 73
6.2.1 La fonction d’´ecriture en fichier fprintf . . . . . . . . . . . 73
6.2.2 La fonction de saisie en fichier fscanf . . . . . . . . . . . 73
6.3 Impression et lecture de caract`eres dans un fichier . . . . . . . . 74
6.3.1 Lecture et ´ecriture par caract`ere : fgetc et fputc . . . . 74
6.3.2 Lecture et ´ecriture optimis´ees par caract`ere : getc et putc 74
6.3.3 Relecture d’un caract`ere . . . . . . . . . . . . . . . . . . . 75
6.3.4 Les entr´ees-sorties binaires : fread et fwrite . . . . . . . 76
6.3.5 Positionnement dans un fichier : fseek, rewind et ftell 77
7 Les directives du pr´eprocesseur 79
7.1 La directive #include . . . . . . . . . . . . . . . . . . . . . . . . 79
7.2 La directive #define . . . . . . . . . . . . . . . . . . . . . . . . . 79
7.2.1 D´efinition de constantes symboliques . . . . . . . . . . . . 80
7.2.2 Les macros avec param`etres . . . . . . . . . . . . . . . . . 80
7.3 La compilation conditionnelle . . . . . . . . . . . . . . . . . . . . 81
7.3.1 Condition li´ee `a la valeur d’une expression . . . . . . . . . 82
7.3.2 Condition li´ee `a l’existence d’un symbole . . . . . . . . . 82
7.3.3 L’op´erateur defined . . . . . . . . . . . . . . . . . . . . . 83
7.3.4 La commande #error . . . . . . . . . . . . . . . . . . . . 83
7.3.5 La commande #pragma . . . . . . . . . . . . . . . . . . . 84
8 La programmation modulaire 85
8.1 Principes ´el´ementaires . . . . . . . . . . . . . . . . . . . . . . . . 85
8.2 Eviter les erreurs d’inclusions multiples . . . . . . . . . . . . . . 87
8.3 La compilation s´epar´ee . . . . . . . . . . . . . . . . . . . . . . . . 88
8.4 R´esum´e des r`egles de programmation modulaire . . . . . . . . . . 88
5
8.5 L’outils ’make’ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
8.5.1 Principe de base . . . . . . . . . . . . . . . . . . . . . . . 89
8.5.2 Cr´eation d’un Makefile . . . . . . . . . . . . . . . . . . . 89
8.5.3 Utilisation de macros et de variables . . . . . . . . . . . . 90
9 La biblioth`eque standard 93
9.1 Diagnostics d’erreurs <assert.h> . . . . . . . . . . . . . . . . . . 93
9.2 Gestion des nombres complexes <complex.h> . . . . . . . . . . . 94
9.3 Classification de caract`eres et changements de casse <ctype.h> . 94
9.4 Valeur du dernier signal d’erreur <errno.h> . . . . . . . . . . . . 95
9.5 Gestion d’un environnement `a virgule flottante <fenv.h> . . . . 95
9.5.1 Gestion des exceptions . . . . . . . . . . . . . . . . . . . . 95
9.5.2 Gestion des arrondis . . . . . . . . . . . . . . . . . . . . . 96
9.5.3 Gestion des environnements en virgule flottante. . . . . . 96
9.6 Intervalle et pr´ecision des nombres flottants <float.h> . . . . . . 96
9.7 D´efinitions de types entiers de taille fix´ee <inttypes.h> . . . . . 96
9.8 Alias d’op´erateurs logiques et binaires <iso646.h> . . . . . . . . 97
9.9 Intervalle de valeur des types entiers <limits.h> . . . . . . . . . 97
9.10 Gestion de l’environnement local <locale.h> . . . . . . . . . . . 97
9.11 Les fonctions math´ematiques de <math.h> . . . . . . . . . . . . . 98
9.11.1 Fonctions trigonom´etriques et hyperboliques . . . . . . . . 98
9.11.2 Fonctions exponentielles et logarithmiques . . . . . . . . . 98
9.11.3 Fonctions diverses . . . . . . . . . . . . . . . . . . . . . . 98
9.12 Branchements non locaux <setjmp.h> . . . . . . . . . . . . . . . 98
9.13 Manipulation des signaux <signal.h> . . . . . . . . . . . . . . . 99
9.14 Nombre variable de param`etres <stdarg.h> . . . . . . . . . . . . 99
9.15 D´efinition du type bool´een <stdbool.h> . . . . . . . . . . . . . . 99
9.16 D´efinitions standards <stddef.h> . . . . . . . . . . . . . . . . . . 99
9.17 D´efinitions de types entiers <stdint.h> . . . . . . . . . . . . . . 100
9.18 Entr´ees-sorties <stdio.h> . . . . . . . . . . . . . . . . . . . . . . 100
9.18.1 Manipulation de fichiers . . . . . . . . . . . . . . . . . . . 100
9.18.2 Entr´ees et sorties format´ees . . . . . . . . . . . . . . . . . 100
9.18.3 Impression et lecture de caract`eres . . . . . . . . . . . . . 100
9.19 Utilitaires divers <stdlib.h> . . . . . . . . . . . . . . . . . . . . 101
9.19.1 Allocation dynamique . . . . . . . . . . . . . . . . . . . . 101
9.19.2 Conversion de chaˆınes de caract`eres en nombres . . . . . . 101
9.19.3 G´en´eration de nombres pseudo-al´eatoires . . . . . . . . . 101
9.19.4 Arithm´etique sur les entiers . . . . . . . . . . . . . . . . . 102
9.19.5 Recherche et tri . . . . . . . . . . . . . . . . . . . . . . . . 102
9.19.6 Communication avec l’environnement . . . . . . . . . . . 102
9.20 Manipulation de chaˆınes de caract`eres <string.h> . . . . . . . . 103
9.21 Macros g´en´eriques pour les fonctions math´ematiques <tgmath.h> 103
9.22 Date et heure <time.h> . . . . . . . . . . . . . . . . . . . . . . . 104
9.23 Manipulation de caract`eres ´etendus <wchar.h> et <wctype.h> . 104
6
A Etude de quelques exemples 106
A.1 Gestion des arguments de la ligne de commande . . . . . . . . . 106
A.2 Exemple de liste chaˆın´ee . . . . . . . . . . . . . . . . . . . . . . . 110
B Makefile g´en´erique 112
C Le bˆetisier 116
C.1 Erreur avec les op´erateurs . . . . . . . . . . . . . . . . . . . . . . 116
C.1.1 Erreur sur une comparaison . . . . . . . . . . . . . . . . . 116
C.1.2 Erreur sur l’affectation . . . . . . . . . . . . . . . . . . . . 116
C.2 Erreurs avec les macros . . . . . . . . . . . . . . . . . . . . . . . 116
C.2.1 Un #define n’est pas une d´eclaration . . . . . . . . . . . 117
C.2.2 Un #define n’est pas une initialisation . . . . . . . . . . 117
C.2.3 Erreur sur macro avec param`etres . . . . . . . . . . . . . 117
C.2.4 Erreur avec les effets de bord . . . . . . . . . . . . . . . . 117
C.3 Erreurs avec l’instruction if . . . . . . . . . . . . . . . . . . . . . 117
C.4 Erreurs avec les commentaires . . . . . . . . . . . . . . . . . . . . 118
C.5 Erreurs avec les priorit´es des op´erateurs . . . . . . . . . . . . . . 118
C.6 Erreur avec l’instruction switch . . . . . . . . . . . . . . . . . . 119
C.6.1 Oubli du break . . . . . . . . . . . . . . . . . . . . . . . . 119
C.6.2 Erreur sur le default . . . . . . . . . . . . . . . . . . . . 119
C.7 Erreur sur les tableaux multidimensionnels . . . . . . . . . . . . 119
C.8 Erreur avec la compilation s´epar´ee . . . . . . . . . . . . . . . . . 119
C.9 Liens utiles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
7
Avant-propos
Ce polycopi´e vient en compl´ement au cours de programmation avanc´ee dispens´e
au sein de l’universit´e du Luxembourg en DUT d’informatique. En aucun cas il
ne dispense de la pr´esence en cours. Ce document n’a pas pour vocation d’ˆetre
un ouvrage de r´ef´erence du Langage C (pour cela, voir par exemple [KR88]). Il
doit simplement permettre au lecteur d’appr´ehender rapidement ce langage.
Pour la petite histoire, le langage de programmation C a ´et´e d´evelopp´e dans les
ann´ees 70 par Dennis Ritchie aux laboratoires Bell. Il puise ses origines dans
le langage de programmation sans type BCPL (Basic Combined Programming
Language, developp´e par M. Richards) et dans B (developp´e par K. Thompson).
En 1978, Brian Kernighan et Dennis Ritchie produisent la premi`ere description
officielle de C.
Le design de C lui conf`ere un certain nombre d’avantages :
– Le code source est hautement portable
– Le code machine est tr`es efficace
– les compilateurs C sont disponibles dans tous les syst`emes courants.
La version courante de ce document ainsi que le code source des exemples
abord´es dans ce polycopi´e sont disponibles sur mon site
http://www-id.imag.fr/~svarrett/
1
Chapitre 1
Les bases
1.1 Un langage compil´e
1.1.1 G´en´eralit´es sur les langages de programmation
Dans le domaine de la programmation, on utilise aujourd’hui des langages de
programmation de haut niveau (i.e ”facilement” compr´ehensibles par le programmeur)
par opposition aux langages de bas niveau (de type assembleur) qui
sont plus orient´es vers le langage machine. Parmi les exemples de langages de
haut niveau, on peut citer les langages C, C++, Java, Cobol etc...
Il est n´eanmoins n´ecessaire d’op´erer une op´eration de traduction du langage
de haut niveau vers le langage machine (binaire). On distingue deux types de
traducteurs :
1. l’interpr´eteur qui traduit les programmes instruction par instruction dans
le cadre d’une int´eraction continue avec l’utilisateur. Ainsi, le programme
est traduit `a chaque ex´ecution.
2. le compilateur qui traduit les programmes dans leur ensemble : tout le
programme doit ˆetre fourni en bloc au compilateur pour la traduction. Il
est traduit une seule fois.
Il en r´esulte deux grands types de langages de programmation :
– les langages interpr´et´es : dans ce cas, l’ex´ecution des programmes se fait sous
un ”terminal”`a la vol´ee. Ce type de langage est donc adapt´e au d´eveloppement
rapide de prototypes (on peut tester imm´ediatement ce que l’on est en train
de faire) On citera par exemple les langages Scheme, Ruby, etc...
– les langages compil´es : les source du code est dans un premier temps pass´e
dans une ”moulinette” (le compilateur) qui va g´en´erer, sauf erreur, un ex´ecutable
qui est un fichier binaire compr´ehensible par votre machine. Ce comportement
est illustr´e dans la figure 1.1. Ce type de langage est donc adapt´e
`a la r´ealisation d’applications plus efficaces ou de plus grande envergure (il y
a une optimisation plus globale et la traduction est effectu´ee une seule fois et
non `a chaque ex´ecution).
En outre, un langage compil´e permet de diffuser les programmes sous forme
binaire, sans pour autant imposer la diffusion le source du code, permettant
2
ainsi une certaine protection de la propri´et´e intellectuelle. Quelques exemples
de langages compil´es : C, C++, Java etc...
A noter que certains langages peuvent ˆetre `a la fois compil´es et interpr´et´es,
comme par exemple CAML.
COMPILATION int main(){
...
}
void f(int a){}
prog.c
source du programme executable binaire
a.out
Fig. 1.1 – Compilation d’un code source
1.1.2 Le C comme langage compil´e
Le langage C est donc un langage compil´e (mˆeme s’il existe des interpr´eteurs
plus ou moins exp´erimentaux). Ainsi, un programme C est d´ecrit par un fichier
texte, appel´e fichier source. La compilation se d´ecompose en fait en 4 phases
successives1 :
1. Le traitement par le pr´eprocesseur : le fichier source (portant l’extension
.c) est analys´e par le pr´eprocesseur qui effectue des transformations purement
textuelles (remplacement de chaˆınes de caract`eres, inclusion d’autres
fichiers sources, compilation conditionnelle ...). Les diff´erentes commandes
qui peuvent ˆetre fournies au pr´eprocesseur seront expos´ees dans le chapitre
7.
2. La compilation : la compilation proprement dite traduit le fichier g´en´er´e
par le pr´eprocesseur (d’extension .i) en assembleur, c’est-`a-dire en une
suite d’instructions du microprocesseur qui utilisent des mn´emoniques rendant
la lecture possible.
3. L’assemblage : cette op´eration transforme le code assembleur (extension
.s) en un fichier binaire, c’est-`a-dire en instructions directement compr´ehensibles
par le processeur. G´en´eralement, la compilation et l’assemblage
se font dans la foul´ee, sauf si l’on sp´ecifie explicitement que l’on veut
le code assembleur. Le fichier produit par l’assemblage est appel´e fichier
objet (et porte l’extension .o). Les fichiers objets correspondant aux librairies
pr´e-compil´ees ont pour suffixe .a.
4. L’´edition de liens : un programme est souvent s´epar´e en plusieurs fichiers
sources (voir le chapitre 8 consacr´e `a la programmation modulaire), pour
des raisons de clart´e mais aussi parce qu’il fait g´en´eralement appel `a des
librairies de fonctions standards d´ej`a ´ecrites. Une fois chaque fichier de
code source assembl´e, il faut donc lier entre eux les diff´erents fichiers
objets. L’´edition de liens produit alors un fichier dit ex´ecutable (a.out
par d´efaut).
1Les extensions mentionn´ees peuvent varier selon le compilateur utilis´e (gcc ici).
3
1.1.3 Notes sur la normalisation du langage C
[KR88], publi´e initialement en 1978 par BrianW. Kernighan et Denis M. Ritchie
a fait office de norme pendant longtemps (on parle alors de C K&R, en r´ef´erence
aux 2 auteurs).
Devant la popularit´e du C, l’American National Standard Institut2 charge en
1983 le comit´e X3J11 de standardiser le langage C. Le fruit de ce travail est
finalement ´et´e approuv´e en d´ecembre 1989, le standard ANSI C89 est n´e.
L’International Organization for Standardization3 a adopt´e en 1990 ce standard
en tant que standard international sous le nom de ISO C90. Ce standard ISO
remplace le pr´ec´edent standard ANSI (C89) mˆeme `a l’int´erieur des USA, car
rappelons-le, ANSI est une organisation nationale, et non internationale comme
l’ISO.
Les standards ISO, en tant que tel, sont sujets `a des r´evisions, notamment en
1995 (C95) et en 99 (on parle alors de C99). Comme on le verra dans la suite,
certaines fonctionnalit´es d´ecrites dans ce document h´eritent de la norme C99
et n’´etait pas pr´esentes avant.
1.1.4 C en pratique : compilation et debuggage
Compilation d’un programme C
Il existe ´evidemment un grand nombre de compilateurs C pour chaque plateforme
(Windows, Linux, MAC etc...). Le compilateur d´etaill´e dans cette section
correspond au compilateur libre gcc, disponible sur quasiment toutes les plateformes
UNIX.
La compilation du fichier prog.c se d´eroule en g´en´eral en deux ´etapes :
1. traduction du langage C dans le langage de la machine (binaire) sous
forme d’un fichier objet prog.o :
gcc -O3 -Wall -c prog.c
2. regroupement des variables et des fonctions du programme avec les fonctions
fournies par des bibliotheques du systeme ou faites par l’utilisateur
(edition de liens) : gcc -o prog prog.o
Ces deux commandes ont donc permis de g´en´erer l’ex´ecutable prog qui peut
alors est lanc´e (./prog dans une fenˆetre de shell).
Si un seul fichier source est utilis´e pour g´en´erer l’ex´ecutable, les deux commandes
pr´ec´edentes peuvent ˆetre regroup´ees en une seule :
gcc -O3 -Wall prog.c -o prog
Quelques explications sur les options de gcc s’imposent :
– -O : effectue des optimisations sur le code g´en´er´e. La compilation peut alors
prendre plus de temps et utiliser plus de m´emoire. Un niveau, variant de 0 `a 3
sp´ecifie le degr´e d’optimisation demand´e. Ainsi, l’option -O3 permet d’otenir
le code le plus optimis´e.
– -Wall : active des messages d’avertissements (warnings) suppl´ementaires ;
– -c : demande au compilateur de ne faire que la premiere ´etape de compilation ;
2www.ansi.org
3www.iso.ch
4
– -o : permet de pr´eciser le nom du fichier produit.
il est important de noter que l’option -Wall est indispensable puisqu’elle
permet de detecter la plupart des erreurs d’inattention, de mauvaise pratique
du langage ou d’usage involontaire de conversion implicite.
Il existe bien sˆur de nombreuses autres options disponibles4. En voici certaines
qui pourront ˆetre utiles :
– -Idir : ajoute le r´epertoire dir dans la liste des r´epertoires ou se trouvent
des fichiers de header .h (voir chapitre 8). Exemple : -Iinclude/
– -Werror : consid`ere les messages d’avertissements (warnings) comme des erreurs
– -g : est n´ecessaire pour l’utilisation d’un debugger. Il est possible de fournir
plus ou moins d’informations au debugger. Ce crit`ere est caract´eris´e par
un niveau variant entre 1 et 3 (2 par d´efaut). Ainsi, l’utilisation du niveau
maximal (option -g3) assure que le maximum d’informations sera fourni au
debugger ;
– -pg : n´ecessaire pour l’utilisation du profiler GNU : gprof5 (qui permet de
d´eterminer par exemple quelles sont les parties du code qui prennent le plus
de temps d’ex´ecution et qui doivent donc ˆetre optimis´ees).
Enfin, il est possible d’automatiser la compilation de l’ensemble des fichiers d’un
projet `a l’aide de l’utilitaire make qui sera pr´esent´e au §8.5.
Note sur les erreurs de compilation : A ce stade, on peut classer les erreurs de
compilation en deux categories :
– les erreurs de syntaxe produites lors de la premi`ere phase lorsque le code n’est
pas ´ecrit en C correct.
– les erreurs de resolution des symboles produites lors de la deuxi`eme phase
lorsque des variables ou des fonctions utilis´ees ne sont definies nulle part.
Outils de debuggage
Il existe un certain nombre de debugger qui peuvent ˆetre utilis´es pour corriger
les erreurs d’un programme :
– en ligne de commande : gdb (http://www.gnu.org/software/gdb/gdb.html)
– sous forme d’une application graphique :
– xxgdb (http://www.cs.uni.edu/Help/xxgdb.html)
– ddd (http://www.gnu.org/software/ddd/)
A noter ´egalement l’existence de valgrind qui permet de d´etecter les fuites de
m´emoire6
1.2 Les mots-cl´es
Un certains nombres de mots sont r´eserv´es pour le langage C. Avant de commencer,
il convient donc d’en donner la liste exhaustive :
4man gcc si vous avez du courage
5http://www.gnu.org/software/binutils/manual/gprof-2.9.1/
6Voir http://developer.kde.org/~sewardj/docs-2.1.2/manual.html
5
auto double int struct
break else long switch
case enum register typedef
char extern return union
const float short unsigned
continue for signed void
default goto sizeof volatile
do if static while
1.3 Les commentaires
Les commentaires sont tr`es important dans n’importe quel langage de programmation
car ils permettent de documenter les fichiers de sources.
/* Ceci est un commentaire
qui peut s’´etaler sur plusieurs lignes */
// Ceci est un commentaire qui ne peut s’´etaler que sur une ligne
Attention car le commentaire sur une ligne est une fonctionnalit´e qui n’a ´et´e
d´efinie qu’avec la norme C99 !
Une bonne habitude `a prendre dans le domaine g´en´eral de la programmation
est de d´ecouper chaque fichier source selon plusieurs niveaux de commentaires :
1. le fichier : pour indiquer le nom de l’auteur, du fichier, les droits de copyright,
la date de cr´eation, les dates et auteurs des modifications ainsi que
la raison d’ˆetre du fichier ;
2. la proc´edure : pour indiquer les param`etres et la raison d’ˆetre de la proc
´edure
3. groupe d’instruction : pour exprimer ce que r´ealise une fraction significative
d’une proc´edure.
4. d´eclaration ou instruction : le plus bas niveau de commentaire.
On consid´erera ainsi l’exemple fourni en annexe A.1 page 106.
Attention : Une erreur classique est d’oublier la s´equence fermante */.
Dans l’exemple suivantes, les instructions 1 `a n seront ignor´ees `a la compilation :
/* premier commentaire
Instruction 1
...
Instruction n
/* deuxi`eme commentaire */
Il convient ´egalement de prendre la bonne habitude de comment´e son code
source ”`a la mode Doxygen”7. Doxygen est un syst`eme de documentation pour
de nombreux langages de programmation (C++, C, Java, Objective-C, Python,
IDL etc...). Il permet de g´en´erer facilement une documentation compl`ete sur un
code source en utilisant un format de commentaires particulier.
7www.doxygen.org
6
1.4 Structure g´en´erale d’un programme C
De mani`ere g´en´erale, un programme C consiste en la construction de blocs individuels
appel´ees fonctions qui peuvent s’invoquer l’un l’autre. Chaque fonction
r´ealise une certaine tˆache8.
Pour pouvoir s’ex´ecuter, un programme C doit contenir une fonction sp´eciale
appel´ee main qui sera le point d’entr´ee de l’ex´ecution (c’est `a dire la premi`ere
fonction invoqu´ee au d´emarrage de l’ex´ecution). Toutes les autres fonctions sont
en fait des sous-routines.
Un programme C comporte ´egalement la d´eclaration de variables qui correspondent
`a des emplacements en m´emoire. Ces emplacements permettent de
stocker des valeurs qui peuvent ˆetre utilis´ees/modifi´ees par une ou plusieurs
fonctions. Les noms des variables sont des identificateurs quelconques (voir
§1.5).
Voici deux exemples significatifs :
1. D’abord, le petit classique qui affiche `a l’´ecran ”Hello World !” :
#include <stdio.h> // Directive de preprocesseur
void main() {
printf("Hello world!");
}
2. Un exemple un peu plus complet, faisant apparaˆıtre toutes les composantes
d’un programme C :
#include <stdio.h> // Directive de preprocesseur
#define TVA 15 // idem - la TVA au Luxembourg
float prix_TTC; //d´eclaration d’une variable externe
/* Exemple de d´eclaration d’une fonction secondaire */
/* ajoute la TVA au prix HT; renvoie le prix TTC */
float ajoute_TVA(float prix_HT) {
return prix_HT*(1 + (TVA/100));
}
/* Fonction main: point d’entree de l’ex´ecution*/
void main() {
float HT; //d´eclaration d’une variable interne
puts("Entrer le prix H.T. : "); // appel de fonctions
scanf("%f",&HT); // d´efinies dans stdio.h
prix_TTC = ajoute_TVA(HT); //appel de notre fonction
printf("prix T.T.C. : %.2f ",prix_TTC);
}
On voit sur ces deux exemples les diff´erentes composantes d’un programme C :
1. les directives du pr´eprocesseur : elles permettent d’effectuer des manipulations
sur le texte du programme source, avant la compilation. Une
8A noter qu’il existe en C des fonctions pr´ecompil´ees qui peuvent ˆetre incluses dans le
programme, ces fonctions ´etant contenues dans des fichiers sp´eciaux appel´es library files (ex-
tension *.lib). Pour acc´eder `a ces fonctions, une directive doit ˆetre issue au compilateur
indiquant d’inclure les header files (extension *.h) contenant les d´eclarations correspondantes
`a ces fonctions. Dans tous les cas, les autres fonctions doivent ˆetre ´ecrites par le programmeur.
7
directive du pr´eprocesseur est une ligne de programme source commen-
¸cant par le caract`ere di`ese (#). Ces instructions seront d´etaill´ees dans le
chapitre 7 mais on distingue d´ej`a les deux directives les plus utilis´ees :
– #include qui permet d’inclure un fichier. Ici, le fichier stdio.h d´efinit
(on pr´ef`ere dire d´eclare) les fonctions standards d’entr´ees/sorties (en
anglais STanDard In/Out), qui feront le lien entre le programme et la
console (clavier/´ecran). Dans cet exemple, on utilise les fonctions puts,
printf et scanf (voir §2.5).
– #define qui d´efinit une constante. Ici, a chaque fois que le compilateur
rencontrera le mot TVA, il le remplacera par 15.
2. les d´eclarations de variables : Ces d´eclarations sont de la forme :
type nom variable [= <valeur>] ;
Les variables peuvent ˆetre d´eclar´ees soit de fa¸con externe (c’est `a dire en
dehors d’une fonction), soit de fa¸con interne (on dit aussi locale) `a une
fonction.
Par exemple, dans la d´eclaration float prix_TTC ; on a d´efini la variable
externe identifi´ee par prix_TTC, de type float (le type des nombres r´eels
dit `a virgule flottante, d’o`u ce nom). Les trois types scalaires de base du C
sont l’entier (int), le r´eel (float) et le caract`ere (char) et seront abord´es
au §1.7.
Toute variable C est typ´ee. Cela permet notamment de savoir quelle
place la variable occupera en m´emoire et comment il faudra interpr´eter la
suite de bits stock´ee en m´emoire. On peut bien sˆur d´efinir ses propres types
(voir chapitre 4), mais il existe un certain nombre de types disponibles de
fa¸con native. Ces types de base sont d´etaill´es dans la section 1.7.
Enfin, on ne peut jamais utiliser de variable sans l’avoir d´eclar´ee.
3. la d´efinition de fonctions. Ce sont des sous-programmes dont les instructions
vont d´efinir un traitement sur des variables. La d´eclaration d’une
fonction est de la forme :
type_resultat nom fonction (type1 arg1 . . . typen argn) {
<d´eclaration de variables locales >
<liste d’instructions >
}
En C, une fonction est d´efinie par :
(a) une ligne d´eclarative qui contient :
– type_resultat : le type du r´esultat de la fonction.
– nom fonction : le nom qui identifie la fonction.
– type1 arg1 . . . typen argn : les types et les noms des param`etres de
la fonction
(b) un bloc d’instructions d´elimit´e par des accolades { ... }, contenant
:
8
– <d´eclaration de variables locales > : les d´eclarations des donn´ees
locales (c’est `a dire des donn´ees qui sont uniquement connues `a
l’int´erieur de la fonction)
– <liste d’instructions > : la liste des instructions qui d´efinit l’action
qui doit ˆetre ex´ecut´ee.
Une instructions est une expression termin´ee par un ”;”. C’est un
ordre ´el´ementaire que l’on donne `a la machine et qui manipulera
les donn´ees (variables) du programme. Dans notre exemple, on a
vu deux types de manipulation : l’appel de fonctions d’une part
(puts, printf, scanf ou ajoute_TVA) et une affectation (=).
– puts affiche `a l’´ecran le texte fourni en argument.
– scanf attend que l’on entre une valeur au clavier, puis la met
dans la variable HT, sous format r´eel (%f).
– printf affiche `a l’´ecran un texte formatt´e.
Ces fonctions seront d´etaill´ees au §2.5 page 27.
Par d´efinition, toute fonction en C fournit un r´esultat dont le type
doit ˆetre d´efini. Le retour du r´esultat se fait en g´en´eral `a la fin de
la fonction par l’instruction return. Le type d’une fonction qui ne
fournit pas de r´esultat est d´eclar´e comme void (voir §1.7.4).
4. Des commentaires (voir §1.3) : ils sont ´elimin´es par le pr´eprocesseur.
A noter que pour ignorer une partie de programme il est pr´ef´erable d’utiliser
une directive du pr´eprocesseur (#ifdef ... #endif : voir le chapitre
7).
D’autres exemples de fichiers source sont fournis en annexe A page 106.
1.5 Notion d’identificateur
Un identificateur, comme son nom l’indique, permet de donner un nom `a une
entit´e du programme (qu’il s’agisse d’une variable ou d’une fonction). Ils sont
sujets aux r`egles suivantes :
1. Ils sont form´es d’une suite de lettres (’a’ `a ’z’ et ’A’ `a ’Z’), de chiffres (0
`a 9) et du signe ’_’. En particulier, les lettres accentu´ees sont interdites ;
2. le premier caract`ere de cette suite ne peut pas ˆetre un chiffre ;
3. les identificateurs sont case-sensitive.
Ainsi, les noms var1, S_i, _start et InitDB sont des identificateurs valides,
tandis que i:j ou 1i ne le sont pas.
1.6 Conventions d’´ecritures d’un programme C
Avant d’aller plus loin, il convient d’´etablir un certain nombre de r`egles de
pr´esentation que devra suivre tout bon programmeur qui souhaite ´ecrire des
programmes C lisibles9 :
– ne jamais placer plusieurs instructions sur une mˆeme ligne ;
9Ces r`egles peuvent largement ˆetre ´etendues `a tout langage de programmation
9
– utiliser des identificateurs significatifs ;
– grˆace `a l’indentation des lignes, faire ressortir la structure syntaxique du
programme ;
– laisser une ligne blanche entre la derni`ere ligne des d´eclarations et la premi`ere
ligne des instructions ;
– a´erer les lignes de programme en entourant par exemple les op´erateurs avec
des espaces ;
– bien commenter les listings tout en ´evitant les commentaires triviaux.
A ce propos, la lecture de [Her01] devrait permettre au lecteur d’aqu´erir de bon
r´eflexes de programmation.
1.7 Les types de base
1.7.1 Les caract`eres
On utilise le mot-cl´e char pour d´esigner une variable de type char. Il s’agit
en fait d’un entier cod´e sur 8 bits interpr´et´e comme un caract`ere utilis´e sur la
machine (il s’agit en g´en´eral du code ASCII de ce caract`ere).
Ex :
char c1 = ’a’; // D´eclaration d’une variable c1 de type char
// a laquelle on affecte la valeur ’a’
// A noter l’utilisation du simple quote
char c2 = 97; //c2 correspond ´egalement au caract`ere ’a’
Le tableau 1.1 donne la liste des principaux codes ASCII en d´ecimal (pour
ˆetre pr´ecis, il s’agit de la premi`ere moiti´e des caract`ere ASCII, l’autre moiti´e
correspondants en g´en´eral `a des caract`eres ´etendus comme ¨A).
code 0 1 2 3 4 5 6 7 8 9
0 NUL SOH STX ETX EOT ENQ ACK BEL BS HT
10 LF VT NP CR SO SI DLE DC1 DC2 DC3
20 DC4 NAK SYN ETB CAN EM SUB ESC FS GS
30 RS US SP ! ” # $ % & ’
40 ( ) * + , - . / 0 1
50 2 3 4 5 6 7 8 9 : ;
60 < = > ? @ A B C D E
70 F G H I J K L M N O
80 P Q R S T U V W X Y
90 Z [ ] ^ ‘ a b c
100 d e f g h i j k l m
110 n o p q r s t u v w
120 x y z { | } - DEL
Tab. 1.1 – Codes ASCII en d´ecimal
10
Caract`eres particuliers
Il existe un certain nombre de caract`eres particuliers dont les principaux sont
r´esum´es dand le tableau 1.2.
Caract`ere ’ ’ ’ ’ ’’ ’’’ ’\"’ ?
S´emantique retour `a la ligne tabulation backspace ’ ” ?
Tab. 1.2 – Quelques caract`eres sp´eciaux
Ces caract`eres ont une signification particuli`ere pour le langage C. Par exemple,
le caract`ere " doit ˆetre utilis´e dans les chaˆınes de caract`ere (et la s´equence ’\"’
permet de repr´esenter ce caract`ere dans une chaˆıne). De mˆeme, le caract`ere ?
est utilis´e dans les trigraphes qui sont des s´equences de trois caract`eres permettant
de d´esigner les caract`eres # [ ] ^ { } | ~. Le tableau 1.3 d´etaille les
trigraphes disponibles.
Trigraphe ??= ??( ??/ ??) ??’ ??< ??! ??> ??-
Signification # [ ] ^ { | } ~
Tab. 1.3 – Les 9 trigraphes disponibles en norme ANSI
Et les chaˆınes de caract`eres ?
En empi´etant un peu sur la suite, les chaˆınes de caract`eres sont vues comme un
pointeur sur des caract`eres et sont donc de type char *. Ex :
char * chaine = "Hello World !";// une chaine de caract`ere
// noter l’utilisation du double
// quote
Plus de d´etails dans le chapitre 3 consacr´e aux pointeurs.
1.7.2 Les entiers
On utilise alors le mot-cl´e int. Exemple :
/* d´eclaration la plus courante d’une variable de type int */
int a = 14; // la variable a est initialis´ee `a la valeur 14
/* Utilisation des pr´ecisions (cas le + g´en´eral)*/
short int b; // b est cod´e sur 16 bits
int c; // c est cod´e sur 16 ou 32 bits
long int d; // d est cod´e sur 32 bits
// la possibilit´e de l’´ecriture suivante d´epend du compilateur
long long int e; // d est cod´e sur 64 bits.
11
/* Pr´ecision du signe */
unsigned long int n; //n est un entier non sign´e sur 32 bits
Par d´efaut, les entiers sont en repr´esentation sign´ee (principalement en repr´esentation
par compl´ement `a 2 pour les acharn´es).
Ainsi, un short int peut contenir tout entier donc la valeur est comprise entre
−215 et 215 − 1. Au contraire, un unsigned short int (non sign´e) pourra
contenir un entier compris entre 0 et 216 − 1.
Le tableau 1.4 regroupe les types entiers standards avec quelques informations
suppl´ementaires (la taille utilis´ee en m´emoire et l’intervalle des valeurs possibles.
Voici quelques valeurs num´eriques pour avoir un meilleur ordre d’id´ee
des intervalles utilis´es :
215 = 32.768 216 = 65.536
231 = 2.147.483.648 232=4.294.967.295
263=9.223.372.036.854.775.808 264=18.446.744.073.709.551.616
.
Type Taille m´emoire Intervalle de valeurs
char 1 octet [-128 ;127] ou [0,255]
unsigned char 1 octet [0 ;255]
signed char 1 octet [-128 ;127]
int 2 ou 4 octets [-215 ;215 − 1] ou [−231; 231 − 1]
unsigned int 2 ou 4 octets [0 ;216 − 1] ou [0 ;232 − 1]
short int 2 octets [−215; 215 − 1]
unsigned short int 2 octets [0; 216 − 1]
long 4 octets [−231; 231 − 1]
unsigned long 4 octets [0; 232 − 1]
long long(*) 8 octets [−263; 263 − 1]
unsigned long long(*) 8 octets [0; 264 − 1]
Tab. 1.4 – Les types entiers standards. (*) : d´epend du compilateur utilis´e
Remarque : les valeurs limites des differents types sont indiqu´ees dans le fichier
header <limits.h> (voir §9.9). En principe, on a
sizeof(short) <= sizeof(int) <= sizeof(long)
Cas des constantes enti`eres
On distingue 3 notations :
– d´ecimale (´ecriture en base 10) : c’est l’´ecriture usuelle. Ex : 372 ;
– octale (base 8) : on commence par un 0 suivi de chiffres octaux. Ex : 0477 ;
– h´exad´ecimale (base 16) : on commence par 0x (ou 0X) suivis de chiffres h´exad
´ecimaux (0-9 a-f). Ex : 0x5a2b, 0X5a2b, 0x5A2b.
Par d´efaut, le type des constantes enti`eres est le type int. On peut aussi sp´ecifier
explicitement le type en ajoutant l’un des suffixes L ou l (pour le type long),
LL or ll (pour le type long long). On peut aussi ajouter le suffixe U ou u (pour
unsigned). Des exemples sont fournis dans le tableau suivant :
12
D´ecimal Octal Hexa Type
15 017 Oxf int
32767 077777 0x7FFF int
10U 012U 0xAU unsigned int
32768U 0100000U 0x8000u unsigned int
16L 020L 0x10L long
27UL 033ul 0x1BUL unsigned long
1.7.3 Les flottants
On distingue trois types de flottants allant de la pr´ecision la plus faible `a la
plus forte (qui d´epend de l’impl´ementation) : float, double et long double.
Ex : double Pi = 3.14159;
Representation Interne
La taille de stockage et la repr´esentation interne des nombres flottant d´epend
du compilateur utilis´e mais la plupart utilise le standard IEEE 754-1985[Ste90].
Un nombre flottant x est ainsi compos´e d’un signe s (s = 1 pour un nombre
n´egatif, 0 sinon) , d’une mantisse m et un exposant exp en base 2 tels que :
x = s ∗ m ∗ 2exp avec

1.0 ≤ m < 2 ou
m = 0
La pr´ecision du nombre flottant d´etermine le nombre de bits utilis´ee pour la
mantisse, alors l’intervalle de valeur est d´etermin´e par le nombre de bits utilis
´es par l’exposant. Le tableau 1.5 d´etaille ces informations pour chaque type
flottant.
Type Taille m´emoire Intervalle de valeurs Pr´ecision
float 4 octets [1, 2 ∗ 10−38; 3, 4 ∗ 1038] 6 chiffres d´ecimaux
double 8 octets [2, 3 ∗ 10−308; 1, 7 ∗ 10308] 15 chiffres d´ecimaux
long double 10 octets [3, 4 ∗ 10−4932; 1, 1 ∗ 104932] 19 chiffres d´ecimaux
Tab. 1.5 – Les types flottant pour les nombres r´eels.
La figure 1.2 montre le format de stockage d’un nombre de type float dans la
repr´esentation IEEE :
31 22 0
s
Position du bit:
exp
Exposant
m
Mantisse
Fig. 1.2 – Format de stockage IEEE pour un nombre de type float
En binaire, le premier bit de la mantisse non nulle sera toujours 1 : il n’est donc
pas repr´esent´e. L’exposant est stock´e avec un biais (qui vaut 127 pour le type
float) Un petit exemple pour bien comprendre : −2, 5 = −1 ∗ 1, 25 ∗ 21. Les
valeurs stock´ees seront donc : S = 1 ; exp = 127 + 1 ; m = 0.25
13
Cas des constantes flottantes
Des exemples d’utilisation des constantes flottantes sont r´esum´ees dans le tableau
suivant :
notation C correspondance notation C correspondance
2. 2 .3 0.3
1.4 1.4 2e4 2 ∗ 104
2.e4 2 ∗ 104 .3e4 0.3 ∗ 104
1.4e-4 1.4 ∗ 10−4
1.7.4 Le type void
On a vu que toute variable C ´etait typ´ee, de mˆeme que toute valeur de retour
d’un fonction. Mais il peut arriver qu’aucune valeur ne soit disponible. Pour
exprimer l’id´ee de ”aucune valeur”, on utilise le mot-cl´e void. Ce type est utilis´e
dans trois situations :
1. les expressions de type void : on les rencontre principalement dans la
d´eclaration de fonctions qui n’ont pas de valeur de retour.
Ex : void exit (int status);
On utilise aussi dans le cadre d’une conversion de type (on parle de cast,
voir §2.4) vers le type void, ce qui n’a de sens que si la valeur r´esultat
n’est pas utilis´ee. Ex : (void)printf("Un exemple.");
2. le prototype de fonctions sans param`etres : int rand(void);
A noter qu’en pratique, on ´ecrira plus simplement : int rand();
3. les pointeurs vers le type void : le type void * est le type pointeur g´en
´erique, c’est `a dire qu’il est capable de pointer vers n’importe quel type
d’objet. Il repr´esente donc l’adresse de l’objet et non son type. Plus de
pr´ecisions dans la section3 page 33 consacr´ee aux pointeurs.
14
Chapitre 2
La syntaxe du langage
2.1 Expressions et Op´erateurs
Une expression est une combination d’op´erateurs et d’op´erandes. Dans les cas les
plus simples, une expression se r´esume `a une constante, une variable ou un appel
de fonction. Des expressions peuvent ´egalement ˆetre utilis´ees comme op´erandes
et ˆetre jointes ensembles par des op´erateurs pour obtenir des expressions plus
complexes.
Toute expression a un type et si ce type n’est pas void, une valeur. Quelques
exemples d’expressions :
4 * 512 // Type: int
4 + 6 * 512 // Type: int; equivalent to 4 + (6 * 512)
printf("Un exemple! ") // Appel de fonction, type: int
1.0 + sin(x) // Type: double
srand((unsigned)time(NULL)) // Appel de fonction; type: void
(int*)malloc(count*sizeof(int)) // Appel de fonction; type: int *
Les op´erateurs peuvent ˆetre unaires (`a une op´erande) ou binaires (`a deux op´erandes).
Il existe un seul op´erateur ternaire (il s’agit de ?:) Les paragraphes
qui suivent d´etaillent les op´erateurs disponibles.
2.1.1 Op´erateurs arithm´etiques
Les principaux op´erateurs arithm´etiques sont r´esum´es dans le tableau 2.1. Les
op´erandes de ces op´erateurs peuvent appartenir `a tout type arithm´etique1
Quelques remarques :
– On peut effectuer une conversion de type aux op´erandes. Le r´esultat de l’op´eration
prend le type de cette conversion. Ainsi ; 2.0/3 est ´equivalent `a 2.0/3.0
et le r´esultat est de type float.
– Le r´esultat d’une division d’entiers est aussi un entier ! Ex :
6 / 4 // Resultat: 1
6 % 4 // Resultat: 2
6.0 / 4.0 // Resultat: 1.5
1Seul l’op´erateur % requiert des types entiers
15
Op´erateur Traduction Exemple R´esultat
+ Addition x + y l’addition de x et y
- Soustraction x - y la soustraction de x et y
* Produit x * y la multiplication de x et y
/ Division x / y le quotient de x et y
% Reste x % y Reste de la division euclidienne de x par y
+(unaire) Signe positif +x la valeur de x
-(unaire) Signe n´egatif -x la n´egation arithm´etique de x
++(unaire) Incr´ement x++ ou ++x x est incr´ement´e (x = x + 1). L’op´erateur
pr´efixe ++x (resp. suffixe x++) incr´emente
x avant (resp. apr`es) de l’´evaluer
--(unaire) Decr´ement x-- ou --x x est d´ecrement´e (x = x−1). L’op´erateur
pr´efixe --x (resp. suffixe x--) d´ecr´emente
x avant (resp. apr`es) de l’´evaluer
Tab. 2.1 – Les principaux op´erateurs arithm´etiques
– Concernant l’incr´ementation pr´e/postfixe, voici un petit exemple pour bien
comprendre :
Supposons que la valeur de N soit ´egale `a 5 :
– Incr´ementation postfixe : X = N++ ; R´esultat : N = 6 et X = 5
– Incr´ementation pr´efixe : X = ++N ; R´esultat : N = 6 et X = 6
2.1.2 Op´erateurs d’affectation
Op´erateur Traduction Exemple R´esultat
= affectation simple x = y assigne la valeur de y `a x
(op)= affectation compos
´ee
x += y x (op)=y est ´equivalent `a
x = x (op) y
Tab. 2.2 – Les op´erateurs d’affectation
On distingue deux types d’op´erateurs d’assignement qui sont r´esum´es dans le
tableau 2.2. L’op´erande de gauche doit ˆetre une expression qui d´esigne un objet
(une variable par exemple).
Dans l’´ecriture x(op)=y, la variable x n’est ´evalu´ee qu’une seule fois. (op) est un
op´erateur arithm´etique ou de manipulation de bits Les op´erateurs d’affectation
compos´es sont donc :
+= -= *= /= %= &= ^= |= <<= >>=
2.1.3 Op´erateurs relationnels
Toute comparaison est une expression de type int qui renvoie la valeur 0 (false)
ou 1 (true). Il faut que les op´erandes soient du mˆeme type arithm´etique (ou des
pointeurs sur des objets de mˆeme type).
16
Op´erateur Traduction Exemple R´esultat
< inf´erieur x < y 1 si x est inf´erieur `a y
<= inf´erieur ou ´egal x <= y 1 si x est inf´erieur ou ´egal `a y
> sup´erieur x > y 1 si x est sup´erieur `a y
>= sup´erieur ou ´egal x >= y 1 si x est sup´erieur ou ´egal `a y
== ´egalit´e x == y 1 si x est ´egal `a y
!= in´egalit´e x != y 1 si x est diff´erent de y
Tab. 2.3 – Les op´erateurs relationnels
Les diff´erents op´erateurs relationnels sont d´etaill´es dans le tableau 2.3.
Attention : Une erreur ultra classique est de confondre l’op´erateur d’´egalit´e
(==) avec celui d’affectation (=). Ainsi, consid´erons le code suivant2 :
/* Fichier: erreur.c */
#include <stdio.h>
int main () {
int x=14,y=1; // x est diff´erent de y
if (x = y) //erreur!!! il faudrait ´ecrire ’if (x == y)’
printf("x est ´egal `a y (%i=%i) ",x,y);
else
printf("x est diff´erent de y (%i!=%i) ",x,y);
return 0;
}
A priori, x et y sont diff´erents et on s’attend `a obtenir un message le confirmant.
N´eanmoins, l’erreur d’ecriture a permis d’effectuer une affectation de la valeur
de x si bien que ce programme renvoit la ligne3 : x est ´egal `a y (1=1)) (not´e
la valeurs de x)
Avec une ´ecriture correcte, on aurait obtenu : x est diff´erent de y (14!=1)
Remarque : en initialisant y a 0 (et non a 1), on aurait obtenu le message x est
diff´erent de y (0 !=0) car le r´esultat de l’affectation est la valeur affect´ee
(ici 0). Cette valeur est consid´er´ee comme ’fausse’ pour la condition test´ee dans
le if si bien que les instruction de la branche else sont ex´ecut´ees (voir §2.2.1).
2.1.4 Op´erateurs logiques
Les op´erateurs logiques, list´es dans le tableau 2.4 permettent de combiner le
r´esultat de plusieurs expressions de comparaison en une seule expression logique.
Les op´erandes des op´erateurs logiques peuvent ˆetre n’importe quel scalaire (i.e
arithm´etique ou pointeur). Toute valeur diff´erente de 0 est interpr´et´ee comme
vraie (et 0 correspond `a ’faux’). Comme pour les expressions relationnelles, les
expressions logiques renvoient une valeur enti`ere (0=false ; 1=true).
Remarque : les op´erateurs && et || ´evaluent les op´erandes de gauche `a droite et
le r´esultat est connu d`es l’op´erande de gauche. Ainsi, l’op´erande de droite n’est
2Compiler ce code avec la commande gcc -Wall erreur.c -o erreur
3l’option de compilation ’-Wall’ a quand mˆeme soulev´e un warning
17
Op´erateur Traduction Exemple R´esultat
&& ET logique x && y 1 si x et y sont diff´erents de 0
|| OU logique x || y 1 si x et/ou y sont diff´erents de 0
! NON logique !x 1 si x est ´egal `a 0. Dans tous les autres
cas, 0 est renvoy´e.
Tab. 2.4 – Les op´erateurs logiques
´evalu´ee que si celle de gauche est vraie dans le cas de l’op´erateur && (respectivement
fausse dans le cas de l’op´erateur ||). Par exemple, dans l’expression
(i < max) && (f(14) == 1), la fonction f n’est appel´ee que si i < max.
2.1.5 Op´erateurs bit `a bit
Op´erateur Traduction Exemple R´esultat (pour chaque position
de bit)
& ET bit `a bit x & y 1 si les bits de x et y valent 1
| OU bit `a bit x | y 1 si le bit de x et/ou de y vaut 1
^ XOR bit `a bit x ^ y 1 si le bit de x ou de y vaut 1
~ NON bit `a bit ~x 1 si le bit de x est 0
<< d´ecalage `a gauche x << y d´ecale chaque bit de x de y positions
vers la gauche
>> s´ecalage `a droite x >> y d´ecale chaque bit de x de y positions
vers la droite
Tab. 2.5 – Les op´erateurs de manipulation des bits
Les op´erateurs bits `a bits n’op`erent que sur des entiers. Les op´erandes sont interpr
´et´ees bits par bits (le bit 1 correspondant `a une valeur vraie, 0 est consid´er´e
comme une valeur fausse). Quelques exemples pour bien comprendre (chaque
op´erande est fournie sous forme d´ecimale et binaire) :
x y Op´eration R´esultat
14 1110 9 1001 x & y 8 1000
14 1110 9 1001 x | y 15 1111
14 1110 9 1001 x ^ y 7 0111
14 1110 ~x 1 0001
14 1110 2 0010 x << y 56 111000
14 1110 2 0010 x >> y 3 11
2.1.6 Op´erateurs d’acc`es `a la m´emoire
L’op´erande de l’op´erateur d’adresse & doit ˆetre une expression qui d´esigne un
objet ou une expression. Ainsi, &x renvoie l’adresse m´emoire de x et est donc
un pointeur vers x. Plus de d´etails dans le chapitre 3 d´edi´e aux pointeurs.
18
Op. Traduction Exemple R´esultat
& Adresse de &x l’adresse m´emoire de x
* Indirection *p l’objet (ou la fonction) point´ee par p
[ ] El´ement de tableau t[i] L’´equivalent de *(x+i), l’´el´ement
d’indice i dans le tableau t
. Membre d’une structure
ou d’une union
s.x le membre x dans la structure ou
l’union s
-> Membre d’une structure
ou d’une union
p->x le membre x dans la structure ou
l’union point´ee par p
Tab. 2.6 – Les op´erateurs d’acc`es `a la m´emoire
Les op´erateurs d’acc`es aux membres d’une structure ou d’une union seront plus
amplement d´etaill´es dans les §4.3 et §4.4.
2.1.7 Autres op´erateurs
Il existe un certain nombre d’autres op´erateurs qui sont list´es dans le tableau 2.7.
On y retrouve notamment l’appel de fonction et la conversion de type (qui ne
s’applique qu’aux types scalaires et qui sera abord´ee plus en d´etail dans la
section 2.4 page 25).
Op. Traduction Exemple R´esultat
() Appel de fonction f(x,y) Ex´ecute la fonction f avec les arguments
x et y
(type) cast (long)x la valeur de x avec le type sp´ecifi´e
sizeof taille en bits sizeof(x) nombre de bits occup´e par x
? : Evaluation conditionnelle
x?:y:z si x est diff´erent de 0, alors y sinon z
, s´equencement x,y Evalue x puis y
Tab. 2.7 – Les autres op´erateurs
L’op´erateur sizeof renvoie le nombre de bits requis pour stocker un objet du
type sp´ecifi´e. Le r´esultat est une constante de type size_t.
L’op´erateur ?: permet une ecriture plus compacte de l’´evaluation conditionnelle
if...then...else. Ainsi l’expression :
(x >= 0) ? x : -x
renvoie la valeur absolue de de x. Plus de d´etails au §2.2.1.
2.2 Les structures de contrˆole
Comme pour la plupart des langages de programmation, le langage C d´efinit
un certain nombre de structures de contrˆole (boucles et branchements).
19
2.2.1 Instruction if...else
Syntaxe :
1. if ( expression ) Instruction1
2. if ( expression ) Instruction1 else Instruction2
La valeur de expression est ´evalu´ee et si elle est diff´erente de 0, Instruction1 est
ex´ecut´ee, sinon Instruction2 est ex´ecut´ee (si elle existe). Exemple :
if (x > y) max = x; // Assigne la plus grande valeur entre x et y
else max = y; // `a la variable max.
Remarque :
– l’exemple pr´ec´edent aurait pu s’´ecrire plus simplement (mais moins lisiblement)
: (x > y)?(max = x):(max = y); ou mˆeme max = (x>y)? x : y;
– Attention aussi au fait que expression doit ˆetre correctement parenth´es´ee.
– la partie ’then’ n’est pas introduite par le mot-cl´e then comme dans certains
langages.
2.2.2 Instruction for
Syntaxe :
– for (expression1 ; expression2 ; expression3)
Instruction
expression1 et expression3 peuvent ˆetre n’importe quelle expression. expression2
est une expression de contrˆole et doit donc ˆetre de type scalaire. Le d´eroulement
de cette instruction est illustr´e par l’organigramme de la figure 2.1.
Instruction
Expression3
Expression1
?
Expression2 != 0 non fin du for
oui
Fig. 2.1 – Organigramme de l’instruction for
Un exemple pour bien comprendre :
int i,MAX=14; //compteur
for (i=0; i < MAX ; i++) {
printf("Valeur de i : %i ",i);
}
2.2.3 Instruction while
Syntaxe :
– while (expression) Instruction
Il s’agit d’une boucle : tant que expression est vraie, Instruction est ex´ecut´ee,
conform´ement `a l’organigramme de la figure 2.2.
Exemple :
20
Instruction
?
Expression != 0 non fin du while
oui
Fig. 2.2 – Organigramme de l’instruction while
#define MAX 14
int i=0;
while (i < MAX ) {
printf("Valeur de i : %i ",i);
i++;
}
2.2.4 Instruction do...while
Syntaxe :
do instruction while (expression) ;
l’instruction do...while a un fonctionnement globalement similaire `a celui
d’une boucle while mise `a part le fait que le corps de la boucle est ex´ecut´e
avant que l’expression de contrˆole soit ´evalu´ee, comme dans l’organigramme de
la figure 2.3. Ainsi, le corps de la boucle est toujours ex´ecut´e au moins une
fois.
Instruction
?
Expression != 0 non
oui
fin du do
Fig. 2.3 – Organigramme de l’instruction do
L’exemple suivant imprimera ”Valeur de i : 14” (alors que i ≥ 14) :
#include <stdio.h>
#define MAX 14
int main() {
int i=MAX;
do {
printf("Valeur de i : %i ",i);
i++;
} while (i < MAX);
return 0;
}
21
2.2.5 Instruction switch
Cette instruction est un if g´en´eralis´e. Sa syntaxe est la suivante :
switch (expression) {
case constante1 : liste d′instructions1 break ;
. . .
case constanten : liste d′instructionsn break ;
default : liste d′instructions
}
1. expression est ´evalu´ee, puis le r´esulat est compar´e avec constante1, constante2
etc...
2. A la premi`ere constantei dont la valeur est ´egale `a expression, la (ou les4)
liste d′instructions correspondante(s) est ex´ecut´ee jusqu’`a la rencontre
d’une instruction break. La rencontre d’un break termine l’instruction
swith.
3. s’il n’existe aucune constantei dont la valeur soit ´egale `a celle de expression,
on ex´ecute la liste d′instructions de l’alternative default si celle-ci existe,
sinon rien n’est fait.
ATTENTION : expression est n´ecessairement une valeur enti`ere (voir le tableau
1.4 page 12). C’est pourquoi on utilisera souvent dans les exemples qui
suivent des enum´erations (voir §4.1 page 41).
Compte tenu du nombre d’´el´ements optionnels, on peut distinguer 3 types d’utilisations
possibles :
1. pour chaque alternative case, on trouve un break. Exemple :
enum {FALSE, TRUE};
void print_boolean(int bool) {
switch (bool) {
case FALSE: printf("faux"); break;
case TRUE: printf("vrai"); break;
default: printf("Erreur interne");
}
}
2. on peut avoir une ou plusieurs alternatives ne poss´edant ni liste d′instructions,
ni break. Par exemple, si on veut compter le nombre de caract`eres qui
sont des chiffres et ceux qui ne le sont pas, on peut utiliser le switch
suivant :
switch (c) {
case ’0’:
case ’1’:
case ’2’:
case ’3’:
case ’4’:
case ’5’:
case ’6’:
case ’7’:
case ’8’:
4l’instruction break est optionnelle
22
case ’9’: nb_chiffres++; break;
default: nb_non_chiffres++;
}
3. enfin on peut ne pas avoir de break comme dans l’exemple suivant :
enum {POSSIBLE, IMPOSSIBLE};
void print_cas(int cas) {
switch (cas) {
case IMPOSSIBLE: printf("im");
case POSSIBLE: printf("possible");
}
}
L’instruction break peut ˆetre ´etendue `a d’autres cas de branchements (ou de
sauts) comme on le verra au §2.2.7.
Les paragraphes qui suivent abordent les branchement non conditionnels (par
opposition aux branchements conditionnels correspondant aux instructions if...else
et switch) qui permettent de naviguer au sein des fonctions du programme.
2.2.6 Instruction goto
Syntaxe :
goto etiquette ;
La directive goto permet de brancher directement `a n’importe quel endroit de la
fonction courante identifi´ee par une ´etiquette. Une ´etiquette est un identificateur
suivi du signe ”:”.
Exemple :
for ( ... )
for ( ... )
if ( erreur )
goto TRAITEMENT_ERREUR;
...
TRAITEMENT_ERREUR: // le traitement d’erreur est effectu´e ici
printf("Erreur: ....");
...
Remarque :
– Encore une fois, l’instruction goto et la d´eclaration de l’etiquette doivent ˆetre
contenu au sein de la mˆeme fonction.
– N’utiliser cette instruction que lorsque vous ne pouvez pas faire autrement.
Le plus souvent, vous pouvez vous en passer alors n’en abusez pas !
– Pour une saut en dehors d’une mˆeme fonction, on pourra utiliser les fonctions
setjmp et longjmp.
2.2.7 Instruction break
Syntaxe :
break;
On a vu le rˆole de l’instruction break au sein de la directive de branchement
multiple switch. On peut l’utiliser plus g´en´eralement au sein de n’importe
23
quelle boucle (instructions for, while ou do...while) pour interrompre son
d´eroulement et passer directement `a la premi`ere instruction qui suit la boucle.
Exemple :
while (1) {
...
if (command == ESC) break; // Sortie de la boucle
...
}
// on reprend ici apr`es le break
En cas de boucles imbriqu´ees, break fait sortir de la boucle la plus interne
2.2.8 Instruction continue
Syntaxe :
continue ;
L’instruction continue ne peut ˆetre utilis´ee que dans le corps d’une boucle
(instruction for, while ou do). Elle permet de passer directement `a l’it´eration
suivante.
Exemple :
for (i = -10; i < 10; i++) {
if (i == 0) continue; // passer `a 1 sans ex´ecuter la suite
...
}
2.3 La r´ecursivit´e
Comme pour beaucoup de langages, la r´ecursivit´e est possible en C. Qu’estce
que la r´ecursivit´e ? C’est la propri´et´e qu’a une fonction de s’auto-appeler,
c’est-`a-dire de se rappeler elle-mˆeme plusieurs fois. La notion de r´ecurrence est
largement pr´esente dans le domaine math´ematique, notamment dans la d´efinition
des suites. Exemple :
(
u0 = 1
un = 2un−1 + 1 ∀ n ≥ 1
Les premiers termes cons´ecutifs de cette suite sont donc :
u0 = 1
u1 = 2u0 + 1 = 3
u2 = 2u1 + 1 = 7 . . .
Bien sˆur, on pourrait calculer explicitement la valeur de un en fonction de n (ici,
on peut montrer que ∀n ∈ N, un = 2n+1 − 1) mais on peut utiliser directement
la d´efinition de la fonction pour la programmation du calcul correspondant :
#include <stdio.h>
/***********************************************************
* Exemple de fonction traduisant la d´efinition de la suite:
* u_0 = 1
* u_n = 2*u_{n-1} + 1 si n >= 1
24
* Contrainte: n>=0
************************************************************/
int ma_suite(unsigned int n) {
if (n == 0) return 1; //u_0 = 1
else return 2*ma_suite(n-1) + 1; //appel r´ecursif
}
/*** Fonction main: point d’entr´ee de l’ex´ecution ***/
int main() {
unsigned int n;
puts("Entrer la valeur de n : ");
scanf("%u",&n);
printf("valeur de la suite : %u ",ma_suite(n));
return 0;
}
2.4 Les conversions de types
Une conversion de type renvoie la valeur d’une expression (de type A) dans un
nouveau type B. Les conversions ne peuvent ˆetre effectu´ees que sur les types
scalaires (c’est `a dire les types arithm´etiques et les pointeurs).
Une conversion de type conserve toujours la valeur originale dans la mesure ou
le nouveau type est capable de la repr´esenter. Ainsi, un nombre flottant peut
ˆetre arrondi dans une conversion de double vers float. Enfin, la conversion
peut ˆetre soit implicite, c’est `a dire effectu´ee par le compilateur (par exemple,
si i est une variable de type float et j de type int, alors l’expression i+j est
automatiquement de type float).
Elle peut ˆetre ´egalement explicite grˆace `a l’op´erateur de cast (transtypage en
fran¸cais). Il est de bon goˆut et tr`es important (pour la clart´e du programme)
d’utiliser l’op´erateur de cast d`es qu’une conversion de type est requise. Cela
´evite aussi les warnings du compilateur. Exemple :
void f(float i, float j) { // fonction avec 2 param`etres de type
... // float
}
int main() {
int i = 14;
float j = 2.0;
...
f( (float)i, j); // appel de f avec conversion de i vers
// le type float
}
2.4.1 Les situations de la conversion de type
En C, les situations o`u se produisent les conversions sont les suivantes :
1. une valeur d’un certain type est utilis´ee dans un contexte qui en requiert
un autre :
– passage de param`etre : le param`etre effectif n’a pas le type du param`etre
formel (c’´etait le cas dans l’exemple pr´ec´edent) ;
25
– affectation : la valeur `a affecter n’a pas le mˆeme type que la variable ;
– valeur rendue par une fonction : l’op´erande de return n’a pas le type
indiqu´e dans la d´eclaration de la fonction.
2. op´erateur de conversion : le programmeur demande explicitement une
conversion.
3. un op´erateur a des op´erandes de types diff´erents.
Dans ce dernier cas, et contrairement aux deux cas pr´ec´edents, c’est le compilateur
qui effectue la conversion selon des r`egles soigneusement d´efinies qui sont
d´etaill´ees dans les deux paragraphes qui suivent.
2.4.2 La r`egle de ”promotion des entiers”
Cette r`egle est appliqu´ee aux op´erandes des op´erateurs unaires + et -, ainsi
qu’aux op´erateurs binaires de d´ecalage << et >> et dans les conversions arith-
m´etiques habituelles, abord´ees au §2.4.3. Elle a pour but d’amener les ”petits
entiers” `a la taille des int.
Ainsi, toute op´erande de type char, unsigned char, short , unsigned short
ou champs de bits sur les op´erateurs pr´ec´edent est automatiquement convertie
en int (ou en unsigned int si le type int ne permet pas de repr´esenter
l’op´erande).
2.4.3 Les conversions arithm´etiques habituelles
Cette r`egle est appliqu´ee `a tous les op´erateurs arithm´etiques binaires, except´es
les op´erateurs de d´ecalage << et >> et les seconde et troisi`eme op´erandes de
l’op´erateur ?:.
La r`egles qui s’applique est la suivante :
Dans le cas d’op´erandes enti`eres, on applique d´ej`a la r`egle de promotion des
entiers, ce qui permet de se d´ebarrasser des petits entiers. Ensuite, si les op´erandes
sont toujours de types diff´erents, on les convertit dans le type le plus
haut dans la hi´erarchie expos´ee dans la figure 2.4.
long double
double
float
long
unsigned long long
long long
unsigned long
unsigned int
int
Fig. 2.4 – Hi´erarchie des conversions arithm´etiques habituelles
26
2.4.4 Les surprises de la conversion de type
En g´en´eral, les conversions fonctionnent de fa¸con satisfaisante vis `a vis du programmeur.
Cependant, il existe une situation permettant d’obtenir des r´esultats
surprenants : lors de la comparaison entre des entiers sign´es et non sign´es.
Consid´erer ainsi le programme suivant :
#include <stdio.h>
int main () {
unsigned int i = 0;
if (i < -1) printf("Cha ch’est louche ");
else printf("On est super content! ");
return 0;
}
Celui ci imprimera le message Cha ch’est louche, laissant croire que 0 < −1...
L’explication est la suivante : l’op´erateur < poss`ede une op´erande de type
unsigned int (i) et l’autre de type int (la constante -1). D’apr`es la figure 2.4,
cette derni`ere est convertie en unsigned int et le compilateur r´ealise la comparaison
non sign´ee entre 0 et 4294967295 (puisque -1 = 0xFFFFFFFF= 4294967295).
CQFD.
Pour que tout revienne dans l’ordre, il suffit d’utiliser l’op´erateur de conversion
dans le test qui devient : if ( (int) i < -1)
Attention car on peut aussi utiliser des unsigned int sans s’en rendre compte !
Ainsi :
#include <stdio.h>
int main () {
if ( sizeof(int) < -1) printf("cha ch’est louche ");
else printf("On est super content! ");
return 0;
}
imprimera le message... Cha ch’est louche. Pourtant, les int n’ont pas une
longueur n´egative... Il se trouve simplement qui dans la d´eclaration de la fonction
sizeof (d´efinie dans stddef.h) renvoie un ´el´ement de type size_t, un
entier non sign´e...
Conclusion :
1. Ne jamais m´elanger entiers sign´es et non sign´es dans les comparaisons
(l’option -Wall du compilateur le signale normalement) ! Utiliser l’op´erateur
de conversion pour amener les op´erandes de la comparaison dans le
mˆeme type.
2. bien noter que sizeof renvoit une valeur de type entier non sign´e.
2.5 Principales fonctions d’entr´ees-sorties standard
Il s’agit des fonctions de la librairie standard stdio.h (voir aussi le §9.18 et le
chapitre 6) utilis´ees avec les unit´es classiques d’entr´ees-sorties, qui sont respectivement
le clavier et l’´ecran. Sur certains compilateurs, l’appel `a la librairie
stdio.h par la directive au pr´eprocesseur #include <stdio.h> n’est pas n´ecessaire
pour utiliser les fonctions pr´esent´ees ici, notamment printf et scanf.
27
2.5.1 La fonction getchar
La fonction getchar permet la r´ecup´eration d’un seul caract`ere `a partir du
clavier. La syntaxe d’utilisation de getchar est la suivante :
var=getchar();
Notez que var doit ˆetre de type char. Exemple :
#include <stdio.h>
int main() {
char c;
printf("Entrer un caract`ere:");
c = getchar();
printf("Le caract`ere entr´e est %c ",c);
return 0;
}
A noter enfin que cette fonction est strictement ´equivalente `a getc(stdin)
(Cette fonction sera abord´ee au chapitre 6).
2.5.2 La fonction putchar
La fonction putchar permet l’affichage d’un seul caract`ere sur l’´ecran de l’ordinateur.
putchar constitue alors la fonction compl´ementaire de getchar. La
syntaxe d’utilisation est la suivante :
putchar(var);
o`u var est de type char. Exemple :
#include <stdio.h>
int main() {
char c;
printf("Entrer un caract`ere:");
c = getchar();
putchar(c);
return 0;
}
2.5.3 La fonction puts
Syntaxe :
puts(ch);
Cette fonction affiche, sur stdout, la chaˆıne de caract`eres ch puis positionne
le curseur en d´ebut de ligne suivante. puts retourne EOF en cas d’erreur.
Exemple :
#include <stdio.h>
int main() {
char * toto = "on est super content!";
puts(toto);
return 0;
}
28
2.5.4 La fonction d’´ecriture `a l’´ecran formatt´ee printf
La fonction printf est une fonction d’impression format´ee, ce qui signifie que
les donn´ees sont converties selon le format particulier choisi. Sa syntaxe est la
suivante :
printf("cha^ıne de contr^ole", expression1, . . . , expressionn);
La chaˆıne de contrˆole contient le texte `a afficher et les sp´ecifications de format
correspondant `a chaque expression de la liste. Les sp´ecifications de format ont
pour but d’annoncer le format des donn´ees `a visualiser. Elles sont introduites
par le caract`ere %. Le i-`eme format de la chaˆıne de contrˆole sera remplac´e par
la valeur effective de expressioni.
Pour ˆetre plus pr´ecis, une sp´ecification de i-`eme format est de la forme :
%[flag][largeur][.pr´ecision][modificateur]type
Les ´el´ements entre crochet sont facultatifs. Les diff´erents ´el´ements de cette sp´ecification
sont les suivants :
1. [flag] fournit des options de cadrage (par d´efaut, expressioni est justifi´e
`a droite) et peut prendre les valeurs suivantes :
- expressioni sera justifi´e `a gauche (ajout de blancs si n´ecessaire)
+ affichage du signe (+ ou -) avant la valeur num´erique
espace impression d’un espace devant un nombre positif, `a la place du
signe
Il existe d’autres valeurs possibles mais qui sont moins utiles.
2. [largeur] est le nombre minimal de caract`eres `a ´ecrire (des blancs sont
ajout´es si n´ecessaire). Si le texte `a ´ecrire est plus long, il est n´eanmoins
´ecrit en totalit´e. En donnant le signe * comme largeur, expressioni fournira
la largeur (et expressioni+1 la variable `a afficher)
Exemple : printf("%*f",14,var).
3. [.pr´ecision] d´efinit, pour les r´eels, le nombre de chiffres apr`es la virgule
(ce nombre doit ˆetre inf´erieur `a la largeur). Dans le cas de nombres entiers,
ce champ indique le nombre minimal de chiffres d´esir´es (avec l’ajout de 0
sinon), alors que pour une chaˆıne (%s), elle indique la longueur maximale
imprim´ee (tronqu´ee si trop longue). Comme pour la largeur, on peut ´ecrire
.* pour que expressioni+1 d´esigne la pr´ecision `a appliquer.
Valeur par d´efaut : 6.
4. [modificateur] modifie l’interpr´etation de expressioni et peut valoir h
(pour short), l (long pour les entiers, double pour les r´eels), ou encore
L (long double pour les r´eels).
5. type pr´ecise l’interpr´etation `a donner `a expressioni. Les valeurs possibles
sont d´etaill´ees dans la table 2.8
printf retourne le nombre de caract`eres ´ecrits, ou EOF en cas de probl`eme.
Voici plusieurs exemples didactiques :
printf("|%d| ",14); |14|
printf("|%d| ",-14); |-14|
printf("|%+d| ",14); |+14|
29
format conversion en ´ecriture
%d int d´ecimale sign´ee
%ld long int d´ecimale sign´ee
%u unsigned int d´ecimale non sign´ee
%lu unsigned long d´ecimale non sign´ee
%o unsigned int octale non sign´ee
%lo unsigned long octale non sign´ee
%x unsigned int hexad´ecimale non sign´ee
%lx unsigned long hexad´ecimale non sign´ee
%f double d´ecimale virgule fixe
%lf long double d´ecimale virgule fixe
%e double d´ecimale notation exponentielle
%le long double d´ecimale notation exponentielle
%g double d´ecimale, repr´esentation la plus courte parmi %f et %e
%lg long double d´ecimale, repr´esentation la plus courte parmi %lf et %le
%c unsigned char caract`ere
%s char* chaˆıne de caract`eres
Tab. 2.8 – Les diff´erents formats de la fonction printf
printf("|%+d| ",-14); |-14|
printf("|% d| ",14); | 14|
printf("|% d| ",-14); |-14|
printf("|%x| ",0x56ab); |56ab|
printf("|%X| ",0x56ab); |56AB|
printf("|%#x| ",0x56ab); |0x56ab|
printf("|%#X| ",0x56ab); |0X56AB|
========================
printf("|%o| ",14); |16|
printf("|%#o| ",14); |016|
========================
printf("|%10d| ",14); | 14|
printf("|%10.6d| ",14); | 000014|
printf("|%.6d| ",14); |000014|
printf("|%*.6d| ",10,14); | 000014|
printf("|%*.*d| ",10,6,14); | 000014|
========================
printf("|%f| ",1.234567890123456789e5); |123456.789012|
printf("|%.4f| ",1.234567890123456789e5); |123456.7890|
printf("|%.20f| ",1.234567890123456789e5); |123456.78901234567456413060|
printf("|%20.4f| ",1.234567890123456789e5); | 123456.7890|
========================
printf("|%e| ",1.234567890123456789e5); |1.234568e+05|
printf("|%.4e| ",1.234567890123456789e5); |1.2346e+05|
printf("|%.20e| ",1.234567890123456789e5); |1.23456789012345674564e+05|
printf("|%20.4e| ",1.234567890123456789e5); | 1.2346e+05|
========================
printf("|%.4g| ",1.234567890123456789e-5); |1.235e-05|
printf("|%.4g| ",1.234567890123456789e5); |1.235e+05|
30
printf("|%.4g| ",1.234567890123456789e-3); |0.001235|
printf("|%.8g| ",1.234567890123456789e5); |123456.79|
2.5.5 La fonction de saisie scanf
La fonction scanf permet de r´ecup´erer les donn´ees saisies au clavier, dans le
format sp´ecifi´e. Ces donn´ees sont stock´ees aux adresses sp´ecifi´ees par les arguments
de la fonction scanf (on utilise donc l’op´erateur d’adressage & pour les
variables scalaires). scanf retourne le nombre de valeurs effectivement lues et
m´emoris´ees (en ommettant les %*). La syntaxe est la suivante :
scanf("cha^ıne de contr^ole", arg1, . . . , argn);
La chaˆıne de contrˆole indique le format dans lequel les donn´ees lues sont converties.
Elle ne contient pas d’autres caract`eres (notamment pas de ). Comme
pour printf, les conversions de format sont sp´ecifi´ees par un caract`ere pr´ec´ed´e
du signe %. Les formats valides pour la fonction scanf diff`erent l´eg`erement de
ceux de la fonction printf.
Les donn´ees `a entrer au clavier doivent ˆetre s´epar´ees par des blancs ou des
<RETURN> sauf s’il s’agit de caract`eres.
Pour ˆetre plus pr´ecis, les sp´ecifications de format ont la forme suivante :
%[*][larg][modif]type
* la valeur sera lue mais ne sera pas stock´ee
larg pr´ecise le nombre maximal de caract`eres `a lire
modif sp´ecifie une taille diff´erente pour les donn´ees point´ees par l’argument :
h : short int
l : long int (pour les entier) ou double (pour les nombres flottants).
L : long double
type sp´ecifie le type de donn´ee lue et comment il faut le lire.
La plupart des types de scanf sont les mˆemes que pour printf :
Type Description Argument requis
c caract`ere simple (espace inclu) char *
d entier : nombre optionnellement pr´ec´ed´e par un signe int *
e,E,
f,g,G
Floating point : nombre d´ecimal pr´ec´ed´e eventuellement
d’un signe et suivi ´eventuellement du caract`ere
e ou E et d’un nombre d´ecimal. Exemple :-732.103
ou 7.12e4
float *
o entier en notation octale. int *
s Chaˆıne de caract`eres (jusqu’`a la lecture d’un espace) char *
u entier non sign´e unsigned int *
x entier hexadecimal int *
Exemple :
#include <stdio.h>
int main() {
31
int i;
printf("entrez un entier sous forme hexadecimale i = ");
scanf("%x",&i);
printf("i = %d ",i);
return 0;
}
Les autres fonctions d’entr´ees/sorties (permettant notamment la manipulation
des fichiers) seront abord´ees dans le chapitre 6.
32
Chapitre 3
Les pointeurs
Toute variable manipul´ee dans un programme est stock´ee quelque part en m´emoire
centrale. Cette m´emoire est constitu´ee d’octets qui sont identifi´es de
mani`ere univoque par un num´ero qu’on appelle adresse comme l’illustre la figure
3.1.
Adresse
adresses croissantes
32 bits
(un pointeur vers une zone mémoire)
MEMOIRE CENTRALE
0x5E00 p
0x5E04
0x5E08
0x5E0C
0x5E10
0x5E14
0x5E18
0x5E1C
0x5E20
Fig. 3.1 – Illustration de l’adressage de la m´emoire centrale
Par analogie, on peut voir la m´emoire centrale comme une armoire constitu´ee
de tiroirs num´erot´es. Un num´ero de tiroir correspond `a une adresse.
Ainsi, lorsqu’on d´eclare une variable var de type T, l’ordinateur r´eserve un
espace m´emoire (de sizeof(T) octets) pour y stocker les valeurs de var.
Pour retrouver cette variable, il suffit donc de connaˆıtre l’adresse du premier
octet o`u elle est stock´ee (ou, s’il s’agit d’une variable qui recouvre plusieurs
octets contigus, l’adresse du premier de ces octets).
Un pointeur est une variable de type adresse.
Les pointeurs pr´esentent de nombreux avantages :
– Ils permettent de manipuler de fa¸con simple des donn´ees de taille importante
(comme les tableaux, les structures etc...). Ainsi, au lieu de passer en param`
etre `a une fonction un ´el´ement tr`es grand (en taille), on pourra se contenter
33
de lui fournir un pointeur vers cet ´el´ement... On gagne ´evidemment alors en
efficacit´e dans l’ex´ecution du programme.
– Comme on le verra au chapitre 4, les tableaux ne permettent de stocker qu’un
nombre fix´e d’´el´ements de mˆeme type. Si les composantes du tableau sont des
pointeurs, il sera possible de stocker des ´el´ements de tailles diverses (comme
des chaˆınes de caract`eres : voir §4.2.5)
– Il est possible de cr´eer des structures chaˆın´ees (on dit aussi structures autor
´ef´er´ees) qui sont utilis´ees pour d´efinir des listes chain´ees. De telles listes sont
beaucoup utilis´ees en programmation (le nombre d’´el´ements de cette liste
peut ´evoluer dynamiquement, ce qui permet une utilisation plus souple que
celle des tableaux). Cette notion sera abord´ee en d´etail au §4.3.5.
3.1 D´eclaration d’un pointeur
En C, chaque pointeur est limit´e `a un type de donn´ee. En effet, mˆeme si la
valeur d’un pointeur (une adresse) est toujours un entier (ou ´eventuellement
un entier long), le type d’un pointeur d´epend du type de l’objet point´e. Cette
distinction est indispensable `a l’interpr´etation (en fait la taille) de la valeur
point´ee.
On d´eclare un pointeur par l’instruction :
type *nom-du-pointeur ;
o`u type est le type de l’objet point´e. Exemple :
int *pi; // pi est un pointeur vers un int
short int *psi; // psi est un pointeur vers un short int
char *pc; // pc pointeur vers un char
A noter aussi l’existence de pointeurs g´en´eriques, c’est `a dire capable de pointer
vers n’importe quel type d’objet. On utilise pour cela le type void *.
Sans un tel type, il ne serait pas possible par exemple d’indiquer le type d’objet
rendu par les fonctions d’allocation de m´emoire qui rendent un pointeur vers
l’objet allou´e, puisque ce type varie d’une invocation `a l’autre de la fonction.
Par exemple, la fonction malloc de la biblioth`eque standard est d´efinie de la
mani`ere suivante : void *malloc(size_t size);
3.2 Op´erateurs de manipulation des pointeurs
Lors du travail avec des pointeurs, nous avons besoin :
– d’un op´erateur ’adresse de’ & pour obtenir l’adresse d’une variable.
– d’un op´erateur ’contenu de’ * pour acc´eder au contenu d’une adresse.
3.2.1 L’op´erateur ’adresse de’ &
L’op´erateur & permet d’acc´eder `a l’adresse d’une variable. La syntaxe est la
suivante :
34
&nom-variable
Cette adresse peut alors ˆetre utilis´ee pour initialiser la valeur d’un pointeur.
Dans l’exemple suivant, on d´efinit un pointeur p qui pointe vers un entier i :
int * p; //´etape (1): pointeur vers un entier non initialis´e
int i = 14; //´etape (2): variable enti`ere initialis´ee a 14
p=&i; //´etape (3): p pointe vers i
Les diff´erentes ´etapes de ce sc´enario sont illustr´ees dans la figure 3.2.
Ox352C 14 i Ox352C 14 i
Ox1A30 p Ox1A30 p Ox1A30 p
(déclaration d’un pointeur
p vers un entier)
Ox352C
(déclaration et initialisation
d’une variable entière i)
int * p; p = &i;
(1) (3)
int i = 14;
(2)
(affectation de
l’adresse de i à p)
Fig. 3.2 – Illustration m´emoire d’une affectation par l’op´erateur &
L’op´erateur & peut seulement ˆetre appliqu´e `a des objets qui se trouvent dans
la m´emoire interne, c’est `a dire `a des variables et des tableaux. Il ne peut pas
ˆetre appliqu´e `a des constantes ou des expressions.
3.2.2 L’op´erateur ’contenu de’ : *
L’op´erateur unaire d’indirection * permet d’acc´eder directement `a la valeur de
l’objet point´e (on dit qu’on d´er´ef´erence un pointeur). La syntaxe est la suivante :
*nom-pointeur
Ainsi, si p est un pointeur vers un entier i, *p d´esigne la valeur de i. Exemple :
int main() {
int i = 14;
int *p;
p = &i; //p contient l’adresse de i (0x352C)
printf("*p = %d ",*p); //affiche "*p = 14"
}
Pour r´esumer, apr`es les instructions pr´ec´edentes :
– i d´esigne le contenu de i (soit 14)
– &i d´esigne l’adresse de i (soit 0x352C)
– p d´esigne l’adresse de i (soit 0x352C)
– *p d´esigne le contenu de i (soit 14)
En outre :
35
– &p d´esigne l’adresse de p (soit 0x1A30)
– *i est en g´en´eral ill´egal (puisque i n’est pas un pointeur mais il peut arriver
que la valeur *i ait un sens)
Petites devinettes (Merci Bernard Cassagne [Cas98]) : Soient i et j deux pointeurs
vers des entiers,
1. A quoi correspond l’expression *i**j ?
2. Et *i/*j ?
3.3 Initialisation d’un pointeur
Par d´efaut, lorsque l’on d´eclare un pointeur p sur un objet de type T, on ne
sait pas sur quoi il pointe. En effet, la case m´emoire qu’il occupe contient une
certaine valeur qui risque de le faire pointer vers une zone hasardeuse de la
m´emoire.
Comme toute variable, un pointeur doit ˆetre initialis´e !
Cette initialisation peut s’effectuer de trois fa¸cons :
1. affectation `a l’adresse d’une autre variable de p. Si la variable est un pointeur,
on peut faire l’affectation directement, sinon on doit utiliser l’op´erateur
&. Exemple :
int *p1, *p2;//d´eclaration de 2 pointeurs vers des entiers
int i = 14; //supposons que i se trouve `a l’adresse 0x352C
p1 = &i; //affectation `a l’adresse de i de p1 , soit 0x352C
p2 = p1; //affectation de p2 `a p1:
//p2 contient aussi l’adresse de i
2. affectation de p `a la valeur NULL : on peut dire qu’un pointeur ne pointe
sur rien en lui affectant la valeur NULL (cette valeur est d´efinie dans le
fichier stddef.h). Exemple :
int * p = NULL;
3. affectation directe de *p (la zone m´emoire point´ee par p). Pour cela, il faut
d’abord r´eserver `a *p un espace-m´emoire de taille ad´equate (celui du type
point´e par p, soit sizeof(T) octets). L’adresse de cet espace-m´emoire sera
la valeur de p. Cette op´eration consistant `a r´eserver un espace-m´emoire
pour stocker l’objet point´e s’appelle une allocation dynamique et sera
abord´ee plus en d´etail au §3.5.
Pour bien montrer l’int´erˆet de l’initialisation de tout pointeur, reprenons l’exemple
pr´ec´edent dans lequel on remplace l’affectation p2 = p1 par *p2 = *p1 :
int * p1, *p2;
int i = 14;
p1 = &i;
*p2 = *p1;
Que va t il se passer ?
36
– p1 est bien initialis´e et pointe sur i (*p1=14)
– mais p2 n’a pas ´et´e initialis´e : *p2 d´esigne une adresse m´emoire a priori inconnue.
L’instruction *p2 = *p1 force l’´ecriture de la valeur *p1=14 dans
la case m´emoire point´ee par p2 ce qui pourrait avoir des cons´equences d´esastreuses
! Cette ecriture est en g´en´eral sanctionn´e par le message d’erreur
”Segmentation fault” `a l’ex´ecution.
Il faut quand mˆeme noter que certain compilateurs ont la bonne id´ee d’initialiser
automatiquement `a la valeur NULL un pointeur non affect´e. Mais cela ne doit
pas empˆecher de prendre la bonne habitude d’initialiser tout pointeur.
3.4 Arithm´etique des pointeurs
La valeur d’un pointeur ´etant un entier, on peut lui appliquer un certain nombre
d’op´erateurs arithm´etiques classiques. Les seules op´erations arithm´etiques valides
sur les pointeurs sont :
– l’addition d’un entier `a un pointeur −→ p + i
Le r´esultat est un pointeur de mˆeme type que le pointeur de d´epart.
– la soustraction d’un entier `a un pointeur −→ p – i
Le r´esultat est un pointeur de mˆeme type que le pointeur de d´epart.
– la diff´erence de deux pointeurs −→ p1 – p2
Il faut absolument que les deux pointeurs pointent vers des objets de mˆeme
type T. Le r´esultat est un entier dont la valeur est ´egale `a (p1 - p2)/sizeof(T).
Attention : la somme de deux pointeurs n’est pas autoris´ee !
Ainsi, soit i un entier et p un pointeur sur un objet de type T (donc d´eclar´e
par l’instruction : T *p ;). Alors p+i (respectivement p-i) d´esigne un poin-
teur sur un objet de type T. Sa valeur est ´egale `a celle de p incr´ement´ee
(respectivement d´ecr´ement´ee) de i*sizeof(T). Exemple :
#include <stdio.h>
int main() {
int i = 2;
int *p1, *p2;
p1 = &i;
p2 = p1 + 1;
printf("Adresse p1 = Ox%lx Adresse p2 = 0x%lx ",
(unsigned long)p1,
(unsigned long)p2);
printf("Diff´erence des adresses: %lu sizeof(int)=%u ",
(unsigned long)p2-(unsigned long)p1,
sizeof(int));
printf("MAIS p2-p1 = %i ! ",p2-p1);
return 0;
}
Ce programme renverra :
Adresse p1 = Oxbffffb24 Adresse p2 = 0xbffffb28
Diff´erence des adresses: 4 sizeof(int)=4
MAIS p2-p1 = 1 !
37
On peut faire la mˆeme exp´erience avec le type double (au lieu du type int).
On obtiendrait alors :
Adresse p1 = Oxbffffb20 Adresse p2 = 0xbffffb28
Diff´erence des adresses: 8 sizeof(double)=8
MAIS p2-p1 = 1 !
A noter qu’on peut ´egalement utiliser les op´erateurs ++ et -- avec des pointeurs.
En g´en´eral, on les utilise pour r´ealiser des parcours de tableaux et plus
particuli`erement dans les chaˆınes de caract`eres. Exemple (comme on le verra
au §4.2.1, toute chaˆıne se termine par un caract`ere null, le caract`ere ’’) :
#include <stdio.h>
int main() {
char * mess = "On est super content!";
char *p;
for (p = &mess[0]; *p != ’’; p++) {
printf("Adresse: %ld | Contenu: %c ",(long)p,*p);
}
// Autre m´ethode classique, avec while
p = mess; // ´equivalent de p = &mess[0] dans ce cas
puts("========================================");
while (*p != ’’) {
printf("Adresse: %ld | Contenu: %c ",(long)p,*p);
p++;
}
return 0;
}
Les chaˆınes de caract`eres seront plus amplement d´etaill´ees au §4.2.1 page 43.
A noter que les op´erateurs de comparaison sont ´egalement applicables aux pointeurs
1.
3.5 Allocation dynamique de m´emoire
Nous avons vu que l’utilisation de pointeurs permet de m´emoriser ´economiquement
des donn´ees de diff´erentes grandeurs (puisqu’on se contente de m´emoriser
l’adresse de ces donn´ees). Pour permettre une utilisation efficace de la m´emoire,
il est primordial de disposer de moyens pour r´eserver et lib´erer de la m´emoire
dynamiquement au fur et `a mesure de l’ex´ecution. C’est ce qu’on appelle un
allocation dynamique de la m´emoire. Les fonctions pour la gestion dynamique
de la m´emoire sont d´eclar´ees dans le fichier stdlib.h (`a inclure).
L’op´eration consistant `a r´eserver un espace-m´emoire est r´ealis´e par la fonction
malloc. Sa syntaxe est :
malloc(nb octets)
Cette fonction retourne un pointeur de type char * pointant vers une zone
m´emoire de nb octets octets. Pour initialiser des pointeurs vers des objets qui
1`a condition bien sˆur de comparer des pointeurs qui pointent vers des objets de mˆeme type.
38
ne sont pas de type char, il faut convertir le type de la sortie de la fonction
malloc `a l’aide d’un cast (d´ej`a ´etudi´e au §2.4).
A noter enfin qu’en pratique, on utilise la fonction sizeof() pour d´eterminer
la valeur nb octets. Ainsi, pour initialiser un pointeur vers un entier, on ´ecrit :
#include <stdlib.h>
int main() {
int *p;
p = (int*)malloc(sizeof(int)); // allocation dynamique
*p = 14;
}
Il est primordial de bien comprendre qu’avant l’allocation dynamique (et
plus g´en´eralement avant toute initialisation de p), *p n’a aucun sens !
En particulier, toute manipulation de la variable *p g´en´ererait en g´en´eral une
violation m´emoire, d´etectable `a l’ex´ecution par le message d’erreur Segmentation
fault.
La fonction malloc permet ´egalement d’allouer un espace pour plusieurs objets
contigus en m´emoire. On peut ´ecrire par exemple
#include <stdlib.h>
#include <stdio.h>
int main() {
int *p;
p = (int*)malloc(2 * sizeof(int)); //allocation pour 2 int
*p = 14;
*(p + 1) = 10;
printf("p = Ox%lx *p = %i p+1 = 0x%lx *(p+1)=%i ",
(unsigned long)p, *p, (unsigned long)(p+1), *(p+1));
return 0;
}
L’appel `a la fonction malloc a ainsi permis de r´eserver 8 octets en m´emoire
(qui permettent de stocker 2 objets de type int) et d’affecter `a p l’adresse de
cette zone m´emoire. Le programme affiche :
p = Ox8049718 *p = 14 p+1 = 0x804971c *(p+1)=10.
La fonction calloc a le mˆeme rˆole que la fonction malloc mais elle permet
de r´eserver nb-objets objets de nb octets octets et de les initialiser `a z´ero. Sa
syntaxe est :
calloc(nb-objets,nb octets)
Ainsi, si p est de type int*, l’instruction
p = (int*)calloc(N,sizeof(int));
est s´emantiquement ´equivalente `a
p = (int*)malloc(N * sizeof(int));
for (i = 0; i < N; i++)
*(p + i) = 0;
L’emploi de calloc est simplement plus pratique et plus rapide.
39
3.6 Lib´eration dynamique avec la fonction free
Lorsque l’on n’a plus besoin de l’espace-m´emoire allou´e dynamiquement par
malloc ou calloc (c’est-`a-dire quand on n’utilise plus le pointeur p initialis´e
par ces fontions), il faut lib´erer ce bloc de m´emoire. Ceci se fait `a l’aide de
l’instruction free qui a pour syntaxe :
free(nom-du-pointeur);
Cette instruction lib`ere le bloc de m´emoire d´esign´e par nom-du-pointeur mais
n’a pas d’effet si le pointeur a la valeur NULL.
A toute instruction de type malloc ou calloc doit ˆetre associ´ee une instruction
de type free.
Attention :
– La fonction free peut aboutir `a un d´esastre si on essaie de lib´erer de la
m´emoire qui n’a pas ´et´e allou´ee par malloc ou calloc.
– free ne change pas le contenu du pointeur.
– Si la m´emoire n’est pas lib´er´ee `a l’aide free, alors elle l’est automatiquement
`a la fin du programme. Cependant, cela ne doit pas dispenser de l’utilisation
de cette fonction. L’exemple des listes chaˆın´ees (d´etaill´e au §4.3.5) est tout
`a fait adapt´e : si la m´emoire n’est pas lib´er´ee `a chaque suppression d’un
´el´ement de la liste, on aboutira rapidement `a une violation de m´emoire et au
traditionnel message d’erreur ”Segmentation fault”.
40
Chapitre 4
Les types d´eriv´es
A partir des types pr´ed´efinis en C (caract`eres, entiers, flottants), on peut cr´eer
de nouveaux types, appel´es types d´eriv´es, qui permettent de repr´esenter des
ensembles de donn´ees organis´ees.
4.1 Les ´enum´erations
Les ´enum´erations permettent de d´efinir un type pour des variables qui ne sont
affect´ees qu’a un nombre fini de valeurs.
Un objet de type ´enum´eration est d´efini par le mot-clef enum et un identificateur
de mod`ele, suivi de la liste des valeurs que peut prendre cet objet :
enum modele {constante1, constante2,. . . ,constanten} ;
En r´ealit´e, les objets de type enum sont repr´esent´es comme des int. Les valeurs
possibles constante1, constante2,. . . ,constanten sont cod´ees par des entiers de
0 `a n-1.
Dans l’exemple suivant, le type enum boolean associe l’entier 0 `a la valeur
FALSE et l’entier 1 `a la valeur TRUE.
#include <stdio.h>
enum boolean {FALSE, TRUE}; //d´efinition de l’enum´eration boolean
int main () {
enum boolean b1 = TRUE; //declaration
printf("b = %d ",b1);
return 0;
}
On peut modifier le codage par d´efaut des valeurs de la liste lors de la d´eclaration
du type ´enum´er´e, par exemple :
enum boolean {FALSE = 12, TRUE = 23};
4.2 Les tableaux
Un tableau est un ensemble fini d’´el´ements de mˆeme type, stock´es en m´emoire
`a des adresses contigu¨es.
41
La d´eclaration d’un tableau `a une dimension se fait de la fa¸con suivante :
type nom-du-tableau[nombre-´el´ements];
o`u nombre-´el´ements est une expression constante enti`ere positive correspondant
au nombre maximal d’´el´ement dans le tableau. On l’appelle aussi la dimension
(ou taille) du tableau. Chaque ´el´ement du tableau est une composante de celuici.
Par exemple, la d´eclaration int tab[10]; indique que tab est un tableau de
10 ´el´ements de type int. En faisant le rapprochement avec les math´ematiques,
on dit encore que ”t est un vecteur de dimension 10”. Cette d´eclaration alloue
donc en m´emoire pour l’objet tab un espace de 10*4 octets cons´ecutifs.
Pour plus de clart´e, il est recommand´e de donner un nom `a la constante nombre-
´el´ements par une directive au pr´eprocesseur (voir chapitre 7), par exemple :
#define TAILLE 20 //noter l’absence de ";"
int t[TAILLE]; //t tableau de TAILLE int
Voici les points importants `a retenir :
1. On acc`ede `a un ´el´ement du tableau en lui appliquant l’op´erateur [].
2. les index des el´ements d’un tableau vont de 0 `a nombre-´el´ements-1
3. la taille d’un tableau DOIT ˆetre connue statiquement par le compilateur.
Impossible donc d’´ecrire int t\n ou n est une variable.
Ainsi, le programme suivant imprime les ´el´ements du tableau tab :
#define N 10
int main() {
int tab[N];
int i;
...
for (i = 0; i < N; i++)
printf("tab[%d] = %d ",i,tab[i]);
...
}
Un tableau correspond en fait `a un pointeur vers le premier ´el´ement du tableau.
Ce pointeur est constant, ce qui implique en particulier qu’aucune op´eration globale
n’est autoris´ee sur un tableau. Notamment, un tableau ne peut pas figurer
`a gauche d’un op´erateur d’affectation : on ne peut pas ´ecrire ”tab1 = tab2;”.
Il faut effectuer l’affectation pour chacun des ´el´ements du tableau.
4.2.1 Initialisation d’un tableau
On peut initialiser un tableau lors de sa d´eclaration par une liste de constantes
de la fa¸con suivante :
type nom-du-tableau[N] = {constante1,constante2,. . . , constanteN} ;
Par exemple :
#define N 5
int t[N] = {14, 27, 3, 18, 81};
int main() {
int i;
42
for (i = 0; i < N; i++)
printf("t[%d] = %d ",i,t[i]);
...
}
La figure 4.1 montre ce qu’il se passe en pratique au niveau de la m´emoire
lors de l’initialisation du tableau t. En supposant qu’une variable du type int
occupe 4 octets (c’est `a dire : sizeof(int)=4), alors le tableau t r´eservera
N ∗ 4 = 5 ∗ 4 = 20 octets en m´emoire.
t[4]
t[3]
t[2]
t[1]
t[0]
Adresse
adresses croissantes
t
t[i]=*(t+i)
32 bits
int t[5]={14,27,3,18,81};
(pointeur vers le premier
élément du tableau)
81
18
3
27
14
0x5E04
0x5E00
0x5E08
0x5E0C
0x5E10
0x5E14
0x5E18
0x5E1C
0x5E20
Fig. 4.1 – Expression d’un tableau en m´emoire physique
On peut donner moins de valeurs d’initialisations que le tableau ne comporte
d’´el´ements. Dans ce cas, les premiers ´el´ements seront initialis´es avec les valeurs
indiqu´ees, tandis que les autres seront initalis´ees `a z´ero. Exemple :
#define N 10
int t[N] = {14,27};
Les ´el´ements d’indice 0 et 1 seront initialis´es respectivement avec les valeurs 14
et 27, les autres ´el´ements (d’indices 2 `a 9) seront initialis´es `a z´ero.
R´eservation automatique
Si la dimension n’est pas indiqu´ee explicitement lors de l’initialisation, alors le
compilateur r´eserve automatiquement le nombre d’octets n´ecessaires. Exemple :
int A[] = {1, 2, 3, 4, 5}; //r´eservation de 5*sizeof(int) octets
float B[] = {-1.05, 3.3, 87e-5, -1.3E4}; //r´eservation de
//4*sizeof(float) octets
Cas particulier des tableaux de caract`eres
La repr´esentation interne d’une chaˆıne de caract`eres est termin´ee par le symbole
nul ’’.
Ainsi, pour un texte de n caract`eres, il faut pr´evoir n + 1 octets.
43
– un tableau de caract`eres peut ˆetre initialis´e comme une liste de constantes
caract`eres. Exemple : char ch[3] = {’a’, ’b’, ’c’};
Cela devient ´evidemment tr`es vite lourd.
– On peut aussi initialiser un tableau de caract`ere par une chaˆıne litt´erale :
char ch[8]="exemple"; La figure suivante montre le contenu de la m´emoire
`a la suite de cette initialisation.
’e’ ’x’ ’e’ ’m’ ’p’ ’l’ ’e’ ’’
Adresses: 1E04 1E06 1E07 1E08 1E09 1E0A 1E0B
ch
1E05
– on peut d´eclarer une taille sup´erieure `a celle de la chaˆıne litt´erale :
char ch[100]="exemple";
– enfin, on peut ne pas indiquer la taille et le compilateur comptera le nombre
de caract`eres de la chaˆıne litt´erale pour dimensionner correctement le tableau
(sans oublier le caract`ere null ’’). Exemple :
char ch[]="ch aura 22 caract`eres";
– Il est ´egalement possible de donner une taille ´egale au nombre de caract`eres
de la chaˆıne. Dans le cas, le compilateur comprend qu’il ne faut pas rajouter
le null en fin de chaˆıne. Exemple : char ville[10]="luxembourg";
Mais dans ce cas, attention aux surprises ! Consid´erer l’exemple suivant :
#include <stdio.h>
int main() {
char t1[10]="luxembourg"; //sans ’’
char t2[]="luxembourg"; //avec ’’
printf("t1 (de taille %i)=%s ",sizeof(t1)/sizeof(char),t1);
printf("t2 (de taille %i)=%s ",sizeof(t2)/sizeof(char),t2);
return 0;
}
Ce programme renverra :
t1 (de taille 10)=luxembourgߨuP°A@˘ad@ ^ußXA^u߯-@
t2 (de taille 11)=luxembourg
En effet, pour t1, toute la zone m´emoire est affich´ee jusqu’`a ce que le carat`ere
nul soit rencontr´e.
4.2.2 Tableaux multidimensionnels
En C, un tableau multidimensionnel est consid´er´e comme un tableau dont les
´el´ements sont eux-mˆeme des tableaux. Un tableau `a deux dimensions se d´eclare
donc de la mani`ere suivante :
int mat[10][20];
En faisant le rapprochement avec les math´ematiques, on peut dire que mat est
une matrice de 10 lignes et de 20 colonnes. Les mˆemes consid´erations que celles
que nous avons d´evelopp´e sur les tableaux unidimensionnels s’appliquent :
1. A la d´eclaration, le compilateur allouera une zone m´emoire permettant de
stocker de mani`ere contig¨ue 10 tableaux de 20 entiers, soit 200 entiers ;
44
2. toute r´ef´erence ult´erieure `a mat sera convertie en l’adresse de sa premi`ere
ligne, avec le type pointeur vers un tableau de 20 int.
On acc`ede `a un ´el´ement du tableau par l’expression mat[i][j].
Pour initialiser un tableau `a plusieurs dimensions `a la compilation, on utilise
une liste dont chaque ´el´ement est une liste de constantes :
#include <stdio.h>
#define L 3 //nombre de lignes
#define C 2 //nombre de colonnes
short tab[L][C] = {{1, 2}, {14, 15}, {100, 200}};
int main() {
int i, j;
for (i = 0 ; i < L; i++) {
for (j = 0; j < C; j++)
printf("tab[%d][%d]=%d ",i,j,tab[i][j]);
}
return 0;
}
On vient de d´efinir la matrice
tab =


1 2
14 15
100 200


La figure suivante montre le contenu de la m´emoire `a la suite de cette initialisation.
Adresses: 1E04
1 2 14 15 100 200
tab
1E08 1E0C 1E10 1E14 1E18 1E1C 1E20
On comprend bien avec cette figure que si l’on souhaite utiliser la r´eservation
automatique, il faudra quand mˆeme sp´ecifier toutes les dimensions sauf la premi`
ere (le nombre de lignes dans le cas d’un tableau bi-dimentionnel.
short int mat[][3] = {{1, 0, 1},
{0, 1, 0}};
r´eservation de 2*3*2 = 12 octets et
mat =

1 0 1
0 1 0

4.2.3 Passage de tableau en param`etre
Puisqu’un identificateur de type tableau correspond `a l’adresse du premier ´el´ement
du tableau (voir figure 4.1), c’est cette adresse qui est pass´ee en param`etre
formel. Consid´erons par exemple une fonction print_tab qui affiche le contenu
d’un tableau d’entiers :
45
#include <stdio.h>
/* Affiche le contenu du tableau d’entiers tab ayant nb_elem composantes */
void print_tab(int tab[], int nb_elem) {
int i; //compteur
for (i=0; i < nb_elem; i++) printf("tab[%i]=%i ",i,tab[i]);
}
#define TAILLE 4
int main() {
int t[TAILLE] = {1, 2, 3, 4};
print_tab(t, TAILLE);
return 0;
}
Dans le prototype d’une fonction, l’´ecriture int tab[] est strictement ´equivalente
`a int * tab (mais on pr´eferera la premi`ere ´ecriture pour des raisons de
lisibilit´e).
Modification des ´el´ements d’un tableau pass´e en param`etre
Puisque finalement on passe un pointeur en param`etre, on peut modifier au
besoin le contenu du tableau. On peut ´ecrire par exemple :
#include <stdio.h>
void print_tab(int tab[], int nb_elem) {...}
/* incr´emente chaque composantes d tableau */
void incr_tab(int tab[], int nb_elem) {
int i;
for (i=0; i < nb_elem; i++) tab[i]++;
}
#define TAILLE 4
int main() {
int t[TAILLE] = {1, 2, 3, 4};
incr_tab(t, TAILLE);
print_tab(t, TAILLE);
return 0;
}
Interdire la modification des ´el´ements d’un tableau pass´e en para-
m`etre
On utilise pour cela le mot-cl´e const qui sp´ecifie que la variable associ´ee ne
peut pas ˆetre modifi´ee1. Exemple :
const int i = 14; //cette variable ne pourra plus ^etre modifi´ee
On voit tout de suite l’int´erˆet de const pour les param`etres de fonction. Ainsi,
dans la proc´edure print_tab, on peut exprimer le fait que cette proc´edure ne
doit pas modifier le contenu du tableau pass´e en param`etre. Le prototype de la
proc´edure deviendra pour cela :
void print_tab(const int tab[], int nb_elem);
1En pratique, la m´emoire n’est pas prot´eg´ee en ´ecriture et seule l’utilisation directe de la
variable constante empˆeche l’´ecriture. Il est toujours possible d’´ecrire `a l’adresse de la variable
par des moyens d´etourn´es.
46
4.2.4 Relation entre tableaux et pointeurs
Pointeurs et tableaux `a une dimension
On rappelle que tout tableau en C est en fait un pointeur constant.
Ainsi, la d´eclaration int tab[10]; d´efinit un tableau de 10 valeurs enti`eres dont
les indices varient entre les valeurs 0 et 9 et tab est un pointeur constant
(non modifiable) dont la valeur est l’adresse du premier ´el´ement du tableau.
Autrement dit, tab a pour valeur &tab[0]. On peut donc utiliser un pointeur
initialis´e `a tab pour parcourir les ´el´ements du tableau, comme dans l’exemple
suivant :
#define N 5
int tab[5] = {1, 2, 6, 0, 7};
int main() {
int i;
int *p;
p = tab;
for (i = 0; i < N; i++) {
printf(" %d ",*p);
p++;
}
return 0;
}
Comme on acc`ede `a l’´el´ement d’indice i du tableau tab grˆace `a l’op´erateur
d’indexation [] par l’expression tab[i], on peut d´eduire la relation entre cet
op´erateur d’indexation et l’op´erateur * :
tab[i] = *(tab + i)
Pour r´esumer, on a les ´equivalences suivantes :
tab + 1 ⇐⇒ &(tab[1]) (adresse du 2i`eme ´el´ement)
*(tab + 1) ⇐⇒ tab[1] (valeur du 2i`eme ´el´ement)
*tab ⇐⇒ tab[0] (valeur du 1er ´el´ement)
*(tab + k) ⇐⇒ tab[k] (valeur du (k+1)`eme ´el´ement)
tab + k ⇐⇒ &(tab[k]) (adresse du (k+1)`eme ´el´ement)
Pointeurs et tableaux se manipulent donc exactement de mˆeme mani`ere. Attention
cependant puisqu’aucun contrˆole n’est effectu´e pour s’assurer que l’´el´ement
du tableau adress´e existe effectivement. En outre, la manipulation de tableaux
poss`ede certains inconv´enients par rapport `a la manipulation des pointeurs dˆus
au fait qu’un tableau est un pointeur constant :
– On ne peut pas cr´eer de tableaux dont la taille est une variable du programme
2 ;
– on ne peut pas cr´eer de tableaux bidimensionnels dont les lignes n’ont pas
toutes le mˆeme nombre d’´el´ements.
Ces op´erations deviennent possibles d`es que l’on manipule des pointeurs allou´es
dynamiquement. Ainsi, pour cr´eer un tableau d’entiers `a n ´el´ements o`u n est
une variable du programme, on ´ecrit :
#include <stdlib.h>
int main() {
2En fait, on peut le faire depuis la norme C99 mais on ignorera cette possibilit´e ici.
47
int n;
int *tab;
...
tab = (int*)malloc(n * sizeof(int));
...
free(tab);
}
Si on veut en plus que tous les ´el´ements du tableau tab soient initialis´es `a 0, on
remplace l’allocation dynamique avec malloc par :
tab = (int*)calloc(n, sizeof(int));
On pourra aussi utiliser la fonction memset.
Les ´el´ements de tab sont manipul´es avec l’op´erateur d’indexation [], exactement
comme pour les tableaux.
Pour conclure, on retiendra que les deux principales diff´erence entre un tableau
et un pointeur sont les suivantes :
– un pointeur doit toujours ˆetre initialis´e, soit par une allocation dynamique,
soit par affectation `a une expression de type adresse, par exemple p = &i ;
– un tableau est un pointeur constant ne peut donc pas figurer `a gauche d’un
op´erateur d’affectation. En particulier, un tableau ne pas ˆetre utilis´e directement
dans une expression arithm´etique (on ne peut pas ´ecrire tab++ ; par
exemple).
Pointeurs et tableaux `a plusieurs dimensions
Un tableau `a deux dimensions peut ˆetre vu de deux fa¸cons :
– soit comme un pointeur sur une zone m´emoire de taille le produit des deux
dimensions, comme c’est le cas dans l’´ecriture :
int tab[L][C];
Avec cette ´ecriture, tab a une valeur constante ´egale `a l’adresse du premier
´el´ement du tableau, c’est `a dire &tab[0][0].
– soit comme un tableau de pointeurs. Chaque ´el´ement du tableau est alors luim
ˆeme un pointeur sur un tableau. On d´eclare un pointeur qui pointe sur un
objet de type type * (deux dimensions) de la mˆeme mani`ere qu’un pointeur,
c’est-`a-dire par la d´eclaration :
type **nom-du-pointeur;
De mˆeme un pointeur qui pointe sur un objet de type type ** (´equivalent `a
un tableau `a 3 dimensions) se d´eclare par
type ***nom-du-pointeur;
L’exemple suivant illustre la d´eclaration d’un tableau `a deux dimensions (une
matrice de k lignes et n colonnes `a coefficients entiers) sous forme d’un tableau
de pointeurs :
int main() {
int k=14, n=5;
int **tab; // pointeur vers la matrice
tab = (int**)malloc(k * sizeof(int*));
for (i = 0; i < k; i++)
tab[i] = (int*)malloc(n * sizeof(int));
48
....
for (i = 0; i < k; i++)
free(tab[i]);
free(tab);
return 0;
}
La premi`ere allocation dynamique r´eserve pour l’objet point´e par tab l’espacem
´emoire correspondant `a k pointeurs sur des entiers. Ces k pointeurs correspondent
aux lignes de la matrice. Les allocations dynamiques suivantes r´eservent
pour chaque pointeur tab[i] l’espace-m´emoire n´ecessaire pour stocker n entiers.
Si on d´esire en plus que tous les ´el´ements du tableau soient initialis´es `a 0, il
suffit d’utiliser la fonction calloc au lieu de malloc (voir §3.5 page 38)
L’un des avantages des pointeurs de pointeurs sur les tableaux multi-dimensionn´es
est que cela permet de choisir par exemple des tailles diff´erentes pour chacune
des lignes tab[i].
4.2.5 Cas des tableaux de chaˆınes de caract`eres
On peut initialiser un tableau de ce type par :
char * jour[] = {"lundi", "mardi", "mercredi", "jeudi",
"vendredi", "samedi", "dimanche"};
L’allure du tableau jour est illustr´e dans la figure 4.2.
’l’ ’u’ ’n’ ’d’ ’i’ ’’
’m’ ’a’ ’r’ ’d’ ’i’ ’’
’m’ ’e’ ’r’ ’c’ ’r’ ’e’ ’d’ ’i’ ’’
’j’ ’e’ ’u’ ’d’ ’i’ ’’
’v’ ’e’ ’n’ ’d’ ’r’ ’e’ ’d’ ’i’ ’’
’s’ ’a’ ’m’ ’e’ ’d’ ’i’ ’’
’d’ ’i’ ’m’ ’a’ ’n’ ’c’ ’h’ ’e’ ’’
jour
Fig. 4.2 – Illustration de la d´eclaration d’un tableau de chaˆınes de caract`eres
A noter que cette ´ecriture directe n’est possible qu’avec les char. Il est impossible
d’´ecrire par exemple :
int * tab[] = {{1}, {1,2}, {1,2,3}};
(on a ici un tableau de pointeurs vers des tableaux d’entiers de taille diff´erentes).
Une boucle d’impression des valeurs du tableau jour pourra s’´ecrire :
#define NB_JOUR 7
int i;
for (i=0 ; i < NB_JOUR; i++) printf("%s ",jour[i]);
49
4.2.6 Gestion des arguments de la ligne de commande
Les tableaux de pointeurs vers des chaˆınes de caract`eres sont une structure de
donn´ee tr`es importante puisqu’elle est utilis´ee dans la transmission de param`
etres lors de l’ex´ecutution d’un programme.
Lorsqu’un utilisateur lance l’ex´ecution du programme prog avec les param`etres
param_1, param_2, . . . , param_n, l’interpr´eteur collecte tous ces mots sous forme
de chaˆıne de caract`eres, cr´ee un tableau de pointeurs vers ces chaˆınes et lance
la proc´edure main en lui passant deux param`etres :
– un entier contenant la taille du tableau (appel´e classiquement argc) ;
– le tableau des pointeurs vers les chaˆınes (traditionnellement argv).
Pour que le programmeur puisse exploiter ces ´el´ements, la fonction main doit
ˆetre d´efinie de la fa¸con suivante :
int main(int argc, char * argv[]) {...}
Voici un exemple d’utilisation de ces param`etres :
#include <stdio.h>
int main(int argc, char * argv[]) {
int i;
printf("Nom du programme: %s ", argv[0]);
for (i=1 ; i < argc ; i++)
printf("Param`etre %i: %s ",i,argv[i]);
return 0;
}
En compilant ce programme par la commande gcc -Wall -g3 toto.c, l’appel
./a.out nif naf nouf
renvoit :
Nom du programme: ./a.out
Param`etre 1: nif
Param`etre 2: naf
Param`etre 3: nouf
La librairie getopt.h permet ´egalement de g´erer plus simplement les arguments
de la ligne de commande. Supposons par exemple vouloir compiler un programme
prog au format d’appel suivant (les ´el´ements entre crochets sont optionnels)
: prog [-i val_i] [-f val_f] [-s string] [-k key] [-h] file
sachant que :
1. l’option -i permet de sp´ecifier la valeur de la variable val_i, de type int
(14 par d´efaut) ;
2. l’option -f permet de sp´ecifier la valeur de la variable val_f, de type
float (1.5 par d´efaut).
3. l’option -s permet de sp´ecifier la valeur de la variable string, une chaˆıne
de caract`eres poss´edant au plus 256 caract`eres (”On est super content !”
par d´efaut).
4. l’option -k permet de sp´ecifier au format hexad´ecimal la valeur de la
variable key, de type unsigned long (0x90ABCDEF par d´efaut).
50
5. l’option -h affiche l’aide (auteur, but du programme et usage) et sort du
programme.
6. file est un param`etre obligatoire du programme qui sp´ecifie la valeur de
la chaˆıne de caract`eres input_file.
Pour g´erer ces options, on peut soit adapter l’exemple de code pr´ec´edent et
traiter individuellement les ´el´ements du tableau argv ainsi que les cas d’erreurs,
soit utiliser la librairie <getopt.h>3 qui permet de g´erer facilement les options
de la ligne de commande.
Cet exemple est trait´e avec <getopt.h> en annexe A.1 page 106.
4.3 Les structures
Une structure est une suite finie d’objets de types diff´erents. Ce m´ecanisme permet
de grouper un certain nombre de variables de types diff´erents au sein d’une
mˆeme entit´e. Contrairement aux tableaux, les diff´erents ´el´ements d’une structure
n’occupent pas n´ecessairement des zones contigu¨es en m´emoire. Chaque
´el´ement de la structure, appel´e membre ou champ, est d´esign´e par un identificateur.
L’utilisation pratique d’une structure se d´eroule de la fa¸con suivante :
1. On commence par d´eclarer la structure elle-mˆeme. Le mod`ele g´en´eral de
cette d´eclaration est le suivant :
struct nom structure {
type_1 membre1 ;
type_2 membre2 ;
...
type_n membren ;
} ;
2. Pour d´eclarer un objet de type structure correspondant au mod`ele pr´ec´edent,
on utilise la syntaxe :
struct nom structure identificateur-objet ;
Ou bien, si le mod`ele n’a pas encore ´et´e d´eclar´e au pr´ealable :
struct nom structure {
type_1 membre1 ;
type_2 membre2 ;
...
type_n membren ;
} identificateur-objet ;
3. On acc`ede aux diff´erents membres d’une structure grˆace `a l’op´erateur
membre de structure, not´e ”.”. Le i-`eme membre de objet est d´esign´e par
l’expression :
identificateur-objet.membrei
On peut effectuer sur le i-`eme membre de la structure toutes les op´erations
valides sur des donn´ees de type type_i.
3informations : man 3 getopt ou plus simplement le tutorial disponible `a l’adresse :
http://www.stillhq.com/extracted/howto-getopt/output.ps
51
Ainsi, le programme suivant d´efinit la structure complexe, compos´ee de deux
champs de type double et calcule la norme d’un nombre complexe.
#include <math.h>
struct complexe {
double reelle; //partie r´eelle
double imaginaire; //partie imaginaire
}; // ne pas oublier le ";"
int main() {
struct complexe z; //d´eclaration d’un objet de type struct complexe
double norme;
...
norme = sqrt(z.reelle * z.reelle + z.imaginaire * z.imaginaire);
printf("norme de (%f + i %f) = %f ",z.reelle,z.imaginaire,norme);
}
4.3.1 Initialisation et affectation d’une structure
Les r`egles d’initialisation d’une structure lors de sa d´eclaration sont les mˆemes
que pour les tableaux. On ´ecrit par exemple :
struct complexe i = {0. , 1.};
A noter qu’a la diff´erence des tableaux, on peut appliquer l’op´erateur d’affectation
aux structures. Dans le contexte pr´ec´edent, on peut ´ecrire :
int main() {
struct complexe z1, z2;
...
z2 = z1;
}
4.3.2 Comparaison de structures
Aucune comparaison n’est possible sur les structures (en particulier, on ne peut
appliquer les op´erateurs == ou !=).
4.3.3 Tableau de structures
On d´eclare un tableau dont les composantes sont des structures de la mˆeme
fa¸con qu’on d´eclarerait un tableau d’´el´ements de type simple. Exemple :
struct personne {
char nom[20];
char prenom[20];
};
...
struct personne t[100]; //tableau de 100 personnes
Pour r´ef´erencer le nom de la personne qui a l’index i, on ´ecrira :
t[i].nom
52
4.3.4 Pointeur vers une structure
En reprenant la structure personne pr´ec´edentes, on d´eclarera une variable de
type pointeur vers cette structure de la mani`ere suivante :
struct personne *p;
On peut affecter `a ce pointeur des adresses sur des struct personne. Exemple :
struct personne {...};
int main() {
struct personne pers; // pers = variable de type struct personne
struct personne * p; // p = pointeur vers une struct personne
p = &pers; // p pointe maintenant vers pers
}
Pour acc`eder aux membres de la structure point´ee :
– il faudrait ´ecrire normalement (*p).nom4 ;
– mais on utilise en pratique l’´ecriture simplifi´ee p->nom.
4.3.5 Structures auto-r´ef´er´ees
Il s’agit de cas particulier de structures dont un des membres pointe vers
une structure du mˆeme type. Cette repr´esentation permet en particulier de
construire des listes chaˆın´ees.
En effet, il est possible de repr´esenter une liste d’´el´ements de mˆeme type par un
tableau (ou un pointeur). Toutefois, cette repr´esentation, dite contigu¨e, impose
que la taille maximale de la liste soit connue a priori (on a besoin du nombre
d’´el´ements du tableau lors de l’allocation dynamique). Pour r´esoudre ce probl`
eme, on utilise une repr´esentation chaˆın´ee : l’´el´ement de base de la chaˆıne
est une structure appel´ee cellule qui contient la valeur d’un ´el´ement de la liste
et un pointeur sur l’´el´ement suivant. Le dernier ´el´ement pointe sur le pointeur
NULL (d´efini dans stddef.h). La liste est alors d´efinie comme un pointeur sur
le premier ´el´ement de la chaˆıne.
Consid´erons par exemple la structure toto poss´edant 2 champs :
1. un champ data de type int ;
2. un champ next de type pointeur vers une struct toto.
La liste est alors un objet de type pointeur sur une struct toto. Grˆace au
mot-clef typedef, on peut d´efinir le type liste, synonyme du type pointeur
sur une struct toto (voir §4.6). On peut alors d´efinir un objet liste l qu’il
convient d’initialiser `a NULL. Cette repr´esentation est illustr´ee dans la figure 4.3.
Un des avantages de la repr´esentation chaˆın´ee est qu’il est tr`es facile d’ins´erer
un ´el´ement `a un endroit quelconque de la liste. Ainsi, pour ins´erer un ´el´ement
en tˆete de liste, on utilise la fonction suivante :
4et non *p.nom qui sera interpr´et´e (compte tenu des priorit´es entre op´erateurs) par
*(p.nom) ce qui n’a de sens que si le champ nom est un pointeur !
53
14
next
data
struct toto
l
struct toto {
#include <stddef.h>
int data;
struct toto *next;
};
typedef struct toto *liste;
...
liste l = NULL;
//ajout d’éléments
NULL
next
data
struct toto
10
Fig. 4.3 – Representation d’une liste chaˆın´ee
liste insere(int element, liste Q) {
liste L;
L = (liste)malloc(sizeof(struct toto)); // allocation de la zone
// m´emoire nec´essaire
L->data = element;
L->next = Q;
return L;
}
De fa¸con g´en´eral, l’ajout d’un nouvel ´el´ement dans une liste chaˆın´ee s’effectue
en trois ´etapes :
1. recherche de l’endroit o`u la nouvelle cellule devra ˆetre ins´er´ee ;
2. cr´eation de la nouvelle cellule (par malloc) et mise `a jour de ses champs.
Au niveau de l’allocation en m´emoire, on prendra garde :
(a) de r´eserver le bon nombre d’octets (en reprenant l’exemple pr´ec´edent,
sizeof(struct toto) et non sizeof(struct toto *))
(b) de convertir le r´esultat du malloc vers le bon type (pointeur vers la
structure).
3. mise `a jour des champs des cellules voisines.
Supposons par exemple que l’on souhaite ins´erer un ´el´ement en queue de liste :
liste insereInTail(int element, liste Q) {
liste L, tmp=Q;
// creation de la nouvelle cellule
L = (liste)malloc(sizeof(struct toto)); // allocation de la zone
// m´emoire nec´essaire
L->data = element;
L->next = NULL;
if (Q == NULL)
return L;
// maintenant Q est forcement non vide ==> champ tmp->next existe
// recherche de l’endroit ou ins´erer
while (tmp->next != NULL) tmp = tmp->next; // d´eplacement jusqu’au
// dernier ´el´ement de la liste
54
// mise `a jour des cellules voisines (ici, une seule: tmp)
tmp->next = L;
return Q;
}
Pour supprimer le premier ´el´ement de la liste par exemple, il est important de
lib´erer l’espace m´emoire allou´e :
liste supprime_tete(liste L) {
liste suivant = L;
if (L != NULL) { // pour etre s^ur que L->next existe
suivant= L->next;
free(L); //lib´eration de l’espace allou´e pour une cellule
}
return suivant;
}
Il est primordial d’utiliser free dans le cas des listes chaˆın´ees. Sinon, la m´emoire
risque d’ˆetre rapidement satur´ee (par exemple lors d’ajouts et de suppressions
successives qui ne lib`erent pas l’espace allou´e). Ainsi, de fa¸con similaire
`a l’ajout, la suppression d’une cellule dans une liste chaˆın´ee s’effectue en trois
´etapes :
1. Recherche de la cellule `a supprimer ;
2. mise `a jour des champs des cellules voisines ;
3. lib´eration de la cellule avec free.
Un exemple plus complet de l’utilisation des listes chaˆın´ees est founi en annexe
A.2 page 110.
4.4 Les unions
Il est parfois n´ecessaire de manipuler des variables auxquelles on d´esire affecter
des valeurs de types diff´erents. Une union d´esigne un ensemble de variables
de types diff´erents susceptibles d’occuper alternativement une mˆeme zone m´emoire.
Une union permet donc de d´efinir un objet comme pouvant ˆetre d’un
type au choix parmi un ensemble fini de types. Si les membres d’une union
sont de longueurs diff´erentes, la place r´eserv´ee en m´emoire pour la repr´esenter
correspond `a la taille du membre le plus grand.
4.4.1 D´eclaration d’une union
La d´eclaration et la d´efinition d’une union ne diff`erent de celles d’une structure
que par l’utilisation du mot-cl´e union (qui remplace le mot-cl´e struct).
Dans l’exemple suivant, la variable hier de type union jour peut ˆetre soit un
caract`ere, soit un entier (mais pas les deux `a la fois) :
#include <stdio.h>
union jour {
char lettre;
int numero;
55
};
int main() {
union jour hier, demain;
hier.lettre = ’J’; //jeudi
printf("hier = %c ",hier.lettre);
hier.numero = 4;
demain.numero = (hier.numero + 2) % 7;
printf("demain = %d ",demain.numero);
return 0;
}
Si on se r´ef`ere au tableau 1.4 page 12, la zone m´emoire allou´ee pour une variable
de type union jour sera de sizeof(int) (2 ou 4 octets).
On aura compris qu’on acc`ede aux ´el´ements d’une union avec le mˆeme op´erateur
de s´election (. ou ->) que celui utilis´e dans les structures.
4.4.2 Utilisation pratique des unions
Lorsqu’il manipule des unions, le programmeur n’a malheureusement aucun
moyen de savoir `a un instant donn´e quel est le membre de l’union qui poss`ede
une valeur.
Pour ˆetre utilisable, une union doit donc toujours ˆetre associ´ee `a une variable
dont le but sera d’indiquer le membre de l’union qui est valide. En pratique,
une union et son indicateur d’´etat sont g´en´eralement englob´es `a l’int´erieur d’une
structure. Exemple :
#include <stdio.h>
enum etat {CARAC, ENTIER};
/* struct jour a deux membres ’indicateur’ de type int,
et ’day’ de type union de char et de int */
struct jour {
enum etat indicateur; //indique ce qui est dans j
union {
char lettre;
int numero;
} day;
};
/* Affiche le contenu de la variable d, nomm´ee name */
void print_jour(struct jour d, char * name){
if (d.indicateur == CARAC) printf("%s = %c ",name,d.day.lettre);
else if (d.indicateur == ENTIER)
printf("%s = %i ",name,d.day.numero);
else printf("Erreur! ");
}
int main() {
struct jour ex1, ex2; //d´eclaration
//utilisation
ex1.indicateur = CARAC;
ex1.day.lettre = ’j’; //jeudi
print_jour(ex1, "ex1");
ex2.indicateur = ENTIER;
ex2.day.numero = 4;
56
print_jour(ex2, "ex2");
return 0;
}
L’ex´ecution de ce programme renverra :
ex1 = j
ex2 = 4
4.4.3 Une m´ethode pour all´eger l’acc`es aux membres
Quand une union est dans une structure (comme c’est la cas dans l’exemple
pr´ec´edent), on aura noter que l’acc`es aux membres de l’union est relativement
lourd (ex : d.day.numero pour acc´eder au membre numero). On peut all´eger
cette ´ecriture en utilisant les facilit´es du pr´eprocesseur (voir le chapitre 7) :
#define L day.lettre
#define N day.numero
...
ex1.indicateur = CARAC;
ex1.L = ’j’; //initialisation de ex1 `a jeudi
4.5 Les champs de bits
Il est possible en C de sp´ecifier la longueur des champs d’une structure au bit
pr`es si ce champ est de type entier (int ou unsigned int). Cela se fait en
pr´ecisant le nombre de bits du champ avant le ”;” qui suit sa d´eclaration :
type [membre] : nb_bits ;
On utilise typiquement les champs de bits en programmation syst`eme, pour
manipuler des registres particuliers de la machine etc... Par exemple, imaginons
le registre suivant :
ov (inutilisé) privilege masque
Position: 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
Fig. 4.4 – Exemple d’affectation de bits dans un registre
Ce registre peut se d´ecrire de la mani`ere suivante :
struct registre {
unsigned int masque : 3;
signed int privilege : 6;
unsigned int : 6; /* inutilis´e */
unsigned int ov : 1;
};
Le champ masque sera cod´e sur 3 bits, privilege sur 6 bits etc... A noter
que l’ordre dans lequel les champs sont plac´es `a l’int´erieur de ce mot d´epend
de l’impl´ementation. On voit que le C accepte que l’on ne donne pas de nom
57
aux champs de bits qui ne sont pas utilis´es. Le champ ov de la structure ne
peut prendre que les valeurs 0 ou 1. Aussi, si r est un objet de type struct
registre, l’op´eration r.ov += 2 ; ne modifie pas la valeur du champ.
Voici les quelques contraintes `a respecter :
– La taille d’un champ de bits doit ˆetre inf´erieure au nombre de bits d’un entier
(long).
– un champ de bits n’a pas d’adresse ; on ne peut donc pas lui appliquer l’op´erateur
&.
4.6 D´efinition de synonymes de types avec typedef
Pour all´eger l’´ecriture des programmes, on peut affecter un nouvel identificateur
`a un type `a l’aide du mot-cl´e typedef :
typedef type synonyme ;
Exemple :
typedef unsigned char UCHAR;
typedef struct { double x, y } POINT;
typedef POINT *P_POINT; //pointeur vers un POINT
A partir de ce moment, l’identificateur UCHAR peut ˆetre utilis´e comme une abbr
´eviation pour le type unsigned char, l’identificateur POINT peut ˆetre utilis´e
pour sp´ecifier le type de structure associ´e et P_POINT permet de caract´eriser un
pointeur vers un POINT :
UCHAR c1, c2, tab[100];
POINT point;
P_POINT pPoint;
58
Chapitre 5
Retour sur les fonctions
La structuration de programmes en sous-programmes se fait en C `a l’aide de
fonctions. L’utilisation de fonctions a d´ej`a ´et´e vu, par exemple avec celle pr´ed´efinies
dans des biblioth`eques standards (comme printf de <stdio.h>, malloc
de <stdlib.h> etc...) ou encore la fonction d’entr´ee main. Lorsque les listes
chaˆın´ees ont ´et´e abord´ees au §4.3.5, on a ´egalement d´efini des fonctions d’insertion
et de suppression d’´el´ements.
On l’aura compris : comme dans la plupart des langages, on peut (doit ?) en
C d´ecouper un programme en plusieurs fonctions. Il existe une unique fonction
obligatoire : la fonction principale appel´ee main.
5.1 D´eclaration et d´efinition d’une fonction
Comme on l’a vu au §1.4, le format g´en´eral d’une d´efinition de fonction est de
la forme :
type_resultat nom fonction (type1 arg1, . . . , typen argn) {
<d´eclaration de variables locales >
<liste d’instructions >
}
Dans cette d´efinition de fonction, on rappelle que :
– type_resultat correspond au type du r´esultat de la fonction.
– nom fonction est le nom qui identifie la fonction.
– type1 arg1 . . . typen argn d´efinit les types et les noms des param`etres de
la fonction. Ces arguments sont appel´es param`etres formels, par opposition
aux param`etres effectifs qui sont les param`etres avec lesquels la fonction est
effectivement appel´ee (voir §5.2).
On peut se contenter de d´eclarer le prototype d’une fonction (sans le corps) :
type_resultat nom fonction (type1 arg1, . . . , typen argn) ;
Dans ce cas, la d´efinition de la fonction (avec le corps des instructions qui la
59
compose) peut ˆetre d´eclar´ee plus loin dans le programme1.
Contrairement `a la d´efinition de la fonction, le prototype n’est donc pas suivi
du corps de la fonction (contenant les instructions `a ex´ecuter).
ATTENTION! Le prototype est une instruction, il est donc suivi d’un pointvirgule
!
Quelques remarques compl´ementaires :
1. Une fonction peut fournir comme r´esultat :
– un type arithm´etique,
– une structure (voir §4.3),
– une union (voir §4.4),
– un pointeur (voir chapitre 3),
– void (voir ci-apr`es).
Une fonction ne peut pas fournir comme r´esultat des tableaux, des chaˆınes
de caract`eres ou des fonctions, mais il est cependant possible de renvoyer
un pointeur sur le premier ´el´ement d’un tableau ou d’une chaˆıne de caract`
eres.
2. Si une fonction ne fournit pas de r´esultat, il faut indiquer void comme
type du r´esultat.
Ex : void print_liste(liste L){...}
3. Si une fonction n’a pas de param`etres, on peut d´eclarer la liste des param`
etres comme (void) ou simplement comme ().
Ex : int main(){...} // equivalent a ’int main(void){...}’
4. Il est interdit de d´efinir des fonctions `a l’int´erieur d’une autre fonction
(comme en Pascal).
5. En principe, l’ordre des d´efinitions de fonctions dans le texte du programme
ne joue pas de rˆole, mais chaque fonction doit ˆetre d´eclar´ee ou
d´efinie avant d’ˆetre appel´ee.
Dans le cas o`u type_resultat est diff´erent du type void, le corps de la fonction
doit obligatoirement comporter une instruction return, dite instruction
de retour `a la fonction appelante. La syntaxe de cette instruction est :
return expression ;
La valeur de expression correspond `a la valeur que retourne la fonction et doit
donc ˆetre de type type_resultat.
Plusieurs instructions return peuvent apparaˆıtre dans une fonction. Le retour
au programme appelant sera alors provoqu´e par le premier return rencontr´e
lors de l’ex´ecution (voir la fonction puissance de l’exemple qui suit). Ex :
/***Renvoit le produit de 2 entiers ***/
int produit (int a, int b) {
return(a*b);
}
/*** Renvoit la valeur de a^n (version na¨ıve) ***/
int puissance (int a, int n) {
1mais cette d´efinition doit respecter la d´eclaration du prototype : n’h´esiter pas `a utiliser le
copier/coller du prototype !
60
if (n == 0) return 1;
return a*puissance(a, n-1); //noter l’absence du ’else’
}
Enfin, il faut noter que les variables ´eventuellement d´eclar´ees au sein d’une fonction
seront locales `a celle-ci et n’auront de sens que dans le corps de la fonction.
De mˆeme, les identificateurs des arguments de la fonction (arg1,. . . ,argn) n’ont
d’importance qu’`a l’int´erieur de celle-ci.
5.2 Appel d’une fonction
L’appel d’une fonction se fait par l’expression :
nom fonction ( arg1, . . . ,argn )
L’ordre et le type des param`etres effectifs de la fonction doivent concorder avec
ceux donn´es dans la d´efinition de l’en-tˆete de la fonction. Les param`etres effectifs
peuvent ˆetre des expressions2.
De plus, l’ordre d’´evaluation des param`etres effectifs n’est pas assur´e et d´epend
du compilateur. Il est donc d´econseill´e, pour une fonction `a plusieurs param`
etres, de faire figurer des op´erateurs d’incr´ementation ou de d´ecr´ementation
(++ ou --) dans les expressions d´efinissant les param`etres effectifs.
5.3 Dur´ee de vie des identificateurs
Les variables manipul´ees dans un programme C ne sont pas toutes trait´ees de
la mˆeme mani`ere. En particulier, elles n’ont pas toutes la mˆeme dur´ee de vie.
On distingue deux cat´egories de variables.
1. Les variables permanentes (ou statiques) Une variable permanente occupe
un emplacement en m´emoire qui reste le mˆeme durant toute l’ex´ecution
du programme. Cet emplacement est allou´e une fois pour toutes lors de la
compilation. La partie de la m´emoire contenant les variables permanentes
est appel´ee segment de donn´ees. Par d´efaut, les variables permanentes
sont initialis´ees `a z´ero par le compilateur. Elles sont caract´eris´ees par le
mot-clef static.
2. Les variables temporaires se voient allouer un emplacement en m´emoire de
fa¸con dynamique lors de l’ex´ecution du programme. Elles ne sont pas initialis
´ees par d´efaut. Leur emplacement en m´emoire est lib´er´e par exemple
`a la fin de l’ex´ecution d’une fonction secondaire.
Par d´efaut, les variables temporaires sont situ´ees dans la partie de la m´emoire
appel´ee segment de pile. Dans ce cas, la variable est dite automatique. Le sp´ecificateur
de type correspondant, auto, est rarement utilis´e puisqu’il ne s’applique
qu’aux variables temporaires qui sont automatiques par d´efaut.
2La virgule qui s´epare deux param`etres effectifs est un simple signe de ponctuation ; il ne
s’agit pas de l’op´erateur virgule.
61
Une variable temporaire peut ´egalement ˆetre plac´ee dans un registre de la machine.
Un registre est une zone m´emoire sur laquelle sont effectu´ees les op´erations
machine. Il est donc beaucoup plus rapide d’acc´eder `a un registre qu’`a
toute autre partie de la m´emoire. On peut demander au compilateur de ranger
une variable tr`es utilis´ee dans un registre, `a l’aide de l’attribut de type
register.
Le nombre de registres ´etant limit´e, cette requˆete ne sera satisfaite que s’il
reste des registres disponibles. Cette technique permettant d’acc´el´erer les programmes
a aujourd’hui perdu tout son int´erˆet. Grˆace aux performances des optimiseurs
de code int´egr´es au compilateur (cf. options -O de gcc : voir §1.1.4),
il est maintenant plus efficace de compiler un programme avec une option d’optimisation
que de placer certaines variables dans des registres.
La dur´ee de vie des variables est li´ee `a leur port´ee, c’est-`a-dire `a la portion du
programme dans laquelle elles sont d´efinies.
5.4 Port´ee des variables
Selon l’endroit o`u on d´eclare une variable, celle-ci pourra ˆetre accessible (visible)
partout dans le code ou seulement dans une partie de celui-ci (`a l’int´erieur d’une
fonction typiquement), on parle de la port´ee (ou visibilit´e) de la variable.
Lorsqu’une variable est d´eclar´ee dans le code mˆeme, c’est-`a-dire `a l’ext´erieur
de toute fonction ou de tout bloc d’instruction, elle est accessible de partout
dans le code (n’importe quelle fonction du programme peut faire appel `a cette
variable). On parle alors de variable globale.
Lorsque l’on d´eclare une variable `a l’int´erieur d’un bloc d’instructions (entre des
accolades), sa port´ee se limite `a l’int´erieur du bloc dans lequel elle est d´eclar´ee.
On parle de variable locale.
5.4.1 Variables globales
Un variable globale est donc une variable d´eclar´ee en dehors de toute fonction,
au d´ebut du fichier, `a l’ext´erieur de toutes les fonctions et sont disponibles `a
toutes les fonctions du programme. En g´en´eral, les variables globales sont d´eclar
´ees imm´ediatement derri`ere les instructions #include au d´ebut du programme.
Elles sont syst´ematiquement permanentes.
Remarque : Les variables d´eclar´ees au d´ebut de la fonction principale main ne
sont pas des variables globales, mais locales `a main !
Exemple :
La variable STATUS est d´eclar´ee globalement pour pouvoir ˆetre utilis´ee dans les
proc´edures A et B.
#include <stdio.h>
int STATUS;
void A(...){
...
if (STATUS>0)
STATUS--;
62
else
...
...
}
void B(...) {
...
STATUS++;
...
}
Conseils :
– Les variables globales sont `a utiliser avec pr´ecaution, puisqu’elles cr´eent des
liens invisibles entre les fonctions. La modularit´e d’un programme peut en
souffrir et le programmeur risque de perdre la vue d’ensemble.
– Il faut faire attention `a ne pas cacher involontairement des variables globales
par des variables locales du mˆeme nom. (voir section suivante)
– Je vous conseille donc d’´ecrire nos programmes aussi ’localement’
que possible.
5.4.2 Variables locales
Les variables d´eclar´ees dans un bloc d’instructions sont uniquement visibles `a
l’int´erieur de ce bloc. On dit que ce sont des variables locales `a ce bloc.
Exemple : la variable nom est d´efinie localement dans le bloc ext´erieur de la
fonction hello. Ainsi, aucune autre fonction n’a acc`es `a la variable nom :
void hello() {
char nom[20];
printf("Introduisez votre nom : ");
scanf("%s",&nom);
printf("Bonjour %s ! ", nom);
}
Attention ! Une variable d´eclar´ee `a l’int´erieur d’un bloc cache toutes les va-
riables du mˆeme nom des blocs qui l’entourent. Exemple :
int X; //d´eclaration d’une variable globale
int fonction(int A) {
double X; //variable locale qui cache la variable X globale
...
}
Remarque : bien qu’il soit possible en C3 de d´eclarer des variables `a l’int´erieur
d’une boucle ou d’un bloc conditionnel, il est d´econseill´e de le faire et toutes les
d´eclarations locales devront se faire au d´ebut des fonctions.
3depuis la norme C99 en fait
63
5.5 R´ecursivit´e
Les d´efinitions par r´ecurrence sont assez courantes en math´ematiques. Par
exemple, on peut consid´erer la fameuse suite de Fibonacci :


u0 = 0
u1 = 1
un = un−1 + un−2 ∀ n ≥ 2
Le fonctions d´efinies en C ont la propri´et´e de r´ecursivit´e, c’est `a dire la capacit´e
de s’appeler elle-mˆeme.
Tous les d´etails de la r´ecursivit´e ont ´et´e fournis au §2.3 page 24.
5.6 Passage de param`etres `a une fonction
Les param`etres ou arguments sont les ’boˆıtes aux lettres’ d’une fonction. Elles
acceptent les donn´ees de l’ext´erieur et d´eterminent les actions et le r´esultat de
la fonction.
5.6.1 G´en´eralit´es
En ce qui concerne le passage de param`etres `a une proc´edure, le programmeur
a deux besoins fondamentaux :
– soit il d´esire passer une valeur qui sera exploit´ee par l’algorithme de la proc
´edure (c’est ce dont on a besoin quand on ´ecrit par exemple sin(x)). Une
telle fa¸con de passer un param`etre s’appelle du passage par valeur ;
– soit il d´esire passer une r´ef´erence `a une variable, de mani`ere `a permettre
`a la proc´edure de modifier la valeur de cette variable. C’est ce dont on a
besoin quand on ´ecrit une proc´edure r´ealisant le produit de deux matrices
prodmat(a,b,c) o`u l’on veut qu’en fin d’ex´ecution, la matrice c soit ´egale
au produit matriciel des matrices a et b. prodmat a besoin des valeurs des
matrices a et b, et d’une r´ef´erence vers la matrice c. Une telle fa¸con de passer
un param`etre s’appelle du passage par adresse.
Conversion automatique
Lors d’un appel, le nombre et l’ordre des param`etres doivent n´ecessairement
correspondre aux indications de la d´eclaration de la fonction. Les param`etres
sont automatiquement convertis dans les types de la d´eclaration avant d’ˆetre
pass´es `a la fonction.
Exemple : Le prototype de la fonction pow (biblioth`eque <math.h>) est d´eclar´e
comme suit :
double pow(double x, double y);
Au cours des instructions,
int A, B;
...
A = pow (B, 2);
64
On assiste `a trois conversions automatiques : avant d’ˆetre transmis `a la fonction,
la valeur de B est convertie en double ; la valeur 2 est convertie en 2.0 . Comme
pow est du type double, le r´esultat de la fonction doit ˆetre converti en int avant
d’ˆetre affect´ee `a A.
5.6.2 Passage des param`etres par valeur
En C, le passage des param`etres se fait toujours par valeur, autrement dit les
fonctions n’obtiennent que les valeurs de leurs param`etres et n’ont pas d’acc`es
aux variables elles-mˆemes.
Les param`etres d’une fonction sont `a consid´erer comme des variables locales
qui sont initialis´ees automatiquement par les valeurs indiqu´ees lors d’un appel.
A l’int´erieur de la fonction, On peut donc changer les valeurs des param`etres
sans influencer les valeurs originales dans les fonctions appelantes.
Exemple :
La fonction ETOILES dessine une ligne de N ´etoiles. Le param`etre N est modifi´e
`a l’int´erieur de la fonction mais pas `a l’ext´erieur :
#include <stdio.h>
void ETOILES(int N) {
while (N>0) {
printf("*");
N--;
}
printf(" ");
}
int main() {
int compteur=14;
ETOILES(compteur);
printf("Valeur de compteur: %i ",compteur);
return 0;
}
L’appel de ce programme renvoit :
**************
Valeur de compteur: 14
la valeur de compteur n’a donc pas ´et´e modifi´e par l’appel de la fonction
ETOILES.
5.6.3 Passage des param`etres par adresse
Comme on vient de le voir, tout param`etre est pass´e par valeur, et cette r`egle
ne souffre aucune exception. Cela pose le probl`eme de r´ealiser un passage de
param`etre par adresse lorsque le programmeur en a besoin.
Pour changer la valeur d’une variable de la fonction appelante, on proc`ede
comme suit :
– la fonction appelante doit fournir l’adresse de la variable ;
65
– la fonction appel´ee doit d´eclarer le param`etre comme pointeur.
On peut alors atteindre la variable `a l’aide du pointeur.
Exemple : Supposons qu’on d´esire ´ecrire une proc´edure add, admettant trois param`
etres a, b et c. On d´esire que le r´esultat de l’ex´ecution de add soit d’affecter
au param`etre c la somme des valeurs des deux premiers param`etres.
Le param`etre c ne peut ´evidemment pas ˆetre pass´e par valeur, puisqu’on d´esire
modifier la valeur du param`etre effectif correspondant. Il faut donc programmer
add de la mani`ere suivante :
void add(int a, int b, int *c) {
/* c rep`ere l’entier o`u on veut mettre le r´esultat */
*c = a + b;
}
int main() {
int i=10,j=14,k;
/* on passe les valeurs de i et j comme premiers param`etres */
/* on passe l’adresse de k comme troisi`eme param`etre */
add(i,j,&k);
}
5.6.4 Passage de tableau en param`etre
Comme il est impossible de passer ’la valeur’ de tout un tableau `a une fonction,
on fournit l’adresse d’un ´el´ement du tableau4.
En g´en´eral, on fournit l’adresse du premier ´el´ement du tableau, qui est donn´ee
par le nom du tableau.
Dans la liste des param`etres d’une fonction, on peut d´eclarer un tableau par le
nom suivi de crochets,
<type> <nom>[]
ou simplement par un pointeur sur le type des ´el´ements du tableau :
<type> *<nom>
On pr´ef`erera la premi`ere ´ecriture car il n’est pas possible dans la seconde de
savoir si le programmeur a voulu passer en param`etre un pointeur vers <type>
(c’est `a dire un pointeur vers un seul <type>), ou au contraire si il a voulu
passer un tableau, c’est `a dire un pointeur vers une zone de n <type>.
Exemple :
#include <stdlib.h>
/* Initialise les ´el´ements d’un tableau */
void init_tab (int tab[], int n){
int i;
for (i = 0; i < n; i++)
tab[i] = i;
return;
}
int main() {
4Rappelons qu’un tableau est un pointeur constant (sur le premier ´el´ement du tableau)
66
int i, n = 5;
int *tab;
tab = (int*)malloc(n * sizeof(int));
init(tab,n);
}
Remarque : Quand une fonction admet un param`etre de type tableau, il y `a
deux cas possibles :
1. soit les diff´erents tableaux qui lui sont pass´es en param`etre effectif ont des
tailles diff´erentes, et dans ce cas la taille doit ˆetre un param`etre suppl´ementaire
de la fonction, comme dans l’exemple pr´ec´edent ;
2. soit les diff´erents tableaux qui lui sont pass´es en param`etre effectif ont
tous la mˆeme taille, et dans ce cas la taille peut apparaˆıtre dans le type
du param`etre effectif :
#define NB_ELEM 10
void init_tab (int tab[NB_ELEM]){{
...
}
5.7 Fonction `a nombre variable de param`etres
Il est possible de d´eclarer une fonction comme ayant un nombre variable de
param`etres en ”d´eclarant” les param`etres optionnels par l’unit´e lexicale ”...”
(3 points `a la suite). Une fonction peut avoir `a la fois des param`etres obligatoires
et des param`etres optionnels, les param`etres obligatoires apparaissant en
premier et l’unit´e lexicale ”...” apparaissant en derni`ere position dans la liste
de d´eclaration des param`etres formels.
Dans le corps de la fonction on ne dispose pas de nom pour d´esigner les param`
etres. L’acc`es `a ceux-ci ne peut se faire qu’en utilisant les macros suivantes
d´efinies dans la biblioth`eque standard <stdarg.h> :
va list permet de d´eclarer une variable opaque au programmeur, `a passer en
param`etre aux autres macros. Cette variable s’appelle traditionnellement
ap (pour argument pointer), et a pour but de rep´erer le param`etre effectif
courant.
va start doit ˆetre appel´ee avant toute utilisation de va_arg. La macro va_start
a deux param`etres : la variable ap et le nom du dernier param`etre obligatoire
de la fonction.
va arg d´elivre le param`etre effectif courant : le premier appel `a va_arg d´elivre
le premier param`etre, puis chaque nouvel appel `a va_arg d´elivre le param`
etre suivant. La macro va_arg admet deux param`etres : la variable ap
et le type du param`etre courant.
va end doit ˆetre appel´ee apr`es toutes les utilisations de va_arg. La macro
va_end admet un seul param`etre : la variable ap.
Rien n’est pr´evu pour communiquer `a la fonction le nombre et le type des param`
etres effectivement pass´es : c’est un probl`eme `a la charge du programmeur.
67
Exemple Voici la d´efinition d’une fonction de d´ebuggage sur le mod`ele de
printf admettant un nombre variable d’arguments. L’affichage ne s’effectue
que si le niveau sp´ecifi´e `a l’appel (par le parametre level) est inf´erieur `a une
variable globale DEBUG_LEVEL.
#define DEBUG_LEVEL
...
/**
* Fonction de debuggage
*/
int trace (int level, char * chaine, ...) {
va_list args;//les arguments du println
int length = 0;
va_start(args, chaine);//initialisation de la structure va_list a partir
//d’un parametre d’appel de la fonction
if (level <= DEBUG_LEVEL) {
while (chaine[length]!=0) {
//traitement des argument %...
if (chaine[length] == ’%’) {
length++;//on passe au caractere suivant
switch (chaine[length]) {
case ’d’:
printf("%d", va_arg(args, long));
break;
case ’i’:
printf("%i", va_arg(args, long));
break;
case ’x’:
printf("%x", va_arg(args, unsigned long));
break;
case ’X’:
printf("%X", va_arg(args, unsigned long));
break;
case ’s’:
printf("%s", va_arg(args, char *));
break;
}
} else {
//on a une chaine classique donc on l’affiche
printf("%c", chaine[length]);
}
length++;
}
//on termine correctement l’utilisation d’une structure va_list
va_end(args);
printf(" ");//on va a la ligne!
return 0;
}
}
Une utilisation classique d’une telle fonction, on associera `a l’affichage d’informations
de debuggage pr´ecises un niveau level ´elev´e tandis que les affichages
de base auront un param`etre level faible. Ensuite, c’est l’utilisateur qui en
68
faisant varier la valeur de DEBUG_LEVEL modifie la quantit´e et la pr´ecision des
informations affich´ees `a l’ex´ecution. Exemple :
/**
* Insertion d’une valeur val dans un tableau tab de taille n
* contenant n-1 premiers elements tries
*/
void insertion(int tab[], int n, int val) {
int i=n;
trace(1, "Debut de insertion - param n=%i, val=%i",n,val);
trace(2, " Debut decalage des elements");
while( i!=0 && val < tab[i-1]) {
trace(3, "Decalage de tab[%i] en position %i",i-1,i);
tab[i] = tab[i-1];
i--;
}
trace(2, " Fin decalage des elements");
trace(2, " Insertion de la valeur %i en position %i",val,i)
tab[i] = val;
trace(1, "Fin de insertion");
}
A l’appel de cette fonction, on aura le comportement suivant :
– si DEBUG_LEVEL = O : pas d’affichage ;
– si DEBUG_LEVEL = 1 : l’entr´ee et la sortie de la fonction sera visible par un
message affich´e. Cela permet de savoir rapidement en cas de bug quelle fonction
pose probl`eme ;
– si DEBUG_LEVEL = 2, on aura en plus l’affichage des ´etapes dans l’algorithme
utilis´e pour la fonction ;
– si DEBUG_LEVEL > 2, on aura tous les d´etails possibles.
5.8 Pointeurs sur une fonction
Il est parfois utile de passer une fonction comme param`etre d’une autre fonction.
Cette proc´edure permet en particulier d’utiliser une mˆeme fonction pour
diff´erents usages. Pour cela, on utilise un m´ecanisme de pointeur.
Un pointeur sur une fonction correspond `a l’adresse du d´ebut du code de la
fonction. Un pointeur p_fonction sur une fonction ayant pour prototype
type fonction(type_1 arg_1,...,type_n arg_n);
se d´eclare par :
type (*p_fonction)(type_1,...,type_n);
Ainsi, consid´erons une fonction operateur_binaire prenant pour param`etres
deux entiers a et b, ainsi qu’une fonction f de type int qui prend elle-mˆeme
deux entiers en param`etres. On suppose que operateur_binaire renvoit l’entier
f(a,b).
Le prototype de cette fonction sera :
int operateur_binaire(int a, int b, int (*f)(int, int))
Pour appeler la fonction operateur_binaire, on utilisera comme troisi`eme
param`etre effectif l’identificateur de la fonction utilis´ee. Par exemple, si somme
est la fonction
69
int somme(int a, int b) { return a+b; }
on appelle la fonction operateur_binaire pour la fonction somme par l’expression
:
operateur_binaire(a,b,somme)
A noter que la notation &somme n’est pas utilis´ee comme param`etre effectif.
Pour appeler la fonction pass´ee en param`etre dans le corps de la fonction
operateur_binaire, on ´ecrit (*f)(a, b). Ainsi, cette fonction pourrait s’´ecrire :
int operateur_binaire(int a, int b, int (*f)(int, int)) {
return((*f)(a,b));
}
Les pointeurs sur les fonctions sont notamment utilis´es dans la fonction de tri
des ´el´ements d’un tableau qsort et dans la recherche d’un ´el´ement dans un
tableau bsearch. Ces deux fonctions sont d´efinies dans la librairie standard
<stdlib.h>.
Le prototype de la fonction de tri5 (algorithme quicksort) est ainsi :
void qsort(void *tab, size_t nb_elements, size_t taille_elements,
int(*compar)(const void *, const void *));
Cette gymnastique sur les pointeurs de fonction n’est pas forc´ement triviale
mais reste utile dans certains cas, en particulier dans la d´efinition de fonction
”g´en´erique” comme la fonction qsort ´evoqu´ee pr´ec´edemment.
5Elle permet de trier les nb_elements premiers ´el´ements du tableau tab. Le param`etre
taille_elements donne la taille des ´el´ements du tableau. Le type size_t utilis´e ici est un
type pr´ed´efini dans <stddef.h>. Il correspond au type du r´esultat de l’´evaluation de sizeof.
La fonction qsort est param´etr´ee par la fonction de comparaison compar de prototype :
int compar(void *a, void *b) ;
Les deux param`etres a et b de la fonction compar sont des pointeurs g´en´eriques de type void *.
Ils correspondent `a des adresses d’objets dont le type n’est pas d´etermin´e. Cette fonction de
comparaison retourne
– un entier qui vaut 0 si les deux objets point´es par a et b sont ´egaux ;
– une valeur strictement n´egative (resp. positive) si l’objet point´e par a est strictement
inf´erieur (resp. sup´erieur) `a celui point´e par b.
70
Chapitre 6
Gestion des fichiers
Comme beaucoup de langages, le C offre la possibilit´e de lire et d’´ecrire des
donn´ees dans un fichier.
Pour des raisons d’efficacit´e, les acc`es `a un fichier se font par l’interm´ediaire
d’une m´emoire-tampon (on parle de buffer), ce qui permet de r´eduire le nombre
d’acc`es aux p´eriph´eriques (disque...).
Pour pouvoir manipuler un fichier, un programme a besoin d’un certain nombre
d’informations : l’adresse de l’endroit de la m´emoire-tampon o`u se trouve le
fichier, la position de la tˆete de lecture, le mode d’acc`es au fichier (lecture ou
´ecriture)... Ces informations sont rassembl´ees dans une structure, dont le type,
FILE *, est d´efini dans <stdio.h>. Un objet de type FILE * est appel´e flot de
donn´ees (stream en anglais).
Avant de lire ou d’´ecrire dans un fichier, on notifie son acc`es par la commande
fopen. Cette fonction prend comme argument le nom du fichier, d´efini avec
le syst`eme d’exploitation le mode d’ouverture du fichier et initialise un flot de
donn´ees, qui sera ensuite utilis´e lors de l’´ecriture ou de la lecture. Apr`es les
traitements, on annule la liaison entre le fichier et le flot de donn´ees grˆace `a la
fonction fclose.
6.1 Ouverture et fermeture de fichiers
6.1.1 Ouverture de fichiers : la fonction fopen
Lorsqu’on d´esire acc´eder `a un fichier, il est n´ecessaire avant tout acc`es d’ouvrir
le fichier `a l’aide de la fonction fopen.
Cette fonction, de type FILE * ouvre un fichier et lui associe un flot de donn´ees.
Sa syntaxe est :
fopen("nom-de-fichier","mode")
La valeur retourn´ee par fopen est
– soit un flot de donn´ees ;
– soit NULL si l’ex´ecution de cette fonction ne se d´eroule pas normalement.
Je vous conseille donc de toujours tester si la valeur renvoy´ee par la fonction
fopen est ´egale `a NULL afin de d´etecter les erreurs d’ouverture de fichier (lecture
d’un fichier inexistant...).
71
Le premier argument de fopen fournit donc le nom du fichier. Le second argument,
mode, est une chaˆıne de caract`eres qui sp´ecifie le mode d’acc`es au fichier.
Les sp´ecificateurs de mode d’acc`es diff`erent suivant le type de fichier consid´er´e.
On distingue
– les fichiers textes, pour lesquels les caract`eres de contrˆole (retour `a la ligne
...) seront interpr´et´es en tant que tels lors de la lecture et de l’´ecriture ;
– les fichiers binaires, pour lesquels les caract`eres de contrˆole se sont pas interpr
´et´es.
Les diff´erents modes d’acc`es sont list´es dans le tableau 6.1.
"r" ouverture d’un fichier texte en lecture
"w" ouverture d’un fichier texte en ´ecriture
"a" ouverture d’un fichier texte en ´ecriture `a la fin
"rb" ouverture d’un fichier binaire en lecture
"wb" ouverture d’un fichier binaire en ´ecriture
"ab" ouverture d’un fichier binaire en ´ecriture `a la fin
"r+" ouverture d’un fichier texte en lecture/´ecriture
"w+" ouverture d’un fichier texte en lecture/´ecriture
"a+" ouverture d’un fichier texte en lecture/´ecriture `a la fin
"r+b" ouverture d’un fichier binaire en lecture/´ecriture
"w+b" ouverture d’un fichier binaire en lecture/´ecriture
"a+b" ouverture d’un fichier binaire en lecture/´ecriture `a la fin
Tab. 6.1 – Les modes d’acc`es aux fichiers dans la fonction fopen
Conditions particuli`eres et cas d’erreur
– Si le mode contient la lettre r, le fichier doit exister, sinon c’est une erreur.
– Si le mode contient la lettre w, le fichier peut ne pas exister. Dans ce cas, il
sera cr´e´e, et si le fichier existait d´ej`a, son ancien contenu est perdu.
– Si le mode contient la lettre a, le fichier peut ne pas exister. Comme pour le
cas pr´ec´edent, si le fichier n’existe pas, il est cr´e´e ; si le fichier existe d´ej`a, son
ancien contenu est conserv´e.
– Si un fichier est ouvert en mode ”´ecriture `a la fin”, toutes les ´ecritures se font `a
l’endroit qui est ´etait la fin du fichier lors de l’ex´ecution de l’ordre d’´ecriture.
Cela signifie que si plusieurs processus partagent le mˆeme FILE*, r´esultat de
l’ouverture d’un fichier en ´ecriture `a la fin, leurs ´ecritures ne s’´ecraseront pas
mutuellement.
Les modes de communication standard
Quand un programme est lanc´e par le syst`eme, celui-ci ouvre trois fichiers correspondant
aux trois modes de communication standard : standard input, stan-
dard output et standard error. Il y a trois constantes pr´ed´efinies dans stdio.h
qui correspondent aux trois fichiers :
– stdin (standard input) : flot d’entr´ee (par d´efaut, le clavier) ;
72
– stdout (standard output) : flot de sortie (par d´efaut, l’´ecran) ;
– stderr (standard error) : flot d’affichage des messages d’erreur (par d´efaut,
l’´ecran).
Il est fortement conseill´e d’afficher syst´ematiquement les messages d’erreur sur
stderr afin que ces messages apparaissent `a l’´ecran mˆeme lorsque la sortie
standard est redirig´ee.
Utilisation typique de fopen
#include <stdio.h>
FILE *fp;
...
if ((fp = fopen("donnees.txt","r")) == NULL) {
fprintf(stderr,"Impossible d’ouvrir le fichier donn´ees en lecture ");
exit(1);
}
6.1.2 Fermeture de fichiers : la fonction fclose
Elle permet de fermer le flot qui a ´et´e associ´e `a un fichier par la fonction fopen.
Sa syntaxe est :
fclose(flot)
o`u flot est le flot de type FILE* retourn´e par la fonction fopen correspondante.
La fonction fclose retourne 0 si l’op´eration s’est d´eroul´ee normalement et EOF
si il y a eu une erreur.
6.2 Les entr´ees-sorties format´ees
6.2.1 La fonction d’´ecriture en fichier fprintf
La fonction fprintf, analogue `a printf (voir §2.5.4 page 29), permet d’´ecrire
des donn´ees dans un flot. Sa syntaxe est :
fprintf(flot,"cha^ıne de contr^ole", expression1, . . . , expressionn);
o`u flot est le flot de donn´ees retourn´e par la fonction fopen. Les sp´ecifications
de format utilis´ees pour la fonction fprintf sont les mˆemes que pour printf
puisque :
printf(...) ⇐⇒ fprintf(stdout,...)
6.2.2 La fonction de saisie en fichier fscanf
La fonction fscanf, analogue `a scanf (voir §2.5.5 page 31), permet de lire des
donn´ees dans un fichier. Sa syntaxe est semblable `a celle de scanf :
fscanf(flot,"cha^ıne de contr^ole", arg1, . . . , argn);
o`u flot est le flot de donn´ees retourn´e par fopen. Les sp´ecifications de format
sont ici les mˆemes que celles de la fonction scanf.
73
6.3 Impression et lecture de caract`eres dans un fi-
chier
6.3.1 Lecture et ´ecriture par caract`ere : fgetc et fputc
Similaires aux fonctions getchar et putchar (voir §2.5.1 et §2.5.2), les fonctions
fgetc et fputc permettent respectivement de lire et d’´ecrire un caract`ere dans
un fichier.
La fonction fgetc retourne le caract`ere lu dans le fichier et la constante EOF
lorsqu’elle d´etecte la fin du fichier. Son prototype est :
int fgetc(FILE* flot);
o`u flot est le flot de type FILE* retourn´e par la fonction fopen.
La fonction fputc ´ecrit un caract`ere dans le flot de donn´ees :
int fputc(int caractere, FILE *flot)
Elle retourne l’entier correspondant au caract`ere lu (ou la constante EOF en cas
d’erreur).
6.3.2 Lecture et ´ecriture optimis´ees par caract`ere : getc et putc
Il existe ´egalement deux versions optimis´ees des fonctions fgetc et fputc qui
sont impl´ement´ees par des macros. Il s’agit respectivement de getc et putc.
Leur syntaxe est similaire `a celle de fgetc et fputc :
int getc(FILE* flot);
int putc(int caractere, FILE *flot)
Ainsi, le programme suivant lit le contenu du fichier texte entree.txt, et le
recopie caract`ere par caract`ere dans le fichier sortie.txt :
#include <stdio.h>
#include <stdlib.h>
#define ENTREE "entree.txt"
#define SORTIE "sortie.txt"
int main(void) {
FILE *f_in, *f_out;
int c;
// Ouverture du fichier ENTREE en lecture
if ((f_in = fopen(ENTREE,"r")) == NULL) {
fprintf(stderr, " Erreur: Impossible de lire %s ",ENTREE);
return(EXIT_FAILURE);
}
// Ouverture du fichier SORTIE en ecriture
if ((f_out = fopen(SORTIE,"w")) == NULL) {
fprintf(stderr, " Erreur: Impossible d’ecrire dans %s ",SORTIE);
return(EXIT_FAILURE);
}
74
// Recopie du contenu de ENTREE dans SORTIE
while ((c = fgetc(f_in)) != EOF)
fputc(c, f_out);
// Fermeture des flots de donnees
fclose(f_in);
fclose(f_out);
return(EXIT_SUCCESS);
}
On notera l’utilisation des constantes EXIT_SUCCESS (valant 0) et EXIT_FAILURE
(valant 1), d´efinies dans la librairie <stdlib.h> et qui sont les param`etres privil
´egi´es de la fonction exit (voir §9.19).
6.3.3 Relecture d’un caract`ere
Il est possible de replacer un caract`ere dans un flot au moyen de la fonction
ungetc :
int ungetc(int carac, FILE *f);
Cette fonction place le caract`ere carac (converti en unsigned char) dans le
flot f. En particulier, si carac est ´egal au dernier caract`ere lu dans le flot, elle
annule le d´eplacement provoqu´e par la lecture pr´ec´edente. Toutefois, ungetc
peut ˆetre utilis´ee avec n’importe quel caract`ere (sauf EOF). L’exemple suivant
permet d’illustrer le comportement de ungetc :
#include <stdio.h>
#include <stdlib.h>
#define ENTREE "entree.txt"
int main(void) {
FILE *f_in;
int c;
if ((f_in = fopen(ENTREE,"r")) == NULL) {
fprintf(stderr, " Erreur: Impossible de lire le fichier %s ",ENTREE);
return(EXIT_FAILURE);
}
while ((c = fgetc(f_in)) != EOF) {
if (c == ’0’)
ungetc(’.’,f_in);
putchar(c);
}
fclose(f_in);
return(EXIT_SUCCESS);
}
Sur le fichier entree.txt dont le contenu est 097023, ce programme affiche `a
l’´ecran 0.970.23.
75
6.3.4 Les entr´ees-sorties binaires : fread et fwrite
Les fonctions d’entr´ees-sorties binaires permettent de transf´erer des donn´ees
dans un fichier sans transcodage. En ce sens, elles sont plus portables car elles
´ecrivent ce qu’on leur dit. Elles sont ainsi plus efficaces que les fonctions d’entr´eesortie
standard.
Elles sont notamment utiles pour manipuler des donn´ees de grande taille ou
ayant un type compos´e. Leurs prototypes sont :
size_t fread(void *pointeur, size_t taille, size_t nbre, FILE *f);
size_t fwrite(void *pointeur, size_t taille, size_t nbre, FILE *f);
o`u pointeur est l’adresse du d´ebut des donn´ees `a transf´erer, taille la taille des
objets `a transf´erer et nbre leur nombre. Rappelons que le type size_t, d´efini
dans <stddef.h>, correspond au type du r´esultat de l’´evaluation de sizeof
(voir §2.1.7 et §2.4.4).
La fonction fread lit les donn´ees sur le flot f et la fonction fwrite les ´ecrit.
Ces deux fonctions retournent le nombre de donn´ees transf´er´ees.
Par exemple, le programme suivant ´ecrit un tableau d’entiers (contenant les 50
premiers entiers) avec fwrite dans le fichier sortie.txt, puis lit ce fichier avec
fread et imprime les ´el´ements du tableau.
#include <stdio.h>
#include <stdlib.h>
#define NB 50
#define F_SORTIE "sortie.txt"
int main(void) {
FILE *f_in, *f_out;
int *tab1, *tab2;
int i;
// allocation memoire des tableaux
tab1 = (int*)malloc(NB * sizeof(int));
tab2 = (int*)malloc(NB * sizeof(int));
for (i = 0 ; i < NB; i++)
tab1[i] = i;
/* ecriture du tableau dans F_SORTIE */
if ((f_out = fopen(F_SORTIE, "w")) == NULL) {
fprintf(stderr, " Impossible d’ecrire dans %s ",F_SORTIE);
return(EXIT_FAILURE);
}
fwrite(tab1, NB * sizeof(int), 1, f_out);
fclose(f_out);
/* lecture dans F_SORTIE */
if ((f_in = fopen(F_SORTIE, "r")) == NULL) {
fprintf(stderr, " Impossible de lire dans %s ",F_SORTIE);
return(EXIT_FAILURE);
}
fread(tab2, NB * sizeof(int), 1, f_in);
76
fclose(f_in);
for (i = 0 ; i < NB; i++)
printf("%d ",tab2[i]);
printf(" ");
return(EXIT_SUCCESS);
}
Les ´el´ements du tableau sont bien affich´es `a l’´ecran. Par contre, on constate que
le contenu du fichier sortie n’est pas encod´e.
6.3.5 Positionnement dans un fichier : fseek, rewind et ftell
Les diff´erentes fonctions d’entr´ees-sorties permettent d’acc´eder `a un fichier en
mode s´equentiel : les donn´ees du fichier sont lues ou ´ecrites les unes `a la suite
des autres.
Il est ´egalement possible d’acc´eder `a un fichier en mode direct, c’est-`a-dire que
l’on peut se positionner `a n’importe quel endroit du fichier. La fonction fseek
permet de se positionner `a un endroit pr´ecis et a pour prototype :
int fseek(FILE *flot, long deplacement, int origine);
La variable deplacement d´etermine la nouvelle position dans le fichier. Il s’agit
d’un d´eplacement relatif par rapport `a origine, compt´e en nombre d’octets.
La variable origine peut prendre trois valeurs :
1. SEEK_SET (´egale `a 0) : d´ebut du fichier ;
2. SEEK_CUR (´egale `a 1) : position courante ;
3. SEEK_END (´egale `a 2) : fin du fichier.
La fonction
int rewind(FILE *flot);
permet de se positionner au d´ebut du fichier. Elle est ´equivalente `a fseek(flot,
0, SEEK_SET) ;
La fonction
long ftell(FILE *flot);
retourne la position courante dans le fichier (en nombre d’octets depuis l’origine).
L’exemple suivant illustre l’utilisation des fonctions pr´ec´edentes :
#include <stdio.h>
#include <stdlib.h>
#define NB 50
#define F_SORTIE "sortie.txt"
int main(void) {
FILE *f_in, *f_out;
int *tab;
int i;
// Initialisation du tableau
77
tab = (int*)malloc(NB * sizeof(int));
for (i = 0 ; i < NB; i++)
tab[i] = i;
/* ecriture du tableau dans F_SORTIE */
if ((f_out = fopen(F_SORTIE, "w")) == NULL){
fprintf(stderr, " Impossible d’ecrire dans %s ",F_SORTIE);
return(EXIT_FAILURE);
}
fwrite(tab, NB * sizeof(int), 1, f_out);
fclose(f_out);
/* lecture dans F_SORTIE */
if ((f_in = fopen(F_SORTIE, "r")) == NULL) {
fprintf(stderr, " Impossible de lire dans %s ",F_SORTIE);
return(EXIT_FAILURE);
}
/* on se positionne a la fin du fichier */
fseek(f_in, 0, SEEK_END);
printf(" position %ld", ftell(f_in));
/* deplacement de 10 int en arriere */
fseek(f_in, -10 * sizeof(int), SEEK_END);
printf(" position %ld", ftell(f_in));
fread(&i, sizeof(i), 1, f_in);
printf(" i = %d", i);
/* retour au debut du fichier */
rewind(f_in);
printf(" position %ld", ftell(f_in));
fread(&i, sizeof(i), 1, f_in);
printf(" i = %d", i);
/* deplacement de 5 int en avant */
fseek(f_in, 5 * sizeof(int), SEEK_CUR);
printf(" position %ld", ftell(f_in));
fread(&i, sizeof(i), 1, f_in);
printf(" i = %d ", i);
fclose(f_in);
return(EXIT_SUCCESS);
}
L’ex´ecution de ce programme affiche `a l’´ecran :
position 200
position 160 i = 40
position 0 i = 0
position 24 i = 6
On constate en particulier que l’emploi de la fonction fread provoque un d´eplacement
correspondant `a la taille de l’objet lu `a partir de la position courante.
78
Chapitre 7
Les directives du
pr´eprocesseur
Le pr´eprocesseur est un programme ex´ecut´e lors de la premi`ere phase de la
compilation. Il effectue des modifications textuelles sur le fichier source `a partir
de directives. Les diff´erentes directives au pr´eprocesseur, introduites par le
caract`ere #, ont pour but :
– l’incorporation de fichiers source (#include),
– la d´efinition de constantes symboliques et de macros (#define),
– la compilation conditionnelle (#if, #ifdef,...).
L’utilisation de ces directives a d´ej`a ´et´e introduite au §1.4 page 7. Il s’agit ici
de d´etailler l’ensemble des directives du pr´eprocesseur.
7.1 La directive #include
Elle permet d’incorporer dans le fichier source le texte figurant dans un autre
fichier. Ce dernier peut ˆetre un fichier en-tˆete de la librairie standard (stdio.h,
math.h,...) ou n’importe quel autre fichier. La directive #include poss`ede deux
syntaxes voisines :
#include <nom-de-fichier>
recherche le fichier mentionn´e dans un ou plusieurs r´epertoires syst`emes d´efinis
par l’impl´ementation (typiquement /usr/include/) :
#include "nom-de-fichier"
recherche le fichier dans le r´epertoire courant (celui o`u se trouve le fichier
source). On peut sp´ecifier d’autres r´epertoires `a l’aide de l’option -I du compilateur
(voir chapitre 8).
La premi`ere syntaxe est g´en´eralement utilis´ee pour les fichiers en-tˆete de la
librairie standard, tandis que la seconde est plutˆot destin´ee aux fichiers cr´e´es
par l’utilisateur.
7.2 La directive #define
La directive #define permet de d´efinir des constantes symboliques (on parle
aussi de macros sans param`etres) ou des macros avec param`etre.
79
7.2.1 D´efinition de constantes symboliques
Bien que cela a d´ej`a ´et´e vu pr´ec´edemment, rappelons que lorsque le pr´eprocesseur
lit une ligne du type :
#define nom reste-de-la-ligne
il remplace dans toute la suite du source toute nouvelle occurrence de nom par
reste-de-la-ligne. Il n’y a aucune contrainte quand `a ce qui peut se trouver
dans reste-de-la-ligne : on pourra ainsi ´ecrire
#define BEGIN {
#define END }
L’utilit´e principale des macros sans param`etre est de donner un nom parlant `a
une constante. Les avantages `a toujours donner un nom aux constantes sont les
suivants :
1. un nom bien choisi permet d’expliciter la s´emantique de la constante. Ex :
#define NB_COLONNES 100.
2. la constante chiffr´ee se trouve `a un seul endroit, ce qui facilite la modification
du programme quand on veut changer la valeur de la constante
(cas de la taille d’un tableau, par exemple).
3. on peut expliciter facilement les relations entre constantes. Ex :
#define NB_LIGNES 24
#define NB_COLONNES 80
#define TAILLE_MATRICE NB_LIGNES * NB_COLONNES
D´efinition de constantes symboliques `a l’invocation du compilateur
Certains compilateurs permettent de d´efinir des constantes symboliques `a l’invocation
du compilateur. Il est alors possible d’´ecrire un programme utilisant
une macro qui n’est nulle part d´efinie dans le source.
Ceci est tr`es pratique pour que certaines constantes critiques d’un programme
aient une valeur qui soit attribu´ee `a l’ext´erieur du programme (comme lors
d’une une phase de configuration par exemple).
Ci-dessous, un exemple pour gcc : la compilation du fichier prog.c en d´efinissant
la constante symbolique NB_LIGNES de valeur 14 :
gcc -c -DNB_LIGNES=24 prog.c
Constantes symboliques pr´ed´efinies
Il y a un certain nombre de macros pr´ed´efinies par le pr´eprocesseur, r´ecapitul´ees
dans le tableau 7.1.
7.2.2 Les macros avec param`etres
Une macro avec param`etres se d´efinit de la mani`ere suivante :
#define nom(liste-de-param`etres) corps-de-la-macro
80
Nom Valeur de la macro Type
__LINE__ num´ero de la ligne courante du programme source entier
__FILE__ nom du fichier source en cours de compilation chaˆıne
__DATE__ la date de la compilation chaˆıne
__TIME__ l’heure de la compilation chaˆıne
__STDC__ 1 si le compilateur est ISO, 0 sinon entier
Tab. 7.1 – Les constantes symboliques pr´ed´efinies
o`u liste-de-param`etres est une liste d’identificateurs s´epar´es par des virgules.
Par exemple, avec la directive
#define MAX(a,b) (a > b ? a : b)
le processeur remplacera dans la suite du code toutes les occurences du type
MAX(x,y) o`u x et y sont des symboles quelconques par (x > y ? x : y)
Une macro a donc une syntaxe similaire `a celle d’une fonction, mais son emploi
permet en g´en´eral d’obtenir de meilleures performances en temps d’ex´ecution.
La distinction entre une d´efinition de constante symbolique et celle d’une macro
avec param`etres se fait sur le caract`ere qui suit imm´ediatement le nom de
la macro : si ce caract`ere est une parenth`ese ouvrante, c’est une macro avec
param`etres, sinon c’est une constante symbolique. Il ne faut donc jamais mettre
d’espace entre le nom de la macro et la parenth`ese ouvrante. L’erreur classique
´etant d’´ecrire par erreur :
#define CARRE (a) a * a
la chaˆıne de caract`eres CARRE(2) sera alors remplac´ee par : (a) a * a (2)
Il faut toujours garder `a l’esprit que le pr´eprocesseur n’effectue que des remplacements
de chaˆınes de caract`eres. En particulier, il est conseill´e de toujours
mettre entre parenth`eses le corps de la macro et les param`etres formels qui y
sont utilis´es. Par exemple, si l’on ´ecrit sans parenth`eses :
#define CARRE(a) a * a
le pr´eprocesseur remplacera CARRE(a + b) par a + b * a + b et non par
(a + b) * (a + b). De mˆeme, !CARRE(x) sera remplac´e par ! x * x et non
par !(x * x).
Enfin, il faut ˆetre attentif aux ´eventuels effets de bord que peut entraˆıner l’usage
de macros. Par exemple, CARRE(x++) aura pour expansion (x++) * (x++).
L’op´erateur d’incr´ementation sera donc appliqu´e deux fois au lieu d’une.
En conclusion, les macros avec param`etres sont `a utiliser avec pr´ecaution et en
cas de doutes, il faut mieux s’en passer.
7.3 La compilation conditionnelle
La compilation conditionnelle a pour but d’incorporer ou d’exclure des parties
du code source dans le texte qui sera g´en´er´e par le pr´eprocesseur, le choix ´etant
bas´e sur un test ex´ecut´e `a la compilation. Elle permet d’adapter le programme
au mat´eriel ou `a l’environnement sur lequel il s’ex´ecute, ou d’introduire dans le
programme des instructions de d´ebuggage.
81
Les directives de compilation conditionnelle se r´epartissent en deux cat´egories,
suivant le type de la condition invoqu´ee qui est test´ee :
– la valeur d’une expression
– l’existence ou l’inexistence de symboles
7.3.1 Condition li´ee `a la valeur d’une expression
Sa syntaxe la plus g´en´erale est :
#if condition-1
partie-du-programme-1
#elif condition-2
partie-du-programme-2
...
#elif condition-n
partie-du-programme-n
#else
partie-du-programme-else
#endif
Le nombre de #elif est quelconque et le #else est facultatif. Chaque condition-i
doit ˆetre une expression constante.
Lors de la compilation, une seule partie-du-programme-i sera compil´ee : celle
qui correspond `a la premi`ere condition-i non nulle, ou bien la partie-du-programme-else
si toutes les conditions sont nulles.
Par exemple, on peut ´ecrire :
#define PROCESSEUR ALPHA
...
#if PROCESSEUR == ALPHA
taille_long = 64;
#elif PROCESSEUR == PC
taille_long = 32;
#endif
7.3.2 Condition li´ee `a l’existence d’un symbole
Sa syntaxe est la suivante :
#ifdef symbole
partie-du-programme-1
#else condition-2
partie-du-programme-2
#endif
Si symbole est d´efini au moment o`u l’on rencontre la directive #ifdef, alors
partie-du-programme-1 sera compil´ee et partie-du-programme-2 sera ignor
´ee. Dans le cas contraire, c’est partie-du-programme-2 qui sera compil´ee. La
directive #else est comme pr´ec´edemment facultative. Ce type de directive est
utile pour rajouter des instructions destin´ees au d´ebuggage du programme :
82
#define DEBUG
....
#ifdef DEBUG
for (i = 0; i < N; i++)
printf("%d ",i);
#endif /* DEBUG */
Il suffit alors de supprimer la directive #define DEBUG pour que les instructions
li´ees au debuggage ne soient pas compil´ees1.
De fa¸con similaire, on peut tester la non-existence d’un symbole `a l’aide de la
directive #ifndef :
#ifndef symbole
partie-du-programme-1
#else condition-2
partie-du-programme-2
#endif
On rencontre beaucoup cette directive dans le cadre de la compilation modulaire
(voir chapitre 8) puisqu’elle permet de s’assurer que les d´efinitions d’instructions
dans un fichier d’en-tˆete ne seront effectu´ees qu’une seule fois.
7.3.3 L’op´erateur defined
L’op´erateur defined est un op´erateur sp´ecial : il ne peut ˆetre utilis´e que dans
le contexte d’une commande #if ou #elif. Il peut ˆetre utilis´e sous l’une des
deux formes suivantes :
– defined nom
– defined ( nom )
Il d´elivre la valeur 1 si nom est une macro d´efinie, et la valeur 0 sinon. L’int´erˆet
de cet op´erateur est de permettre d’´ecrire des tests portant sur la d´efinition de
plusieurs macros, alors que #ifdef ne peut en tester qu’une :
#if defined(SOLARIS) || defined(SYSV)
7.3.4 La commande #error
La commande #error a la syntaxe suivante :
#error chaine
La rencontre de cette commande provoquera l’´emission d’un message d’erreur
comprenant la chaine. Cette commande a pour utilit´e de capturer `a la compilation
des conditions qui font que le programme ne peut pas s’ex´ecuter sur cette
plate-forme. Voici un exemple o`u on teste que la taille des entiers est suffisante :
#include <limits.h>
#if INT_MAX < 1000000
#error "Entiers trop petits sur cette machine"
#endif
1A noter aussi que comme on l’a vu pr´ecedemment, on peut remplacer cette derni`ere
directive par l’option de compilation -DDEBUG, qui permet de d´efinir ce symbole.
83
7.3.5 La commande #pragma
La directive #pragma est li´ee `a une impl´ementation sp´ecifique et permet de d´efinir,
pour un compilateur donn´e, une directive de pr´etraitement. Sa syntaxe
est la suivante :
#pragma commande
84
Chapitre 8
La programmation modulaire
D`es que l’on ´ecrit un programme de taille importante ou destin´e `a ˆetre utilis´e
et maintenu par d’autres personnes, il est indispensable de se fixer un certain
nombre de r`egles d’´ecriture. En particulier, il est n´ecessaire de fractionner le
programme en plusieurs fichiers sources, que l’on compile s´eparemment.
Ces r`egles d’´ecriture ont pour objectifs de rendre un programme lisible, portable,
r´eutilisable mais surtout facile `a maintenir et `a modifier.
L’id´ee est alors de regrouper dans un mˆeme fichier les instructions impl´ementant
des fonctionnalit´es similaires (par exemple, tab_management.c contiendra une
biblioth`eque de fonctions g´erant les tableaux, file_management.c fournira une
biblioth`eque de fonctions g´erant les fichiers dans le contexte du projet effectu´e
et main.c contiendra la d´efinition de la fonction main). La programmation
modulaire consiste alors `a op´erer les manipulations n´ecessaires permettant de
lier ces fichiers.
8.1 Principes ´el´ementaires
Trois principes essentiels doivent guider l’´ecriture d’un programme C. Ces principes
s’appliquent en fait dans le cas g´en´eral du g´enie logiciel.
1. L’abstraction des constantes litt´erales
L’utilisation explicite de constantes litt´erales dans le corps d’une fonction
rend les modifications et la maintenance difficiles. Des instructions
comme :
fopen("mon_fichier", "r");
perimetre = 2 * 3.14 * rayon;
sont `a proscrire (il faudrait d´efinir des constantes fournissant le nom du
fichier ou la valeur de ). Sauf cas tr`es particuliers, les constantes doivent
ˆetre d´efinies comme des constantes symboliques au moyen de la directive
#define.
2. La factorisation du code
Le but est d’´eviter les duplications de code. La pr´esence d’une mˆeme portion
de code `a plusieurs endroits du programme est un obstacle `a d’´even-
85
tuelles modifications. Les fonctions doivent donc ˆetre syst´ematiquement
utilis´ees pour ´eviter la duplication de code. Il ne faut pas craindre de
d´efinir une multitude de fonctions de petite taille.
3. La fragmentation du code
Pour des raisons de lisibilit´e, il est pertinent de d´ecouper un programme
en plusieurs fichiers. En plus, cette r`egle permet de r´eutiliser facilement
une partie du code pour d’autres applications.
On s´epare alors le programme en modules, chaque module impl´ementant
des fonctions sur un th`eme similaire et qui se traduiront physiquement
par deux fichiers :
(a) un fichier en-tˆete (on dit aussi de header) ayant l’extension .h et
contenant le prototype des fonctions principales impl´ement´ees dans
ce module.
(b) un fichier source ayant l’extension .c contenant non seulement le
corps des fonctions d´eclar´ees dans le fichier en-tˆete mais ´egalement
celui des fonctions interm´ediaires ´eventuellement utilis´ees. Ce fichier
inclut ´evidemment le fichier en-tˆete par le biais de la directive #include.
Une possibilit´e est de placer une partie du code dans un fichier en-tˆete
(on dit aussi de header) ayant l’extension .h que l’on inclut dans le fichier
contenant le programme principal `a l’aide de la directive #include.
L’exemple suivant illustre ce principe sur un programme qui saisit deux
entiers au clavier et affiche leur produit. Sur cet exemple, il a ´et´e choisi de
d´efinir un module arithm´etique qui fournit la fonction effectuant le produit
1. Ce module est donc impl´ement´e dans les fichiers arithm´etique.h
et arithm´etique.c. Le programme est donc compos´e de trois fichiers :
les 2 fichiers du module et le fichier principale contenant la fonction main
appel´e ici main.c :
/*********************************************/
/* fichier: main.c */
/* saisit 2 entiers et affiche leur produit */
/*********************************************/
#include <stdlib.h>
#include <stdio.h>
#include "arithmetique.h" // Le module d’arithmetique
int main(void) {
int a, b, c;
scanf("%d",&a);
scanf("%d",&b);
c = produit(a,b); // appel de la fonction d´efinie dans arithmetique.h
printf(" le produit vaut %d ",c);
return EXIT_SUCCESS;
}
/********************************************************/
1Il s’agit ´evidemment d’un exemple tr`es simpliste !
86
/* fichier: arithmetique.h */
/* G`ere les op´erations arithm´etiques sur les entiers */
/* (Ici, seul le produit est impl´ement´e */
/********************************************************/
int produit(int a, int b);
/*****************************************************/
/* fichier: arithmetique.c */
/* Corps des fonctions d´efinies dans arithmetique.h */
/*****************************************************/
#include "arithmetique.h"
int produit(int a, int b) {
return(a * b);
}
Remarquer que c’est exactement la proc´edure utilis´ee pour les fonctions de
la librairie standard : les fichiers en-tˆete .h de la librairie standard sont
constitu´es de d´eclarations de fonctions et de d´efinitions de constantes
symboliques (la seule information importante pour un utilisateur) et le
corps des fonctions en lui-mˆeme est s´epar´e.
8.2 Eviter les erreurs d’inclusions multiples
Une derni`ere r`egle, tr`es importante, consiste `a ´eviter les erreurs de double
d´efinition de fonctions qui se traduise `a l’´edition de lien par le message suivant
(avec gcc) :
toto.o:toto.c multiple definition of ’...’
toto.o: first defined here
Cette erreur se produit en g´en´eral lors de l’inclusion multiple d’un mˆeme fichier
en-tˆete.
Pour ´eviter cela, la m´ethode consiste `a d´efinir une constante `a la premi`ere inclusion
du fichier en-tˆete et d’ignorer le contenu de celui-ci si la constante a d´ej`a
´et´e d´efinie (donc si on a d´ej`a fait une inclusion). Pour cela, on utilise la directive
#ifndef (voir 7.3.2). Il est recommand´e d’appeler la constante __TOTO_H
pour le fichier toto.h En appliquant cette r`egle, le fichier arithmetique.h de
l’exemple pr´ec´edent devient :
/********************************************************/
/* fichier: arithmetique.h */
/* G`ere les op´erations arithm´etiques sur les entiers */
/* (Ici, seul le produit est impl´ement´e */
/********************************************************/
#ifndef __ARITHMETIQUE_H
#define __ARITHMETIQUE_H
int produit(int a, int b);
#endif
Je vous recommande de toujours appliquer cette derni`ere r`egle d`es
lors que vous cr´eez un fichier de header !
87
8.3 La compilation s´epar´ee
Ce n’est pas le tout de bien fragmenter son code en plusieurs fichiers, encore
faut-il les compiler pour obtenir un executable.
La m´ethode consiste `a g´en´erer un fichier objet par module (option -c de gcc) :
gcc -O3 -Wall -I. -c module_1.c
gcc -O3 -Wall -I. -c module_2.c
...
gcc -O3 -Wall -I. -c module_n.c
Ces commandes g´en`erent n fichiers objets module_i.o.
L’option -I de gcc permet d’ajouter un r´epertoire en premi`ere position de la
liste des r´epertoires o`u sont cherch´es les fichiers en-tˆete. (ici, le r´epertoire courant,
./ : l’option -I est donc en fait facultative dans ce cas). Cette option
est utile lorsque, pour des raisons de lisibilit´e dans l’architecture des fichiers
sources, un r´epertoire Include est cr´e´e pour contenir tous les fichiers en-tˆete
du programme. La compilation avec gcc comportera alors l’option -IInclude.
Une passe d’´edition de lien entre ces fichiers objets en ensuite n´ecessaire pour
g´en´erer l’ex´ecutable final toto.exe :
gcc -o toto.exe module_1.o module_2.o ... module_n.o
8.4 R´esum´e des r`egles de programmation modulaire
1. D´ecouper le programme en modules impl´ementant des fonctions similaires.
2. Chaque module se traduit en 2 fichiers sources :
– un fichier en-tˆete module.h qui d´efinit son interface, c’est `a dire le
prototype des fonctions du module qui sont export´ees. Plus pr´ecis´ement,
ce fichier se compose :
– des d´eclarations des fonctions d’interface (celles qui sont export´ees et
donc utilis´ees dans d’autres fichiers sources)
– d’´eventuelles d´efinitions de constantes symboliques et de macros.
– d’´eventuelles directives au pr´eprocesseur (inclusion d’autres fichiers,
compilation conditionnelle).
Ce fichier doit respecter ´egalement les conventions d’´ecritures propos´ees
au §8.2 pour ´eviter les erreurs de d´efinitions multiples.
– un fichier module.c contenant le corps des fonctions impl´ement´ees dans
ce module. Ce fichier se compose :
– de variables globales qui ne sont utilis´ees que dans le fichier module.c ;
– du corps des fonctions d’interface dont la d´eclaration se trouve dans
module.h ;
– d’´eventuelles fonctions locales `a module.c.
3. Le fichier module.h est inclus dans le fichier module.c (via la directive
#include) et dans tous les autres fichiers qui font appel aux fonctions
export´ees par ce module.
4. Chaque module est compil´e pour g´en´erer l’ex´ecutable final (voir 8.3).
88
Ces r`egles s’ajoutent aux r`egles g´en´erales d’´ecriture de programmes C abord´ees
au §1.6 page 9.
8.5 L’outils ’make’
Losrqu’un programme est fragment´e en plusieurs fichiers sources compil´es s´eparemment,
la proc´edure de compilation peut tr`es vite devenir longue et fastidieuse.
Il est alors extr`emement pratique de l’automatiser `a l’aide de l’utilitaire ’make’
d’Unix. Une bonne utilisation de make permet de r´eduire le temps de compilation
et ´egalement de garantir que celle-ci est effectu´ee correctement.
8.5.1 Principe de base
L’id´ee principale de make est d’effectuer uniquement les ´etapes de compilation
n´ecessaires `a la cr´eation d’un ex´ecutable. Par exemple, si un seul fichier source
a ´et´e modifi´e dans un programme compos´e de plusieurs fichiers, il suffit de
recompiler ce fichier et d’effectuer l’´edition de liens. Les autres fichiers sources
n’ont pas besoin d’ˆetre recompil´es.
La commande make recherche par d´efaut dans le r´epertoire courant un fichier
makefile ou Makefile. Ce fichier sp´ecifie les d´ependances entre les diff´erents
fichiers sources, objets et ex´ecutables.
Voyons tout de suite les principales options de cette commande (les plus utiles ;
pour plus de d´etails : man make)
– make -f <nom_fichier> : utiliser le fichier <nom_fichier> `a la place du
traditionnel fichier Makefile.
– make -C <path> : ex´ecuter le fichier Makefile situ´e dans le r´epertoire <path>.
8.5.2 Cr´eation d’un Makefile
Un fichier Makefile est compos´e d’une liste de r`egles de d´ependances entre
fichiers, de la forme :
cible: liste_de_d´ependances
<TAB> commandes_a_effectuer
La premi`ere ligne sp´ecifie un fichier cible (g´en´er´e par commandes_a_effectuer)
et la liste des fichiers dont il d´epend (s´epar´es par des espaces) : il s’agit donc
de la liste des ficiers requis pour la g´en´eration de cible.
Les lignes suivantes, qui doivent commencer par une tabulation (le caract`ere
TAB), indiquent les commandes `a ex´ecuter pour g´en´erer le fichier cible. Ces
commandes ne seront ex´ecut´ees que si le fichier cible n’ex´ecute pas ou si l’un
des fichiers de d´ependance est plus r´ecent que le fichier cible.
De fa¸con g´en´erale, l’´ecriture d’un fichier Makefile se rapproche largement de
l’´ecriture d’un script shell :
– On peut placer des commentaires, qui commencent par # :
#Ceci est un commentaire
Les commentaires s’´etendent sur une seule ligne (´equivalent du // en C99).
89
– On peut declarer des variables.
Une variable se d´eclare de la fa¸con suivante : SRC = machin.c
Ensuite, le contenu de cette variable (machin.c) est obtenu par l’appel $(SRC)
Ainsi, consid´erons l’exemple du programme effectuant le produit de deux entiers
pr´esent´e au §??. On souhaite g´en´erer l’ex´ecutable toto.
– Pour une compilation ”`a la main”et en suivant les recommendations du §1.1.4,
il faudrait ex´ecuter les commandes :
gcc -g3 -Wall -c produit.c
gcc -g3 -Wall -c main.c
gcc -g3 -Wall -o toto main.o produit.o
On l’aura compris : cela peut devenir tr`es vite long et lassant !
– Un premier fichier Makefile g´erant cette compilation pourrait ˆetre :
## Premier exemple de Makefile
# il faut generer l’executable toto
all: toto
# Le fichier produit.o d´epend de produit.c et de produit.h
produit.o: produit.c produit.h
gcc -g3 -Wall -c produit.c
# Le fichier main.o d´epend de main.c et de produit.h
main.o: main.c produit.h
gcc -g3 -Wall -c main.c
# Generation de l’ex´ecutable toto qui d´epend des fichiers objets
toto: main.o produit.o
gcc -g3 -Wall -o toto main.o produit.o
Moyennant l’´ecriture de ce fichier de configuration, il suffira ensuite d’ex´ecuter
la commande make pour lancer la compilation du programme toto !
8.5.3 Utilisation de macros et de variables
On suppose maintenant qu’on souhaite en plus que les fichiers de header soient
plac´es dans le r´epertoire include/ et les fichiers objets dans le r´epertoire obj/.
Les fichiers sources (.c) sont quand `a eux dans le r´epertoire courant et on
souhaite qu’il en soit de mˆeme pour l’ex´ecutable final (toto).
L’arborescence g´en´eral des fichiers et le graphe des d´ependances entre les fichiers
est expos´e dans la figure 8.1.
Avec une telle arborescence, on peut modifier le Makefile pr´ec´edent pour g´erer
la compilation du programme toto de la fa¸con suivante :
## Deuxieme exemple de Makefile - gestion d’une arborescence
all: toto
obj/produit.o: produit.c include/produit.h
gcc -g3 -Wall -Iinclude/ -c produit.c -o obj/produit.o
obj/main.o: main.c include/produit.h
gcc -g3 -Wall -Iinclude/ -c main.c -o obj/main.o
toto: obj/main.o obj/produit.o
90
obj/
.o
produit.o
.o
main.o
.c
main.c
.h
produit.h
.c
produit.c
toto
include/
projet/
produit.h
main.o
produit.o
main.c
produit.c
Makefile
toto
ARBORESCENCE GRAPHE DES DEPENDANCES
Exécutable
Objets
Fichiers
Sources
Fichiers
Fig. 8.1 – Arborescence et graphe de d´ependances entre les fichiers
gcc -g3 -Wall -o toto obj/main.o obj/produit.o
On voit qu’un certain nombre d’informations sont redondantes. En particulier,
si on change le nom d’un r´epertoire, il faut le changer `a plusieurs endroit dans
le Makefile d’ou l’id´ee de centraliser ce type d’informations. On utilise pour
cela des variables :
## Troisieme exemple de Makefile - gestion d’une arborescence
# Utilisation de variables
EXE = toto
OBJ_DIR = obj
H_DIR = include
OPT = -g3 -Wall
all: $(EXE)
$(OBJ_DIR)/produit.o: produit.c $(H_DIR)/produit.h
gcc $(OPT) -Iinclude/ -c produit.c -o $(OBJ_DIR)/produit.o
$(OBJ_DIR)/main.o: main.c $(H_DIR)/produit.h
gcc $(OPT) -Iinclude/ -c main.c -o $(OBJ_DIR)/main.o
$(EXE): $(OBJ_DIR)/main.o $(OBJ_DIR)/produit.o
gcc $(OPT) -o $(EXE) $(OBJ_DIR)/main.o $(OBJ_DIR)/produit.o
Mais la encore, on continue `a dupliquer un certain nombre d’informations. Pour
cela, il existe un certain nombre de variables pr´ed´efinies, dont les les plus utiles
sont r´esum´ees dans le tableau 8.1.
A partir de ces variables, on obtient une version beaucoup plus simple du Makefile
pr´ec´edent :
## Quatrieme exemple de Makefile - gestion d’une arborescence
# Utilisation des variables predefinies
EXE = toto
OBJ_DIR = obj
H_DIR = include
91
$@ Le fichier cible (par exemple : obj/main.o)
$* Le fichier cible sans suffixe (ex : obj/main)
$< Le premier fichier de la liste des d´ependances (par exemple : main.c)
$ ? L’ensemble des fichiers de d´ependances
Tab. 8.1 – Les principales variables pr´ed´efinies dans un Makefile
OPT = -Wall -g3
all: $(EXE)
$(OBJ_DIR)/produit.o: produit.c $(H_DIR)/produit.h
gcc $(OPT) -I$(H_DIR)/ -c $< -o $@
$(OBJ_DIR)/main.o: main.c $(H_DIR)/produit.h
gcc $(OPT) -I$(H_DIR)/ -c $< -o $@
$(EXE): $(OBJ_DIR)/main.o $(OBJ_DIR)/produit.o
gcc $(OPT) -o $(EXE) $?
A noter que pour d´eterminer facilement les d´ependances entre les diff´erents
fichiers, on peut utiliser l’option -MM de gcc. Par exemple,
% gcc -MM -Iinclude/ *.c
main.o: main.c include/produit.h
produit.o: produit.c include/produit.h
On rajoute habituellement dans un fichier Makefile une cible appel´ee clean
permettant de d´etruire tous les fichiers objets et une cible distclean qui supprime
´egalement les ex´ecutables g´en´er´es lors de la compilation.
clean:
rm -f $(OBJ_DIR)/*.o
distclean: clean
rm -f $(EXE)
La commande make clean permet donc de ”nettoyer” le r´epertoire courant. Notons
que l’on utilise ici la commande rm avec l’option -f qui ´evite l’apparition
d’un message d’erreur si le fichier `a d´etruire n’existe pas.
Il y aurait encore beaucoup de choses `a dire sur les Makefile mais cela d´epasse
largement le cadre de ce polycopi´e. La meilleur documentation `a ce sujet se
trouve `a l’URL http://www.gnu.org/software/make/manual/make.html.
Pour conclure, on notera que tous les exemples de Makefile propos´es doivent
ˆetre modifi´es d`es lors qu’un nouveau fichier est ajout´e au projet. Un exemple
de Makefile g´en´erique (dans le sens o`u il n’a pas besoin d’ˆetre modifi´e lors de
l’ajout de nouveau fichiers) est fourni an annexe B page 112. Pour r´eutiliser ce
Makefile dans n’importe quel projet, il suffit d’adapter les variables $(INCL),
$(DIR_OBJ) et $(EXE). Pour plus d’informations, il suffit de taper make help.
92
Chapitre 9
La biblioth`eque standard
Ce chapitre est un aide-m´emoire qui donne la liste exhaustive de toutes les
fonctions de la biblioth`eque standard, accompagn´ees la plupart du temps de
leurs prototypes.
Les fichiers d’en-tˆete de la librairie ANSI sont les suivants (ceux correspondants
`a de nouvelles fonctionnalit´es introduites dans la norme ANSI C99 sont
indiqu´ees par un ast´erisque entre parenth`eses (*).
assert.h inttypes.h(*) signal.h stdlib.h
complex.h(*) iso646.h(*) stdarg.h string.h
ctype.h limits.h stdbool.h(*) tgmath.h(*)
errno.h locale.h stddef.h time.h
fenv.h(*) math.h stdint.h(*) wchar.h(*)
float.h setjmp.h stdio.h wctype.h(*)
De mani`ere g´en´erale, pour obtenir le maximum d’informations sur une commande,
on utilisera la commande man.
9.1 Diagnostics d’erreurs <assert.h>
Cette biblioth`eque ne fournit qu’une seule fontion, assert, qui permet de mettre
des assertions dans le source du programme. Son prototype est le suivant :
void assert(int expression);
`A
l’ex´ecution, si le param`etre de assert s’´evalue `a faux, le programme est
stopp´e sur terminaison anormale. Exemple :
#include <assert.h>
#include <stdio.h>
#define TAILLE 512
int calcul(int tab[], int i) {
assert(i >= 0 && i < TAILLE);
return 2 * tab[i] + 5;
}
93
int main() {
int tableau[TAILLE];
#ifndef NDEBUG
printf("DEBUG: tableau[0] = %d ", tableau[0]);
#endif
printf("%d ", calcul(tableau, 1024));
return 0;
}
L’ex´ecution de ce programme renverra :
DEBUG: tableau[0] = 180613
a.out: toto.c:7: calcul: Assertion ‘i >= 0 && i < 512’ failed.
Abandon
9.2 Gestion des nombres complexes <complex.h>
Cette biblioth`eque permet de g´erer les nombres complexes, de la forme
z = a + i ∗ b, o`u a, b ∈ R. On rappelle que a est la partie r´eelle de z, b sa partie
imaginaire et i le nombre imaginaire i = √−1 (i2 = −1).
man complex pour des exemples de code utilisant ces nombres complexes.
9.3 Classification de caract`eres et changements de
casse <ctype.h>
Toutes ces fonctions permettent de tester une propri´et´e sur le caract`ere pass´e
en param`etre :
Fonction Prototype Teste si le param`etre est
isalnum int isalnum(int c) une lettre ou un chiffre
isalpha int isalpha(int c) une lettre
isblank int isblank(int c) un espace ou une tabulation
iscntrl int iscntrl(int c) un caract`ere de commande
isdigit int isdigit(int c) un chiffre d´ecimal
isgraph int isgraph(int c) un caract`ere imprimable ou le blanc
islower int islower(int c) une lettre minuscule
isprint int isprint(int c) un caract`ere imprimable (pas le blanc)
ispunct int ispunct(int c) un signe de ponctuation
isspace int isspace(int c) un caract`ere d’espace blanc
isupper int isupper(int c) une lettre majuscule
isxdigit int isxdigit(int c) un chiffre hexad´ecimal
On dispose ´egalement de deux fonctions de conversions majuscules/minuscules :
Fonction Prototype Action
tolower int tolower(int c) convertit c en minuscule
toupper int toupper(int c) convertit c en majuscule.
94
9.4 Valeur du dernier signal d’erreur <errno.h>
Cette biblioth`eque ne d´efinit qu’une variable globale,
extern int errno;
dont la valeur est un num´ero d’erreur affect´e lors d’appels syst`emes (et par
certaines fonctions de librairie). Cette valeur n’a de sens que si l’appel syst`eme
s’est mal pass´e et retourne une erreur (la valeur -1). Dans ce cas, la variable
errno indique quel type d’erreur s’est produite.
man errno fournit la liste des valeurs d’erreurs valides.
9.5 Gestion d’un environnement `a virgule flottante
<fenv.h>
Le standard C99 a introduit via <fenv.h> la notion d’environnement `a vir-
gule flottante afin de permettre une repr´esentation plus d´etaill´ee des conditions
d’erreurs dans l’arithm´etique en virgule flottante.
Deux variables syst`emes sont d´efinies dans cet environnement :
1. les flags de statut, utilis´es dans la gestion des exceptions en virgule flottante.
2. les modes de contrˆole qui d´eterminent les diff´erents comportements de
l’arithm´etique en virgule flottante (la m´ethode d’arrondi utilis´ee par exemple).
9.5.1 Gestion des exceptions
Toute erreur (exception) en virgule flottante est caract´eris´ee par une constante
symbolique :
– FE_DIVBYZERO : division par 0 ;
– FE_INEXACT : le r´esultat de l’op´eration n’est pas exact ;
– FE_INVALID : r´esultat ind´efini ;
– FE_OVERFLOW : d´epassement de capacit´e ;
– FE_UNDERFLOW : sous-d´epassement de capacit´e.
La constante FE_ALL_EXCEPT est un OU bit-`a-bit de toutes les constantes pr´ec
´edentes.
Les fonctions suivantes sont utilis´ees pour g´erer ces exceptions. Toutes (`a l’exception
de fetestexcept) retournent 0 en cas de succ`es. L’argument excepts
indique l’une des constantes pr´ec´edentes.
Fonction Prototype Action
feclearexcept int feclearexcept(int excepts) Efface les exceptions excepts
fegetexceptflag int fegetexceptflag(fexcept_t
*flagp, int excepts)
Sauvegarde le statut d’exception
sp´ecifi´e dans *flagp
feraiseexcept int feraiseexcept(int excepts) D´eclenche l’exception expects
fesetexceptflag int fesetexceptflag(fexcept_t
*flagp, int excepts)
Sauvegarde le statut de l’exception
expects dans *flagp
fetestexcept int fetestexcept(int excepts) renvoie les bits correspondants
aux exceptions courantes
95
9.5.2 Gestion des arrondis
fenv.h supporte quatre m´ethodes d’arrondis, caract´eris´ees par les constantes
symboliques suivantes :
– FE_DOWNWARD : arrondi vers la valeur inf´erieure la plus proche ;
– FE_TONEAREST : arrondi vers la valeur la plus proche ;
– FE_TOWARDZERO : partie enti`ere ;
– FE_UPWARD : arrondi vers la valeur sup´erieure la plus proche.
Les fonctions int fegetround() et int fesetround( int rounding_mode)
permettent de lire et de changer la m´ethode d’arrondi courante.
9.5.3 Gestion des environnements en virgule flottante.
L’environnement de travail en virgule flottante peut ˆetre manipul´e via les fonctions
suivantes sous forme d’un seul objet opaque de type fenv_t. L’environnement
par d´efaut est repr´esent´e par la constante FE_DFL_ENV.
Fonction Prototype Action
fegetenv int fegetenv(fenv_t *envp) Sauve l’environnement courant dans *envp
feholdexcept int feholdexcept(fenv_t
*envp)
idem, et efface tous les flags d’exceptions et
continue en mode ”sans interruption”
fesetenv int fesetenv(fenv_t *envp) recharge l’environnement `a *envp
feupdateenv int feupdateenv(fenv_t
*envp)
idem et replace les exceptions d´ej`a pr´esentes
dans *envp
9.6 Intervalle et pr´ecision des nombres flottants <float.h>
float.h d´efinit des constantes symboliques qui caract´erisent les types r´eels, en
particulier :
FLT_DIG nombre de chiffres significatifs d’un float
DBL_DIG nombre de chiffres significatifs d’un double
LDBL_DIG nombre de chiffres significatifs d’un long double
FLT_EPSILON le plus petit nombre tel que 1.0 + x != 1.0
DBL_EPSILON le plus petit nombre tel que 1.0 + x != 1.0
LDBL_EPSILON le plus petit nombre tel que 1.0 + x != 1.0
FLT_MAX le plus grand nombre du type float
DBL_MAX le plus grand nombre du type double
LDBL_MAX le plus grand nombre du type long double
FLT_MIN le plus petit nombre du type float
DBL_MIN le plus petit nombre du type double
LDBL_MIN le plus petit nombre du type long double
9.7 D´efinitions de types entiers de taille fix´ee <inttypes.
h>
La biblioth`eque <inttypes.h> d´efinit au moins les types suivants :
96
int8_t type entier sign´e sur 8 bits
int16_t type entier sign´e sur 16 bits
int32_t type entier sign´e sur 32 bits
int64_t type entier sign´e sur 64 bits
uint8_t type entier non sign´e sur 8 bits
uint16_t type entier non sign´e sur 16 bits
uint32_t type entier non sign´e sur 32 bits
uint64_t type entier non sign´e sur 64 bits
intptr_t entier sign´e pouvant stocker un pointeur (une adresse)
uintptr_t entier non sign´e pouvant stocker un pointeur (une adresse)
En pratique, ces types sont d´efinis dans stdint.h (voir §9.17).
9.8 Alias d’op´erateurs logiques et binaires <iso646.h>
<iso646.h> d´efinit des alias (synonymes) aux op´erateurs binaires et logiques :
and ⇐⇒ && and_eq ⇐⇒ &=
bitand ⇐⇒ & bitor ⇐⇒ |
compl ⇐⇒ ~ not ⇐⇒ !
not_eq ⇐⇒ != or ⇐⇒ ||
or_eq ⇐⇒ |= xor ⇐⇒ ^
xor_eq ⇐⇒ ^=
9.9 Intervalle de valeur des types entiers <limits.h>
Le fichier <limits.h> d´efinit les constantes symboliques qui pr´ecisent les valeurs
maximales et minimales qui peuvent ˆetre repr´esent´ees par un type entier donn´e.
Type Minimum Maximum Maximum (types non-sign´es)
char CHAR_MIN CHAR_MAX UCHAR_MAX
signed char SCHAR_MIN SCHAR_MAX
short SHRT_MIN SHRT_MAX USHRT_MAX
int INT_MIN INT_MAX UINT_MAX
long LONG_MIN LONG_MAX ULONG_MAX
long long LLONG_MIN LLONG_MAX ULLONG_MAX
<limits.h> d´efinit ´egalement la constanste CHAR_BIT, correspondant au nombre
de bits du type char.
9.10 Gestion de l’environnement local <locale.h>
Il y a deux fonctions permettant de g´erer les conventions nationales concernant
l’´ecriture du point d´ecimal dans les nombres, le signe repr´esentant l’unit´e mon
´etaire etc... Ces fonctions sont setlocale et localeconv (faire un man sur ces
fonctions pour plus de d´etails).
97
9.11 Les fonctions math´ematiques de <math.h>
Pour utiliser les fonctions de cette librairie, il faut en plus de l’inclusion de la
librairie par la directive #include <math.h> ajouter l’option -lm a l’´edition de
lien. Ainsi, la compilation de prog.c utilisant la librairie <math.h> se fera par
la commande : gcc -g3 -Wall -lm prog.c -o prog
Le r´esultat et les param`etres de toutes ces fonctions sont de type double. Si
les param`etres effectifs sont de type float, ils seront convertis en double par le
compilateur.
9.11.1 Fonctions trigonom´etriques et hyperboliques
Fonction S´emantique Fonction S´emantique
acos arc cosinus cos cosinus
asin arc sinus sin sinus
atan arc tangente tan tangente
cosh cosinus hyperbolique sinh sinus hyperbolique
tanh tangente hyperbolique atan2 arc tangente
9.11.2 Fonctions exponentielles et logarithmiques
Fonction S´emantique
exp exponentielle
frexp ´etant donn´e x, trouve n et p tels que x = n * 2p
ldexp multiplie un nombre par une puissance enti`ere de 2
log logarithme
log10 logarithme d´ecimal
modf calcule partie enti`ere et d´ecimale d’un nombre
9.11.3 Fonctions diverses
Fonction S´emantique
ceil entier le plus proche par les valeurs sup´erieures
fabs valeur absolue
floor entier le plus proche par les valeurs inf´erieures
fmod reste de division
pow puissance
sqrt racine carr´ee
9.12 Branchements non locaux <setjmp.h>
L’instruction goto pr´esent´ee au §2.2.6 ne permet de r´ealiser des branchements
qu’au sein d’une mˆeme proc´edure. Pour r´ealiser des branchements `a l’ext´erieur
d’une proc´edure, il faut utiliser setjmp et longjmp.
Prototype de ces fonctions :
int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);
Plus pr´ecis´ement, les fonctions setjmp et longjmp permettent de g´erer les erreurs
et les interruptions rencontr´ees dans des routines bas-niveau. setjmp sau-
98
vegarde le contexte de pile et d’environnement dans env afin de l’utiliser ult´erieurement
avec longjmp.
Pour plus de d´etails, utiliser le man sur ces fonctions.
9.13 Manipulation des signaux <signal.h>
Lorsqu’un ´ev`enement exceptionnel se produit, le syst`eme d’exploitation peut
envoyer un signal aux processus. Ceci peut arriver en cas d’erreur grave (une
erreur d’adressage par exemple). Le tableau suivant fournit la liste de quelques
constantes symboliques d´efinies dans <signal.h> et caract´erisant ces erreurs :
Signal Signification
SIGABRT Fin anormale du programme (ex : utilisation de la fonction abort())
SIGFPE Exception `a la virgule flottante.
SIGILL Instruction ill´egale rencontr´ee dans le code machine.
SIGINT Interruption par la touche Ctrl-C.
SIGSEGV Violation de Segmentation : acc`es ill´egal `a la m´emoire.
SIGTERM Terminer : requˆete pour terminer le programme.
Deux fonctions permettent d’interagir avec le m´ecanisme des signaux :
Fonct. Prototype Action
raise int raise(int sig) Envoie le signal sig au processus ex´ecutant.
signal typedef void (*sigh_t)(int);
sigh_t signal(int signum,
sigh_t handler);
pr´ecise la r´eponse du programme au signal
signum. handler est un pointeur vers la fonction
effectuant le traitement de ce signal.
Comme toujours, le man reste la meilleur source d’informations sur ce sujet.
9.14 Nombre variable de param`etres <stdarg.h>
Si on d´esire programmer une fonction avec un nombre variable de param`etres
(comme printf), on dispose des macros va_start, va_arg et va_end.
L’utilisation de ces macros et du type associ´e va_list est abord´e en d´etail dans
le §5.7 page 67.
9.15 D´efinition du type bool´een <stdbool.h>
La librairie <stdbool.h>, ajout´ee avec la norme ANSI C99, introduit la notion
de bool´een et d´efinit le type bool´een bool et les valeurs associ´ees, true et false.
9.16 D´efinitions standards <stddef.h>
Cette librairie introduit un ensemble de types pr´ed´efinis tr`es utiles qui sont
list´es dans le tableau suivant :
Type Description
ptrdiff_t Diff´erence entre deux pointeurs (≃ int en g´en´eral)
wchar_t Employ´e pour les caract`eres ´etendus (voir §9.23)
size_t Taille d’un objet en nombres d’octets (≃ unsigned int en g´en´eral)
99
<stddef.h> d´efinit ´egalement le pointeur NULL, utilis´e lors de l’initialisation des
pointeurs (cf chapitre 3) ainsi que la fonction :
size_t offsetof(type, member-designator)
qui permet de connaˆıtre le d´ecalage (en nombre d’octets) entre un champ d’une
structure (member-designator) et le d´ebut de cette structure (type). Ainsi,
si on suppose d´efinie la structure toto ayant un champ next (voir §4.3.5), le
d´ecalage du champ data est donn´e par l’expression :
offsetof (struct toto, data)
9.17 D´efinitions de types entiers <stdint.h>
La libraisie <stdint.h> est un sous ensemble de la librairie <inttypes.h> (voir
§9.7) et en constitue donc une version all´eg´ee, adapt´ee aux environnements embarqu
´es qui peuvent ne pas supporter les fonctions d’entr´ees/sorties formatt´ees.
9.18 Entr´ees-sorties <stdio.h>
9.18.1 Manipulation de fichiers
Fonction Prototype Action (voir chapitre 6)
fopen FILE *fopen(char *path, char *mode) ouverture d’un fichier
fclose int fclose(FILE *fp) Fermeture d’un fichier
fflush int fflush(FILE *stream) ´ecriture des buffers en m´emoire dans le fichier
tmpfile FILE *tmpfile (void) cr´eation d’un fichier temporaire
9.18.2 Entr´ees et sorties format´ees
Fonction Prototype Action (voir §2.5/chapitre 6)
fprintf int fprintf(FILE *stream, char *format, ...) ´ecriture sur un fichier
fscanf int fscanf(FILE *stream, char *format, ...) lecture depuis un fichier
printf int printf(char *format, ...) ´ecriture sur la sortie standard
scanf int scanf(char *format, ...) lecture depuis l’entr´ee standard
sprintf int sprintf(char *s, char *format, ...) ´ecriture dans la chaˆıne de caract`eres s
sscanf int sscanf(char *s, char *format, ...) lecture depuis la chaˆıne de caract`eres s
9.18.3 Impression et lecture de caract`eres
Fonction Prototype Action
fgetc int fgetc(FILE *stream) lecture d’un caract`ere depuis un fichier
fputc int fputc(int c, FILE *stream) ´ecriture d’un caract`ere sur un fichier
getc int getc(FILE *stream) ´equivalent de fgetc mais optimis´ee
putc int putc(int c, FILE *stream) ´equivalent de fputc mais optimis´ee
getchar int getchar(void) lecture d’un caract`ere depuis stdin
putchar int putchar(int c) ´ecriture d’un caract`ere sur stdout
fgets char *fgets(char *s, FILE *stream) lecture d’une chaˆıne de caract`eres depuis un fichier
fputs int *fputs(char *s, FILE *stream) ´ecriture d’une chaˆıne de caract`eres sur un fichier
gets char *gets(char *s) lecture d’une chaˆıne de caract`eres sur stdin
puts int *puts(char *s) ´ecriture d’une chaˆıne de caract`eres sur stdout
100
9.19 Utilitaires divers <stdlib.h>
9.19.1 Allocation dynamique
Ces fonctions sont d´ecrites dans le chapitre 3 et au §3.5.
Fonct. Prototype Action
calloc void *calloc(size_t n, size_t size) allocation dynamique de n*size octects
et initialisation `a z´ero.
malloc void *malloc(size_t size) allocation dynamique
realloc void *realloc(void *ptr, size_t size) modifie la taille d’une zone pr´ealablement
allou´ee par calloc ou malloc
free void free(void *ptr) lib`ere une zone m´emoire
9.19.2 Conversion de chaˆınes de caract`eres en nombres
Les fonctions suivantes permettent de convertir une chaˆıne de caract`eres en un
nombre.
Fonct. Prototype Action
atof double atof(char *chaine) convertit chaine en un double
atoi int atoi(char *chaine) convertit chaine en un int
atol long atol(char *chaine) convertit chaine en un long int
Il semblerait que qu’il faille pr´ef´erer maintenant les fonctions strtol, strtoll
et strtoq.
9.19.3 G´en´eration de nombres pseudo-al´eatoires
La fonction rand fournit un nombre entier pseudo-al´eatoire dans l’intervalle
[0,RAND_MAX]. RAND_MAX est une constante pr´ed´efinie au moins ´egale `a 215 −1.
L’al´ea fournit par la fonction rand n’est toutefois pas de tr`es bonne qualit´e.
La valeur retourn´ee par rand d´epend de l’initialisation (germe ou seed en anglais)
du g´en´erateur. Cette derni`ere est ´egale `a 1 par d´efaut mais elle peut ˆetre
modifi´ee `a l’aide de la fonction srand.
Fonct. Prototype Action
rand int rand(void) fournit un nombre pseudo-al´eatoire
srand void srand(unsigned int seed) modifie la valeur du germe du g´en´erateur
pseudo-al´eatoire
On utilise souvent la valeur de l’horloge interne de l’ordinateur (obtenue `a l’aide
de la librairie <time.h>, voir §9.22) pour fournir un ”bon”1 germe `a srand.
Exemple :
#include <stdlib.h> //see also ’man 3 rand’
#include <time.h>
/* initialize random generator */
void init_rand(){ srand(time(NULL)); }
1Pas suffisamment bon cependant pour les applications cryptographiques
101
/* return a random number between 1 and m */
unsigned long myRand(unsigned long m){
return 1+(unsigned long)(((double) m)*rand()/(RAND_MAX+1.0));
}
9.19.4 Arithm´etique sur les entiers
Fonct. Prototype Action
abs int abs(int n) valeur absolue d’un entier
labs long labs(long n) idem mais pour un long int
div div_t div(int a, int b) quotient et reste de la division euclidienne de
a par b.
ldiv ldiv_t ldiv(long a, long b) idem pour les long int
Les structures div_t et ldiv_t disposent de deux champs, quot et rem, qui
stockent les r´esultats des fonctions div et ldiv.
9.19.5 Recherche et tri
Les fonctions qsort (resp. bsearch) permettent de trier un tableau (resp. rechercher
un ´el´ement dans un tableau d´ej`a tri´e). Pour leurs syntaxes : voir §5.8.
9.19.6 Communication avec l’environnement
Fonct. Prototype Action
abort void abort(void) terminaison anormale du programme
exit void exit(int etat) terminaison du programme ; rend le contrˆole au syst`eme
en lui fournissant la valeur etat (0 = fin normal).
system int system(char *s) ex´ecution de la commande syst`eme d´efinie par s.
102
9.20 Manipulation de chaˆınes de caract`eres <string.h>
Fonct. Prototype Action
strcpy char *strcpy(char *ch1,char *ch2) copie ch2 dans ch1 ; retourne ch1.
strncpy char *strcpy(char *ch1,char *ch2,
int n)
idem mais ne copie que n caract`eres.
strcat char *strcat(char *ch1,char *ch2) copie ch2 `a la fin de ch1 ; retourne ch1.
strncat char *strncat(char *ch1,char *ch2,
int n)
idem mais seuls n caract`eres sont copi´es
strcmp int strcmp(char *ch1,char *ch2) compare ch1 et ch2 pour l’ordre lexicographique
;
strncmp int strcmp(char *ch1,char *ch2,
int n)
idem mais seuls n caract`eres sont compar´es.
strchr char *strchr(char *ch,char c) retourne un pointeur sur la premi`ere occurence
de c dans ch et NULL si c /∈ ch.
strrchr char *strchr(char *chaine,char c) retourne un pointeur sur la derni`ere occurence
de c dans ch et NULL si c /∈ ch
strstr char *strchr(char *ch1,char *ch2) retourne un pointeur sur la premi`ere occurence
de ch2 dans ch1 et NULL si ch2 /∈ ch1
strlen int strlen(char *chaine) retourne la longueur de chaine.
La librairie <string.h> fournit un ensemble de fonctions permettent de g´erer
les chaˆınes de caract`eres pour :
– copier : memcpy, memmove, strcpy, strncpy ;
– concat´ener : strcat, strncat ;
– comparer : memcmp, strcmp, strcoll, strncmp ;
– transformer : strxfrm ;
– rechercher : memchr, strchr, strcspn, strpbrk, strrchr, strspn, strstr,
strtok ;
– initialiser : memset ;
– mesurer : strlen ;
– obtenir un message d’erreur `a partir du num´ero de l’erreur : strerror.
Concernant strcmp, on notera que le r´esultat de cette fonction est
– une valeur n´egative si ch1 est inf´erieure `a ch2 (pour l’ordre lexicographique) ;
– 0 si ch1 et ch2 sont identiques ;
– une valeur positive sinon.
9.21 Macros g´en´eriques pour les fonctions math´ema-
tiques <tgmath.h>
Cette librairie, introduite avec la norme ANSI C99, est un sur-ensemble des
librairies <math.h> et de <complex.h> et d´efinit pour chacune des fonctions
communes `a ces deux librairie une version similaire d´efinie sous forme de macro
pour un type g´en´erique. La liste de ces fonctions est donn´ee dans le tableau
suivant :
103
<math.h> <complex.h> Type-g´en´erique
Fonction Function Macro
acos() cacos() acos()
asin() casin() asin()
atan() catan() atan()
acosh() cacosh() acosh()
asinh() casinh() asinh()
atanh() catanh() atanh()
cos() ccos() cos()
sin() csin() sin()
tan() ctan() tan()
cosh() ccosh() cosh()
sinh() csinh() sinh()
tanh() ctanh() tanh()
exp() cexp() exp()
log() clog() log()
pow() cpow() pow()
sqrt() csqrt() sqrt()
fabs() cabs() fabs()
Pour un compl´ement d’informations :
http://www.opengroup.org/onlinepubs/009695399/basedefs/tgmath.h.html
9.22 Date et heure <time.h>
Plusieurs fonctions permettent d’obtenir la date et l’heure. Le temps est repr
´esent´e par des objets de type time_t ou clock_t, lesquels correspondent
g´en´eralement `a des int ou `a des long int. Les fonctions de manipulation de
la date et de l’heure sont : clock, difftime, mktime, time, asctime, ctime,
gmtime, localtime, strftime.
Les plus utiles sont list´ees dans le tableau suivant :
Fonct. Prototype Action
time time_t time(time_t *tp) retourne dans *tp le nombre de secondes ´ecoul´ees
depuis le 1er janvier 1970, 0 heures G.M.T.
difftime double difftime(time_t t1,
time_t t2)
retourne la diff´erence t1 - t2 en secondes.
ctime char *ctime(time_t *tp) convertit *tp en une chaˆıne de caract`eres explicitant
la date et l’heure (format pr´ed´etermin´e).
clock clock_t clock(void) renvoit le temps CPU (ms) depuis le dernier
clock.
9.23 Manipulation de caract`eres ´etendus <wchar.h>
et <wctype.h>
Le type char est cod´e sur 8 bits et le restera parce qu’il d´esigne la plus petite
unit´e de donn´ees adressable.
Dans le cadre du design d’applications multilingues, on devra donc g´erer des
caract`eres et des chaˆınes au format UNICODE.
104
A cet effet, la norme ANSI C99 a introduit :
– un type de caract`ere cod´e sur 16 bits wchar_t ;
– un ensemble de fonctions similaires `a celles contenues dans <string.h> et
<ctype.h> (d´eclar´ees respectivement dans <wchar.h> et <wctype.h>) ;
– un ensemble de fonctions de conversion entre les types char * et wchar_t *
(d´eclar´ees dans <stdlib.h>).
Pour plus de d´etails :
http://www.opengroup.org/onlinepubs/007908799/xsh/wchar.h.html ou
http://www.freenix.fr/unix/linux/HOWTO/Unicode-HOWTO-5.html
105
Annexe A
Etude de quelques exemples
A.1 Gestion des arguments de la ligne de commande
Le code source suivant traite l’exemple pr´esent´e au §4.2.6 page 50.
Commande de compilation : gcc -O3 -Wall commande.c -o commande
/**
* @file command.c
* @author Sebastien Varrette <Sebastien.Varrette@imag.fr>
* @date Tue Oct 4 2005
*
* @brief see command.h
traitement des arguments de la ligne
de commande utilisant la librairie <getopt.h>
Exemple trait´e: programme ’command’ au format d’appel
suivant (les ´el´ements entre crochets sont optionnels):
./command [-i arg_i] [-f arg_f] [-s arg_s] [-k arg_k] [-h] [-V] arg_file
Les options de la ligne de commande sont donc:
-i: permet de sp´ecifier la valeur de arg_i, de type int
Valeur par d´efaut: 14
-f: permet de sp´ecifier la valeur de arg_f, de type float
Valeur par d´efaut: 1.5
-s: permet de sp´ecifier la valeur de arg_s, une cha^ıne de caract`eres
poss´edant au plus MAX_SIZE caract`eres
Valeur par d´efaut: "On est super content!"
-h: affiche l’aide (auteur, but du programme et usage) et sort du
programme.
-V: affiche la version du programme et sort
arg_file est un param`etre obligatoire du programme qui sp´ecifie
la valeur de la cha^ıne de caract`eres arg_file.
*/
/********************************************************************************/
#include <stdio.h> /* for printf */
#include <stdlib.h> /* for exit */
#include <assert.h> /* for assert */
#include <string.h>
#include <getopt.h>
//#include "toto.h"
106
#define VERSION 0.1 // source version
#define MAX_SIZE 256 // maximal size of strings, including ’’
void printHelp(char * command); // print help message
void printError(char * error_message, char * command); // print error message
void printVersion(char * command); // print version
/**
* Entry point where the program begins its execution.
* @param argc number of arguments of the command line
* @param argv multi-array containing the command line arguments
* (argv[0] is the name of the program, argv[argc]=NULL)
* @return status of the execution (0: correct execution)
*/
int main(int argc, char * argv[]){
// Command line management
extern char *optarg; // specific to getopt
extern int optind; //, opterr, optopt; // id.
char c;
// to deal with options
char opt_i[MAX_SIZE] = "";
char opt_f[MAX_SIZE] = "";
long arg_i = 14;
double arg_f = 1.5;
char arg_s[MAX_SIZE] = "On est super content!";
char arg_file[MAX_SIZE] = "";
// Management of parameters in command line
while ((c = getopt(argc, argv, "i:f:s:hV")) != -1) {
switch(c) {
case ’h’: printHelp(argv[0]); return EXIT_SUCCESS;
case ’V’: printVersion(argv[0]); return EXIT_SUCCESS;
case ’i’:
assert(strlen(optarg) < MAX_SIZE); // check size
strncpy(opt_i,optarg,MAX_SIZE); // copy current arg to opt_i
// Minimal check for validity (opt_i isn’t a command line option)
if (opt_i[0] == ’-’) printError("Bad Format for arg_i!",argv[0]);
arg_i = strtol(opt_i, NULL, 10); // last arg: base for convertion
break;
case ’f’:
assert(strlen(optarg) < MAX_SIZE); // check size
strncpy(opt_f,optarg,MAX_SIZE); // copy current arg to opt_f
// Minimal check for validity (opt_f isn’t a command line option)
if (opt_f[0] == ’-’) printError("Bad Format for arg_f!",argv[0]);
arg_f = strtod(opt_f, NULL); // conversion
break;
case ’s’:
assert(strlen(optarg) < MAX_SIZE); // check size
strncpy(arg_s,optarg,MAX_SIZE); // copy current arg to arg_s
if (arg_s[0] == ’-’) printError("Bad Format for arg_i!",argv[0]);
break;
default: printError("Bad Format!",argv[0]);
}
107
}
// Now proceed to detect errors and/or set the values
// parameter arg_file is required. If not, print error and exit
// argc - optind == 0 : no arg_file
// argc - optind > 1 : to many parameters or error not detected
if ((argc - optind) != 1) printError("Bad Format",argv[0]);
// required parameter: arg_file
assert(strlen(argv[optind]) < MAX_SIZE);
strncpy(arg_file,argv[optind],MAX_SIZE);
//Print values
printf("arg_i = %ld ",arg_i);
printf("arg_f = %f ",arg_f);
printf("Valeur de arg_s: %s ",arg_s);
printf("Valeur de arg_file: %s ",arg_file);
return EXIT_SUCCESS;
}
/**
* Print the help message
* @param command used (argv[0])
*/
void printHelp(char * command) {
printf("NAME " );
printf(" %s ",command );
printf(" SYNOPSIS " );
printf(" %s [-h] [-V] ",command );
printf(" %s [-i arg_i] [-f arg_f] [-s arg_s] arg_file ",command );
printf(" DESCRIPTION " );
printf(" -h : print help and exit " );
printf(" -V : print version and exit " );
printf(" -i arg_i : set arg_i (long) " );
printf(" Default value : 14 " );
printf(" -f arg_f : set arg_f (float) " );
printf(" Default value : 1.5 " );
printf(" -s arg_s : set arg_s (char *), a string with at most MAX_SIZE ");
printf(" characters. Default value : \"Toto\" " );
printf(" AUTHOR " );
printf(" Sebastien Varrette <Sebastien.Varrette@imag.fr> " );
printf(" Web page : http://www-id.imag.fr/~svarrett/ " );
printf(" REPORTING BUGS " );
printf(" Please report bugs to <Sebastien.Varrette@imag.fr> " );
printf(" COPYRIGHT " );
printf(" This is free software; see the source for copying conditions. ");
printf(" There is NO warranty; not even for MERCHANTABILITY or FITNESS ");
printf(" FOR A PARTICULAR PURPOSE." );
printf(" SEE ALSO " );
printf(" http://www-id.imag.fr/~svarrett/perso.html " );
}
/**
* print error message on stderr
108
* @param error_message Error message to print
* @param command command used (argv[0])
*/
void printError(char * error_message, char * command) {
fprintf(stderr, "[ERROR] %s ",error_message);
fprintf(stderr, "Use ’%s -h’ for help ", command);
exit(EXIT_FAILURE);
}
/**
* print version
* @param command used (argv[0])
*/
void printVersion(char * command) {
fprintf(stderr, "This is %s version %f ",command,VERSION);
fprintf(stderr, "Please type ’%s -h’ for help ", command);
}
109
A.2 Exemple de liste chaˆın´ee
Le code source suivant traite l’exemple pr´esent´e au §4.3.5 page 53.
Commande de compilation : gcc -O3 -Wall toto.c -o toto
/*********************************************************
Fichier: toto.c
Auteur: Sebastien Varrette <Sebastien.Varrette@imag.fr>
Un exemple de gestion des listes chain´ees
*********************************************************/
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
/* D´efinition de la structure toto*/
struct toto {
int data;
struct toto *next;
};
//type liste synonyme du type pointeur vers une struct toto
typedef struct toto *liste;
/**************************************
* Ins`ere un ´el´ement en t^ete de liste *
**************************************/
liste insere(int element, liste Q) {
liste L;
L = (liste)malloc(sizeof(struct toto)); // allocation de la zone
// m´emoire nec´essaire
L->data = element;
L->next = Q;
return L;
}
/***************************************
* Ins`ere un ´el´ement en queue de liste *
***************************************/
liste insereInTail(int element, liste Q) {
liste L, tmp=Q;
L = (liste)malloc(sizeof(struct toto)); // allocation de la zone
// m´emoire nec´essaire
L->data = element;
L->next = NULL;
if (Q == NULL)
return L;
// maintenant tmp=Q est forcement non vide => tmp->next existe
while (tmp->next != NULL) tmp = tmp->next; // deplacement jusqu’au
// dernier elt de la liste
tmp->next = L;
return Q;
}
/***************************************
* Supprime l’´el´ement en t^ete de liste *
***************************************/
liste supprime_tete(liste L) {
liste suivant = L;
if (L != NULL) { // pour etre s^ur que L->next existe
suivant= L->next;
110
free(L); //lib´eration de l’espace allou´e pour une cellule
}
return suivant;
}
/*****************************
* Supprime toute la liste L *
*****************************/
liste supprime(liste L) {
while (L != NULL)
L = supprime_tete(L); //suppression de la t^ete de liste
return L;
}
/************************************
* Affiche le contenu de la liste L *
************************************/
void print_liste(liste L) {
liste tmp = L;
while (tmp != NULL) { //on aurait pu ´ecrire ’while (tmp)’
printf("%d ",tmp->data);
tmp = tmp->next;
}
printf("NULL ");
}
/********************************************************/
int main() {
liste L;
L = insere(14,insere(2,insere(3,insere(10,NULL))));
L = insereInTail(15,(insereInTail(14,L)));
printf("Impression de la liste: L = ");
print_liste(L);
printf("Suppression de l’element en tete: L = ");
L = supprime_tete(L);
print_liste(L);
printf("Suppression de la liste: L = ");
L = supprime(L);
print_liste(L);
return 0;
}
111
Annexe B
Makefile g´en´erique
####################################################################################
# Makefile (configuration file for GNU make - see http://www.gnu.org/software/make/)
#
# Compilation of files written in C/C++ (version 0.5)
#
# Author : Sebastien Varrette <Sebastien.Varrette@imag.fr>
# Web page : http://www-id.imag.fr/~svarrett/
#
# --------------------------------------------------------------------------------
# This is a generic makefile in the sense that it doesn’t require to be
# modified when adding/removing new source files.
# --------------------------------------------------------------------------------
#
# Documentation on the Makefile utility may be found at
# http://www.gnu.org/software/make/manual/make.html
#
# Available Commands
# ------------------
# make : Compile files, binary is generated in the current directory
# make force : Force the complete re-compilation, even if not needed
# make clean : Remove backup files generated by emacs and vi
# make realclean : Remove all generated files
# make doc : Generate Doxygen documentation (in Doc/) see www.doxygen.org
# make help : print help message
#
############################## Variables Declarations ##############################
# Name of the executable to generate --- TO BE ADAPTED ---
EXE = toto
# Directory where header files (.h) and object files (.o) will be placed
INCLUDE_DIR = Include
OBJ_DIR = Obj
# File entensions for C, C++ and header files
C_EXT = c
CPP_EXT = cpp
H_EXT = h
# Source files
SRC = $(wildcard *.$(C_EXT) *.$(CPP_EXT))
SRC_H = $(wildcard *.$(H_EXT) $(INCLUDE_DIR)/*.$(H_EXT))
ALL_SRC = $(SRC) $(SRC_H)
# Check avalibility of source files
ifeq ($(SRC),)
all:
@echo "No source files available - I can’t handle the compilation"
@echo "Please check the presence and/or extension of source files "
@echo "(This makefile is configured to manage *.$(C_EXT) or *.$(CPP_EXT) - "
"you may modify variables C_EXT and CPP_EXT to reflect your "
"own programming style)"
else
112
# Object files
OBJ = $(patsubst %.$(C_EXT),%.o,$(SRC:.$(CPP_EXT)=.o))
ABSOBJ = $(OBJ:%.o=$(OBJ_DIR)/%.o)
# Backup files generated by text editors
BACKUP_FILE = $(shell find . -name "*~")
# Doxygen stuff
DOC_DIR = Doc
DOXYGEN = $(shell which doxygen)
DOXYGEN_CONF = .doxygen.conf
YES_ATTRIBUTES := JAVADOC_AUTOBRIEF EXTRACT_ALL EXTRACT_PRIVATE EXTRACT_STATIC
SOURCE_BROWSER GENERATE_MAN
# Compilator configuration
# Detect if you have a C or a C++ project through file extension
ifneq ($(filter %.c,$(SRC)),)
CXX = gcc
YES_ATTRIBUTES := $(YES_ATTRIBUTES) OPTIMIZE_OUTPUT_FOR_C
else
CXX = g++
SPECIAL_CPP_OPTION = -Wno-deprecated
endif
CXXFLAGS = -g3 -O3 -Wall $(SPECIAL_CPP_OPTION) -I$(INCLUDE_DIR) -c
ADD_OPT = #-lntl -lgmp -lm # Optionnal option for the linker
# Specifies the list of directories that make should search
VPATH = $(INCLUDE_DIR):$(OBJ_DIR)
# dependance file used for make rules
MAKEDEP_FILE = .Makefile.dep
############################### Now starting rules ################################
# Required rule : what’s to be done each time
all : $(MAKEDEP_FILE) $(EXE) TAGS
# Generate TAGS for emacs
TAGS : $(ALL_SRC)
etags $<
cp TAGS $(INCLUDE_DIR)/
# Clean Options
clean :
@echo "Remove backup files generated by emacs and vi"
rm -f $(BACKUP_FILE)
# Clean everything (including object files, binaries and documentation)
realclean : clean
@echo "Remove object files"
rm -f $(ABSOBJ)
@echo "Remove generated executable"
rm -f $(EXE)
@if [ ! -z "$(DOC_DIR)" -a ! -z "$(DOXYGEN)" ]; then
echo "Remove documentation (’make doc’ to regenerate it)";
rm -rf $(DOC_DIR)/*;
fi
# Force re-compilation, even if not required
force :
touch $(ALL_SRC) $(ABSOBJ)
@$(MAKE)
# Generate the dependance file
$(MAKEDEP_FILE) : $(ALL_SRC)
$(CXX) $(SPECIAL_CPP_OPTION) -MM -I$(INCLUDE_DIR) $(SRC) > $@
include $(MAKEDEP_FILE)
# Generic description for compilation of object files
%.o : %.$(C_EXT)
$(CXX) $(CXXFLAGS) $< -o $(OBJ_DIR)/$@
%.o : %.$(CPP_EXT)
113
$(CXX) $(CXXFLAGS) $< -o $(OBJ_DIR)/$@
# Generation of the final binary (see $(EXE))
$(EXE) : $(OBJ) $(ALL_SRC)
$(CXX) -g -o $@ $(ABSOBJ) $(ADD_OPT)
@$(MAKE) help
# Help rule - print help message
help :
@echo ’+-----------------------------------------------------------------------------+’
@echo ’| Available Commands |’
@echo ’+----------------+------------------------------------------------------------+’
@echo ’| make | Compile files, binary is generated in the current directory|’
@echo ’| make force | Force the complete re-compilation, even if not required |’
@echo ’| make clean | Remove cache backup files generated by emacs and vi |’
@echo ’| make realclean | Remove all generated files (including .o and binary) |’
@echo ’| make doc | Generate documentation using doxygen (see www.doxygen.org) |’
@echo ’| make help | Print help message |’
@echo ’+----------------+------------------------------------------------------------+’
# Test values of variables - for debug purpose
test :
@echo "INCLUDE_DIR = $(INCLUDE_DIR)"
@echo "OBJ_DIR = $(OBJ_DIR)"
@echo "EXE = $(EXE)"
@echo "SRC = $(SRC)"
@echo "SRC_H = $(SRC_H)"
@echo "ALL_SRC = $(ALL_SRC)"
@echo "OBJ = $(OBJ)"
@echo "BACKUP_FILE = $(BACKUP_FILE)"
@echo "CXX = $(CXX)"
@echo "CXXFLAGS = $(CXXFLAGS)"
@echo "ADD_OPT = $(ADD_OPT)"
@echo "DOC_DIR = $(DOC_DIR)"
@echo "DOXYGEN = $(DOXYGEN)"
@echo "DOXYGEN_CONF = $(DOXYGEN_CONF)"
@echo "YES_ATTRIBUTES = $(YES_ATTRIBUTES)"
@echo "MAKEDEP_FILE = $(MAKEDEP_FILE)"
# Documentation generation through doxygen
# First check if the $(DOXYGEN) and the $(DOC_DIR) directory exist
# Then Check $(DOXYGEN_CONF) availability;otherwise,generate one with ’doxygen -s -g’
# The following attributes should be modified in the generated file:
# - OUTPUT_DIRECTORY should be set to ’$(DOC_DIR)’, INPUT to ’. $(INCLUDE_DIR)’
# - $(YES_ATTRIBUTES) attributes should be set to YES
# - OPTIMIZE_OUTPUT_FOR_C should be set to YES if the project in in C
# Finally, launch documentation generation
doc :
ifeq ($(DOXYGEN),)
@echo "Please install Doxygen to use this option!"
@echo "(’apt-get install doxygen’ under Debian)"
else
@if [ ! -d ./$(DOC_DIR) ]; then
echo "$(DOC_DIR)/ does not exist => creating $(DOC_DIR)/";
mkdir -p ./$(DOC_DIR)/;
fi
@if [ ! -f $(DOXYGEN_CONF) ]; then
echo "I don’t found the configuration file for Doxygen ($(DOXYGEN_CONF))";
echo "Now generating one using ’$(DOXYGEN) -s -g $(DOXYGEN_CONF)’";
$(DOXYGEN) -s -g $(DOXYGEN_CONF);
echo "Now updating OUTPUT_DIRECTORY attribute to ’$(DOC_DIR)’";
cat $(DOXYGEN_CONF) | sed -e "s/^(OUTPUT_DIRECTORY += +).*/1$(DOC_DIR)/"
> $(DOXYGEN_CONF);
echo "Now updating INPUT attribute to ’. $(INCLUDE_DIR)’";
cat $(DOXYGEN_CONF) | sed -e "s/^(INPUT += +).*/1. $(INCLUDE_DIR)/"
> $(DOXYGEN_CONF);
114
for attr in $(YES_ATTRIBUTES); do
echo "now updating $$attr to YES";
cat $(DOXYGEN_CONF) | sed -e "s/^($$attr += +).*/1YES/"
> $(DOXYGEN_CONF);
done;
fi
$(DOXYGEN) $(DOXYGEN_CONF)
@echo
@echo Documentation generated in $(DOC_DIR)/
@echo May be you can try to execute ’mozilla ./$(DOC_DIR)/html/index.html’
endif
endif
115
Annexe C
Le bˆetisier
Cette annexe est une collection des bˆetises qu’il faut faire au moins une fois
dans sa vie pour ˆetre vaccin´e. L’id´ee est reprise de [Cas98]
La caract´eristique de beaucoup de ces erreurs est de ne pas provoquer de message
d’erreur du compilateur, rendant ainsi leur d´etection difficile. La diff´erence
entre le texte correct et le texte erron´e est souvent seulement d’un seul caract`
ere. La d´ecouverte de telles erreurs ne peut donc se faire que par un examen
tr`es attentif du source du programe.
C.1 Erreur avec les op´erateurs
C.1.1 Erreur sur une comparaison
Ce que voulait le programmeur : Comparer a et b
Ce qu’il aurait dˆu ´ecrire : if (a == b)
Ce qu’il a ´ecrit : if (a = b)
Ce qu’il a obtenu : une affectation de b `a a, le r´esultat ´etant la
valeur affect´ee.
Pourquoi tant de haˆıne ? L’affectation est un op´erateur et non pas une
instruction.
C.1.2 Erreur sur l’affectation
C’est l’inverse de l’erreur pr´ec´edente, mais qui reste beaucoup moins fr´equente.
Ce que voulait le programmeur : Affecter b `a a
Ce qu’il aurait dˆu ´ecrire : a = b
Ce qu’il a ´ecrit : a == b
Ce qu’il a obtenu : La comparaison de a `a b.
C.2 Erreurs avec les macros
Le m´ecanisme des macros est r´ealis´e par le pr´eprocesseur qui r´ealise un traitement
en amont du compilateur proprement dit. Ce concept est abord´e en d´etail
dans le chapitre 7. Comme le traitement des macros est un pur traitement
textuel, sans aucun contexte, c’est un nid `a erreurs.
116
C.2.1 Un #define n’est pas une d´eclaration
Ce que voulait le programmeur : la d´efinition de la constante MAX initialis´ee a 10
Ce qu’il aurait dˆu ´ecrire : #define MAX 10
Ce qu’il a ´ecrit : #define MAX 10 ;
Cette erreur peut provoquer ou non une erreur de compilation `a l’utilisation de
la macro :
– L’utilisation x = MAX ; aura pour traduction x = 10 ; ;, ce qui est possible :
il y a une instruction nulle derri`ere l’instruction x = 10 ;
– L’utilisation int t[MAX] ; aura pour expansion int t[10 ;] ; ce qui g´en`erera
un message d’erreur.
C.2.2 Un #define n’est pas une initialisation
Ce que voulait le programmeur : la d´efinition de la constante MAX initialis´ee a 10
Ce qu’il aurait dˆu ´ecrire : #define MAX 10
Ce qu’il a ´ecrit : #define MAX = 10 ;
Cette erreur sera g´en´eralement d´etect´ee `a la compilation, malheureusement le
message d’erreur sera ´emis sur l’utilisation de la macro, et non pas l`a o`u r´eside
l’erreur, `a savoir la d´efinition de la macro.
C.2.3 Erreur sur macro avec param`etres
La distinction entre macro avec param`etres et macro sans param`etre se fait
sur la pr´esence d’une parenth`ese ouvrante juste apr`es le nom de la macro,
sans aucun blanc entre les deux. Ceci peut amener des r´esultats surprenant ;
comparer les deux exemples suivants :
D´efinition de la macro param`etres traduction
#define add(a,b) (a + b) a et b (a + b)
#define add (a,b) (a + b) aucun (a,b) (a + b)
C.2.4 Erreur avec les effets de bord
Le corps d’une macro peut comporter plusieurs occurrences d’un param`etre. Si
`a l’utilisation de la macro on r´ealise un effet de bord sur le param`etre effectif,
cet effet de bord sera r´ealis´e plusieurs fois. Exemple :
#define CARRE(a) ((a) * (a))
l’utilisation de CARRE(x++) aura comme traduction ((x++) * (x++)) et l’op´erateur
++ sera appliqu´e deux fois.
C.3 Erreurs avec l’instruction if
L’instruction if ne comporte ni mot-cl´e introducteur de la partie then, ni terminateur
(pas de fi dans le style des if then else fi).
Ceci peut provoquer les erreurs suivantes :
117
Ce que voulait le programmeur : tester si a > b
Ce qu’il aurait dˆu ´ecrire : if ( a > b)
a = b;
Ce qu’il a ´ecrit : if ( a > b);
a = b;
Le probl`eme vient aussi du fait de l’existence de l’instruction nulle :
Ce que voulait le programmeur : tester si a > b
Ce qu’il aurait dˆu ´ecrire : if ( a > b)
{ if ( x > y) x = y; }
else
...
Ce qu’il a ´ecrit : if ( a > b)
if ( x > y) x = y;
else
...
On rappelle qu’un else est raccroch´e au premier if.
C.4 Erreurs avec les commentaires
Il y a deux erreurs classiques avec les commentaires :
1. le programmeur oublie la s´equence fermante /*. Le compilateur ”mange”
donc tout le texte jusqu’`a la s´equence fermante du prochain commentaire.
2. On veut enlever (en le mettant en commentaire) un gros bloc d’instructions
sans prendre garde au fait qu’il comporte des commentaires. Les
commentaires ne pouvant ˆetre imbriqu´es, ¸ca n’aura pas l’effet escompt´e
par le programmeur. La m´ethode classique pour enlever (tout en le laissant
dans le source) un ensemble d’instructions est d’utiliser le pr´eprocesseur :
#ifdef NOTDEFINED
...
#endif
C.5 Erreurs avec les priorit´es des op´erateurs
Les priorit´es des op´erateurs sont parfois surprenantes. Les cas les plus gˆenants
sont les suivants :
– La priorit´e des op´erateurs bit `a bit est inf´erieure `a celle des op´erateurs de
comparaison.
Le programmeur a ´ecrit il d´esirait il a obtenu
x & 0xff == 0xac (x & 0xff) == 0xac x & (0xff == 0xac)
– La priorit´e des op´erateurs de d´ecalage est inf´erieure `a celle des op´erateurs
arithm´etiques.
Le programmeur a ´ecrit il d´esirait il a obtenu
x << 4 + 0xf (x << 4) + 0xf x << (4 + 0xf)
118
– La priorit´e de l’op´erateur d’affectation est inf´erieure `a celle des op´erateurs
de comparaison. Dans la s´equence ci-dessous, tr`es souvent utilis´ee, toutes les
parenth`eses sont n´ecessaire :
while ((c = getchar()) != EOF) {
...
}
C.6 Erreur avec l’instruction switch
C.6.1 Oubli du break
Sans l’instruction break, le programme continuera `a l’alternative suivante ce
qui peut poser probl`eme. (voir §2.2.5).
C.6.2 Erreur sur le default
L’alternative `a ex´ecuter par d´efaut est introduite par l’etiquette default. Si
une faute de frappe est commise sur cette ´etiquette, l’alternative par d´efaut ne
sera plus reconnue : l’´etiquette sera prise pour une ´etiquette d’instruction sur
laquelle ne sera fait aucun goto (voir §2.2.6).
switch(a) {
case 1 : a = b;
defult : return(1); /* erreur non d´etect´ee */
}
Version diabolique de cette erreur : si la lettre l de default est remplac´ee par
le chiffre 1, avec les fontes utilis´ees pour imprimer les sources, qui verra la
diff´erence entre l et 1 ?
C.7 Erreur sur les tableaux multidimensionnels
La r´ef´erence `a un tableau t `a deux dimensions s’´ecrit t[i][j] et non pas t[i,j]
comme dans d’autres langages de programmation.
Malheureusement, si on utilise par erreur la notation t[i,j] et selon le contexte
d’utilisation, elle pourra ˆetre accept´ee par le compilateur. En effet, dans cette
expression la virgule est l’op´erateur qui d´elivre comme r´esultat l’op´erande droite
apr`es avoir ´evalu´e l’op´erande gauche. Comme l’´evaluation de l’op´erande gauche
ne r´ealise ici aucun effet de bord, cette ´evaluation est inutile , donc t[i,j] est
´equivalent `a t[j] qui est l’adresse du sous-tableau correspondant `a l’index j.
C.8 Erreur avec la compilation s´epar´ee
Une erreur classique est d’avoir un tableau d´efini dans une unit´e de compilation :
int tab[10] ;
et d’utiliser comme d´eclaration de r´ef´erence dans une autre unit´e de compilation
:
119
extern int * tab ;
Rappelons que int tab[] et int *t ne sont ´equivalents que dans le seul cas
de param`etre formel de fonction.
Dans le cas qui nous occupe ici, la d´eclaration de r´ef´erence correcte est :
extern int tab[] ;
120
C.9 Liens utiles
En plus des ouvrages cit´es dans la bibliographie, voici quelques sites web qui
m’ont servis pour r´ealiser ce polycopi´e et/ou qui m’ont parus int´eressants :
– [Cas98] : http://www-clips.imag.fr/commun/bernard.cassagne/Introduction_
ANSI_C.html
– http://fr.wikibooks.org/wiki/Programmation_C
– [Can03] : http://www-rocq.inria.fr/codes/Anne.Canteaut/COURS_C/
– [Fab02] : http://www.ltam.lu/Tutoriel_Ansi_C/ (un tr`es bon tutorial en
ligne).
121
Bibliographie
[Can03] Anne Canteaut. ”Programmation en Langage C”. INRIA -
projet CODES, 2003. http://www-rocq.inria.fr/codes/Anne.
Canteaut/COURS_C/.
[Cas98] Bernard Cassagne. ”Introduction au Langage C”. Laboratoire CLIPS
UJF/CNRS, 1997-1998. http://www-clips.imag.fr/commun/\
bernard.cassagne/Introduction\_ANSI\_C.html.
[Fab02] F. Faber. Introduction `a la programmation en ANSI-C. Technical
report, Tutorial, Aoˆut 2002. http://www.ltam.lu/Tutoriel_Ansi_
C/.
[Her01] Matthieu Herrb. Guide superflu de programmation en langage C.
Technical report, CNRS/LAAS, Dec. 2001. http://www.laas.fr/
~matthieu/cours/c-superflu/index.
[KR88] B.W. Kernighan and D.M. Ritchie. The C Programming Language.
Prentice-Hall, Englewood Cliffs, New Jersey, USA, 1988. 2nd edition.
[Pil04] Jean-Fran¸cois Pillou. Introduction au Langage C. Technical report,
Tutorial, Encyclop´edie Informatique Libre, 2004. http://www.
commentcamarche.net/c/cintro.php3.
[PKP03] Peter Prinz and Ulla Kirch-Prinz. C Pocket Reference. O’Reilly &
Associates, 2003.
[Ste90] David Stevenson. ”IEEE Std 754-1985 IEEE Standard for Binary
Floating-Point Arithmetic”. Technical report, IEEE Standards Association,
1990.
122



15/11/2011
0 Poster un commentaire

A découvrir aussi


Inscrivez-vous au blog

Soyez prévenu par email des prochaines mises à jour

Rejoignez les 6 autres membres