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 ’