Vous êtes sur la page 1sur 214

Le langage ISO C et son

environnement
Versions C90 et C99
Jean-Paul R IGAULT
V 2.1 – 7 février 2010

École polytechnique de l’université de Nice Sophia Antipolis


Département de sciences informatiques
930 route des Colles — 06903 S OPHIA A NTIPOLIS Cedex, France
Tél: +33 4 92 96 50 50 — Fax: +33 4 92 96 50 55
URL : http://www.polytech.unice.fr/informatique
Avant-propos

C onçu à la fin des années 1960 par Dennis Ritchie, le langage C s’est imposé
comme une référence en matière de programmation-système et de réalisa-
tion de logiciel de base. Ceci est bien entendu lié pour partie au succès d’Unix,
système d’exploitation pour lequel C a été conçu. Mais c’est aussi le résultat
des vertus intrinsèques du langage : suffisamment simple pour être efficace et
portable, suffisamment moderne pour être expressif.
Cette origine de langage de programmation-système a longtemps conféré
à C la réputation d’un langage « laxiste », dans lequel le typage n’était qu’une
contrainte surajoutée, presque décorative. Les habitudes et les exigences de
programmation aidant, il était nécessaire de rendre plus rigoureux l’outil. C’est
à cette tâche que s’est consacré le comité de normalisation x3j11 de l’ansi [2].
Le résultat fut un langage plus sûr, doté du typage fort garant d’une certaine
sécurité de programmation, sans que cela nuise à l’efficacité des programmes
générés.
C’est ce langage, dit ansi C, adopté par l’ansi en 1989 puis par l’iso en
1990 (iso/iec 9899:1990) qui est actuellement la norme de fait. Nous le dési-
gnerons simplement par C90. Cette norme a subi quelques révisions mineures
jusqu’à ce que l’évolution parallèle de la normalisation du langage C++ oblige
à une reprise profonde de la norme. Ceci à conduit en 1999, à la norme iso/iec
9899:1999 reprise par l’ansi en 2000. Le nouveau langage, dit C99, n’est pas
encore à ce jour (janvier 2010) complètement implémenté par tous les compila-
teurs, bien qu’un large sous-ensemble le soit généralement.
Le présent texte décrit à la fois iso C90 et iso C99, en signalant les diffé-
rences entre les deux versions. Ces différences sont résumées dans l’annexe 12.
Sauf mention explicite du contraire, les programmes proposés sont compatibles
avec les deux versions. Pour les détails, voir la section 1.5.

Tous les exemples de cet ouvrage ont été développés et testés dans un envi-
ronnement tout à fait habituel dans les établissements d’enseignement et de re-
cherche : stations de travail sous Linux, environnement graphique X Window.
Le compilateur ansi C utlisé comme référence est celui de gnu dû initiale-
ment à Richard Stallman (et à la Free Software Foundation), à savoir gcc [1, 23].
Les programmes devraient être compilables avec les versions de gcc à partir
de 2.95 au moins mais c’est seulement à partir des versions 3.x que l’on a la
compatibilité avec C99 (option -std=c99).
La compilation s’effectue sous le contrôle de make (version de gnu là en-
core). La mise au point utilise gdb [19], autre produit de la Free Software Foun-
dation, utilisé à travers l’une de ses interfaces avec X Window (principalement
ddd [20]). Ont également été utilisés les analyseurs d’exécution gprof [21] et

V 2.1 – 7 février 2010 1 c Jean-Paul R IGAULT



2 Avant-propos

valgrind [15]. Cet environnement de travail est décrit dans le chapitre 10 où


sont également introduits certains environnements intégrés de développement
pour C, sous Unix ou MS Windows.
Le document lui-même a été composé à l’aide de LATEX [10, 26] (distribution
TEXLive 2009) avec les extensions Babel [7] et frenchb [5] pour traiter la typo-
graphie française, ainsi que xindy [14] pour générer l’index comportant des
caractères accentués.

Cet ouvrage est le fruit de près de trente années d’enseignement de C et


d’Unix, à l’Ecole des mines de Paris, au cerics (Centre d’Enseignement et de
Recherche en Informatique, Communication et Systèmes), à l’ESSI (École Supé-
rieure de Sciences Informatiques de l’université de Nice Sophia Antipolis, de-
venue Polytech,Nice-Sophia) et au cours de nombreuses sessions de formation
continue inter- et intra-entreprises que j’ai animées. Je tiens donc à remercier
tous les collègues, étudiants et participants à ces formations pour leur intérêt,
leur patience, et leur motivation.

Jean-Paul Rigault
Professeur d’informatique
Université de Nice Sophia Antipolis
Sophia Antipolis et La Roquette sur Siagne
Février 1993–Janvier 2010

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
Table des matières

Avant-propos 1

Table des matières 3

Table des figures 9

Liste des tableaux 11

Liste des programmes 13

Liste des exercices 15

1 Introduction 17
1.1 Historique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
1.2 Caractéristiques de C . . . . . . . . . . . . . . . . . . . . . . . . 18
1.2.1 Un langage contemporain de Pascal . . . . . . . . . . 18
1.2.2 Un langage d’implémentation de systèmes . . . . . . . 18
1.2.3 Un langage supportant la compilation séparée . . . . . 18
1.2.4 Un langage incomplet. . . pour être portable . . . . . . 18
1.3 Plan . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
1.4 Notations utilisées . . . . . . . . . . . . . . . . . . . . . . . . . 19
1.5 Compilateurs et compatibilité . . . . . . . . . . . . . . . . . . . 20
1.5.1 Environnement de développement de référence . . . . 20
1.5.2 Version du langage C . . . . . . . . . . . . . . . . . . . 21
1.6 Ressources : ouvrages et sites Web sur C . . . . . . . . . . . . 21
1.7 Exercices du chapitre 1 . . . . . . . . . . . . . . . . . . . . . . . 22

2 Premiers pas 25
2.1 Monde, salut ! . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
2.2 Boucle while ; entrées-sorties simples . . . . . . . . . . . . . . 27
2.3 Fonctions ; expressions ; entrées-sorties . . . . . . . . . . . . . 29
2.3.1 Exemple : calcul de la racine carrée d’un réel . . . . . . 29
2.3.2 Entrées-sorties formattées . . . . . . . . . . . . . . . . . 33
2.4 Tableaux et instruction d’itération . . . . . . . . . . . . . . . . 35
2.5 Arguments du shell ; fonction récursive . . . . . . . . . . . . . 38
2.6 Exercices du chapitre 2 . . . . . . . . . . . . . . . . . . . . . . . 41

V 2.1 – 7 février 2010 3 c Jean-Paul R IGAULT



4 TABLE DES MATIÈRES

3 Bases du langage 43
3.1 Eléments lexicaux . . . . . . . . . . . . . . . . . . . . . . . . . . 43
3.1.1 Jeu de caractères . . . . . . . . . . . . . . . . . . . . . . 43
3.1.2 Structure lexicale d’un fichier-source . . . . . . . . . . 44
3.1.3 Commentaires . . . . . . . . . . . . . . . . . . . . . . . . 45
3.1.4 Identificateurs . . . . . . . . . . . . . . . . . . . . . . . . 45
3.1.5 Mots-clés . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
3.1.6 Constantes littérales arithmétiques . . . . . . . . . . . . 46
3.1.7 Chaînes de caractères littérales . . . . . . . . . . . . . . 48
3.2 Types scalaires et déclarations simples . . . . . . . . . . . . . . 50
3.2.1 Panorama des types de C . . . . . . . . . . . . . . . . . 50
3.2.2 Type vide (void) . . . . . . . . . . . . . . . . . . . . . . 50
3.2.3 Types de base entiers . . . . . . . . . . . . . . . . . . . . 51
3.2.4 Types réels . . . . . . . . . . . . . . . . . . . . . . . . . . 53
3.2.5 Définitions d’objets de type de base scalaire . . . . . . 53
3.2.6 Types énumérés . . . . . . . . . . . . . . . . . . . . . . . 54
3.2.7 Synonymie de types : typedef . . . . . . . . . . . . . 55
3.3 Opérateurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
3.3.1 Opérateurs arithmétiques . . . . . . . . . . . . . . . . . 56
3.3.2 Opérateurs relationnels et logiques . . . . . . . . . . . 58
3.3.3 Opérateurs bit à bit . . . . . . . . . . . . . . . . . . . . . 60
3.3.4 Affectations . . . . . . . . . . . . . . . . . . . . . . . . . 63
3.3.5 Opérateurs sur les pointeurs . . . . . . . . . . . . . . . 64
3.3.6 Opérateurs sur les types . . . . . . . . . . . . . . . . . . 64
3.3.7 Opérateurs divers . . . . . . . . . . . . . . . . . . . . . 65
3.4 Evaluation des expressions . . . . . . . . . . . . . . . . . . . . 66
3.4.1 Ordre d’évaluation : précédence et associativité . . . . 66
3.4.2 Conversions . . . . . . . . . . . . . . . . . . . . . . . . . 68
3.4.3 Mélange de types dans les expressions . . . . . . . . . 70
3.5 Instructions et flot de contrôle . . . . . . . . . . . . . . . . . . . 71
3.5.1 Instruction simple et bloc . . . . . . . . . . . . . . . . . 71
3.5.2 Instructions de sélection . . . . . . . . . . . . . . . . . . 72
3.5.3 Instructions de boucle . . . . . . . . . . . . . . . . . . . 75
3.5.4 Instructions de rupture de séquence . . . . . . . . . . . 77
3.6 Exercices du chapitre 3 . . . . . . . . . . . . . . . . . . . . . . . 79

4 Tableaux, structures et unions 83


4.1 Tableaux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
4.1.1 Tableaux mono-dimensionnels . . . . . . . . . . . . . . 83
4.1.2 Tableaux multi-dimensionnels . . . . . . . . . . . . . . 86
4.1.3 Chaînes de caractères . . . . . . . . . . . . . . . . . . . 88
4.1.4 Tableaux de taille variables de C99 . . . . . . . . . . . . 88
4.2 Structures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
4.2.1 Définition du type et déclarations . . . . . . . . . . . . 90
4.2.2 Structures compactes et champs de bits . . . . . . . . . 93
4.2.3 Opérations sur les structures . . . . . . . . . . . . . . . 94
4.2.4 Tableaux de structures . . . . . . . . . . . . . . . . . . . 95
4.3 Unions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
4.3.1 Définition du type . . . . . . . . . . . . . . . . . . . . . 96

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
TABLE DES MATIÈRES 5

4.3.2 Opérations sur les unions . . . . . . . . . . . . . . . . . 97


4.3.3 Structures avec variantes . . . . . . . . . . . . . . . . . 97
4.4 Exercices du chapitre 4 . . . . . . . . . . . . . . . . . . . . . . . 100

5 Pointeurs 103
5.1 Déclarations et opérations sur pointeurs . . . . . . . . . . . . . 103
5.1.1 Pointeurs simples . . . . . . . . . . . . . . . . . . . . . . 103
5.1.2 Pointeurs multiples . . . . . . . . . . . . . . . . . . . . . 105
5.1.3 Opérations arithmétiques sur pointeurs . . . . . . . . . 106
5.1.4 Conversions de pointeurs . . . . . . . . . . . . . . . . . 108
5.1.5 Pointeurs et constantes . . . . . . . . . . . . . . . . . . 109
5.2 Pointeurs sur structures et unions . . . . . . . . . . . . . . . . 110
5.2.1 Opérateur de sélection « flêche » . . . . . . . . . . . . . 110
5.2.2 Types récursifs . . . . . . . . . . . . . . . . . . . . . . . 111
5.3 Pointeurs et tableaux . . . . . . . . . . . . . . . . . . . . . . . . 118
5.3.1 Tableaux de pointeurs ; lecture des déclarations de C . 118
5.3.2 Relations entre tableaux et pointeurs . . . . . . . . . . 119
5.4 Exercices du chapitre 5 . . . . . . . . . . . . . . . . . . . . . . . 122

6 Le préprocesseur ANSI C 129


6.1 Prétraitement des programmes C . . . . . . . . . . . . . . . . . 129
6.2 Inclusion de fichier . . . . . . . . . . . . . . . . . . . . . . . . . 130
6.2.1 Effet de l’inclusion de fichier . . . . . . . . . . . . . . . 130
6.2.2 Recherche du fichier à inclure . . . . . . . . . . . . . . 130
6.2.3 Utilisation de l’inclusion de fichier . . . . . . . . . . . . 131
6.3 Définition de macros . . . . . . . . . . . . . . . . . . . . . . . . 131
6.3.1 Macros sans arguments : définition de constantes . . . 131
6.3.2 Macros à arguments : fonctions en ligne . . . . . . . . 132
6.3.3 Définition récursive de macros . . . . . . . . . . . . . . 134
6.3.4 Concaténation et « stringification » . . . . . . . . . . . 134
6.3.5 Annulation de définition : #undef . . . . . . . . . . . . 135
6.3.6 Macros réservées . . . . . . . . . . . . . . . . . . . . . . 135
6.4 Compilation conditionnelle . . . . . . . . . . . . . . . . . . . . 135
6.4.1 Les directives de compilation conditionnelle . . . . . . 135
6.4.2 Inclusion unique de fichiers . . . . . . . . . . . . . . . . 137
6.5 Autres directives du préprocesseur . . . . . . . . . . . . . . . . 137
6.5.1 Numéro de ligne : #line . . . . . . . . . . . . . . . . . 138
6.5.2 Messages d’erreur : #error . . . . . . . . . . . . . . . 138
6.5.3 Commentaire exécutable : #pragma . . . . . . . . . . . 138
6.6 Exercices du chapitre 6 . . . . . . . . . . . . . . . . . . . . . . . 139

7 Fonctions 141
7.1 Arguments et valeur de retour . . . . . . . . . . . . . . . . . . 141
7.1.1 Type de retour d’une fonction . . . . . . . . . . . . . . 141
7.1.2 Type des arguments d’une fonction . . . . . . . . . . . 142
7.1.3 Mode de passage des arguments . . . . . . . . . . . . . 142
7.2 Déclaration, définition et appel de fonction . . . . . . . . . . . 143
7.2.1 Déclaration du type d’une fonction : prototype . . . . 143
7.2.2 Définition d’une fonction . . . . . . . . . . . . . . . . . 144

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



6 TABLE DES MATIÈRES

7.2.3 Appel d’une fonction . . . . . . . . . . . . . . . . . . . 145


7.2.4 Fonctions « en ligne » de C99 . . . . . . . . . . . . . . . 146
7.3 Fonctions et pointeurs . . . . . . . . . . . . . . . . . . . . . . . 146
7.3.1 Fonction retournant un pointeur . . . . . . . . . . . . . 146
7.3.2 Pointeurs en argument . . . . . . . . . . . . . . . . . . . 147
7.3.3 Pointeurs sur fonctions . . . . . . . . . . . . . . . . . . 148
7.4 La fonction main . . . . . . . . . . . . . . . . . . . . . . . . . . 151
7.5 Exercices du chapitre 7 . . . . . . . . . . . . . . . . . . . . . . . 153

8 Structure des programmes 155


8.1 Espaces de nommage . . . . . . . . . . . . . . . . . . . . . . . . 155
8.2 Structure des programmes C . . . . . . . . . . . . . . . . . . . 156
8.2.1 Compilation séparée . . . . . . . . . . . . . . . . . . . . 156
8.2.2 Unité de compilation . . . . . . . . . . . . . . . . . . . . 158
8.3 Durée de vie et règles de visibilité . . . . . . . . . . . . . . . . 158
8.3.1 Durée de vie . . . . . . . . . . . . . . . . . . . . . . . . . 158
8.3.2 Visibilité des objets . . . . . . . . . . . . . . . . . . . . . 159
8.3.3 Initialisation des objets . . . . . . . . . . . . . . . . . . . 161
8.3.4 Visibilité des fonctions . . . . . . . . . . . . . . . . . . . 161
8.3.5 Synthèse : relation entre durée de vie et visibilité . . . 162
8.3.6 Autres règles de visibilité . . . . . . . . . . . . . . . . . 163
8.4 Programmation modulaire en C . . . . . . . . . . . . . . . . . 164
8.4.1 Notion de module . . . . . . . . . . . . . . . . . . . . . 164
8.4.2 Objets globaux trouvés dans une interface de module 165
8.4.3 Exemple d’organisation modulaire . . . . . . . . . . . . 166
8.5 Exercices du chapitre 8 . . . . . . . . . . . . . . . . . . . . . . . 167

9 La bibliothèque standard 171


9.1 Éléments généraux . . . . . . . . . . . . . . . . . . . . . . . . . 171
9.1.1 Assertions : <assert.h> . . . . . . . . . . . . . . . . . 171
9.1.2 Codes d’erreur : <errno.h> . . . . . . . . . . . . . . . 171
9.1.3 Définitions communes : <stddef.h> . . . . . . . . . . 171
9.1.4 Utilitaires généraux : <stdlib.h> . . . . . . . . . . . 172
9.2 Éléments numériques . . . . . . . . . . . . . . . . . . . . . . . . 172
9.2.1 Booléens : <stdbool.h> . . . . . . . . . . . . . . . . . 172
9.2.2 Types entiers : <stdint.h> . . . . . . . . . . . . . . . 172
9.2.3 Conversion des types entiers : <inttypes.h> . . . . 172
9.2.4 Environnement pour calcul en nombres réels : <fenv.h> 172
9.2.5 Nombres complexes : <complex.h> . . . . . . . . . . 173
9.3 Fonctions mathématiques . . . . . . . . . . . . . . . . . . . . . 173
9.3.1 Bibliothèque mathématique de base : <math.h> . . . 173
9.3.2 Bibliothèque mathématique générique : <tgmath.h> 173
9.4 Caractères et chaînes de caractères . . . . . . . . . . . . . . . . 173
9.4.1 Manipulation de caractères : <ctype.h> . . . . . . . . 173
9.4.2 Manipulation de chaînes de caractères : <string.h> 173
9.4.3 Manipulation de caractères étendus: <wctype.h> . . 174
9.4.4 Manipulation de chaînes de caractères étendus : <wchar.h> 174
9.4.5 Localisation : <locale.h> . . . . . . . . . . . . . . . . 174
9.5 Entrées-sorties : <stdio.h> . . . . . . . . . . . . . . . . . . . 174

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
TABLE DES MATIÈRES 7

9.6 Divers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174


9.6.1 Fonctions à nombre variable d’arguments : <stdarg.h> 175
9.6.2 Date et heure : <time.h> . . . . . . . . . . . . . . . . . 175
9.6.3 Traitement d’événements : <signal.h> . . . . . . . . 175
9.6.4 Points de reprise : <setjmp.h> . . . . . . . . . . . . . 175
9.6.5 Notation alternative de certains opérateurs : <iso646.h> 176

10 Environnement de développement 177


10.1 Compilateurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177
10.2 Développement traditionnel sous Unix . . . . . . . . . . . . . 178
10.2.1 Édition de source . . . . . . . . . . . . . . . . . . . . . . 178
10.2.2 L’outil make . . . . . . . . . . . . . . . . . . . . . . . . . 179
10.2.3 Mise au point avec gdb et compagnie . . . . . . . . . . 180
10.2.4 Outils de documentation . . . . . . . . . . . . . . . . . 182
10.3 Environnements intégrés de développement . . . . . . . . . . 182
10.3.1 Environnements multi-plateformes . . . . . . . . . . . 183
10.3.2 Environnements spécifiques à Unix . . . . . . . . . . . 184
10.3.3 Environnements spécifiques à MS Windows . . . . . . 184
10.4 Remarque finale . . . . . . . . . . . . . . . . . . . . . . . . . . . 185

11 Extensions avancées de C99 189


11.1 Préprocesseur . . . . . . . . . . . . . . . . . . . . . . . . . . . . 189
11.1.1 Macros prédéfinies . . . . . . . . . . . . . . . . . . . . . 189
11.1.2 Macros à nombre variable d’arguments . . . . . . . . . 189
11.2 langage de base . . . . . . . . . . . . . . . . . . . . . . . . . . . 189
11.2.1 Nom de la fonction courante : __func__ . . . . . . . . 189
11.2.2 Pointeurs restreints : restrict . . . . . . . . . . . . . 189
11.2.3 Tableaux . . . . . . . . . . . . . . . . . . . . . . . . . . . 189
11.3 Bibliothèque standard . . . . . . . . . . . . . . . . . . . . . . . 189
11.3.1 Fichiers d’entête . . . . . . . . . . . . . . . . . . . . . . 189
11.3.2 Type booléen . . . . . . . . . . . . . . . . . . . . . . . . 189
11.3.3 Types entiers étendus . . . . . . . . . . . . . . . . . . . 189
11.3.4 Nombres complexes . . . . . . . . . . . . . . . . . . . . 189
11.3.5 Manipulation des caractères multiples . . . . . . . . . 189
11.3.6 Réels en virgule flottante . . . . . . . . . . . . . . . . . 189

12 C traditionnel, iso C90, iso C99 et C++ 191


12.1 D’iso C90 à C traditionnel . . . . . . . . . . . . . . . . . . . . . 191
12.2 D’iso C90 à iso C99 . . . . . . . . . . . . . . . . . . . . . . . . . 191
12.3 D’iso C90 ou iso C99 à C++ . . . . . . . . . . . . . . . . . . . . 191

Bibliographie 193

Glossaire 195

Index 203

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



8 TABLE DES MATIÈRES

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
Table des figures

2.1 Algorithme de tri par insertion . . . . . . . . . . . . . . . . . . 37


2.2 Arbre de calcul récursif de fibo(4) . . . . . . . . . . . . . . 41

3.1 Représentation d’une chaîne de caractères littérale . . . . . . . 49

4.1 Exemple de champs de bits . . . . . . . . . . . . . . . . . . . . 93

5.1 Pointeur simple . . . . . . . . . . . . . . . . . . . . . . . . . . . 104


5.2 Pointeur double . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
5.3 Liste simplement chaînée : simple_list.h . . . . . . . . . . 112
5.4 Insertion dans une liste avec un simple pointeur . . . . . . . . 113
5.5 Insertion dans une liste avec un double pointeur . . . . . . . . 118
5.6 Relation entre pointeurs et tableaux mono-dimensionnels en C 120
5.7 Tableau multi-dimensionnel et tableau de pointeurs . . . . . . 122
5.8 Liste doublement chaînée avec cellule de garde et rebouclage 124
5.9 Structure d’un arbre binaire . . . . . . . . . . . . . . . . . . . . 126
5.10 Un exemple d’arbre binaire . . . . . . . . . . . . . . . . . . . . 127

8.1 Compilation séparée et unités de compilations . . . . . . . . . 157


8.2 Visibilité et durée de vie : quelques exemples . . . . . . . . . . 163
8.3 Organisation d’un module en C . . . . . . . . . . . . . . . . . . 165

10.1 XEmacs à l’œuvre . . . . . . . . . . . . . . . . . . . . . . . . . . 179


10.2 ddd (et gdb) à l’œuvre . . . . . . . . . . . . . . . . . . . . . . . 181
10.3 Eclipse en mode édition/compilation de C/C++ . . . . . . . . 183
10.4 Eclipse en mode mise au point de C/C++ . . . . . . . . . . . . 184
10.5 Encore un IDE multiplateformes, Code::Blocks . . . . . . . . . 185
10.6 Un IDE multiplateformes léger, CodeLite . . . . . . . . . . . . 186
10.7 Visual Studio 2008 en mode édition/compilation de C/C++ . 187
10.8 Visual Studio 2008 en mode mise au point de C/C++ . . . . . 187

7.1 Chaîne de compilation C . . . . . . . . . . . . . . . . . . . . . . 197

V 2.1 – 7 février 2010 9 c Jean-Paul R IGAULT



10 TABLE DES FIGURES

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
Liste des tableaux

3.1 Jeu de caractères ASCII . . . . . . . . . . . . . . . . . . . . . . . 44


3.2 Mots-clés de C . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
3.3 Mots-clés supplémentaires de Cnew . . . . . . . . . . . . . . . 46
3.4 Séquences d’échappement pour les caractères . . . . . . . . . 47
3.5 Panorama des types de C . . . . . . . . . . . . . . . . . . . . . 51
3.6 Sous-types de int . . . . . . . . . . . . . . . . . . . . . . . . . 52
3.7 Liste des opérateurs de C . . . . . . . . . . . . . . . . . . . . . 57
3.8 Précédence et associativité des opérateurs de C . . . . . . . . 67

8.1 Relations entre durée de vie et visibilité . . . . . . . . . . . . 162

V 2.1 – 7 février 2010 11 c Jean-Paul R IGAULT



12 LISTE DES TABLEAUX

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
Liste des programmes

2.1 Le programme « hello, world » original . . . . . . . . . . . . . 25


2.2 Le programme « hello, world » localisé en français . . . . . . . 26
2.3 Copie de fichiers . . . . . . . . . . . . . . . . . . . . . . . . . . 27
2.4 Calcul de la racine carrée d’un nombre réel . . . . . . . . . . . 30
2.5 Programme simple de lecture de scalaires . . . . . . . . . . . . 34
2.6 Tri par insertion d’une suite d’entiers . . . . . . . . . . . . . . 36
2.7 Calcul du terme de rang n de la suite de Fibonacci . . . . . . 39
3.1 Remplacement des fins de ligne par un blanc . . . . . . . . . . 47
3.2 Longueur utile d’une chaîne de caractères . . . . . . . . . . . 49
3.3 Copie de chaînes de caractères . . . . . . . . . . . . . . . . . . 50
3.4 Calcul du pgcd de deux entiers . . . . . . . . . . . . . . . . . . 59
3.5 Pathologie du décalage . . . . . . . . . . . . . . . . . . . . . . . 61
3.6 Manipulation de bits . . . . . . . . . . . . . . . . . . . . . . . . 62
4.1 Produit scalaire de deux vecteurs . . . . . . . . . . . . . . . . . 84
4.2 Transposition d’une matrice . . . . . . . . . . . . . . . . . . . . 87
4.3 Tableaux à dimension variable de C99 . . . . . . . . . . . . . . 89
4.4 Structure avec variante (en C99) . . . . . . . . . . . . . . . . . 98
5.1 Liste d’entiers : définition du type . . . . . . . . . . . . . . . . 112
5.2 Liste d’entiers : insertion par ordre croissant . . . . . . . . . . 114
5.3 Liste d’entiers : programme principal . . . . . . . . . . . . . . 116
5.4 Insertion dans une liste avec un seul (double) pointeur . . . . 117
7.1 Les pointeurs sur fonction dans tous leurs états . . . . . . . . 150
7.2 La commande echo modifiée . . . . . . . . . . . . . . . . . . . 152
8.1 Espaces de nommage en C . . . . . . . . . . . . . . . . . . . . . 156
8.2 Visibilité des objets locaux . . . . . . . . . . . . . . . . . . . . . 159
8.3 Visibilité et durée de vie : fic1.c . . . . . . . . . . . . . . . . 163
8.4 Visibilité et durée de vie : fic2.c . . . . . . . . . . . . . . . . 163
8.5 Visibilité des noms de type en ansi C . . . . . . . . . . . . . . 164
8.6 Spécification du module « pile d’entiers » . . . . . . . . . . . . 167
8.7 Corps du module « pile d’entiers » . . . . . . . . . . . . . . . . 168
8.8 Utilisation du module « pile d’entiers » . . . . . . . . . . . . . 169

V 2.1 – 7 février 2010 13 c Jean-Paul R IGAULT



14 LISTE DES PROGRAMMES

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
Liste des exercices

1.1 Vérifications préliminaires . . . . . . . . . . . . . . . . . . . . . . 22


1.2 Accès à la documentation . . . . . . . . . . . . . . . . . . . . . . 23
2.1 Mise en route . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
2.2 Copie de fichiers . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
2.3 La commande iota . . . . . . . . . . . . . . . . . . . . . . . . . . 42
2.4 Minimum et maximum d’un ensemble d’entiers . . . . . . . . . 42
2.5 Conversion Celsius-Farenheit . . . . . . . . . . . . . . . . . . . 42
2.6 « Dérécursiver » le calcul de la suite de Fibonacci . . . . . . . . 42
2.7 Amélioration du calcul de la racine carrée . . . . . . . . . . . . 42
2.8 Mauvais format . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
3.1 Nombre de bits à 1 dans un octet . . . . . . . . . . . . . . . . . . 79
3.2 Nombre de caractères, mots et lignes d’un texte . . . . . . . . . 80
3.3 Évaluation d’un polynôme . . . . . . . . . . . . . . . . . . . . . . 80
3.4 Nombres premiers . . . . . . . . . . . . . . . . . . . . . . . . . . 80
3.5 Quelques opérations non portables . . . . . . . . . . . . . . . . . 80
4.1 Manipulation de bits . . . . . . . . . . . . . . . . . . . . . . . . . 100
4.2 Calcul matriciel simple . . . . . . . . . . . . . . . . . . . . . . . . 100
4.3 Recherche en table . . . . . . . . . . . . . . . . . . . . . . . . . . 100
4.4 Opérations sur les dates . . . . . . . . . . . . . . . . . . . . . . . 101
4.5 Gestion de liste de contacts . . . . . . . . . . . . . . . . . . . . . 101
5.1 Liste triée simplement chaînée . . . . . . . . . . . . . . . . . . . 122
5.2 Liste triée doublement chaînée . . . . . . . . . . . . . . . . . . . 123
5.3 Manipulation de chaînes de caractères . . . . . . . . . . . . . . . 123
5.4 Tri d’un tableau de pointeurs . . . . . . . . . . . . . . . . . . . . 125
5.5 Commande fgrep . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
5.6 Arbre binaire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
5.7 Recherche dichotomique . . . . . . . . . . . . . . . . . . . . . . . 127
5.8 Pathologie des pointeurs . . . . . . . . . . . . . . . . . . . . . . . 127
7.1 Intégration par la méthode des trapèzes . . . . . . . . . . . . . . 153
7.2 Efficacité des fonctions inline . . . . . . . . . . . . . . . . . . . . 153
8.1 Modification de l’implémentation des piles . . . . . . . . . . . . 167
8.2 File fifo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167
8.3 Du module au type abstrait . . . . . . . . . . . . . . . . . . . . . 167

V 2.1 – 7 février 2010 15 c Jean-Paul R IGAULT



16 LISTE DES EXERCICES

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
Chapitre 1

Introduction

1.1 Historique
Le langage C trouve son origine au début des années 1970, lorsque Dennis
Ritchie rejoint Ken Thomson aux Bell Laboratories. Ce dernier a déjà défini
en 1970 le langage B, dérivé de BCPL, pour implémenter de manière portable
le système d’exploitation qu’il vient d’inventer et qu’il a dénommé Unix.
En 1972, C voit le jour et est utilisé pour réécrire complètement le noyau
d’unix (en un été!). Le langage recevra une définition de référence par la publi-
cation du livre de Brian Kernighan et Dennis Ritchie en 1978 [16]. Le langage
décrit dans ce livre, avec quelques extensions ultérieures (affectation et passage
en arguments de structures et d’unions, introduction du type void), constitue
ce qu’il est convenu d’appeler le « C traditionnel ».
Le développement considérable de C, d’abord entrainé par celui d’Unix
puis relativement autonome, rendit nécessaire une normalisation qui fut entre-
prise à partir de 1983 par le comité x3j11 de l’ansi 1 . Les travaux se terminèrent
fin 1988, par une norme ansi reprise par ieee puis par l’iso (norme iso-9899).
La deuxième édition du livre de Kernighan et Ritchie [17] décrit le langage
ainsi normalisé.
La « compatibilité arrière » a été l’un des soucis de l’ansi, et la plupart des
programmes écrits en C traditionnel peuvent être recompilés avec ansi C. Mais,
sans rien changer à la philosophie du langage, ansi C a apporté de nombreuses
améliorations allant à la fois dans le sens de la sécurité de programmation (typage
fort, meilleure rigueur sémantique) et dans celui de la portabilité (par exemple
définition de la bibliothèque d’exécution minimale).
La version C99 est également largement compatible avec iso C90. Malheu-
reusement de nombreux compilateurs C ne la supporte pas encore complète-
ment. Ainsi, parmi les compilateurs majeurs, gcc 3 et 4 supportent l’essentiel de
C99, mais pas la totalité et Microsoft Visual Studio 2005 (C++ version 8) et 2008
(version 9) ne prétendent même pas supporter cette norme — même s’ils en
ont des parties du fait de leur compatibilité C++. Bref, C99 n’est pas encore très
répandu dix ans après sa normalisation ! C’est pourquoi tous les programmes
donnés ici, sauf mention spéciale, sont compatibles avec C90 aussi bien qu’avec
C99.

1. American National Standards Institute

V 2.1 – 7 février 2010 17 c Jean-Paul R IGAULT



18 1. Introduction

1.2 Caractéristiques de C
1.2.1 Un langage contemporain de PASCAL
C a été conçu à peu près en même temps que des langages comme Pascal
et il reflète donc l’état de l’art de l’époque. C’est donc un langage qui supporte
la programmation structurée (on dit aussi procédurale). Il dispose en particulier de
types structurés définissables par l’utilisateur, analogues aux records de Pas-
cal.

1.2.2 Un langage d’implémentation de systèmes


Cependant C traditionnel garde de son origine de langage d’implémenta-
tion de système d’exploitation un certain nombre de caractéristiques de « bas
niveau » :
– instructions « proches » du langage-machine,
– manipulations d’adresses au travers de pointeurs, typés certes, mais non
controlés,
– typage « laxiste » permettant pratiquement tous les mélanges. . . du mo-
ment que le compilateur est capable de générer du code !
– partage de la même zone de mémoire par des objets de types différents
(unions). . .
ansi C a réagit contre la plupart de ces « licences » tout en préservant la
souplesse indispensable à un langage de programmation-système. Les possi-
bilités précédentes et parfois dangereuses de C restent utilisables en ansi C à
condition d’être explicitées.

1.2.3 Un langage supportant la compilation séparée


Conçu pour réaliser des programmes dont la taille pouvait être relativement
importante (un système d’exploitation tout entier), C se devait de supporter
d’emblée la compilation séparée et la modularité.
Cependant l’approche de la modularité est la même dans les versions iso
qu’en C traditionnel. Elle reste donc assez archaïque si on la compare aux lan-
gages modernes comme Ada ou certains langages à objets.

1.2.4 Un langage incomplet. . . pour être portable


La portabilité est en fait à l’origine de C : c’est une des motivations pour
écrire le noyau d’un système d’exploitation avec un langage de haut niveau. Le
résultat est un succès, et ce malgré l’existence des caractéristiques de bas niveau
déjà mentionnées (1.2.2).
Mais pour assurer cette portabilité les concepteurs du langage ont dû en
exclure toutes les fonctionnalités qui leur paraissaient trop dépendantes des
machines ou des systèmes. C’est ainsi, par exemple, que C ne comporte aucune
instruction d’entrée-sortie, de manipulation de chaînes de caractères, de gestion
du parallélisme ou de la concurrence, ni de communication entre processus.
Cette approche de la portabilité peut apparaitre paradoxale. Après tout, un
langage comme Java assure sa portabilité de manière exactement inverse, en

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
1.3. Plan 19

redéfinissant de manière indépendante de la plate-forme les fonctions du sys-


tèmes d’exploitation (entrées-sorties, gestion de fichiers, de mémoire, gestion
des « threads », etc.), et jusqu’au processeur lui-même (grâce à la machine vir-
tuelle) ! Cependant, n’oublions pas que C est à l’origine un langage conçu pour
réaliser des systèmes d’exploitation ce qui, nous l’avons vu, l’amène à propo-
ser des fonctionnalités de « bas niveau ». Conjuger portabilité et bas niveau
conduit donc à ce minimalisme de C. Par ailleurs, à l’époque, un des slogans
favoris était « small is beautiful » !
Ces fonctionnalités indispensables sont déléguées à un bibliothèque d’exé-
cution standard (la célèbre standard library). ansi C a fait un gros effort pour
normaliser cette bibliothèque, au moins dans ses fonctionnalités minimales.

1.3 Plan
Le chapitre 2 permet, à travers une série d’exemples simples, de faire un
tour rapide du langage C.
Les chapitres 3, 4 et 5 reprennent en détail respectivement la description
du langage de base (types, opérateurs et expressions, instructions de contrôle),
celle des agrégats (tableaux, structures et unions) et enfin celle des pointeurs.
Les chapitres 7 et 8 décrivent en détail la notion de fonction et la structure
des programmes. Y est également abordée la programmation modulaire en C.
Le préprocesseur ansi C est étudié en détail au chapitre 6.
L’environnement d’exécution avec une description succinte des biblio-
thèques standards est l’objet du chapitre 9, et l’environnement de développe-
ment celui du chapitre 10.
Enfin le chapitre 11 résume les extensions introduites par C99 et qui n’ont
pas été traitées dans le reste du texte. Quant au chapitre 12 il synthétise les
différences entre C « traditionnel », C90, C99 et C++.

1.4 Notations utilisées


Les noms de systèmes, langages ou organisations sont composés en petites
capitales : Linux, Algol, iso. . . Les noms propres de personnes sont également
en petites capitales, avec des majuscules, comme pour Dennis Ritchie, Richard
M. Stallman. . . Nous utilisons une police oblique pour les mots anglais ou
techniques non traduits, comme par exemple integer ou lvalue. La police grasse
sans empattement sert pour les noms de commandes au shell comme ls ou
gcc.
Les (brefs) extraits de programme insérés dans le texte sont composés
avec une police à chasse fixe et les identificateurs sont en police grasse,
comme dans
while (*p1 != ’\0’)
*p2++ = *p1++;
Dans ces extraits, les identificateurs composés en police oblique, comme
instruction et condition dans l’extrait suivant, sont à remplacer par des
expressions ou instructions terminales :
while (condition)
instruction

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



20 1. Introduction

Les programmes complets (ceux qui figurent dans la Liste des programmes,
page 13) sont affichés par le paquettage LATEX listings. Ils sont donc décorés syn-
taxiquement pour mettre en évidence les mots-clés, chaînes de caractères litté-
rales, constantes, etc. En outre, les lignes sont numérotées. Le programme 2.1 du
chapitre suivant en fournit un exemple. On pourra s’étonner de ne pas trouver
de caractères accentués dans ces programmes. Cela est dû au fait que j’utilise le
codage des caractères Unicode (utf-8 pour être précis) qui n’est correctement
supporté ni par le paquettage listings de LATEX ni par les compilateurs C90. En
revanche, nous verrons en 11.3.5 qu’un certain support est prévu en C99.
Lorsque l’on présente un exemple d’interaction entre l’utilisateur et le sys-
tème, on suppose toujours que cette interaction a lieu sous un des shell d’Unix
(ou de Linux, de Cygwin. . .). La syntaxe suppose le shell du à Steve Bourne
sh ou l’un de ses descendants comme bash, ksh, ou zsh. . . (personnellement
j’utilise zsh). L’interaction est présentée comme suit :
% ls -F Stack
CVS/ Makefile Stack* Stack.c Stack.h Stack_main.c
%
Ici % représente la « chaîne de sollicitation » (le prompt) envoyé par le shell, le
texte en oblique gras (comme ls -F Stack) est celui entré par l’utilisateur,
et le texte droit (comme CVS/) celui émis par le système. Les caractères de
contrôle comme ctrl-d (simulation de fin de fichier au terminal sous Unix)
sont représentés sous la forme usuelle utilisant le préfixe ^ (ˆD).
Par ailleurs lorsque ces exemples utilisent des accents, ils supposent un sys-
tème Unix ou Linux correctement configuré. Ainsi la variable d’environnment
LANG doit préciser un encodage 8 bits comme iso88591, iso88591@euro, ou
utf8. Par exemple, la valeur de ma propre variable LANG est en_US.UTF-8
(je n’aime pas avoir les messages du système en français !).

1.5 Compilateurs et compatibilité


1.5.1 Environnement de développement de référence
L’environnement de développement de référence utilisé dans ce document
est le suivant (décembre 2010) :
– processeur intel Core Duo 2.6 Mhz quad, mémoire centrale 4 Goctets, 2
disques durs 500 Goctets (SATA, 7200 tr/min) ;
– système d’exploitation Linux (Fedora11, noyau 2.6.30, glibc 2.10), sys-
tèmes de fichiers ext3 ;
– environnement graphique X Window 11 et gestionnaire gnome 2.26 ;
– éditeur emacs 23.1, gnu make 3.81, mise au point avec gdb 6.8 ;
– compilateur gcc 4.4.3.
Les programmes se compilent et fonctionnent également sous MacOs X
(versions Leopard et Snow Leopard) avec le compilateur fourni par Apple (gcc-
4.2) comme avec les compilateurs gcc-4.4.x.
En ce qui concerne Microsoft Windows, les programmes ont été compi-
lés et exécutés avec succès en utilisant Cygwin et gcc-3.4), et aussi avec les
compilateurs de Microsoft Visual Studio 2005 et 2008 (en mode C++). . Voir le
chapitre 10 pour les détails.

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
1.6. Ressources : ouvrages et sites Web sur C 21

1.5.2 Version du langage C


Ce document décrit à la fois les normes iso C90 et iso C99, ce qui veut dire
que, sauf mention explicite, le texte s’applique aux deux versions.
Lorsque l’on veut attirer l’attention sur une propriété du langage C diffé- C99
rente entre C90 et C99, on utilise une marque marginale comme pour ce para-
graphe. Si toute une section ne s’applique qu’à C99, son titre est ainsi. Enfin,
certaines extensions « lourdes » de C99 ne seront pas utilisées dans ce cours,
mais elles sont résumées au chapitre 11.
Le compilateur gcc 4.4 utilisé supporte presque entièrement (voir [22]) la
norme C99 et c’est dans ce mode qu’il est utilisé ici (option -std=c99). Ce-
pendant, la plupart des exemples de programmes donnés dans ce document
n’utilisent pas — sauf mention explicite — les fonctionnalités idiosyncratiques
de C99 et doivent donc être compilables également avec un compilateur C90
(option -ansi de gcc) ou un compilateur C++ raisonnable (comme g++ ou Mi-
crosoft Visual C++ déjà mentionnés).
Pour compiler on utilise ici systématiquement la commande make. Beau-
coup de programmes étant réduits à une seule « unité de compilation » (un
seul fichier source .c), on peut utiliser make sous Unix sans avoir besoin de
Makefile. Il suffit de définir les deux variables d’environnement CC et CFLAGS,
par exemple en plaçant dans l’un des fichiers d’initialisation du shell 2 :
export CC=gcc
export CFLAGS="-g -std=c99 -Wall"
Ensuite, pour compiler un programme tenant tout entier dans un seul fichier
source, disons prog.c, il suffit d’exécuter la commande make prog qui va
produire (s’il n’y a pas d’erreur) un fichier exécutable de même nom que le
source mais sans l’extension .c (donc ici, prog), qu’il suffira d’exécuter à son
tour :
% make prog
% prog
... affichage des resultats de prog ...
Lorsqu’une Makefile est nécessaire, elle est décrite dans le texte. En outre on
trouvera des compléments sur make en 10.2.2.
Avertissements (warnings) lors de la compilation
Noter que nous compilons toujours avec tous les warnings activés (option
de compilation -Wall). Un programme C doit être exempt de tels warnings.
Certains peuvent vous sembler sans conséquence, mais il faut beaucoup
de discernement et d’expérience pour en être sûr.

1.6 Ressources : ouvrages et sites Web sur C


Le langage C a donné lieu à une abondante littérature et je ne donnerai ici
que quelques titres parmi ceux qui me paraissent les plus recommandables.
En tête vient bien sûr l’ouvrage des inventeurs du langage, Brian Kernighan
et Denis Ritchie, dans sa seconde édition (ansi) en anglais [17] ou dans sa
traduction française [18]. C’est un livre assez court comparé à la plupart des
2. selon les cas et les shells, c’est un fichier comme .profile, .zshenv, .login, .bashrc...
présent dans votre répertoire initial (home directory)

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



22 1. Introduction

autres et qui va directement à l’essentiel. Il contribue aussi à promouvoir un


certain style de programmation bien dans la philosophie du « small is beautiful »
chère à Unix. Bref, cet ouvrage est hautement recommandé. Bien entendu, s’il
couvre la norme ansi 89 (iso C90), il n’aborde absolument pas les extensions
de C99.
Pour cette dernière version de la norme, il existe quelques ouvrages récents
comme celui de Kochan [8] ou encore, si vous aimez les gros livres très dé-
taillés, de Prata [3]. Parmi les (nombreux) ouvrages directement écrits en fran-
çais, citons l’ouvrage de Claude Delannoy [12], auteur renommé pour sa pé-
dagogie, ainsi que le livre très complet d’Achille Braquelaire [27]. Ce dernier
déborde le cadre de ce cours puisqu’il présente aussi la programmation-système
avec l’api Posix (c’est-à-dire avec Unix ou ses clones comme Linux).
Un grand nombre de sites sur l’Internet ont des sections consacrés au lan-
gage C. En particulier le très intéressant site français developpez.com [29] et
son forum [28] permettent d’échanger des informations couvrant de nombreux
langages et systèmes, dont C et C++. Inévitables dans tous ces sites interactifs,
les interventions incompétentes, les jugements péremptoires et non motivés,
mêmes les débuts d”engueulades, sans parler des orthographes douteuses s’y
rencontrent beaucoup moins qu’ailleurs, favorisant des informations précises
et utiles.
Hébergé par developpez.com, le site de Nicolas Joseph [6] fournit d’ex-
cellents résumés sur la compatibilité entre C90 et C99, ainsi que des conseils de
programmation, des pièges à éviter et des liens sur d’autres ressources concer-
nant C.
Si, au delà de C, vous décidez de vous intéresser à C++, le livre de l’inven-
teur de ce langage, Bjarne Stroustrup, dans sa troisème édition [24] peut être
un début à moins que vous préfériez une approche plus progressive avec le
récent livre du même Stroustrup, véritable introduction à la programmation
en général [25] ou encore avec [11, 4]. En ce qui concerne la compatibilité entre
C(90 ou 99) et C++, on peut consulter l’excellent résumé dû à David Tribble [9].

1.7 Exercices du chapitre 1

Exercice 1.1 (Vérifications préliminaires) Déterminez le compilateur C dont


vous disposez. Sous Unix/Linux ou Cygwin, cela a des chances d’être gcc.
Déterminez-en la version (commande gcc -v). Il serait bon que ce soit 3.4 ou
4.x pour avoir la compatibilité avec le standard C99. Vérifier aussi la disponibi-
lité d’un outil de mise au point comme gdb ou l’une de ses formes graphiques
kdbg, xxgdb, ddd. . .
Choisissez votre éditeur. vérifiez qu’il est correctement configuré, qu’il sait
mettre en valeur (par changement de polices ou de couleurs) la syntaxe du
langage. Explorez la manière dont il collabore avec make, vous permettant de
lancer vos compilations et surtout de récupérez les messages d’erreurs sans
quitter l’éditeur. Si votre éditeur n’a pas cette dernière fonctionnalité, changez-
en avant qu’il ne soit trop tard !
Si vous décidez d’utiliser un environnement intégré de développement (ide)
comme kdevelop ou eclipse, apprenez à l’utiliser correctement, à définir et
gérer des projets, à interagir avec make et les débogeurs.

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
1.7. Exercices du chapitre 1 23

Si vous êtes sous MS Windows et que vous décidez d’utilisez Microsoft Visual
Studio, sachez que vous disposerez d’un excellent ide à condition de savoir
l’utiliser. L’aide en ligne sur les bibliothèques C est également de haute qua-
lité 3 . Malheureusement, avec Visual Studio, vous n’aurez pas un compilateur
réellement compatible avec la norme C99. Par ailleurs il vous faudra au moins
la version 2005.
Une autre possibilité sous MS Windows est d’apprendre à utiliser l’un des
multiples ide disponibles et qui sont souvent connectés à un compilateurs gcc:
voir 10.

Exercice 1.2 (Accès à la documentation) Elle doit se faire en ligne. Tout sys-
tème de type Unix dispose de la célèbre commande man qui permet d’accéder
à la descriptions des commandes à shell, des fonctions de bibliothèque, des
fichiers système, etc. Apprenez à l’utiliser correctement (faites donc la com-
mande man man), elle ou l’une de ses interfaces comme info ou autre. Notez
que les fonctions des bibliothèques standards de C se trouvent dans la section
2 ou 3 du manuel.
Vous pouvez également recourir à un site Web fournissant une description dé-
taillée de la bibliothèque standard. Il y en a un grand nombre que Google se
fera un plaisir de vous indiquer. Un bon point de départ, comme souvent, est
Wikipédia 4 .

3. http://msdn2.microsoft.com/en-us/library/
4. http://en.wikipedia.org/wiki/C_standard_library. C’est la version en an-
glais, beaucoup plus complète que celle en français.

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



24 1. Introduction

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
Chapitre 2

Premiers pas

C e chapitre passe rapidement en revue quelques unes des caractéristiques


principales du langage C qui seront reprises en détail par la suite. Il
constitue un tour rapide du langage et introduit au passage quelques fonctions
d’entrée-sortie utiles. Le but est de permettre au lecteur de rédiger rapidement
quelques programmes simples.
La présentation utilise une suite d’exemples assez classiques :
– l’un des programmes les plus simples possibles, avec le célèbre "hello,
world!" ;
– un programme de copie de fichier, introduisant la notion de boucle (ici
une boucle while) et des fonctions simples d’entrée-sortie ;
– un programme introduisant les notions de fonction, d’expression arith-
métique et d’entrée-sortie formattée (calcul de la racine carrée d’un
nombre réel) ;
– un programme de tri par insertion afin d’introduire la boucle for et la
notion de tableau ;
– enfin le calcul du terme de rang n de la suite de Fibonacci, présenté
comme un exemple de fonction récursive.

2.1 Monde, salut !


Depuis la première édition du livre de Kernighan et Ritchie [16], de nom-
breux ouvrages sur C débutent par un programme très simple, pratiquement
toujours le même. Il n’est absolument pas déshonorant de sacrifier à une tradi-
tion si bien établie. Le programme 2.1 est donc le célèbre « hello, world! ».

Programme 2.1 – Le programme « hello, world » original


/***** Fichier: hello.c *****/

#include <stdio.h>

5 int main()
{
printf("hello, world!\n");
}

V 2.1 – 7 février 2010 25 c Jean-Paul R IGAULT



26 2. Premiers pas

On peut en préférer la version française en 2.2.

Programme 2.2 – Le programme « hello, world » localisé en français


/***** Fichier: salut.c *****/

#include <stdio.h>

5 int main()
{
printf("Monde, salut !\n");
}

Ce programme (prenons la version américaine 2.1 par exemple) est composé


de trois entités :
1. un commentaire (entre /* et */),
2. une directive au préprocesseur (la ligne débutant par #),
3. une définition de fonction constituée de toutes les lignes suivantes.
La directive au préprocesseur
#include <stdio.h>
permet d’importer les déclarations des fonctions d’entrée-sortie de la bibliothè-
que standard, c’est-à-dire les prototypes de ces fonctions qui précisent les types
des arguments et le type de retour. Ici, une seule fonction de bibliothèque est
directement utilisée : printf. Les directives au préprocesseur sont reconnais-
sables au dièse (#) qui doit être le premier caractère de la ligne (autre qu’un
blanc ou une tabulation horizontale). Le nom de cette directive (include)
évoque son mécanisme : il s’agit d’une inclusion textuelle du fichier stdio.h.
Les « piquants » (<...>) indiquent que ce fichier est à chercher dans des réper-
toires par défaut dépendant de l’installation (ici sans doute /usr/include).
Le programme ne comporte qu’une seule fonction, nommée main. Par
convention expresse, la fonction main est celle par laquelle commence l’exé-
cution d’un programme C. La définition de la fonction main comporte l’entête
de la fonction
int main()
qui en précise le nom et surtout la signature : ici une fonction sans argument à
type de retour entier 1 (int pour integer).
Le corps de la fonction main est un bloc, reconnaisable au fait qu’il est
encadré par une paire d’accolades :
{
printf("hello, world!\n");
}
Ici, ce bloc ne comporte qu’une instruction simple, l’invocation (ou l’appel) de
la fonction printf avec un argument effectif qui est une chaîne de caractères
littérale "hello, world!n". Cette fonction affiche la chaîne de caractères
sur la sortie standard (stdout), suivie d’une fin de ligne représentée par la
1. La signature de la fonction main est imposée parmi un nombre de choix limité (voir 7.4).

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
2.2. Boucle while ; entrées-sorties simples 27

séquence \n (newline). Noter le point-virgule final : toute instruction simple


et toute déclaration de C se termine par un point-virgule.

Supposons que ce programme ait été saisi, à l’aide d’un éditeur de texte
quelconque, dans le fichier hello.c. Il ne reste plus qu’à le compiler, par
exemple en utilisant la commande make (que nous considérons comme la seule
commande de compilation sous Unix 2 ) :
% make hello
gcc -g -std=c99 -Wall -o hello hello.c
%
puis à exécuter le fichier binaire produit (qui a été nommé hello par l’option
-o de la commande précédente) :
% hello
hello, world!
%

2.2 Boucle while ; entrées-sorties simples


Le programme 2.3 lit chaque caractère de son entrée standard (stdin) et le
copie tel quel sur sa sortie standard (stdout).

Programme 2.3 – Copie de fichiers


/***** Fichier: copy.c *****/

#include <stdio.h>

5 int main()
{
int c;

while ((c = getchar()) != EOF)


10 putchar(c);
return 0;
}

La première ligne du corps de la fonction main est une définition de variable


locale :
int c;
Le nom de la variable est c et son type est int pour integer, c’est-à-dire entier
(signé).
Le reste du corps de main est une boucle while qui a la structure suivante :
while (condition)
corps
2. Pour la configuration de make, voir le chapitre 10. Ici, les règles par défaut doivent suffire
si les variables d’environnement ont été correctement positionnées comme indiqué en 1.5.2; il
n’y a pas besoin de définir de Makefile.

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



28 2. Premiers pas

Une telle boucle exécute son corps, constitué par une instruction simple ou un
bloc, tant que la condition, qui est une expression entière quelconque, a une
valeur non nulle. La condition est évaluée et testée avant chaque itération, y
compris la première. La boucle while est une des trois formes de boucles dont
dispose C (avec for et do ... while — voir 3.5.3).
Ici la condition
(c = getchar()) != EOF
est une expression relationnelle car != est l’opérateur d’inégalité. Dans a != b
les deux expressions a et b sont évaluées et comparées : si elle sont différentes,
la valeur de l’expression a != b est 1, sinon c’est 0 3 . C possède aussi l’opéra-
teur d’égalité == (attention ==, deux signes = collés !), ainsi que les opérateurs
de comparaison (<, <=, >, >=).
L’opérande droit de != est la valeur prédéfinie EOF (voir ci-après). L’opé-
rande gauche
c = getchar()
est une expression d’affectation, = étant l’opérateur d’affectation. La valeur de
l’expression de droite devient la nouvelle valeur de l’objet référencé par l’ex-
pression de gauche (ici la variable c). La valeur de l’expression d’affectation
elle-même est la nouvelle valeur de l’objet en partie gauche. Donc
(c = getchar()) != EOF
invoque la fonction getchar, copie sa valeur de retour dans la variable c, et
compare cette nouvelle valeur de c à la constante EOF. La boucle while sera
exécutée tant que cette comparaison donnera une valeur non nulle (c’est-à-dire
tant que la nouvelle valeur de c n’est pas EOF). Noter que les parenthèses sont
indispensables pour grouper l’affectation, sinon
c = getchar() != EOF
s’interprète comme
c = (getchar() != EOF)
à cause de la précédence des opérateurs (voir 3.4.1).
Une expression comme
(c = getchar()) != EOF
illustre bien ce type de programmation avec « effet de bord » puisque cette
expression non seulement compare c à EOF, mais encore le fait après avoir
modifié c.
En éliminant l’effet de bord, on aurait pu écrire la boucle while sous une
forme plus habituelle pour un programmeur Pascal, mais certainement moins
« idiomatique » pour un programmeur C :
c = getchar();
while (c != EOF)
{
putchar(c);
c = getchar();
}

3. Contrairement à C99 et C++, C90 ne possède pas de type de donnée booléen mais que ce
sont les valeurs entières 0 et 1 qui sont utilisées pour jouer les rôles respectifs de faux et vrai —
voir 3.3.2 et 11.3.2.

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
2.3. Fonctions ; expressions ; entrées-sorties 29

Notez la nécessité de répéter l’instruction c = getchar(), une (légère) nui-


sance.

La fonction getchar lit le caractère suivant sur l’entrée standard et re-


tourne sa valeur. En cas fin de fichier détectée par le système d’exploitation,
getchar retourne la valeur EOF, prédéfinie dans stdio.h et qui ne corres-
pond à aucune valeur possible de caractère 4 . La boucle while se terminera
donc lorsque la fin de fichier sera atteinte sur l’entrée standard.
Enfin, le corps de la boucle est constitué par une instruction simple qui est
l’invocation de la fonction putchar. Celle-ci affiche le caractère qu’elle reçoit
en argument (ici c) sur la sortie standard.
Le programme, dont le source est dans le fichier copy.c, peut donc servir
à recopier un fichier dans un autre grâce au mécanisme de redirection du shell :

% make copy
gcc -g -std=c99 -Wall -o copy copy.c
% copy > fic
ceci est le texte qui sera
recopié sur la sortie standard
ˆD
% cat fic
ceci est le texte qui sera
recopié sur la sortie standard
% copy < fic > fic1
% cat fic1
ceci est un texte qui sera
recopié sur la sortie standard
% cmp fic fic1
%
On rappelle que le caractère ctrl-d (eot) représenté par ˆD simule une fin
de fichier au terminal. Par ailleurs, la commande cmp compare 2 fichiers :
l’absence d’affichage indique que les deux fichiers sont identiques à l’octet près.

2.3 Fonctions ; expressions ; entrées-sorties


2.3.1 Exemple : calcul de la racine carrée d’un réel
Le programme 2.4 calcule la racine carrée d’un nombre réel positif grâce
à la méthode de Newton. Si t est un tel nombre, sa racine carrée est en effet
donnée par la limite de la suite

1 t
 
un = u n −1 +
2 u n −1

avec la condition initiale u0 = t, par exemple.


Le programme comporte, au plus haut niveau, sept éléments.
– Trois directives au préprocesseur qui, comme précédemment, permettent
d’importer les prototypes des fonctions de la bibliothèque standard :
4. Ceci explique que getchar retourne un entier (int) et non pas un caractère (char) et
que la variable locale c doive être déclarée comme un entier.

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



30 2. Premiers pas

Programme 2.4 – Calcul de la racine carrée d’un nombre réel


/***** Fichier: square_root.c *****/

#include <stdio.h>
#include <stdlib.h>
5 #include <math.h>

double square_root(double);

int main()
10 {
double x;

printf("entrez un nombre? ");


scanf("%lg", &x);
15 printf("resultat = %g\n", square_root(x));
exit(0);
}

const double EPS = 1.0e-06; /* precision maximale */


20
double square_root(double t)
{
double previous = 0.0;
double current = t;
25
if (t < 0.0)
{
fprintf(stderr, "argument negatif pour square_root\n");
exit(1);
30 }
while (fabs(previous - current) > EPS)
{
previous = current;
current = 0.5 * (current + t / current);
35 }
return current;
}

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
Nous avons déjà remarqué que l’inclusion de stdio.h permet d’impor-
ter les prototypes des fonctions d’entrée-sortie de la bibliothèque stan-
dard ; celle de stdlib.h concerne une bonne partie du reste des pro-
totypes de cette bibliothèque (par exemple celui de la fonction exit) ;
quant à celle de math.h, elle importe les fonctions de la bibliothèque
mathématique comme fabs.
– Un prototype déclarant la fonction square_root, car celle-ci est utilisée
avant d’être définie (on parle de « déclaration en avant » (forward declara-
tion) :

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
2.3. Fonctions ; expressions ; entrées-sorties 31

double square_root(double);
Cette fonction a un argument qui est un réel double précision (type
double) et retourne une valeur de même type.
– La définition de la constante globale EPS, également de type double, ini-
tialisée à 10−6 .
– La définition des deux fonctions main et square_root.
On note que le format est libre et que l’indentation et le changement de
ligne sont largement utilisés pour rendre le programme lisible (ils n’ont pas
d’autre signification pour le compilateur que celle de séparateurs).
La fonction main utilise une variable locale x, de type double. La valeur
de x est lue grâce à la fonction de bibliothèque scanf. Le premier argument
de scanf est une chaîne de caractères qui spécifie le format de lecture : ici, la
spécification %lg signifie qu’on cherche à lire un réel dans toute présentation
raisonnable (le g), et que ce réel est double précision (le l, comme « long »).
Le deuxième argument de scanf est l’adresse de la variable que l’on cherche
à lire, ici celle de x. En effet l’opérateur & sert à prendre l’adresse d’un objet
(il retourne un pointeur). De manière générale, les arguments de scanf doivent
être des pointeurs 5 . L’avant-dernière instruction de main affiche le résultat de
l’invocation de la fonction square_root avec l’argument x. Là encore le pre-
mier argument de printf est un format où la spécification %g indique que
l’on veut afficher un réel double précision dans un format raisonnable pour
sa valeur. On constate au passage que printf et scanf sont des fonctions à
nombre variable d’arguments.
Bien entendu, l’invocation de exit termine l’exécution du programme,
en renvoyant au shell la valeur de son argument comme code de retour. Par
convention, la valeur 0 indique que tout s’est bien passé 6 . Il n’est pas né-
cessaire d’appeler systématiquement exit à la fin de main, mais ce n’est
pas une mauvaise pratique puisque cela permet de maîtriser le code de re-
tour transmis au shell. Une autre possibilité équivalente est de remplacer
exit(code) par return code (par exemple ici, return 0) — mais évi-
demment ceci n’est équivalent à exit que pour le retour de main, pas d’une
autre fonction ! Notez aussi que le fichier d’entête <stdio.h> définit les deux
constantes entières EXIT_SUCCESS (avec la valeur 0) et EXIT_FAILURE (1) ce
qui permet de remplacer les utilisations de exit ou return précédentes par
exit(EXIT_SUCCESS) ou return EXIT_SUCCESS.
Après la fonction main est définie une constante globale EPS initialisée à la
valeur 10−6 . Enfin, on a déjà remarqué que les commentaires se placent entre
/* et */.

Puis vient la définition de la fonction square_root elle-même. Elle vérifie


d’abord que l’argument t n’est pas négatif. L’instruction if est l’une des deux
instructions de sélection de C (voir 3.5.2). Elle peut prendre la forme simple
suivante
5. Essayez donc d’exécuter ce programme en oubliant de prendre l’adresse de x, c’est-à-dire
en écrivant :
texttscanf("%lg", x)
6. Rappelons que ce code de retour peut être consulté par les instructions de contrôle condi-
tionnelles du shell : if, while, until...

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



32 2. Premiers pas

if (condition)
instruction-si-vrai
ou encore celle d’une alternative
if (condition)
instruction-si-vrai
else
instruction-si-faux
Sa signification est évidente. Les deux instructions sont soit des instructions
simples soit des blocs 7 .
La fonction fprintf est analogue à printf, mais son premier argument
précise le flux de sortie, ici l’erreur standard stderr 8 .
Le cœur de square_root est un boucle while où current est la valeur
courante de un et previous la valeur précédente (i.e., un−1 ). La fonction de
bibliothèque fabs calcule la valeur absolue réelle double précision (son proto-
type est dans math.h). La boucle s’exécute tant que l’écart en valeur absolue
entre un et un−1 est supérieur à EPS. Le corps de la boucle étant constitué de
deux instructions simples (deux instructions d’affectation en fait), il faut donc
là encore les englober dans un bloc. L’expression arithmétique
0.5 * (current + t / current)
se comprend sans problème. C dispose entr’autres, des opérateurs arithméti-
ques habituels : addition (+), soustraction (-), multiplication (*), division (/).
Ces opérateurs s’appliquent à des valeurs entières ou réelles : attention, si ses
deux opérandes sont entiers, / est la division entière.
A la fin de la boucle, la valeur de current est le résultat cherché et est donc
transmise à l’appelant grâce à l’instruction return qui termine la fonction.

Il ne reste plus qu’à compiler et exécuter ce fichier (supposons qu’il soit


nommé square_root.c) :
% make square_root
gcc -g -std=c99 -Wall -o square_root square_root.c
% square_root
entrez un nombre? 64
result = 8
% square_root
entrez un nombre? 2
result = 1.41421
% square_root
entrez un nombre? 0
result = 0
% square_root
entrez un nombre? 1.E+18
resultat = 1e+09
% square_root
entrez un nombre? -12
argument negatif pour square_root
%

7. Remarquer l’absence du mot-clé then habituel dans d’autres langages.


8. printf(...) est donc équivalent à fprintf(stdout, ...).

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
2.3. Fonctions ; expressions ; entrées-sorties 33

2.3.2 Entrées-sorties formattées


On a vu que printf et scanf ont un premier argument 9 qui est un format
précisant les conversions à effectuer en en sortie ou en entrée. Nous donnons
ici quelques indications pour permettre la compréhension des exemples à venir.
Pour les détails on se reportera à la documentation en ligne.
Le format est une chaîne de caractères comportant des spécifications de
conversion , chacune débutant par le caractère %. Une telle spécification in-
dique la conversion à effectuer sur l’argument correspondant (rappelons que
les deux fonctions ont un nombre variable d’arguments). La correspondance
entre spécification et argument s’effectue donc de manière positionnelle et il doit
en principe y avoir autant d’arguments à convertir que de spécifications dans
le format.

Conversions en sortie

Dans le cas de printf (ou de fprintf), tout ce qui n’est pas une spécifica-
tion de conversion est imprimé tel quel. Ainsi
printf("hello, world!\n");
est un exemple où il n’y a aucune spécification de conversion dans le format.
Dans
int i = 3;
int j = 4;
...
printf("i = %d, j = %d\n", i, j);
les valeurs de i et j sont imprimées en format décimal (%d) produisant
i = 3, j = 4
suivi d’une fin de ligne.
Il existe un grand nombre de possibilités de spécifications de conversion;
voici quelques unes des plus fréquentes :
%d conversion d’un entier en décimal
%o conversion d’un entier en octal
%x conversion d’un entier en hexadécimal

%c affichage d’un caractère individuel


%s affichage d’une chaine de caractères

%lf conversion d’un réel double en format fixe


%le conversion d’un réel double en format avec exposant
%lg conversion d’un réel double en format général

%% le caractère % lui-même
Le format, comme toute chaîne, peut contenir des « séquences d’échappe-
ment » qui représentent certains caractères non imprimables. Ces séquences se
composent toutes du caractère \ suivi d’un seul caractère. Nous avons déjà
rencontré \n, voici les plus fréquentes :
9. La discussion qui suit s’applique aussi au deuxième argument de fprintf ou de fscanf
dont le premier argument désigne le flux d’entrée-sortie.

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



34 2. Premiers pas

\n fin de ligne
\t tabulation horizontale (tab)
\f saut de page (form feed)
\0 le caractère « nul » (nul)
Le caractère nul joue un rôle fondamental comme terminateur de chaîne
de caractères (voir 3.1.7). Ne pas confondre ce caractère nul avec la constante
NULL désignant le pointeur nul (voir 5.1.1).

Conversions en entrée

Nous avons déjà vu que les arguments de scanf sont des pointeurs. Les
formats et les spécifications de conversion ont pratiquement la même syntaxe
que pour printf, tout au moins en première approximation. Le texte autre
que les spécifications de conversion et les espaces doit être présent dans l’en-
trée effective. Les espaces eux ne servent que de séparateurs et pour le reste
sont ignorés (le blanc, \n, \t, \f sont des espaces). L’exécution de scanf se
termine lorsque toutes les spécifications de conversion ont été satisfaites ; elle
est abandonnée prématurément dès qu’une conversion ne peut être effectuée.
Dans tous les cas, la valeur de retour est le nombre de conversions effectivement
réalisées.

A titre d’exemple, considérons le programme 2.5 (fichier scanf.c) qui lit


un entier (i) et un réel (x) et les affiche avec le nombre d’objets lus (n).

Programme 2.5 – Programme simple de lecture de scalaires


/***** Fichier: scanf.c *****/

#include <stdio.h>

5 int main()
{
int i;
double x;
int n;
10
n = scanf("%d toto %lg", &i, &x);
printf("n = %d, i = %d, x = %lg\n", n, i, x);
}

Voici quelques exemples de son exécution :


% make scanf
gcc -g -std=c99 -Wall -o scanf scanf.c
% scanf
12 toto 3
n = 2, i = 12, x = 3
% scanf
12
toto
3
n = 2, i = 12, x = 3
% scanf

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
2.4. Tableaux et instruction d’itération 35

12 toto
3
n = 2, i = 12, x = 3
% scanf
12toto3
n = 2, i = 12, x = 3
% scanf
12toto
3
n = 2, i = 12, x = 3
% scanf
12 3
n = 1, i = 12, x = 1.9064e-310
% scanf
12 to
n = 1, i = 12, x = 1.9064e-310
% scanf
aaaaaa 12 toto 3
n = 0, i = -1081747800, x = 1.9064e-310
%
Les valeurs étranges de x et i (en rouge) sont bien évidemment le symp-
tome que ces variables n’ont pas été initialisées (d’ailleurs, on remarque que n
est inférieur à 2 dans ces cas).
Les exemples précédents montrent, s’il en est besoin, que scanf est une
fonction extrêmement délicate à manipuler! À l’avenir, nous l’éviterons le plus
possible!

2.4 Tableaux et instruction d’itération


Comme troisième exemple, le programme 2.6 réalise le tri par insertion
d’un tableau d’entiers. Cet algorithme de tri s’effectue sur place (c’est-à-dire ne
nécessite aucune mémoire supplémentaire) et requiert de l’ordre de n2 compa-
raisons (O(n2 )) si n est le nombre d’éléments à trier 10 .
Cette méthode est celle qu’utilise naturellement quelqu’un qui range ses
cartes (au bridge ou au poker par exemple). La figure 2.1 schématise un pas
d’itération de la boucle principale. On parcourt la liste de gauche à droite, et
on insère l’élément courant (d’indice i) à sa place dans la sous-liste de gauche
(déjà triée, par construction). Au besoin, pour ce faire, on crée un espace en
décalant des éléments vers la droite.
On trouve, au niveau principal, cinq éléments dans ce programme :
– deux directives #include pour le préprocesseur ;
– la définition de la fonction insertion_sort ;
– une directive au préprocesseur qui définit la constante NMAX égale à 1000
#define NMAX 1000
– la définition de la fonction main.
La fonction insertion_sort réalise l’agorithme décrit précédemment. Si
le nombre d’éléments à trier n vaut 1 (ou moins) il n’y à rien à faire. L’ins-
truction return termine alors l’exécution de la fonction. Cette instruction peut
10. Les meilleurs algorithmes de tri séquentiels sont en O(n log n) comparaisons.

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



36 2. Premiers pas

Programme 2.6 – Tri par insertion d’une suite d’entiers


/***** Fichier: insertion_sort.c *****/

#include <stdio.h>
#include <stdlib.h>
5
void insertion_sort(int tab[], int n)
{
int i, j, current;

10 if (n <= 1)
return;
for (i = 1; i < n; i++)
{
current = tab[i];
15 for (j = i - 1; j >= 0; j--)
{
if (tab[j] <= current)
break;
else
20 tab[j + 1] = tab[j];
}
tab[j + 1] = current;
}
}
25
#define NMAX 1000

int main()
{
30 int n, i;
int t[NMAX];

printf("entrez une liste d’entiers terminee par EOF? ");


for (n = 0; n < NMAX && scanf("%d", &t[n]) != EOF; n++)
35 /* Rien */;

insertion_sort(t, n); /* Fait tout le travail */

printf("liste triee = ");


40 for (i = 0; i < n; i++)
printf("%d ", t[i]);
putchar(’\n’);
return 0;
}

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
2.4. Tableaux et instruction d’itération 37

Figure 2.1 Algorithme de tri par insertion

être suivie d’une expression dont l’évaluation fournit la valeur retournée par
la fonction. Mais ici, la fonction square_root a pour type de retour void ce
qui signifie qu’elle ne retourne aucune valeur (plutôt qu’une fonction, c’est une
procédure).
La boucle for permet de parcourir le tableau tab, passé en argument, dont
la dimension (le nombre de composantes) est n. Malgré sa syntaxe étrange, la
boucle for est assez simple à comprendre. En fait
for (initialisation; entretien; rebouclage)
corps
est (à peu près) équivalent à
initialisation;
while (entretien)
{
corps
rebouclage
}
Donc dans notre cas
for (i = 1; i < n; i++)
{
corps
}
est très précisément équivalent à
i = 1;
while (i < n)
{
corps
i++;
}
L’instruction i++ est l’incrémentation de i, ici équivalente à i = i + 1. Cette
boucle for exécute son corps pour les valeurs de i variant de 1 à n-1 inclus.

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



38 2. Premiers pas

L’expression tab[i] désigne bien sûr la composante d’indice i du tableau


tab. A noter qu’en C les indices débutent à 0.
La boucle interne (indexée par j) parcourt la liste vers la gauche, à partir
de la position courante, en décalant au fur et à mesure les éléments pour faire
de la place pour insérer current. Elle se termine soit sur l’instruction break
qui l’interrompt prématurément, soit lorsque j vaut -1. Dans les deux cas, la
valeur courante de j est, à 1 près, celle de l’indice où il convient d’insérer
current.
Une forme plus idiomatique de la boucle de tri
A cause de l’équivalence mentionnée avec la boucle while, la boucle in-
terne (en j) aurait pu prendre une forme plus « idiomatique » en utilisant
l’opérateur et logique && :
for (j = i - 1; j >= 0 && tab[j] > current; j--)
{
tab[j + 1] = tab[j];
}

Par ailleurs, le corps de la boucle ne comportant qu’une seule instruction,


les accolades sont inutiles :
for (j = i - 1; j >= 0 && tab[j] > current; j--)
tab[j + 1] = tab[j];

C possède aussi l’opérateur ou logique représenté par ||. Ces deux opéra-
teurs (|| et &&) sont évalués de gauche à droite, et l’évaluation s’interrompt
dès que le résultat est déterminé.
La fonction main définit un tableau local t de dimension maximale NMAX.
Ce tableau est initialisé composante par composante grâce à scanf. On re-
marque que le corps de la boucle est vide, tout le travail ayant été fait par
effet de bord dans la condition d’entretien. A la fin de la boucle, n est le
nombre d’éléments lus. La liste triée par invocation de insertion_sort
est imprimée élément par élément grâce à printf. On n’oublie pas d’ajou-
ter une fin de ligne à la fin de la liste. Noter la différence entre les chaînes
de caractères littérales (entre ") et les caractères littéraux (entre ’).
Voici un exemple d’exécution :
% make insertion_sort
gcc -g -std=c99 -Wall -o insertion_sort insertion_sort.c
% insertion_sort
entrez une liste d’entiers terminée par EOF? 12 -3
2 -1 7 -13 18 12 -27
ˆD
liste triee = -27 -13 -3 -1 2 7 12 12 18
%

2.5 Arguments du shell ; fonction récursive


Le terme de rang n de la suite de Fibonacci est défini ainsi :

 u n = u n −1 + u n −2

u =0
 0
u1 = 1

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
2.5. Arguments du shell ; fonction récursive 39

Le programme 2.7 calcule un . La fonction fibo est une implémentation


directe de la formule précédente. Elle s’invoque récursivement. La récursion se
termine car n décroit à chaque appel et finit donc par être inférieur à 2.

Programme 2.7 – Calcul du terme de rang n de la suite de Fibonacci


/***** Fichier: fibo.c *****/

#include <stdio.h>
#include <stdlib.h>
5
int fibo(int n)
{
if (n < 2)
return n;
10 else
return fibo(n-1) + fibo(n-2);
}

int main(int argc, char *argv[])


15 {
int i;

if (argc == 2)
{
20 i = atoi(argv[1]);
}
else
{
fprintf(stderr, "usage: fibo n\n");
25 exit(1);
}
printf("fibo(%d) = %d\n", i, fibo(i));
exit(0);
}

La valeur de n est obtenue grâce aux arguments de la ligne de commande qui


correspondent aux arguments de la fonction main. On sait que sous le shell une
commande est invoquée par son nom avec une éventuelle liste d’arguments qui
sont des chaînes de caractères, comme dans
% ls -lg -t -d /usr/bin /usr/local/bin
A l’exécution de la commande correspond celle d’un programme et donc
d’une fonction main si ce programme a été écrit en C. Les arguments de main
permettent de récupérer les arguments positionnels de la commande à shell :
– l’entier argc est le nombre d’arguments positionnels y compris le nom
de la commande (dans l’exemple de ls, argc vaut 6) ;
– le tableau de chaînes de caractères argv, dont la déclaration sera ex-
plicitée en 5.3.2, contient lui les arguments positionnels, c’est-à-
dire ici les six chaînes "ls", "-lg", "-t", "-d", "/usr/bin" et
"/usr/local/bin".

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



40 2. Premiers pas

La fonction de bibliothèque standard atoi 11 transforme une chaîne de ca-


ractères (ici le premier argument positionnel de la commande, soit argv[1])
en entier, si toutefois cela a un sens.

La fonction fibo aurait pu être écrite de manière plus « idiomatique » en


utilisant une expression conditionnelle :
int fibo(int n)
{
return (n < 2) ? n : fibo(n-1) + fibo(n-2);\\
}
Une expression comme
a ? b : c
se lit: « si a alors b sinon c ». Elle s’évalue comme cette lecture le laisse supposer.
Voici un exemple d’utilisation :
% make fibo
gcc -g -std=c99 -Wall -o fibo fibo.c
% fibo 20
fibo(20) = 6765
% fibo 25
fibo(25) = 75025
% fibo 1
fibo(1) = 1
%
Une modification élémentaire de la fonction fibo permet de voir la récur-
sivité à l’œuvre (fichier fibo1.c) :
int fibo(int n)
{
int r;

printf("entre dans fibo --- n = %d\n", n);

if (n < 2)
r = n;
else
r = fibo(n-1) + fibo(n-2);

printf("sort de fibo --- n = %d, fibo = %d\n", n, r);


return r;
}
Voici le résultat :
% fibo1 4
entrée dans fibo -- n = 4
entrée dans fibo -- n = 3
entrée dans fibo -- n = 2
entrée dans fibo -- n = 1
sortie de fibo -- n = 1, fibo = 1
entrée dans fibo -- n = 0
sortie de fibo -- n = 0, fibo = 0
11. pour ASCII to integer

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
2.6. Exercices du chapitre 2 41

sortie de fibo -- n = 2, fibo = 1


entrée dans fibo -- n = 1
sortie de fibo -- n = 1, fibo = 1
sortie de fibo -- n = 3, fibo = 2
entrée dans fibo -- n = 2
entrée dans fibo -- n = 1
sortie de fibo -- n = 1, fibo = 1
entrée dans fibo -- n = 0
sortie de fibo -- n = 0, fibo = 0
sortie de fibo -- n = 2, fibo = 1
sortie de fibo -- n = 4, fibo = 3
fibo(4) = 3
%

Figure 2.2 Arbre de calcul récursif de fibo(4)

On peut représenter ce calcul récursif sous forme d’un arbre comme dans
la figure 2.2. La fonction fibo évalue cet arbre de la gauche vers la droite,
« en profondeur d’abord », c’est à-dire en évaluant effectivement les « feuilles »
(indiquées en bleu) d’abord.

2.6 Exercices du chapitre 2

Exercice 2.1 (Mise en route) Utiliser votre éditeur favori pour saisir le texte de
l’un des exemples de ce chapitre. Compiler et exécuter cet exemple. Noter la co-
loration syntaxique (syntax highlighting) utilisée par votre éditeur (la plupart des
éditeurs actuels en disposent). Si votre éditeur le permet ou si vous utilisez un
environnement intégré comme kdevelop, eclipse, visual studio ou dev-c++,
habituez-vous à compiler et à corriger vos erreurs sans quitter cet éditeur ou cet
environnement : cela fait gagner beaucoup de temps et évite les manipulations
stupides 12 !

Exercice 2.2 (Copie de fichiers) Modifier copy (programme 2.3) pour qu’il im-
prime le nombre de lignes copiées sur l’erreur standard (stderr).
12. comme par exemple modifier un fichier et le compiler sans l’avoir sauvegardé au préa-
lable. . .

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



42 2. Premiers pas

Exercice 2.3 (La commande iota) Écrire le programme iota qui, lorsqu’il est
invoqué depuis le shell avec l’argument n produit la suite des entiers de 1 à n ;
par exemple
% iota 7
1 2 3 4 5 6 7
%

Exercice 2.4 (Minimum et maximum d’un ensemble d’entiers) Écrire un prog-


ramme qui imprime le maximum et le minimum d’une liste d’entiers.

Exercice 2.5 (Conversion C ELSIUS -FARENHEIT) Écrire un programme qui im-


prime la table de conversion des degrés Celsius en degrés Farentheit entre
-50˚C et +110˚C par pas de 5˚C , ainsi que la table inverse de -60˚F à +220 ˚F
par pas de 10˚F.
On rappelle la formule de conversion : si C est la température en ˚C et F celle
en ˚F, on a

9
F= C + 32
5

Exercice 2.6 (« Dérécursiver » le calcul de la suite de F IBONACCI) « Dérécursi-


ver » fibo, c’est-à-dire remplacer les appels récursifs par des boucles.

Exercice 2.7 (Amélioration du calcul de la racine carrée) Considérez le pro-


gramme de calcul de la racine carrée par la méthode de Newton (pro-
gramme 2.4). Exécutez-le pour de petites valeurs de x, par exemple de l’ordre
de 10−6 . Que se passe-t-il ? Identifiez le problème. Quelle solution peut-on
trouver pour améliorer la situation ? Vérifiez le résultat.
Suggestion
Si un est la suite utilisée dans la méthode de Newton, que vaut la limite sui-
vante ?
u n +1
lim
n→∞ un

Exercice 2.8 (Mauvais format) Écrire un programme qui utilise printf pour
imprimer un réel (float ou double) en format %d, un entier (int) en format
%f et un caractère (char) en format %d.
Faites le même exercice mais en lecture, avec scanf. Que concluez-vous ?
Remarque
Pour cet exercice, ignorez les messages d’avertissement (warnings) éventuels du
compilateur. Pouvez-vous imaginer un moyen de faire taire ces messages ?

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
Chapitre 3

Bases du langage

O n décrit ici les éléments fondamentaux du langage C : éléments lexi-


caux (identificateurs, séparateurs, constantes diverses. . .), types de don-
nées scalaires, opérateurs et expressions, enfin instructions.

3.1 Eléments lexicaux


3.1.1 Jeu de caractères
L’écriture d’un programme C utilise un jeu de caractères qui doit au mini-
mum comporter les éléments suivants : lettres minuscules et majuscules, ca-
ractères spéciaux (ponctuations, parenthèses...), espaces (blanc, fin de ligne,
tabulations...). Les majuscules et les minuscules sont considérées comme des
caractères différents (en anglais, on parle de case-sensitivity).
Le jeu de caractères le plus utilisé est l’ascii dont la table 3.1 donne la
description. Certaines machines relevant en particulier de la mouvance des gros
ordinateurs ibm utilisent le code ebcdic qui contient les caractères imprimables
du code ascii plus quelques autres.
Dans le code ascii, les 32 premiers caractères (de nul à us) ainsi que del
sont non imprimables. La table 3.1 permet de trouver le code numérique corres-
pondant à un caractère : par exemple, le caractère A a pour code hexadécimal
41, soit 65 en décimal ou encore 101 en octal.
En fait la norme ansi C suppose que tous les caractères du code ascii sont
représentables dans le jeu courant 1 , sauf @ et ‘ (la backquote).
Portabilité du jeu de caractères
Il importe de remarquer que tout programme C qui fait une hypothèse
implicite sur le jeu de caractères utilisé est par essence non portable. Un
programme qui n’utilise que le code ascii est en principe sûr. Les codes
dans lesquels un caractère est codé sur 8 bits peuvent également être ma-
nipulés sans trop de problème dans les chaînes de caractères et seront
rendus correctement si votre environnment est adéquat (variables LANG
par exemple).
Dès que l’on veut utiliser des jeux de caractères multi-octets comme Uni-
code, il vaut mieux se mettre en mode C99. Les fonctions correspondantes C99
sont décrites en 11.3.5.
1. mais pas nécessairement dans le codage ascii!

V 2.1 – 7 février 2010 43 c Jean-Paul R IGAULT



44 3. Bases du langage

Table 3.1 Jeu de caractères ASCII

0 1 2 3 4 5 6 7
0 nul dle sp 0 @ P ‘ p
1 soh dc1 ! 1 A Q a q
2 stx dc2 " 2 B R b r
3 etx dc3 # 3 C S c s
4 eot dc4 $ 4 D T d t
5 enq nak % 5 E U e u
6 ack syn & 6 F V f v
7 bel etb ’ 7 G W g w
8 bs can ( 8 H X h x
9 tab em ) 9 I Y i y
A nl sub * : J Z j z
B vt esc + ; K [ k {
C ff fs , < L \ l |
D cr gs - = M ] m }
E so rs . > N ^ n ~
F si us / ? O _ o del

Remarque sur les « digraphs » et les « trigraphs »


Au cas où le jeu de caractères disponible serait insuffisant (!), la norme
ansi C définit une représentation particulière de certains caractères spé-
ciaux, dénommée « trigraph ». Les trigraphs comportent 3 caractères (!)
dont les deux premiers sont ?? et sont remplacés par leur caractère
équivalent dans tout contexte (y compris les chaînes de caractères et les
constantes litérales). On se porte aussi bien en ignorant cette bizarrerie 2
mais il convient d’éviter le double point d’intérrogation en particulier dans
les commaentaires (voir 3.1.3) et constantes littérales représentant des ca-
ractères individuels (voir3.1.6) ou des chaînes (voir 3.1.7) !
Ajoutant le saugrenu à la bizarrerie, C++a aussi défini des séquences de
deux caractères (donc des « digraphs ») permettant de représenter aussi
C99 une partie de caractères spéciaux. C99 s’est précipité pour adopter cette
idée géniale. Ainsi les crochets carrés ([ et ]) peuvent-ils être représen-
tés par <: et :>, les accolades ({ et }) par <% et %>, le dièse (#) par %:
et le double dièse (##) évidemment par %:%:. Cela n’améliore certes pas
la lisibilité ! Contraitrement aux trigraphs, les digraphs ne sont pas substi-
tués à l’intérieur d’un commentaire ni d’une constante littérale (chaîne ou
caractère) ; en fait leur substitution n’a lieu que s’ils représentent par eux-
mêmes un élément lexical (un token — voir 3.1.2). Il est peu probable que
vous ayez à utiliser les digraphs...

3.1.2 Structure lexicale d’un fichier-source


Un fichier de source C est constitué d’éléments lexicaux qui se divisent
en six catégories : mots-clés, identificateurs, constantes arithmétiques littérales,
chaînes de caractères littérales, opérateurs et autres séparateurs.
2. Cette bizarrerie a une raison, cependant. Les caractères correspondants sont ceux utilisés
par C++ qui sont absents du jeu de caractères normalisé supposé être implanté sur toutes les
machines, l’Invariant Code Set iso 646-1983.

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
3.1. Eléments lexicaux 45

Les séparateurs permettent d’isoler les autres éléments lexicaux. Les opé-
rateurs sont aussi des séparateurs. Les espaces, c’est-à-dire le blanc (sp), la fin
de ligne (nl), les tabulations horizontale (tab ou ht) ou verticale (vt) et le saut
de page (ff) sont des séparateurs « purs » en ce sens que pour le reste ils sont
ignorés.

3.1.3 Commentaires
En C90 les commentaires sont encadrés entre /* et */ et peuvent s’étendre
sur plusieurs lignes mais ne peuvent pas s’imbriquer :
/* Un long commentaire
sur
3 lignes */

/* Une /* erreur de
syntaxe */ à coup sûr ! */
Ce type de commentaire est équivalent à un espace pour le compilateur (c’est
donc un séparateur).
C99 autorise aussi les commentaires « à la C++ » commençant par // et C99
s’étendant jusqu’à la fin de la ligne. Il est possible d’imbriquer un commentaire
C90 dans un commentaire C99 ou l’inverse :
// un commentaire-ligne

/* Toute une section en commentaire,


y compris ses commentaires-lignes
x = 0; // plus de x
y = 1; // encore un y
*/

3.1.4 Identificateurs
Les identificateurs permettent de nommer des objets (constantes, variab-
les, fonctions). Ils commencent par une lettre ou le caractère souligné (_) suivi
d’une séquence de lettres, de chiffres ou de soulignés. Par exemple :
x X toto NBUF
j21 a2ps char_0_to_9
Prix_HT Square_Root num_secu PascalStyleId
_strcmp __DATE__ __STDC__ _1993
En revanche, les mots suivants ne sont pas des identificateurs :
#x 2#$!**)((_+%^ 2toto Prix-TTC
On rappelle que les majuscules et les minuscules sont considérées comme
des caractères différents : Square_Root et square_root ne sont pas le même
identificateur.
Portabilité : jeu de caractères étendu pour les identificateurs
En C90, seul le code ascii peut être utilisé pour coder les identificateurs
(noms de constantes, variables, fonctions, macros. . .).
En revanche, C99 permet l’utilisation des caractères étendus (Unicode) C99

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



46 3. Bases du langage

dans les identificateurs. Cependant, peu de compilateurs réalisent actuel-


lement cette fonctionalité (en particulier gcc-4.2 ne la supporte pas). On
évitera donc les identifcateurs comme été42 ou δθ.

Portabilité : identificateurs réservés


Les identificateurs débutant par un double caractère « souligné » (_) sont
réservés à des usages spéciaux (voir 6.3.6) et ne devraient pas être utilisés
en dehors de ces usages.
En principe, les noms de fonctions de la bibliothèque normalisée (fopen,
printf, exit... — voir 9) ne devraient pas être utilisés pour d’autres
usages.
Enfin, les mots-clés (voir 3.1.5) sont strictement réservés.

Portabilité : longueur d’un identificateur


La norme ansi C réclame que les 31 premiers caractères au moins d’un
identificateur soient pris en compte sauf pour les identificateurs d’objets
externes qui sont manipulés par l’éditeur de liens. Pour ces derniers, les
6 premiers caractères au moins doivent être significatifs. Les compilateurs
et les éditeurs de liens « modernes » ne placent pratiquement aucune li-
mite sur la longueur des identificateurs. Se méfier cependant : quant à la
modernité, les éditeurs de liens sont bien plus rares que les compilateurs !

3.1.5 Mots-clés
Les mots-clés de C sont strictement réservés. Ils ne doivent pas être utili-
sés comme identificateurs sinon le programme devient incompilable. Tous les
mots-clés sont en minuscules ; ils sont au nombre de 32 pour C90 et le ta-
bleau 3.2 en donne la liste.

Table 3.2 Mots-clés de C


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

C99 La norme C99 ajoute 5 mots-clés supplémentaires (table 3.3).

Table 3.3 Mots-clés supplémentaires de Cnew

restrict inline _Complex _Imaginary _Bool

3.1.6 Constantes littérales arithmétiques

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
3.1. Eléments lexicaux 47

Constantes littérales caractères

La séquence ’a’ représente le caractère a. Une constante caractère est donc


un caractère encadré de simples « quotes » (’). Lorsque le caractère n’est pas
imprimable, on peut utiliser une séquence d’échappement comme ’\n’. La
table 3.4 donne la liste complète de ces séquences.

Table 3.4 Séquences d’échappement pour les caractères

\a alarme alarm bel


\b retour arrière backspace bs
\f saut de page form feed ff
\n saut de ligne new line nl
\r retour chariot carriage return cr
\t tabulation horizontale horizontal tab tab (ht)
\v tabulation verticale vertical tab vt
\\ le \ lui-même backslash
\0 le caractère de code 0 nul nul
\’ la simple quote elle-même
\" la double quote elle-même
\? le point d’interrogation

A titre d’exemple, le programme 3.1 remplace chaque fin de ligne de l’entrée


standard par un blanc.

Programme 3.1 – Remplacement des fins de ligne par un blanc


/***** Fichier: rm_nl.c *****/

#include <stdio.h>

5 int main()
{
int c;

while ((c = getchar()) != EOF)


10 {
if (c == ’\n’) c = ’ ’;
putchar(c);
}
putchar(’\n’);
15 }

Portabilité : valeur entière d’un caractère


La norme permet de représenter tout caractère par son code octal ou hexa-
décimal. Ainsi ’A’, ’\101’ et ’\x41’ sont trois représentations équiva-
lentes de la constante caractère A. Conformément à la remarque déjà faite
en 3.1.1, ceci peut être une cause de non-portabilité.

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



48 3. Bases du langage

Constantes entières littérales

ansi C supporte trois représentations principales des constantes entières


littérales :
– décimale, constituée des chiffres de 0 à 9 (0, 123, 2812727837...) ;
– octale, débutant par le caractère 0 et ne comportant que les chiffres de 0
à 7 (0123, 0177) ;
– hexadécimale, débutant par le préfixe 0x ou 0X et constituée des chiffres
de 0 à 9 et des lettres de a à f en majucules ou minuscules indifférem-
ment (0x10 , 0xff, 0XaaBBcc00...).
Une constante entière littérale peut être précédée d’un signe (+ ou -). Ce
signe ne fait pas réellement partie de la constante mais désigne en fait l’opéra-
teur correspondant (voir 3.3.1).
Il existe de nombreuses variations de syntaxe des constantes entières qui
correspondent aux différents sous-types des entiers et qui seront étudiées
en 3.2.3.

Constantes réelles littérales

Les constantes réelles littérales peuvent prendre deux formes :


– virgule fixe avec une partie entière et une partie fractionnaire (décimales)
séparées par un point (2.718285, 0.314159...) ;
– exponentielle, identique à la virgule fixe avec en plus un facteur d’échelle
introduit par le caractère e ou E et qui représente un exposant en base 10
(6.02e+23 pour 6, 02 × 1023 , 1.e-6 pour 10−6 ...).
Les deux remarques précédentes sur la présence possible d’un signe et
l’existence de variations s’appliquent ici aussi (voir 3.2.4 pour les variations).

3.1.7 Chaînes de caractères littérales


Une chaîne de caractères littérale est une suite de caractères encadrées de
doubles « quotes ». Les séquences d’échappement (3.1.6 et table 3.4) sont bien
entendu utilisables :
"ceci est une chaine de caracteres litterale"
"hello, world\n"
"Article\tPrix unitaire\tQuantite\tPrix total"
"la double quote comme \" cad avec un \ devant"
Une chaîne de caractères littérale ne peut être coupée par un retour à la
ligne : ceci
printf("hello, world
\n");
est illégal. Cependant quand des chaînes littérales sont adjacentes (c’est-à-dire
uniquement séparées par des espaces), elles sont concaténées par le compilateur :

printf("hello, " "world"


"\n");
est équivalent à
printf("hello, world\n");

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
3.1. Eléments lexicaux 49

Figure 3.1 Représentation d’une chaîne de caractères littérale

En mémoire, une chaîne littérale est représentée par la suite des caractères
qui la constitue (sans les doubles « quotes » bien sûr) terminée par le caractère
nul (\0). Ainsi, une chaîne occupe-t-elle un caractère de plus que sa longueur
« utile » (figure 3.1).
Cette propriété, qui est imposée par le compilateur pour les chaînes littéra-
les, est une convention qui doit être respectée par le programmeur en manipu-
lant des chaînes variables. A titre d’exemple, les deux programmes 3.2 et 3.3
utilisent cette propriété de terminaison des chaînes : le premier pour détermi-
ner la longueur utile, le second pour recopier une chaîne dans une autre —
noter que le caractère nul final est lui aussi recopié.

Programme 3.2 – Longueur utile d’une chaîne de caractères


/***** Fichier: string_length.c *****/

#include <stdio.h>

5 int string_length(const char orig[])


{
int i;

for (i = 0; orig[i] != ’\0’; i++) {}


10 return i;
}

int main()
{
15 printf("%d\n", string_length("hello, world\n"));
}

Pour ces deux programmes, on remarque que l’argument orig est passé
sous forme d’un tableau de caractères constants
const char orig[]
ce qui signifie que les éléments du tableau ne sont pas modifiés dans le
corps de la fonction (voir 7.3.2). En revanche, le tableau dest, paramètre de
copy_string, est évidemment modifiable, d’ou l’absence de const. Noter
aussi le corps vide de la boucle de la fonction string_length : tout le travail
est en effet réalisé dans l’entête de l’instruction for.
Attention : ’a’ et "a"
On ne doit donc pas confondre la constante charactère ’a’ avec la chaîne

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



50 3. Bases du langage

Programme 3.3 – Copie de chaînes de caractères


/***** Fichier: copy_string.c *****/

#include <stdio.h>

5 void copy_string(char dest[], const char orig[])


{
int i;

for (i = 0; orig[i] != ’\0’; i++)


10 dest[i] = orig[i];
dest[i] = ’\0’; /* copie du nul terminal */
}

int main()
15 {
char str[100];

copy_string(str, "hello, world\n");


printf(str);
20 }

de caractères "a" réduite à a et dont la représentation en mémoire com-


porte en fait deux caractères (’a’ et ’\0’).

3.2 Types scalaires et déclarations simples


3.2.1 Panorama des types de C
Il y a deux manières de classifier les types de données en C :
– les types scalaires d’une part et les agrégats d’autre part ;
– les types de base d’une part et les types utilisateurs d’autre part.
Le tableau 3.5 schématise cette situation.
Comme son titre l’indique, cette section ne concerne que les types de base.
Les agrégats et les pointeurs sont respectivement étudiés aux chapitres 4 et 5.

3.2.2 Type vide (void)


Le type void (qui ne figure pas dans le tableau 3.5) joue un rôle particulier
en ce sens qu’il n’y a aucun objet de ce type — il désigne en quelque sorte
l’ensemble vide.
A quoi peut-il servir? En fait, il n’est utilisé que dans trois circonstances qui
seront étudiées plus tard :
– pour déclarer qu’une fonction n’a pas d’arguments (7.2.1) ;
– pour déclarer qu’une fonction n’a pas de résultat — c’est une procédure
(7.2.1) ;
– enfin, dans le sens conventionnel de pointeur sur void (void* — 5.1.4).

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
3.2. Types scalaires et déclarations simples 51

Table 3.5 Panorama des types de C

Types de base Types utilisateurs


Types scalaires entiers char avec variations pointeurs,
de signe (signed et énumérations (enum)
unsigned),
int avec variations
de longueur (short, long)
et de signe (signed et
unsigned)

Types scalaires réels float,


double,
long double
Agrégats tableaux,
structures (struct),
unions (union)

3.2.3 Types de base entiers


C possède une grande variété de types « de nature entière » (integral types).
Comme indiqué dans le tableau 3.5 cela inclut bien entendu la représentation
des entiers signés ou non de différentes tailles mais aussi les énumérations, les
caractères, sans oublier les pointeurs.

Le type int et ses variations

Le type int constitue la représentation « naturelle » 3 des valeurs entières


avec les opérations habituelles (voir 3.3.1). Cependant plusieurs sous-types sont
définis, répartis en deux catégories indépendantes selon la possibilité d’être
signé ou non d’une part, et selon le nombre de valeurs représentables d’autre
part. Un entier non signé (unsigned) est toujours positif et obéit aux lois de
l’arithmétiques modulo 2n , où n est le nombre de bits de sa représentation.
Le tableau 3.6 résume toutes les variations possibles des entiers, avec les
contraintes minimales imposées par la norme ansi C sur l’ensemble des valeurs
représentables. L’examen de ce tableau appelle un certain nombre de commen-
taires et de remarques :
1. Les modifieurs de type (signed, unsigned, short, long...) sont option-
nels et signed est le défaut, sauf pour char. Le type de base peut éga-
lement être omis, c’est alors int. Donc le type signed short int est
équivalent à short int ou même à short tout court. ;
2. Les charactères sont considérés comme des (petits) entiers et le fait que,
par défaut, ils soient signés ou non dépend de l’implémentation ;
3. Les entiers ordinaires sont garantis être représentés sur 16 bits au moins ;
en fait les valeurs indiquées sont des contraintes minimales et une implé-
mentation peut les dépasser : ainsi sur la plupart des processeurs à mots
3. La représentation est « naturelle » au sens du matériel : c’est le type d’entier que le pro-
cesseur manipule le plus aisément. Sur nos machines actuelles, c’est généralement un mot de 32
bits, voire de 64.

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



52 3. Bases du langage

de 32 bits, un int est codé sur 32 bits (ce qui donne un intervalle de va-
leurs de -2 147 483 648 à +2 147 483 647 pour un signed en complément
à 2) et de 0 à 4 294 967 295 pour un unsigned).
4. Les intervalles de valeurs effectivement utilisés par une implémentation
sont définis dans le fichier d’environnement <limits.h>.

Table 3.6 Sous-types de int

Nom du type Intervalle minimal de valeurs


char 0 .. 255 ou -127 .. +127
signed char -127 .. +127
unsigned char 0 .. 255
signed short int -32 767 .. +32 767
signed int -32 767 .. +32 767
long int -2 147 483 647 .. +2 147 483 647
unsigned short int 0 .. 65 535
unsigned int 0 .. 65 535
unsigned long int 0 .. 4 294 967 295

Portabilité : ensemble de valeurs


Le respect des intervalles de valeurs du tableau 3.6 permet d’assurer la
portabilité.
Le flou sur le signe ou l’absence de signe du type char doit inciter à
la prudence : si l’on veut utiliser les caractères comme de petits entiers
et faire dessus des opérations arithmétiques, il est prudent de spécifier
explicitement si l’on veut des unsigned char ou des signed char.

Type d’une constante entière littérale

Une constante caractère correspondant à un caractère du jeu de caractères


(ASCII par exemple) comme ’a’ ou ’\n’ est de type char et est garantie
positive.
Une constante entière littérale en notation décimale, comme 123 ou encore
1 512 827 304, a le premier parmi les types suivants qui permet de représen-
ter sa valeur : int, long, unsigned long. Si la représentation est octale
ou hexadécimale, les types testés sont dans l’ordre : int, unsigned, long,
unsigned long.
On peut forcer une constante entière littérale, quelle que soit sa représenta-
tion, à être d’un des sous-types des entiers en utilisant les suffixes suivants : u
ou U pour unsigned, l ou L pour long. Voici quelques exemples :
123U unsigned int
123L long
123UL unsigned long
123LU unsigned long

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
3.2. Types scalaires et déclarations simples 53

3.2.4 Types réels


La norme connait essentiellement deux types de réels : simple et double
précision (float) et double). Elle permet aussi le type long double mais
C90 ne précise pas ses caractéristiques.
Pour les float comme pour les double, l’intervalle des valeurs absolues
représentables doit être au minimum de 10−37 à 10+37 ; un float doit avoir une
précision d’au moins 6 chiffres, alors que c’est 10 pour un double. Les valeurs
effectivement utilisées par une implémentation sont dans le fichier d’environ-
nement standard <float.h>.
Une constante réelle littérale, comme 3.141592 ou 6.02E+23 est par dé-
faut de type double. On peut la forcer à être float en utilisant le suffixe f ou
F (3.141592f ou 6.02E+23F).
Types réels en C99
La norme C99 a fait un gros effort pour rendre plus portables et plus C99
rigoureuses les manipulations de nombres réels. Ceci est évoqué en 11.3.6.

3.2.5 Définitions d’objets de type de base scalaire


Définitions simples

La syntaxe d’une déclaration de variable d’un type de base scalaire est im-
médiate : elle a la forme
nom-du-type liste-d’identificateurs ;
Par exemple :
int i;
short int i, j, k;
long l1, l2;
Une variable peut être initialisée lors de sa définition :
int i = 1;
unsigned long ul1 = 0xFFFF0000;
float x = 3.141592f;
double y = 2.7182818284;
L’initialisation se fait individuellement pour chaque variable : ainsi, dans
int i, j = 2, k, l = 1;
seuls j et l sont initialisés.
Si une variable n’est pas initialisée lors de sa définition, C utilise des règles
par défaut qui dépendent du contexte de définition (voir 8.3.3) : dans certains
cas la variable n’est pas initialisée du tout. Moralité : si une variable doit être
initialisée, elle doit l’être explicitement.

Modifieurs de type : const et volatile

Une définition peut être précédées de const : l’objet est alors une constante
(symbolique) : sa valeur ne pourra plus être modifiée. Une conséquence immé-
diate est qu’une définition de constante comporte nécessairement son initiali-
sation.

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



54 3. Bases du langage

const int LINE_LENGTH = 100;


const double PI = 3.141592;
const double AVOGADRO = 6.02E+23;
Notez qu’il est usuel (mais conventionnel) de nommer les constantes avec des
majuscules.

Une définition de variable peut également être précédée de volatile. Ceci


signifie que la variable peut changer de valeur en dehors de la volonté du
programme (c’est-à-dire sans qu’il y ait de modification de cette variable dans
le programme). C’est le cas des variables qui représentent des registres d’entrée-
sortie de contrôleurs et dont la valeur peut être changée par le monde extérieur.
Un autre est constitué par les segments de mémoire partagée entre processus.
La présence de volatile indique au compilateur qu’il doit éviter certaines
optimisations. Par exemple dans le cas de l’extrait de programme
void test_io(void)
{
volatile int io_register = 0;

while (io_register == 0)
{
sleep(1); /* attendre 1 seconde */
}
}
un compilateur astucieux pourrait, en l’absence de la déclaration volatile,
s’apercevoir que io_register n’est pas modifié dans le corps de la boucle,
qu’étant une variable locale, il ne peut être modifié ailleurs, et que l’on peut
donc transformer la fonction en la forme équivalente suivante comportant une
boucle infinie et sans la variable locale puisqu’elle ne sert à rien !
void test_io(void)
{
while (1) /* boucle infinie */
{
}
}
La présence de volatile, en signalant la possibilité que io_register puisse
être modifié à l’extérieur du programme empêche cette optimisation : l’état de
de io_register sera donc bien vérifié toutes les secondes jusqu’à ce qu’il
devienne non nul.
Une constante peut être volatile :
const volatile double vc;
Cela signifie que l’objet vc ne peut être modifié par programme mais peut l’être
par l’extérieur. Dans ce cas, la définition de constante n’exige pas d’initialisa-
tion.

3.2.6 Types énumérés


Il s’agit d’un sous-type des entiers, mais défini par l’utilisateur. La défini-
tion d’un type énuméré indique la liste des valeurs que peut prendre un objet
du type : ainsi

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
3.2. Types scalaires et déclarations simples 55

enum color {GREEN, ORANGE, RED};


définit-il un type, de nom enum color 4 . On peut définir des objets du type et
les affecter entre eux :
enum color c1;
enum color c2 = GREEN;
...
c1 = c2;
c2 = RED;
On peut à la fois définir le type et un ou plusieurs objets du type :
enum first_name {BOB, CAROL, TED, ALICE} p1, p2 = CAROL;
Dans ce cas on peut même faire l’économie du tag :
enum {BOB, CAROL, TED, ALICE} p1, p2 = CAROL;
Le type devient anonyme et il sera donc impossible de définir plus tard d’autres
objets du même type.

En fait, les valeurs que peut prendre un objet de type é numéré sont des
entiers, débutant à 0 par défaut. Dans les exemples précédents, VERT vaut 0 et
ALICE vaut 3. On peut même forcer les valeurs que l’on souhaite pour certains
éléments :
enum instruction
{
LOAD = 0xfa,
STORE, /* 0xfb */
ADD = 0x10,
SUB, /* 0x11 */
MUL, /* 0x12 */
DIV, /* 0x13 */
JUMP = 0x20,
HALT = 0xff,
};
Dans une certaine mesure (voir cependant 3.4.2) on peut mélanger les objets de
types énumérés et les entiers.
Deux types énumérés différents ne peuvent avoir des valeurs de même
nom :
enum color {GREEN, ORANGE, RED};
enum fruit {APPLE, PEAR, ORANGE}; /* erreur! */

3.2.7 Synonymie de types : typedef


L’instruction typedef permet de définir des noms de types synonymes. La
syntaxe est la même que celle d’une déclaration d’objet précédée du mot-clé
typedef. Ainsi
unsigned int U_int;
est la déclaration d’une variable U_int, alors que
4. enum est un mot-clé de C, color est un identificateur choisi par le programmeur, le tag,
et le nom du type est enum color.

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



56 3. Bases du langage

typedef unsigned int U_int;


définit le nom U_int comme étant synonyme de unsigned int. On peut
donc maintenant déclarer ou définir des objets de type U_int :
U_int u1, u2;
U_int u3 = 12u;
U_int add = 0xFFFF;
Voici quelques autres exemples :
typedef double Length;
typedef double Surface;

typedef const int C_int;


typedef volatile double V_double;
Cette instruction permet de donner un nom plus manipulable aux types
énumérés. Par exemple, en supposant que le type enum color soit déjà défini,
mais pas enum doc :
typedef enum color Colors;
typedef enum doc {ARTICLE, REPORT, BOOK} Document;
typedef enum {MALE, FEMALE} Sex;
typedef enum {FALSE, TRUE} Boolean;
...
Colors c = GREEN;
Document d, d1 = REPORT;
Sex sx = MALE;
Boolean flag = FALSE;
Noter que typedef définit une simple synonymie, et non de la création
d’un nouveau type. Ainsi, après
Length l;
Surface s;
les deux variables s et l sont de même type (double) et on peut par exemple
écrire l’affectation
s = l;

3.3 Opérateurs
Les opérateurs combinés avec les identificateurs et les constantes littérales
ou symboliques permettent d’écrire des expressions. Chaque expression a un
type et une valeur, résultat de son évaluation. Le tableau 3.7 donne la liste
de tous les opérateurs du langage avec leur signification et la référence de la
section où ils sont étudiés.

3.3.1 Opérateurs arithmétiques


Sauf mention explicite du contraire, les opérateurs décrits ici s’appliquent à
tous les objets (constants ou variables) de type entier ou réel (y compris toutes
les variations). Si les entiers sont unsigned, les règles de l’arithmétique mo-
dulo 2n sont utilisées.

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
3.3. Opérateurs 57

Table 3.7 Liste des opérateurs de C

Opérateur Utilisation Signification Référence


() f(x1, x2, ...) appel de fonction 3.3.7
[] t[i] indexation 3.3.5
-> p->champ sélection de champ de structure 3.3.5
. s.champ sélection de champ de structure 3.3.7
! !a négation logique 3.3.2
~ ~a complément à 1 3.3.3
- -a moins unaire 3.3.1
+ +a plus unaire 3.3.1
* *p indirection 3.3.5
& &x adresse de 3.3.5
++ x++ ou ++x post ou pré-incrémentation 3.3.4
-- x-- ou --x post ou pré-décrémentation 3.3.4
(type) (type)a conversion explicite (cast) 3.3.6
sizeof sizeof(x) taille mémoire d’un objet 3.3.6
* a*b multiplication 3.3.1
/ a/b division 3.3.1
% a%b modulo (reste de la division) 3.3.1
+ a+b addition 3.3.1
- a-b soustraction 3.3.1
<< a<<b décalage gauche 3.3.1
>> a>>b décalage droit 3.3.1
> a<b inférieur 3.3.2
<= a<=b inférieur ou égal 3.3.2
> a>b supérieur 3.3.2
>= a>=b supérieur ou égal 3.3.2
== a==b égal 3.3.2
!= a!=b différent de 3.3.2
& i&j « et » bit à bit 3.3.3
^ i^j « ou exclusif » bit à bit 3.3.3
| i|j « ou » bit à bit 3.3.3
&& a&&b « et » logique séquentiel 3.3.2
|| a||b « ou » logique séquentiel 3.3.2
? : a?b:c expression conditionelle 3.3.1
= += *= ... a=b affectations 3.3.4
, x1,x2,...,xn évaluation séquentielle 3.3.7

ansi C connait deux opérateurs arithmétiques unaires : + et -. Cinq opé-


rateurs binaires sont définis : addition (+), soustraction (-), multiplication (*),
division (/), modulo (%). Noter qu’il n’existe pas d’opérateur d’exponentiation.
La division est la division entière si les deux opérandes sont entiers. L’opé-
ration modulo est possible seulement sur les entiers, pas sur les réels : a%b
donne le reste de la division entière de a par b.
A titre d’exemple, le programme 3.4 calcule de manière récursive le Plus
Grand Commun Diviseur de deux entiers (qui sont donnés en arguments de la
ligne de commande). En voici quelques exemples d’exécution :
% gcd 0 1
gcd(0, 1) = 1
% gcd 12 7

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



58 3. Bases du langage

gcd(12, 7) = 1
% gcd 24 12
gcd(24, 12) = 12
% gcd 27 63
gcd(27, 63) = 9
% gcd -27 63
gcd(-27, 63) = 9
% gcd -27 -63
gcd(-27, -63) = 9
% gcd 0 0
gcd(0, 0) = 0
%

Portabilité : signe du reste et du quotient


Pour la division entière et le modulo, le signe du quotient (a/b) et du reste
(a%b) dépend de l’implémentation. C’est bien entendu une calamité, et on
doit donc éviter les opérandes négatifs pour ces deux opérations. En par-
ticulier, remarquez les précautions qui sont prises dans le programme 3.4.

3.3.2 Opérateurs relationnels et logiques


Ces opérateurs retournent une valeur logique c’est-à-dire, comme il n’existe
pas de type booléen en C90, un entier (int) : 0 représente la valeur faux et 1 la
valeur vrai.
C99 En revanche, C99 propose un type booléen, nommé _Bool (c’est un mot-
clé) dont les éléments ne peuvent prendre que les valeurs 0 et 1. En outre, après
avoir inclus le fichier d’entête <stdbool.h>, vous pouvez utiliser le synonyme
bool (pour _Bool) et les deux valeurs true (1) et false (0). Notez que bool,
true et false ne sont pas des mots-clés (mais des macros du pré-processeur).

Opérateurs relationnels

Les opérateurs relationnels sont utilisables avec tout type scalaire entier ou
réel. Ils sont au nombre de six : <, >, <=, >=, == et !=. Noter que l’opérateur
d’égalité est == et non pas = — ce dernier est l’opérateur d’affectation (3.3.4).
Remarque sur l’identité entre booléens et entiers
Le fait qu’une expression comme a < b retourne un entier (0 ou 1) permet
d’écrire des expressions « étranges » comme
4*(a < 0) + 8*(a < 4) + 16*(a < 8)
Ce style de programmation est en général douteux.
Portabilité : comparaison des réels
Comme souvent dans les langages de programmation, on doit être très
prudent dans les tests entre réels : le résultat peut dépendre de la précision
du processeur.

Valeur de vérité d’une expression

La valeur de vérité d’un expression arithmétique e, notée VV (e) est définie


ainsi :
vrai si e 6= 0

VV (e) =
faux si e = 0

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
3.3. Opérateurs 59

Programme 3.4 – Calcul du pgcd de deux entiers


/***** Fichier: gcd.c *****/

#include <stdio.h>
#include <stdlib.h>
5
int max(int a, int b)
{
return (a > b) ? a : b;
}
10
int min(int a, int b)
{
return (a < b) ? a : b;
}
15
int gcd(int a, int b)
{
int a0 = abs(a); /* abs est defini dans <stdlib.h> */
int b0 = abs(b);
20 int p = max(a0, b0);
int q = min(a0, b0);

return (q == 0) ? p : gcd(q, p%q);


}
25

int main(int argc, char *argv[])


{
int x, y;
30
if (argc != 3)
{
fprintf(stderr, "usage: gcd a b\n");
return 1;
35 }

x = atoi(argv[1]);
y = atoi(argv[2]);
printf("gcd(%d, %d) = %d\n", x, y, gcd(x, y));
40 return 0;
}

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



60 3. Bases du langage

En principe e peut être de n’importe quel type arithmétique, mais, comme


on l’a déjà remarqué, on doit se méfier du test à 0 des réels. Il vaut donc mieux
se limiter à des expressions entières. Noter aussi que même les valeurs (stricte-
ment) négatives donnent la valeur de vérité vrai.

Négation logique

L’expression !a inverse la valeur de vérité de l’expression a :

0 si a 6= 0

!a =
1 si a = 0

Connecteurs logiques séquentiels

Dans l’expression a&&b, l’opérateur && dénote le et logique (conjonction)


des deux valeurs de vérité des expressions a et b. Cependant l’évaluation s’ef-
fectue séquentiellement de gauche à droite et s’interrompt dès que la valeur de l’ex-
pression est déterminée, c’est-à-dire au premier opérande faux. Ainsi, compte
tenu de l’associativité de cet opérateur, dans
a$_1$ && a$_2$ && ... && a$_n$
les expressions ai sont évaluées dans l’ordre, et l’on s’arrête à la première dont
la valeur de vérité est faux, le résultat global étant alors faux également. Dans
cet autre exemple
int t[100];
...
if (n < 100 && t[n] == 0) ...
l’expression t[n] n’est évaluée que si n est inférieur à 100, évitant ainsi le
potentiel débordement d’indice.
L’opérateur || est dual de && : c’est le ou logique (disjonction). Son évalua-
tion est elle-aussi séquentielle, de gauche à droite, et s’arrête donc à la première
expression qui à la valeur vrai.

3.3.3 Opérateurs bit à bit


Ces opérateurs travaillent directement sur la représentation binaire de leurs
opérandes.

Complément à 1

L’opérateur unaire ~ inverse les 0 et les 1 de la représentation binaire de son


opérande.

ET et OU bit à bit

Les deux opérateurs & et | réalisent respectivement le et et le ou bit à bit


de leurs opérandes. L’opérateur ^ réalise le ou exclusif.

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
3.3. Opérateurs 61

Décalages

Une expression comme a<<b décale la valeur de a à gauche de b positions


de bits alors que a>>b effectue le même décalage mais vers la droite.
Portabilité des opérations de décalage
La portabilité des opérations de décalage n’est garantie que si les deux
conditions suivantes sont réunies :
1. le premier opérande (celui que l’on décale) est unsigned int ;
2. le second opérande (la valeur du décalage) est un entier positif — ou
non signé — de valeur inférieure au nombre de bits correspondant à
la représentation du type du premier opérande.
Dans ces cas a<<b (resp. a>>b) est équivalent à la multiplication (resp. la
division) par 2b .
Dans tous les autres cas, la valeur du résultat est laissée à la discrétion
de l’implémentation ! Le programme 3.5 illustre cette « pathologie » du
décalage. Voici le résultat de son exécution :
% chkshift
ui = 2147483647, (ui << 1) = -2
i = 2147483647, (i << 1) = -2
j = -1, (j >> 1) = -1
k = -1073741825, (k >> 1) = 2147483646
%

Programme 3.5 – Pathologie du décalage


/***** Fichier: chkshift.c *****/

#include <stdio.h>

5 int main()
{
unsigned int ui = 0x7FFFFFFF; /* 01111...11 */
int i = ui;
int j = -1;
10 int k = 0xBFFFFFFF; /* 10111...11 */

printf("ui = %d, (ui << 1) = %d\n", ui, ui << 1);


printf("i = %d, (i << 1) = %d\n", i, i << 1);
printf("j = %d, (j >> 1) = %d\n", j, j >> 1);
15 printf("k = %d, (k >> 1) = %d\n", k, k << 1);
return 0;
}

Exemple

Afin d’illustrer ces opérations de manipulation de bits, le programme 3.6


réalise des opérations courantes de test et de positionnement d’un bit dans un
tableau de bits (ici un unsigned char).
Noter l’utilisation de la fonction (en fait la macro) assert définie dans le
fichier d’entête <assert.h>.

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



62 3. Bases du langage

Programme 3.6 – Manipulation de bits


/***** Fichier: bit_ops.c *****/

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

/* Les bits asont numerotes a partir de 0 */

/* Il y a exactement CHAR_BITS (defini dans <limits.h>) bits


10 dans un unsigned char
*/

typedef unsigned char Bit_Array;

15 unsigned char set_bit(Bit_Array bits, unsigned int n)


{
assert(n < CHAR_BIT);
return bits | (1 << n);
}
20
unsigned char reset_bit(Bit_Array bits, unsigned int n)
{
assert(n < CHAR_BIT);
return bits & ~(1 << n);
25 }

int test_bit(Bit_Array bits, unsigned int n)


{
assert(n < CHAR_BIT);
30 return bits & (1 << n);
}

int main()
{
35 Bit_Array b = 0x00;

set_bit(b, 156);

b = set_bit(b, 3);
40 if (test_bit(b, 3))
printf("Jusqu’ici, tout va bien\n");
b = reset_bit(b, 3);
if (b == 0)
printf("Cela a l’air de marcher...\n");
45
return 0;
}

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
3.3. Opérateurs 63

L’instruction assert(cond) évalue la condition cond. Si elle est vraie,


rien ne se passe ; sinon l’exécution se termine avec un message d’erreur. Par
exemple, imaginons que nous invoquions set_bit(b, 156) dans le pro-
gramme 3.6. L’exécution se termine alors ainsi :
% bit_ops
bit_ops: bit_ops.c:17: set_bit: Assertion ‘n < 8’ failed.
zsh: abort bit_ops
%

3.3.4 Affectations
Affectation simple

L’affectation simple est désignée par l’opérateur =. Dans une expression


d’affectation comme a = b, l’opérande de gauche ne peut pas être une simple
valeur (comme 12) ; ce doit être une lvalue modifiable c’est-à dire une référence
sur une variable (par exemple le nom de cette variable). La valeur de cette va-
riable est remplacée par celle de l’expression en opérande gauche b.
Tous les types scalaires sont affectables, ainsi que les structures et les unions
comme nous le verrons (4.2.3).
Comme toute expression, a = b a un type, celui de a, et une valeur, celle
de a après que l’affectation ait été réalisée. On peut utiliser cette valeur dans
une expression plus complexe comme
j = 4*(i = 5);
qui donne à j la valeur 5. Ce type d’expression avec effet de bord doit être
utilisé avec parcimonie. Néanmoins, dans certains contextes il est dans la lo-
gique — sinon le génie — du langage ; nous avons déjà rencontré en 2.2 des
expressions comme
while ((c = getchar()) != EOF) ...

Attention : = et ==
La possibilité de cet effet de bord rend encore plus dangereuse la confusion
entre l’opérateur d’affectation (=) et celui d’égalité (==). Dans
if (a = 0) ...

il n’y a aucune erreur de syntaxe ; ce n’est pas le test de a à 0, mais l’affec-


tation de 0 à a suivi du test de la nouvelle valeur de a (c’est-à-dire 0 !). La
condition ne sera donc jamais vraie ici.

Affectation composée

Pour tout opérateur binaire ∇, C définit un opérateur ∇= 5 d’affectation


« sur place » tel que
a ∇= b
est équivalent à
a = a ∇ (b);
5. Il n’y a pas d’espace entre ∇ et =.

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



64 3. Bases du langage

avec cette différence que a n’est évalué qu’une seule fois. Cette dernière restric-
tion ne pose pas de problème si a est une simple variable. mais nous verrons
que la situation peut être plus complexe (5.1.1). Noter les parenthèses autour
de b qui forcent l’évaluation de cette expression avant d’appliquer l’opérateur
∇.
Les opérateurs binaires composables avec l’affectation (c’est-à-dire éligibles
pour remplacer ∇) sont les suivants :
* / % + - << >> & ^ |
Voici quelques exemples d’utilisation :
a += 2 incrémentation de a de 2
a *= 3 triplement de a
a %= 2 a remplacé par son « imparité »
a <<= 3 a decalé à gauche de 3 bits
a |= m les bits à 1 de m le seront aussi dans a
a &= ∼m les bits à 1 de m seront à 0 dans a ...
Dans les deux derniers cas, les autres bits de a sont inchangés.

Incrémentation et décrémentation

L’expression ++a, où a est une lvalue modifiable, est strictement équivalente


à a += 1
Elle rend donc la valeur de a après l’incrémentation : on parle d’incrémenta-
tion préfixe. En revanche, la valeur de l’expression ++a est celle de a avant son
incrémentation. On parle d’incrémentation postfixe.
Bien entendu, l’opérateur de décrémentation -- est lui aussi défini avec les
deux formes, préfixe et postfixe.

3.3.5 Opérateurs sur les pointeurs


Il s’agit des opérateurs d’indexation ([]) — eh oui, c’est un opérateur ! —
de sélection de champ (->), d’indirection (* unaire) et d’adresse (&). Ils sont
étudiés en 5.1.1.

3.3.6 Opérateurs sur les types


Conversions explicites (casts)

Pour (tenter de) convertir une expression e dans le type T, il suffit d’écrire
(T)e :
float x = (float)3; /* float x = 3.0; */
...
x = 1/(double)2; /* x = 1/2.0; */
...
c = (char)0; /* c = ’\0’; */
En C, cette conversion n’a de sens que si le type T et celui de a sont scalaires.
La sémantique de ces conversions est étudiée en 3.4.2 et en 5.1.4. En C il s’agit
toujours d’un mécanisme résolu à la compilation.
On trouve le nom d’un type en considérant une déclaration d’un objet du
type et en y retirant l’identificateur correspondant à l’objet déclaré :

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
3.3. Opérateurs 65

int i; type : int


unsigned short int ui; type : unsigned short int ou unsigned short
const double PI = ...; type : const double
volatile char c; type : volatile char
Nous verrons des applications de cette règle simple dans des cas bien plus
complexes plus loin.

Taille mémoire d’un objet

L’expression sizeof(x) où x est soit une référence sur un objet, soit un


nom de type. Le résultat est la taille mémoire occupé par l’objet ou un objet du
type. Par définition
sizeof(char) == 1
Cette taille est donc exprimée en nombre de caractères.
En C, la taille des objets est connue à la compilation, et c’est donc à la
compilation que sizeof(x) est évalué : c’est un exemple d’expression statique.

3.3.7 Opérateurs divers


Les opérateurs d’appel de fonction (()) et de sélection de champ de struc-
ture (.) sont étudiés respectivement au chapitre 7 et en 4.2.3.

Expression conditionnelle

Nous avons déjà rencontré cette expression en 2.5. Elle s’écrit


a ? b : c
et se lit : « si a alors b sinon c ». Elle a donc la valeur de l’expression b si
l’expression a est vrai (i.e. a est non nulle) et celle de c sinon.
Une remarque supplémentaire : seule l’expression qui détermine la valeur
est évaluée 6 . Ainsi dans
int t[100];
...
a = (n < 100) ? t[n] : 0;
t[n] n’est évalué que si n est inférieur à 100, évitant un potentiel débordement
d’indice.
Remarque : question de style
Il est clair qu’une expression comme la précédente est équivalente à
if (n < 100)
a = t[n];
else
a = 0;

Quelle forme préférer? J’ai tendance dans ce cas à privilégier la première


qui indique clairement que a va changer de valeur. Dans la seconde forme,
c’est tout bonnement un hasard que a se retrouve cible de l’affectation
dans les deux instructions du if ; une lecture trop rapide pourrait la
confondre avec
6. Non seulement c’est bien naturel, mais en plus c’est garanti !

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



66 3. Bases du langage

if (n < 100)
b = t[n];
else
a = 0;

On peut cependant regretter la syntaxe cryptique de cette instruction


conditionnelle.
On peut noter au passage que a&&b est équivalent à a?(b!=0):0 et a||b
à a?1:(b!=0).

Opérateur d’évaluation séquentielle

Une expression comme


a1 , a2
évalue d’abord a1 , puis a2 . La valeur et le type de l’expression tout entière sont
ceux de la dernière expression, c’est-à-dire ici a2 .
Par exemple, a++ est équivalent à
((tmp = a), (a += 1), tmp)
où tmp est une variable temporaire.
Attention : il y a virgule et virgule
La virgule qui sépare les arguments d’un appel de fonction
sort(t, n);

n’est pas l’opérateur d’évaluation séquentielle. En fait, il n’y a aucune ga-


rantie sur l’ordre d’évaluation des paramètres d’une fonction (voir 7.2.3).

3.4 Evaluation des expressions


3.4.1 Ordre d’évaluation : précédence et associativité
Le grand nombre d’opérateurs disponibles en C a comme conséquence une
certaine complexité d’interprétation des expressions, en particulier pour le dé-
butant. C définit la précédence — on dit aussi la priorité — des opérateurs
ainsi que leur associativité. Les règles en sont données dans le tableau 3.8, les
opérateurs les plus « liants » étant cités d’abord. Quand deux opérateurs ont la
même force de liaison, la dernière colonne indique le sens de l’associativité :
ainsi une expression comme
a - b - c
s’interprète bien comme
(a - b) - c
puisque l’opérateur binaire - associe de gauche à droite. Bien entendu, il est
toujours possible d’utiliser des parenthèses pour forcer un autre ordre d’éva-
luation — ou tout simplement pour améliorer la lisibilité.
Le sens d’associativité est défini pour tous les opérateurs bien que cela n’ait
pas nécessairement de sens, l’opération n’étant pas naturellement associative.
Par exemple

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
3.4. Evaluation des expressions 67

Table 3.8 Précédence et associativité des opérateurs de C

Associativité
() [] -> . gauche à droite
Opérateurs unaires droite à gauche
! ∼ - + *
& ++ -- (type) sizeof
* / \% gauche à droite
+ - gauche à droite
<< >> gauche à droite
< <= > >= gauche à droite
== != gauche à droite
& gauche à droite
^ gauche à droite
| gauche à droite
&& gauche à droite
|| gauche à droite
? : (conditionnelle) droite à gauche
Affectations simples et composées droite à gauche
= += *= ...
, (virgule) gauche à droite

a < b < c
doit s’interpréter comme
(a < b) < c
ce qui compare le résultat de (a < b) (0 ou 1) à la valeur de c, ce qui n’est
sans doute pas ce qui est désiré.
De manière plus souhaitable, on note que les affectations associent de droite
à gauche, ce qui permet d’écrire
a = b = c
qui est interprété comme
a = (b = c)
et qui affecte à a et b la même valeur, celle de c — au type près (voir 3.4.3).
Certaines priorités sont « piégeantes » : par exemple
if (a == b&0xff) ...
doit se lire
if ((a == b)&0xff) ...
ce qui n’est pas particulièrement naturel. On doit également se méfier de l’ex-
pression conditionnelle :
x = 3 + a ? b : c;
s’interprête comme
x = (3 + a) ? b : c;

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



68 3. Bases du langage

ce qui là encore n’est pas très naturel.


En revanche, les affectations sont très peu liantes, ce qui est normal. Ainsi
les parenthèses sont nécessaires dans
while ((c = getchar()) != EOF) ...
car la forme non parenthésée se lirait
while (c = (getchar() != EOF)) ...
ce qui teste bien la fin de fichier, mais affecte à c le résultat de cette comparai-
son, 0 ou 1, et non pas le caractère lu effectivement. Cependant les affectations
sont plus liantes que l’opérateur d’évaluation séquentielle :
x = a1, a2, a3;
se lit
(x = a1), a2, a3;
affectant donc a1 et non pas a3 à x. Il aurait donc fallut écrire
x = (a1, a2, a3);

Attention : ordre d’évaluation


Précédence et associativité ne sont pas suffisantes pour déterminer l’ordre
d’évaluation d’une expression. En fait, seuls quatre opérateurs garantissent
exactement l’ordre d’évaluation de leurs opérandes : ||, &&, ?: et la vir-
gule ,. Dans tous les autres cas, l’ordre d’évaluation des opérandes est
indéfini. On doit donc être particulièrement vigileant lorsque certains opé-
randes contiennent des effets de bord. Aucun des deux exemples ci-après
n’est portable :
t[i] = i++;
x = ((c = getchar()) ? c : 0) + c;

3.4.2 Conversions
ansi C permet d’effectuer des conversions entre types scalaires, soit expli-
citement (comme en 3.3.6), soit implicitement comme on va le voir un peu plus
loin (3.4.3). Cette section donne les règles qui sont alors appliqueés, c’est-à-dire
la sémantique des conversions.
Une règle générale simple est utilisée pour toutes ces conversions : si la
valeur initiale peut être représentée dans le nouveau type, la valeur est conser-
vée 7 ; sinon le résultat dépend de l’implémentation.

Promotion entière

Cette conversion est effectuée implicitement avant toute autre évaluation


d’une expression.
Elle permet d’utiliser un caractère (char), un entier court (short), une
valeur de type énuméré (enum), ou encore un champ de bits (voir 4.2.2), qu’ils
soient ou non signés, à la place d’un entier ordinaire.
La règle est simple : si le type int est suffisant pour contenir la valeur à
convertir, il est utilisé ; sinon la valeur est convertie en unsigned int.
7. avec éventuellement une perte de précision dans les conversions ayant pour cible un type
réel

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
3.4. Evaluation des expressions 69

Conversions entre entiers

Lorsqu’un entier signé a est converti en non signé, la valeur obtenue u est
telle que

u = min(v ≡ a (mod 2n ))
v >0

où n est le nombre de bits de la représentation. Dans la représentation des


entiers en complément à 2 ceci revient à tronquer à gauche la représentation
binaire si le nouveau type est moins large, ou a compléter à gauche par des 0
dans le cas contraire.
Si un entier est converti en un autre type entier signé, la valeur est préservée
si le nouveau type peut représenter cette valeur ; sinon le résultat dépend de
l’implémentation :
int i = 12715;
short s = i; /* OK: short suffisamment grand */
char c = i; /* ??: resultat non portable */

Conversions entre entier et réel

Lorsqu’un entier est converti en réel (float ou double) le résultat est la


valeur représentable la plus proche ; le fait que cette valeur soit supérieure ou
au contraire inférieure à la valeur entière initiale n’est pas défini et est dépen-
dant de l’implémentation.
Dans le cas inverse d’une conversion de réel en entier, la partie fraction-
naire est simplement ignorée (troncature vers 0 et non pas arrondi !). Si la
partie entière n’est pas représentable dans le type ciblé, le résultat dépend de
l’implémentation. En particulier, la conversion d’un réel en entier non signé
(unsigned) n’est jamais portable.
double x = 3.141592;
double y = 2.7182818;
double z = 1.0e+30;
int i;
...
i = x; /* i == 3 */
i = y; /* i == 2 */
i = -x; /* i == -3 */
i = -y; /* i == -2 */
i = z; /* ??: vraisemblablement non portable */

Conversions entre réels

Si le type ciblé est plus grand — par exemple conversion de float en


double — la valeur reste inchangée. Dans le cas contraire, si la valeur initiale
est dans l’intervalle représentable, le résultat est la plus proche valeur (il n’est
pas défini si elle est inférieure ou supérieure) ; sinon, le résultat dépend de
l’implémentation.

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



70 3. Bases du langage

3.4.3 Mélange de types dans les expressions


C autorise le mélange des types arithmétiques dans les expressions. Les
conversions suivantes — désignées en général sous le vocable de « conversions
usuelles » — sont alors automatiquement effectuées sur les opérandes :
1. Tout d’abord, les promotions entières (voir 3.4.2) sont appliquées, si né-
cessaire, aux opérandes — ceci garantit que l’expression est évaluée au
moins dans le type entier ordinaire (int) et jamais dans un type plus
restreint (il n’y a pas en C d’expression de type char ou short) ;
2. Si l’un des opérandes est long double, l’autre est converti en ce type ;
3. Sinon, si l’un des opérandes est double, l’autre est converti en ce type ;
4. Sinon, si l’un des opérandes est float, l’autre est converti en ce type ;
5. Sinon, si l’un des opérandes est unsigned long, l’autre est converti en
ce type ;
6. Sinon, si l’un des opérandes est long et l’autre unsigned int et si un
long permet de représenter toutes les valeurs de type unsigned int,
alors le second opérande est converti en long ; en revanche, si la der-
nière condition n’est pas satisfaite, les deux opérandes sont convertis en
unsigned long ;
7. Sinon, si l’un des opérandes est long, l’autre est converti en ce type ;
8. Sinon, si l’un des opérandes est unsigned int, l’autre est converti en ce
type ;
9. Sinon, c’est que les deux opérandes sont int ainsi que le résultat.

Cas particulier des affectations

Dans une affectation simple entre types scalaires, comme


v = e
les deux opérandes sont évalués chacun avec leur type propre. Puis, si néces-
saire (et possible !), la valeur de l’opérande droit est convertie implicitement
dans le type de celui de gauche.
double x = 3.141592;
int i;
...
i = x; /* troncature: i == 3 */
x = 5/2; /* division entiere: x == 2.0 */
On voit donc que le type de la partie gauche de l’affectation n’a aucune in-
fluence sur la manière d’évaluer la partie droite ni sur le type de cette dernière.
En C le type d’une expression ne dépend que du type de ses opérandes.
Pour les affectations composées, on se rappelle l’équivalence de a += b
avec a = a+(b), mais avec une seule évaluation de a. Les règles de conver-
sions se déduisent de la forme expansée.
Attention : affectations en chaîne
Le mélange de types dans des affectations en chaîne comme
a = b = c

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
3.5. Instructions et flot de contrôle 71

peut donner des résultats surprenants : après cette expression, a, b et c ne


sont pas nécessairement égaux. Après
double a;
int b;
...
a = b = 3.141592;
b vaut 3 et a vaut 3.0 (et pas 3.141592!).

3.5 Instructions et flot de contrôle


Il y a trois grandes catégories d’instructions en C : les instructions simples,
les blocs et les instructions de contrôle. Ces dernières se subdivisent en instruc-
tion de sélection, boucles et instructions de rupture de séquence.

3.5.1 Instruction simple et bloc


Une instruction simple est une expression que l’on termine par un point-
virgule.
4; /* ne fait pas grand chose! */
x = a + b; /* instruction d’affectation */
printf("%d %d\n", a, b);
Un bloc permet de regrouper plusieurs instructions : c’est une suite d’ins-
tructions encadrées par des accolades.
{
x = a + b;
y = a > b ? a : b;
printf("%d %d\n", x, y);
}
Un bloc peut comporter des définitions de variables qui sont alors locales au
bloc, c’est-à-dire ne sont visibles — et même éventuellement n’ont d’existence
— que dans le bloc (voir le chapitre 8) :
{
int i = 0, j = 3;
x = 2*i;
y = j -i;
}
En C90, les variables locales d’un bloc doivent être déclarées 8 en tête de
ce bloc, avant toute instruction exécutable. Ceci n’est plus nécessaire en C99 : C99
comme en C++, les variables locales peuvent être déclarées n’importe où dans
le bloc, du moment que c’est avant leur utilisation. Ainsi, le bloc suivant est
légal en C99, mais pas en C90 :
{
int i = 0;
x = 2*i;
int j = 3;
y = j -i;
}
8. En fait, cette déclaration est en fait une définition — voir 8.2.

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



72 3. Bases du langage

Un bloc joue le même rôle syntaxique qu’une instruction simple. Dans la


suite, nous désignerons par instruction aussi bien une instruction simple
qu’un bloc ou une instruction de contrôle (objet de la section suivante). Les
blocs peuvent bien entendu s’imbriquer (c’est-à-dire que les instructions d’un
bloc peuvent elles-mêmes être des blocs).
Attention : pas de point-virgule après un bloc
Si une instruction simple est bien terminée par un point-virgule, ce ca-
ractère ne figure pas derrière l’accolade fermante d’un bloc. L’erreur est le
plus souvent sans conséquence sauf dans certaines instructions composées
(voir 3.5.2).

3.5.2 Instructions de sélection


Instruction de choix binaire : if

L’instruction if a la forme générale suivante :


if (condition)
instruction-si-vrai
else
instruction-si-faux
Noter que la condition qui est une expression scalaire quelconque (dont on
évalue la valeur de vérité — voir 3.3.2) est entre parenthèses, et qu’il n’y a
pas en C de mot-clé then. Si les plusieurs instructions sont présentes dans
instruction-si-vrai et instruction-si-faux, il faut les regrouper dans
un bloc. Enfin la clause else est optionnelle.
if (a == 0)
fprintf(stderr, "division par 0");
else
x = b/a;

if {a != b)
{
x = a;
y = b;
}
Les instructions if s’enchainent très bien grâce à la forme else if :
if (isalpha(c))
printf("lettre\n");
else if (isdigit(c))
printf("chiffre\n");
else if (isspace(c))
printf("espace\n");
else
printf("%c ni lettre, ni chiffre, ni espace\n");
Bien entendu le dernier else est optionnel. Les fonctions isalpha, isdigit
et isspace permettent de déterminer à quelle catégorie appartient un carac-
tère ; elles sont définies dans le fichier d’environnement standard <ctype.h>.

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
3.5. Instructions et flot de contrôle 73

Ambiguïté syntaxique : if en cascade


Contrairement à la plupart des langages impératifs modernes, le if n’est
pas terminé par un mot-clé comme endif ou fi ; en conséquence, dans
une instruction comme
if (a == b) if (c != d) x = y; else y = x;

il y a une ambiguïté : on ne sait pas à quel if se réfère le else. Bien que C


ait sa propre règle pour lever cette ambiguïté, ce n’est pas un bon style de
s’y fier. Il vaut mieux écrire explicitement ce que l’on veut : par exemple
if (a == b)
{
if (c != d)
x = y;
else
y = x;
}

Attention : pas de point-virgule après une accolade


Dans l’extrait de programme suivant, le point-virgule provoque une erreur
de syntaxe :
if (a != b)
{
x = a;
y = b;
}; else x = y = 0;

Instruction de choix multiple : switch

Cette instruction permet un aiguillage entre plusieurs possibilités ; elle cor-


respond à l’instruction case des langages de la famille Pascal. Elle a la forme
suivante :
switch (expression)
{
case val1 :
instructions1
case val2 :
instructions2
...
case valn :
instructionsn
default:
instructions-default
}
Les instructionsi , et l’instructions-default sont des suites d’instruc-
tions quelconques (simples ou blocs) éventuellement vides. La clause default
et les instructions-default sont optionnelles. Les expressions val1 , val2 ,
..., valn doivent être statiques c’est-à-dire évaluables à la compilation.
L’expression est évaluée et sa valeur est comparée à celle des ex-
pressions vali , dans l’ordre i = 1, 2, ..., n ; si l’égalité est trouvée, disons
pour val j , alors les instructions j sont exécutées puis on passe en sé-
quence pour exécuter instructions j+1 , instructions j+2 , etc... jusqu’à

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



74 3. Bases du langage

instructions-default incluses. Si aucune égalité avec un des vali n’est


trouvée, les instructions-default sont exécutées si elles existent ; sinon
on sort du switch et on passe en séquence.
Lorsque certaines des instructionsi sont vides ce comportement étrange
prend un sens : on obtient alors le ou logique des différents cas :.
switch (c)
{
case ’\n’:
printf("fin de ligne");
/* fall through ... */
case ’ ’:
case ’\t’:
case ’\f’:
printf("espace\n");
}
Dans cet extrait de programme, si c est une fin de ligne on obtient les deux
messages
fin de ligne
espace
Si c est un blanc, un caractère de tabulation horizontal ou un saut de page, on
obtient le seul message
espace
et dans tous les autres cas, on n’a aucun message. Le commentaire
/* fall through ... */ — ou quelque chose d’équivalent — est assez
souvent rencontré dans cette situation pour bien marquer que le programmeur
souhaite effectivement ce passage en séquence.
Mais dans l’immense majorité des cas, on veut retrouver le comportement
habituel de l’instruction case de Pascal, c’est-à-dire sortir du switch dès que
l’une des instructionsi — celle qui correspond à la valeur de l’expression
— a été exécutée ; on utilise alors l’instruction break qui est une rupture de
séquence (un saut) à la fin du switch :
switch (c)
{
case ’0’: case ’1’: case ’2’: case ’3’: case ’4’:
case ’5’: case ’6’: case ’7’: case ’8’: case ’9’:
printf("chiffre\n");
break;
case ’ ’: case ’\t’: case ’\n’: case ’\f’:
printf("espace\n");
break;
default:
printf("ni chiffre, ni espace\n");
break;
}
Le dernier break est bien évidemment inutile, mais il a été placé là par
souci d’homogénéité.

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
3.5. Instructions et flot de contrôle 75

3.5.3 Instructions de boucle


Ces trois instructions permettent d’effectuer des itérations controlées par
une condition d’entretien de boucle.

Boucle while

Cette instruction a la forme suivante :


while (condition)
instruction
Le corps de la boucle, c’est-à-dire instruction, est exécutée tant que la
condition — une expression entière ou convertible en entier — a la valeur
de vérité vrai. La condition est évaluée et testée avant chaque itération, y
compris la première.

Boucle do ... while

L’instruction do ... while dont la syntaxe est


do
instruction
while (condition) ;

effectue l’instruction jusqu’à ce que la valeur de vérité de l’expression


entière — ou convertible en entier — condition devienne faux. La
condition est évaluée en fin de boucle. Le corps de la boucle, c’est-à-
dire instruction, est donc toujours évalué au moins une fois. En ce sens, la
boucle do ... while est donc équivalente à la forme suivante :
instruction
while (condition)
instruction
Notez le point-virgule à la fin de l’instruction, juste après while
(condition).

Boucle for

C’est la boucle d’itération controlée, très particulière à C. Sa syntaxe est la


suivante :
for (initialisation ; entretien ; rebouclage)
instruction
Elle est équivalente à
initialisation ;
while (entretien)
{
instruction
rebouclage ;
}
Ainsi, l’extrait de programme suivant où i est une variable entière, et t et u
des tableaux de même type ou de types compatibles et de dimension au moins
égale a 10,

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



76 3. Bases du langage

for (i = 0; i < 10; i++)


{
t[i] = u[i];
u[i] = 0;
}
est-il équivalent à
i = 0;
while (i < N)
{
{
t[i] = u[i];
u[i] = 0;
}
i++;
}
ce qui a pour effet de transférer les 10 premiers éléments du tableau u dans le
tableau t et de mettre les 10 premiers éléments de u à zéro. Noter qu’en sortie
de boucle, la variable i vaut 10.
Remarque : variable de controle de boucle
En C90, La variable de contrôle de la boucle, i dans l’exemple ci-dessus,
doit être déclarée avant la boucle for elle-même. Sa portée est donc stric-
tement plus grande que celle de la boucle elle-même ; c’est souvent une
variable locale du bloc englobant.
C99 En C99, il est possible comme en C++ de déclarer que la variable de boucle
est locale à la boucle :
for (int i = 0; i < N; ++i)
t[i] = 0.0;
Dans ce cas, la variable de boucle n’est pas connue à l’extérieur de la
boucle :
for (int i = 0; i < N; ++i)
if (t[i] == 0.0)
break; /* sortie, voir plus loin */
printf("element nul en i = %d\n", i);
/* NON ! i inconnu ici */
Enfin, dans tous les cas, il est possible, bien que de style douteux, de mo-
difier cette variable dans le corps de la boucle.
Si la boucle for est très souvent utilisé dans des cas simples comme ceux de
l’exemple ci-dessus, elle est d’une grande richesse d’expression. Dans l’extrait
suivant
for (i = 0; i < 10; i += 2)
t[i] = 0;
un élément sur deux du tableau t est mis à zéro (l’indice de boucle avance par
pas de 2). On peut même avoir plusieurs indices pour une seule boucle :
for (i = 0, j = 0; i < 10; i++, j += 2)
t[i] = u[j];
C99 En C99, ces deux indices peuvent être déclarés locaux :
for (int i = 0, j = 0; i < 10; i++, j += 2)
t[i] = u[j];

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
3.5. Instructions et flot de contrôle 77

Ici un élément sur deux du tableau u est transféré dans les dix premiers élé-
ments (consécutifs) du tableau t.
Attention : boucles simples et imbriquées
Dans l’exemple précédent, il n’y a qu’une seule boucle et les deux indices
i et j progressent en même temps, bien qu’avec des pas différents. Le
nombre d’itérations est donc de 10. Il ne faut pas confondre cet exemple
avec celui des boucles for imbriquées comme
for (i = 0; i < 10; i++)
for (j = 0; j < 20; j += 2)
...

Dans ce dernier cas, il y a deux boucles et 200 itérations.

Remarque : boucle for incomplète


L’un ou plusieurs des trois éléments de contrôle d’une boucle for peuvent
être omises :
for ( ; i < 10; i++) ... /* pas d’initialisation */
for (i = 0; i < 10; ) ... /* pas de rebouclage */
for (i = 0; ; i++) ... /* pas de condition d’entretien */

Dans le dernier cas, la condition d’entretien, absente, est supposée valoir


1, c’est-à-dire vrai : la boucle est a priori infinie. Ainsi
for (;;) {}

est une vraie boucle infinie équivalente à


while (1) {}

La boucle for de C déconcerte souvent les débutants dans ce langage ; ils


préfèrent alors utiliser la forme while équivalente. Ceci est bien entendu une
question de goût. Cependant, j’ai tendance à préférer chaque fois que pos-
sible la forme for car elle a l’avantage de concentrer syntaxiquement les trois
éléments de contrôle de la boucle : point de départ, condition de passage à
l’itération suivante, instruction de progression.

3.5.4 Instructions de rupture de séquence


Abandon de corps de boucle : break

L’instruction break que nous avons déjà rencontré dans le cas de switch
(3.5.2) a une autre signification : elle permet d’abandonner prématurément une
boucle (for, while ou do ... while). Ainsi
for (i = 0; i < 10; i ++)
{
if (t[i] == k)
break;
}
est une boucle qui se termine lorsque t[i] vaut k. Noter qu’en sortie de boucle,
la valeur de i est conservée.
Remarque
L’exemple précédent peut aussi s’écrire

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



78 3. Bases du langage

for (i = 0; i < 10 && t[i] != k; i ++) {}

avec un corps de boucle vide et pas de break.


L’instruction break ne permet de sortir que d’un niveau de boucle : dans
for (i = 0; i < 10; i++)
{
while (j > 0)
{
//...
if (j == i)
break;
}
}
on ne sort que de la boucle interne ; en C le seul moyen de sortir de deux (ou
plus) boucles imbriquées est d’utiliser un goto. De plus, comme break est
aussi utilisée pour sortir d’un switch, il n’est pas possible, sans utiliser un
goto de sortir prématurément d’une boucle depuis un switch interne :
for (i = 0; i < 10; i++)
{
switch (t[i])
{
case 0:
...
case 1:
...
break; // sort de switch, pas de for
...
}
}

Rebouclage prématuré : continue

L’instruction continue est analogue à break, mais au lieu d’abandonner


la boucle, on passe à l’itération suivante (ou plutôt on tente de passer en éva-
luant la condition d’entretien). Dans le cas d’une boucle for les instructions de
rebouclage sont bien entendu exécutées :
for (i = 0; i < 10; i++)
{
...
if (i == 0)
continue; /* va a l’iteration suivante (i == 1) */
...
}

Rupture de séquence inconditionnelle : goto

L’instruction goto permet de transférer le contrôle à un point désigné par


une étiquette.
...
goto La_Bas;

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
3.6. Exercices du chapitre 3 79

...
La_Bas: /* La_Bas, c’est ici */
...
Une étiquette est un identificateur quelconque. Par exemple, pour sortir à la
fois d’un switch et de la boucle qui l’englobe :
for (i = 0; i < 10; i++)
{
switch (t[i])
{
case 0:
...
case 1:
...
goto out;
...
}
}
out:
...
On peut faire un goto uniquement vers une étiquette définie dans le même
bloc ou dans un bloc englobant. Il est interdit de faire des goto de l’extérieur
vers l’intérieur d’un bloc, du corps d’une boucle, d’un if ou d’un switch.
Enfin signalons que l’instruction goto est considérée comme nuisible de-
puis un article célèbre de E. W. Dijsktra [13]. En tout état de cause, son utili-
sation est très souvent l’indice d’une mauvaise organisation du code.

Retour de fonction: return

Nous avons déjà souvent utilisé cette instruction qui permet de terminer
l’exécution d’une fonction. Elle a deux formes possibles :
return;
si la fonction ne rend pas de résultat (son type de retour est void) ou
return expression;
si la fonction à une valeur de retour. L’expression sera convertie dans le type
de retour, si nécessaire et possible — sinon, c’est une erreur.
Parenthésage de l’expression suivant return
Comme return n’est pas une fonction (contrairement, par exemple, à
exit()), il n’y à pas besoin de parenthèses autour de l’expression à re-
tourner.

3.6 Exercices du chapitre 3

Exercice 3.1 (Nombre de bits à 1 dans un octet) Écrire un programme qui im-
prime combien de bits sont à 1 dans un octet.

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



80 3. Bases du langage

Exercice 3.2 (Nombre de caractères, mots et lignes d’un texte) Écrire le pro-
gramme word_count qui lit son entrée standard et imprime sur la sortie stan-
dard le nombre de lignes, mots et caractères lus sur l’entrée standard. Votre
programme doit avoir le même comportement que la commande d’Unix wc(1)
mais, évidemment, ne doit pas utiliser cette dernière !
On pourra utiliser les fonctions définies dans <ctype.h> en particulier
isspace pour tester qu’un caractère est un espace.
Remarque 1
On précisera ce qu’on entend par un mot et donc quels sont les séparateurs de
mots. Ce doit être la même chose que pour la commande standard wc(1).
Remarque 2
Attention à la fin de fichier !
Remarque 3
On pourra ajouter au programme des options -l, -c... comme dans wc(1).

Exercice 3.3 (Évaluation d’un polynôme) Écrire un programme qui reçoit en


ligne de commande les coefficients a0 , a1 , . . . , an d’un polynôme de degré n,
imprime le polynome obtenu, réclame une valeur de la variable x et évalue le
polynôme pour cette valeur. Pour ce programme, vous écrirez deux fonctions
d’évaluation, une où vous utiliserez la fonction pow de <math.h> et l’autre où
vous appliquerez le schéma de Horner. On rappelle que ce schéma consiste à
remarquer qu’un polynôme de degré n peut s’écrire sous la forme

(. . . (( an x + an−1 ) x + an−2 ) x + . . . + a1 ) x + a0

et peut donc être évalué par une procédure récursive ou itérative (que nous
vous laissons le soin de définir et de mettre en place) ne nécessitant que n
multiplications.
Voici un exemple d’utilisation :

% poly 2 3 4
polynome: 2 + 3 * xˆ1 + 4 * xˆ2
valeur de x ? 2
résultat direct: 24
résultat Horner: 24
%

Comme vous pouvez le constater, on a aussi écrit une fonction qui permet
d’afficher le polynôme sous une forme raisonnable.

Exercice 3.4 (Nombres premiers) Écrire un programme qui reçoit une suite
d’entiers en ligne de commande et pour chacun de ces entiers imprime s’il
est premier ou non.

Exercice 3.5 (Quelques opérations non portables) Étudiez comment votre


plate-forme se comporte pour certaines opérations non portables sur les en-
tiers. En particulier :
– que se passe-t-il en cas de dépassement de capacité entière ?

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
3.6. Exercices du chapitre 3 81

– que se passe-t-il en cas de division par zéro entière ?


– que se passe-t-il lorsque l’opérande droit d’une opération de décalage est
négatif ?
– le décalage droit est-il arithmétique ou logique, c’est-à-dire conserve-t-il le
signe ou non de son opérande gauche ? qu’en est-il du décalage gauche ?
– que se passe-t-il lorsque le diviseur d’une opération « modulo » est négatif ?
lorsque le dividende est négatif ? lorsque les deux opérandes sont négatifs ?

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



82 3. Bases du langage

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
Chapitre 4

Tableaux, structures et unions

C e chapitre décrit les trois constructeurs de types « agrégats » disponibles


en C : tableaux, structures et unions. Ces constructeurs permettent de dé-
finir des types-utilisateurs.

4.1 Tableaux
4.1.1 Tableaux mono-dimensionnels
Définition et déclaration

Un tableau est bien sûr une collection d’objets qui sont tous du même type.
Nous avons déjà rencontré des déclarations et définitions de tableaux mono-
dimensionnels (2.4). En voici quelques exemples :
int t[10];
double vec[100];
...
#define NBUF 10000
char buffer[NBUF];
La dimension, c’est-à-dire le nombre d’éléments, doit être une expression sta-
tique : la taille des tableaux de C90 doit donc être connue à la compilation (voir
4.1.4 pour C99). Il y a cependant exception lorsqu’un tableau est passé en argu-
ment d’une fonction 1 : la dimension du tableau peut être laissée libre, à condi-
tion que la fonction puisse la connaitre par ailleurs. La dimension d’un tableau
de C n’est en effet pas conservée à l’exécution : en particulier, C ne sait pas vé-
rifier les débordements d’indice 2 . A titre d’exemple, dans le programme 4.1, la
fonction prod_scal calcule le produit scalaire des deux vecteurs de dimension
n donnés en arguments.
Remarque : dimension d’un tableau et const
En C90, on ne peut malheureusement pas fixer la dimension d’un tableau
à l’aide d’une const :
const int N = 100;
double t[N]; /* ERREUR!! */

1. Nous verrons plus tard (5.3.2) que ce n’est pas vraiment le tableau qui est passé en para-
mètre mais plutôt un pointeur, l’adresse de son premier élément.
2. Certains compilateurs, ou certains outils de l’environnement de programmation, peuvent
cependant proposer un tel service, mais il n’est pas défini dans la norme.

V 2.1 – 7 février 2010 83 c Jean-Paul R IGAULT



84 4. Tableaux, structures et unions

Programme 4.1 – Produit scalaire de deux vecteurs


/***** Fichier: prod_scal.c *****/

#include <stdio.h>
#include <stdlib.h>
5
double prod_scal(int n, double v1[], double v2[])
{
int i;
double p;
10
for (i = 0; i < n; i++)
p += v1[i]*v2[i];
return p;
}
15
int main()
{
double x1[5] = {0, 1, 2, 3, 4};
double x2[5] = {3, 1, -1, -1, 1};
20 printf("<x1, x2> = %g\n", prod_scal(5, x1, x2));

return 0;
}

Bien entendu, il est toujours possible d’utiliser les macros du préproces-


seur et #define :
#define N 100;
...
double t[N];

mais ceci est loin d’avoir les mêmes avantages (voir 6.3.1).
C99 En revanche, C99 permet ce type de dimensionnement par une constante,
mais uniquement pour les variables locales (voir 4.1.4).

On peut bien sûr définir un synonyme pour le nom d’un type tableau ; la
dimension fait partie du type :
typedef double Vector[1000];
Vector v1, v2;
Ici Vector est le type « tableau de 1000 double ».

Initialisation d’un tableau

Un tableau peut être initialisé au moment de sa définition :


int t[5] = {1, 1, 1, 1, 1};
int u[5] = {1, 2};
int v[] = {1, 2, 3, 4, 5, 6};

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
4.1. Tableaux 85

La syntaxe utilisée est dite « initialisation d’agrégat ». Dans l’exemple du ta-


bleau u seules les deux premières composantes sont initialisées. Pour v, la di-
mension est laissée libre ; c’est le compilateur qui la calculera en fonction du
nombre d’éléments dans la liste d’initialisation (ici la dimension est donc 6).
Taille d’un tableau et operateur sizeof
Noter que pour tout tableau, le rapport entre la taille du tableau et la
taille d’un composante (ces tailles étant fournies par l’opérateur sizeof
— 3.3.6) est égal au nombre de composantes. Ainsi pour v plus haut :
const int n_elems_v = sizeof(v)/sizeof(int);

Bien entendu, un tableau peut être constant (resp. volatile), ce qui revient à
dire que toutes ses composantes le sont :
const int ctab[] = {1, 0, 0, 0};
const volatile unsigned status_registers[NDEVICES];
Dans ce cas, ctab[i] ne peut être modifié et status_registers[i] peut
changer de valeur sans que le programme ne le modifie explicitement.

Opération d’indexation

L’opération d’indexation est la principale — et de fait la seule — opération


sur un tableau : pour un tableau t, t[i] désigne une référence sur la compo-
sante d’indice i :
– l’indice doit être une expression entière, ou convertible en entier,
– la première composante a comme indice 0.
Comme l’opérateur retourne une référence, l’expression d’indexation peut donc
apparaitre en partie gauche d’une affectation, à condition que le tableau ne soit
pas constant, bien sûr :
int t[10] = {...};
int u[10];
...
u[0] = 0;
for (i = 1; i < 10; i++)
u[i] = t[i - 1];

Copie de tableaux

Comme nous le justifierons plus tard (5.3.2), on ne peut pas affecter les
tableaux entre eux. Cet extrait de programme est incompilable :
int t[10], u[10];
...
u = t; /* ERREUR */
Pour faire la copie il faut utiliser une boucle
for (int i = 0; i < 10; ++i)
u[i] = t[i];
ou alors utiliser une fonction système telle que memcpy :
memcpy(u, t, 10*sizeof(int));

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



86 4. Tableaux, structures et unions

4.1.2 Tableaux multi-dimensionnels


En C, un tableau bi-dimensionnel est en fait un « tableau de tableaux » et
les syntaxes de déclaration et d’indexation reflètent ce fait :
double mat[10][100];

for (i = 0; i < 10; i++)


for (j = 0; j < 100; j++)
mat[i][j] = 0.0;
mat est une matrice de double à 10 lignes et 100 colonnes. En mémoire, une
telle matrice est rangée par lignes, c’est-à-dire que c’est le dernier indice « qui
varie le plus vite » :
mat[0][0] mat[0][1] mat[0][2] ... mat[0][99]
mat[1][0] mat[1][1] mat[1][2] ... mat[1][99]
...
mat[9][0] mat[9][1] mat[9][2] ... mat[9][99]
La syntaxe d’initialisation d’agrégat reflète aussi cette notion de « tableau
de tableaux » :
int ident[3][3] = {
{1, 0, 0},
{0, 1, 0},
{0, 0, 1},
};
On lit sur cette initialisation les différentes lignes du tableau ident (la matrice
identité de dimension 3).
A titre d’exemple très simple, le programme 4.2 effectue la transposition sur
place d’une matrice carrée 3 × 3 lue sur l’entrée standard.
En voici un exemple d’exécution :
% transpose
ligne 0 (entrez 3 valeurs): 1 -1 -1
ligne 1 (entrez 3 valeurs): 1 2 -2
ligne 2 (entrez 3 valeurs): 1 2 3
Resultat:
1 1 1
-1 2 2
-1 -2 3
%

En C, le nombre de dimensions d’un tableau n’est pas limité : on peut donc


avoir des tableaux à 2, 3, n dimensions qui sont autant de « tableau de ta-
bleaux » :
int t[3][5][7];
double u[][2][2] = {
{{1, 2}, {2, 3}},
{{2, 3}, {3, 4}},
{{3, 4}, {4, 5}},
};
Noter, avec l’exemple de u, que seule la première dimension peut être laissée
libre (ici sa valeur calculée par le compilateur sera 3).

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
4.1. Tableaux 87

Programme 4.2 – Transposition d’une matrice


/***** Fichier: transpose.c *****/

#include <stdio.h>

5 #define N 3
typedef double Mat3x3[N][N];

void Transpose(int n, Mat3x3 m)


{
10 int i, j;
double x;

for (i = 0; i < N; i++)


for (j = i + 1; j < N; j++)
15 {
x = m[i][j];
m[i][j] = m[j][i];
m[j][i] = x;
}
20 }

int main()
{
int i, j;
25 Mat3x3 mat;

for (i = 0; i < N; i++)


{
printf("ligne %d (entrez %d valeurs): ", i, N);
30 for (j = 0; j < N; j++) scanf("%le", &mat[i][j]);
}

Transpose(N, mat);

35 printf("Resultat:\n");
for (i = 0; i < N; i++)
{
for (j = 0; j < N; j++) printf("%g ", mat[i][j]);
putchar(’\n’);
40 }
}

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



88 4. Tableaux, structures et unions

4.1.3 Chaînes de caractères


En C, une chaîne de caractères est simplement un tableau mono-dimen-
sionnel de caractères. Cependant, comme nous l’avons déjà indiqué (3.1.7),
il est conventionnel d’indiquer la terminaison d’une chaîne par le caractère
null (\0). Une chaîne de caractères occupe donc en mémoire un caractère de
plus que sa longueur « utile ». Toutes les fonctions de la bibliothèque standard
(9.4.2) respectent et renforcent cette convention (voir aussi les programmes 3.2
et 3.3). En particulier la fonction strlen de la bibliothèque retourne toujours
la longueur utile ; c’est aussi le cas de la fonction string_length du pro-
gramme 3.2.
On peut initialiser un tableau par une chaîne littérale :
char msg[] = "hello, world\n";
Après cette initialisation on a donc
sizeof(msg) == 14
strlen(msg) == 13
Il est bien entendu possible de définir des tableaux multi-dimensionnels de
caractères (voir aussi 5.3.2) :
const char Days[][10] = {
"lundi", "mardi", "mercredi", "jeudi", "vendredi",
"samedi", "dimanche",
};
Ici Days est une matrice de char dont la dimension est 7 × 10 soit 9 lignes et
10 colonnes (en fait 7 × 9 suffirait, mais rappelons nous que seule la première
dimension peut être calculée automatiquement par le compilateur).
Affectation de chaînes de caractères
Les chaînes de caractères étant des tableaux, comme ces derniers elles ne
sont pas affectables. Ainsi, avec les exemples précédents, les affectations
suivantes sont illégales :
msg = "Bonjour le monde!";
Days[0] = "Monday";

4.1.4 Tableaux de taille variables de C99


C99 Nous avons vu que la (les) dimension(s) d’un tableau en C90 devaient être
des constantes de compilation. Ce n’est plus vrai en C99, au moins pour les
tableaux qui sont des variables locales automatiques d’une fonction ou d’un
bloc. En C99, il est possible de dimensionner un tableau local à l’aide d’une
variable, comme dans
void g(int n)
{
int t[n];
...
}
Notez cependant que la taille n’est toujours pas un attribut du tableau : il est
nécessaire de la connaitre par ailleurs (ici grâce à n). Le programme 4.3 montre
un exemple un peu plus compliqué. Voici le résultat de son exécution :

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
4.1. Tableaux 89

% c99_var_array
n? 10
main: n = 10 sizeof(tab) = 40
f: n = 10 sizeof(t) = 4 sizeof(u) = 4000
%

Programme 4.3 – Tableaux à dimension variable de C99


/***** Fichier: c99_var_array.c *****/

#include <stdio.h>

5 /* Quatre prototypes equivalents pour f */


void f(int n, int t[n]);
void f(int, int t[*]);
void f(int, int t[]);
void f(int, int *t);
10
int main()
{
int n;
printf("n? ");
15 scanf("%d", &n);

int tab[n];
printf("main: n = %d sizeof(tab) = %d\n", n, sizeof(tab));

20 f(n, tab);
}

void f(int n, int t[n])


{
25 int u[n][n*n];
printf("f: n = %d sizeof(t) = %d sizeof(u) = %d\n",
n, sizeof(t), sizeof(u));
}

On peut faire plusieurs remarques sur ce code :


– Les quatre formes du prototype de pré-déclaration de f (lignes 5 à 8) sont
équivalentes en C99 ; seules les deux dernières sont correctes en C90.
– Dans la fonction main, le tableau tab reçoit la dimension n lue juste
avant ; on voit ici au passage l’intérêt de pouvoir mélanger déclaration et
code (3.5.1) au lieu d’être obligé de mettre toutes les déclarations en tête
de bloc.
– Dans main toujours, noter le calcul de sizeof à l’exécution et non pas,
comme la plupart du temps à la compilation.
– Dans la fonction f, on constate que plusieurs dimensions peuvent être
variables (tableau u).
– Enfin, f montre aussi que lorsqu’on passe un tableau à dimensions va-
riable en paramètre (tableau t), en fait rien ne change par rapport à C90.
En particulier, la taille est perdue (sizeof(t) retourne ici 4, c’est-à-dire
la taille d’un pointeur !).

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



90 4. Tableaux, structures et unions

Limitations du mécanisme de dimensionnement variable


Comme il a été indiqué au début, ce mécanisme de dimension variable
ne s’applique qu’aux variables locales (automatiques), et certainement pas
aux variables statiques. Par exemple, ceci ne compile pas :
int n = 10;
int tab[n];

int main()} { ... }

car tab n’est pas locale. Le fait que n soit constante ou pas ne change rien
à l’affaire.
Notons que mécanisme permet au passage de dimensionner un tableau
local par une constante : en fait il permet déjà de le faire par une variable
et donc, qui peut le plus, peut le moins.

4.2 Structures
La structure C est l’analogue du record des langages de la famille de Pas-
cal. Elle permet de regrouper en un seul type « composite » une collection
d’objets dont les types sont possiblement différents.

4.2.1 Définition du type et déclarations


La définition d’un type structure a la forme suivante :
struct tag
{
liste de champs
};
Chaque champ de la liste a la même syntaxe qu’une déclaration 3 d’objet. Par
exemple
struct Address
{
int street_number;
char street_name[100];
short zip_code;
char city_name[50];
};
définit un nouveau type dont le nom est struct Address. Le tag Address
est un identificateur choisi par l’utilisateur. Un objet de ce type est constitué
de quatre champs : un entier (street_number), une chaîne de 100 caractères 4
(street_name), un entier court (zip_code) et une chaîne de 50 caractères 5
(city_name).
Ce type étant défini, on peut en créer des objets :
struct Address addr1, addr2;
struct Address addr_epunsa = {
930, "route des Colles",

3. une déclaration, pas une définition : en particulier, il ne peut pas y avoir d’initialisation.
4. dont 99 utiles
5. dont 49 utiles

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
4.2. Structures 91

6903, "Sophia Antipolis",


};
On remarque l’utilisation de la syntaxe d’initialisation d’agrégat. Ici encore, cette
intialisation peut être partielle : seuls les premiers champs sont alors initialisés.
Remarque: nom d’un type structure
Le nom du type, struct Address, conserve le mot-clé struct. C’est
parfois pénibleet l’on peut toujours faire une synonymie de type grâce à
typedef :
typedef struct aaaa
{
int street_number;
char street_name[100];
short zip_code;
char city_name[50];
} Address;

ou même (voir 8.1)


typedef struct Address
{
int street_number;
char street_name[100];
short zip_code;
char city_name[50];
} Address;

et dans les deux cas, le nom du type sera Address tout court :
Address addr_epunsa = {
930, "route des Colles",
6903, "Sophia Antipolis",
};

Comme toujours, on peut créer un type et un objet de ce type (et même


l’initialiser) en une seule instruction :
struct Person
{
char first_name[50];
char last_name[50];
enum {FACULTY, ADMIN, STUDENT} position;
struct Adresse addr;
} jpr = {
"Jean-Paul", "Rigault", FACULTY,
{ 930, "route des Colles", 6903, "Sophia Antipolis", }
};
On peut même dans certains cas faire l’économie du tag. Le type devient alors
anonyme.
struct {
int a;
double x;
} s1, s2 = {3, 3.141592};

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



92 4. Tableaux, structures et unions

Cependant, dans ce dernier cas, le type étant anonyme, on ne pourra pas créer
d’autres objets de ce type structure.
Combiner dans la même instruction à la fois la définition d’un type et la
définition d’objets de ce type est de style douteux, et devrait être évité.
Remarque : initialisation d’agrégat
En C90, dans la définition de l’objet tel que jpr ci-dessus, en supposant
l’objet addr_epunsa préalablement initialisé, il est impossible d’écrire
simplement (quel que soit le contexte)
struct Person jpr = {
"Jean-Paul", "Rigault",
FACULTY,
addr_epunsa,
};

En effet, dans une initialisation d’aggrégat de C90, tous les initialiseurs


doivent être des constantes statiques.

Initialisation de structure en C99

C99 C99 a apporté deux modifications à l’initialisation des structures. Tout


d’abord, il est possible d’initialiser une variable locale (automatique) avec des
expressions non statiques. Ceci est donc maintenant permis :
void f()
{
struct Person jpr = {
"Jean-Paul", "Rigault",
FACULTY,
addr_epunsa,
};

int i = random();
struct {
int a;
double x;
} s = {i, i * 5};
// ...
}
Dans l’extrait précédent, addr_epunsa aurait pu aussi bien être une variable
gobale, mais jpr doit être locale. Ainsi
struct Person jpr = {
"Jean-Paul", "Rigault",
FACULTY,
addr_epunsa,
};

int main() { ... }


ne compile pas, ni en C90, ni en C99.
La deuxième extension apporté par C99 est la possibilité d’initialiser les
champs de la structure en les nommant :

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
4.2. Structures 93

Figure 4.1 Exemple de champs de bits

struct Person jpr = {


.position = FACULTY,
.last_name = "Rigault",
.first_name = "Jean-Paul",
};
On voit dans ce cas que l’ordre des champs est sans importance et aussi que
l’initialisation peut n’être que partielle (pas d’adresse par exemple). Cette syn-
taxe est également utilisable pour les tableaux de structures (voir 4.2.4).

4.2.2 Structures compactes et champs de bits


Un objet de type structure occupe en mémoire une place au moins égale à
la somme de celles de ses champs. La taille peut être plus grande que la somme
des sizeof des champs à cause des contraintes d’alignement de certains pro-
cesseurs. On peut cependant compacter une structure en indiquant le nombre
de bits consacrés à un champ (bit field). Par exemple, la structure
struct Instruction
{
unsigned code_op : 8;
unsigned index_reg : 4;
unsigned base_reg : 4;
int offset : 16;
};
décrit un format d’instruction d’un langage-machine, schématisé dans la fi-
gure 4.1.
Le nom du champ peut être omis : le champ de bits sert alors d’espace
d’alignement (padding).
Type des champs de bits
Un champ de bits ne peut être que de type entier (int) signé ou non. En
outre, en C90, ce ne peut pas être un type énuméré, et c’est parfois bien
dommage! C99 a levé cette impossibilité. Le code suivant compile donc en C99
C99 mais pas en C90 :
enum Op_Code {MOVE, ADD, SUB, BRANCH};

struct Instruction
{
unsigned code_op : enum Op_Code;
unsigned index_reg : 4;

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



94 4. Tableaux, structures et unions

unsigned base_reg : 4;
int offset : 16;
};

Portabilité des champs de bits


L’implémentation des champs de bits est extrêmement dépendante du
compilateur et du processeur.
Les programmes utilisant les champs de bits peuvent être portables, cepen-
dant, s’ils ne font aucune hypothèse sur l’implémentation, mais cela réduit
de beaucoup leur intérêt. En effet, il est clair que l’utilisation des champs
de bits ne peut se justifier que pour se « rapprocher de la machine », et
dans ce cas, on sait que l’on a des chances de perdre toute portabilité.

4.2.3 Opérations sur les structures


Sélection de champ de structure

C’est l’opération la plus importante : elle est représentée par l’opérateur


. (dot) et permet de récupérer une référence sur l’un des champs. Avec les
exemples précédents:
struct Person pers;
...
if (pers.position == FACULTY)
{
printf("%s %s est un enseignant\n",
pers.first_name, pers.last_name);
};
...
char initial = pers.last_name[0];
...
pers.position = ADMIN;
...
int zip = pers.addr.zip_code;
On remarque l’utilisation en cascade de l’opérateur de sélection dans le dernier
exemple.
Nous verrons (5.2.1) qu’il existe une autre opérateur de sélection applicable
aux pointeurs, la flèche (->)
Attention: initialisation d’agrégat et affectation
La syntaxe d’initialisation d’agrégat ne peut être utilisée, comme son nom
l’indique, que dans une initialisation. Elle est illégale pour une affectation :
struct Address addr_epunsa = {
930, "route de Colles",
6903, "Sophia Antipolis",
}; /* OK: initialisation */

adr_epunsa = {
930, "route de Colles",
6903, "Sophia Antipolis",
}; /* ILLEGAL: affectation */

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
4.2. Structures 95

Affectation de structures

Contrairement aux tableaux, les structures sont affectables ; l’affectation re-


vient à faire une copie champ par champ :
struct Person jpr = {
"Jean-Paul", "Rigault",
{
930, "route des Colles",
6903, "Sophia Antipolis",
}
};
struct Person autre_jpr;

autre_jpr = jpr;
Nous verrons aussi (7.1.3) que les structures peuvent être passées en argu-
ment ou retournées par une fonction.
Retour sur l’affectation des tableaux
On voit que si les tableaux ne sont pas affectables directement (4.1.1), ils le
sont quand on les embarque dans une structure !

4.2.4 Tableaux de structures


On peut bien entendu définir des tableaux de structures :
struct Person foule[100];
Les expressions de sélection peuvent devenir un peu plus complexes:
zip = foule[i].addr.zip_code;
initial = foule[i].last_name[0];
Pour lire ces expressions, et décider si l’on doit utiliser des parenthèses, une
seule solution: consulter le tableau de prédédence et d’associativité des opéra-
teurs (3.8). Sachez cependant que les priorités ont été bien choisies et qu’on a
rarement besoin de parennthèses dans ce type d’expression.
Intiailisation des tableaux de structures en C99
En C99, l’initialisation des structures en nommant les champs est étendu C99
aux tableaux de la façon suivante :
struct Person faculty[] = {
[0].first_name = "Jean-Paul",
[0].last_name = "Rigault",
[1].first_name = "Paul",
[1].last_name = "Franchi",
};

Là encore, l’ordre n’a pas d’importance (mais évitez de perdre le lecteur !).
En outre comme la dimension de faculty est laissée libre, le compilateur
la calculera automatiquement (ici 2).

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



96 4. Tableaux, structures et unions

4.3 Unions
Les agrégats de type union sont une des particularités du langage C, bien
que l’on puisse trouver des constructions analogues dans d’autres langages. Ils
permettent de placer à un même emplacement mémoire des objets de types
différents — mais, contrairement aux structures, un seul objet à la fois.

4.3.1 Définition du type


La définition d’un type union se présente de manière analogue à celle d’une
structure (voir 4.2.1) en remplaçant le mot-clé struct par union :
union tag
{
liste de champs
};
Par exemple :
union Real_Int
{
int int_view;
double real_view;
};
La différence avec une structure est la suivante :
– une structure corrrespond à un emplacement mémoire assez grand pour
contenir l’ensemble de tous ses champs ;
– une union, au contraire, correspond à un emplacement mémoire suffisant
pour contenir le plus grand des champs — et donc n’importe lequel d’entre
eux.
Dans l’exemple précédent, union Real_Int est donc le type d’un objet
qui peut être soit un int, soit un double.
Les objets de type union se déclarent et se définissent comme les structures :
union Real_Int u1, u2;
L’initialisation d’une union doit utiliser la syntaxe d’initialisation d’agrégat :
union Real_Int u3 = {3};

Attention : initialisation d’une union


Une précaution cependant : on ne peut initialiser un objet de type union
qu’avec un objet du type du premier champ de l’union. Ceci peut engendrer
des surprises en cas de conversions implicites ; ainsi l’extrait suivant
union Real_Int u4 = {3.141592};

passe bien la compilation. Malheureusement, il ne range pas la valeur


réelle 3.141592 dans l’objet u4, mais la valeur 3 (résultat de la conversion
de 3.141592 dans le type du premier champ !).
C99 Cependant, en C99, si on utilise la possibilité de nommage des champs
comme pour les structures (4.2.1), on peut préciser le type que l’on veut
utiliser à l’initialisation:
union Real_Int u4 = {.real_view = 3.141592};

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
4.3. Unions 97

4.3.2 Opérations sur les unions


Comme pour les structures (4.2.3) la sélection de champs est l’opération la
plus importante : elle est également représentée par l’opérateur . et permet de
récupérer une référence sur l’un des champs. Mais ici la sélection ne permet
que de déterminer le type avec lequel on interprète le contenu de l’union. Ainsi,
après
union Real_Int u;
l’expression u.int_view est une référence sur le contenu de u interprété
comme un entier, alors que u.real_view est le même objet mais interprété
comme un réel double précision.
L’opérateur de sélection flèche (->) étudié en 5.2.1 est également applicable
aux unions.
Attention : type courant d’une union
Il n’y a aucun mécanisme en C qui permette de conserver, à l’exécution,
la trace du type de l’objet effectivement présent dans une union. De plus,
aucune conversion n’est effectuée lors de la sélection d’un champ d’union.
L’extrait de programme suivant est très certainement erroné :
union Real_int u;
int i;

u.real_view = 3.141592;
i = u.int_view;

En effet, on range d’abord la valeur (réelle) de π dans u, puis l’on in-


terprète cet objet comme un entier dans l’instruction immédiatement sui-
vante. Le résultat de l’interprétation de la configuration binaire de π inter-
prété comme un entier a très peu de chances d’avoir une valeur voisine de
3 (par exemple, sur ma machine, la valeur de i est dans ce cas -57 999 238) !
C’est donc le programmeur qui a la responsabilité de garder la trace des
types des objets qu’il place dans une union (voir 4.3.3).

Portabilité des unions


L’utilisation des unions est portable, sauf si le programme fait des suppo-
sitions sur l’implémentation interne (cadrage, disposition interne, etc.).
Pour toutes les raisons discutées ici, l’utilisation des unions en C doit être
réservés à des cas particuliers très spécifiques (par exemple programmation
système) et doit être faite avec soin, par des programmeurs très prudents !

4.3.3 Structures avec variantes


L’une des utilisation les plus courantes des unions est de permettre de réali-
ser des structures à variantes, analogues au records avec variant de Pascal ou
d’Ada. Le programme 4.4 constitue un exemple de telle structure. Notez qu’il
utilise des facilités propres à C99. C99
Dans ce programme, la structure Person contient 4 champs : le nom, le
prénom et le statut (enseignant, administratif ou étudiant) de la personne ; le
quatrième champ, info, correspond à un objet choisi parmi l’une des trois
structures définies juste au dessus (Faculty, Admin et Student). Compte

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



98 4. Tableaux, structures et unions

tenu du choix des noms, il est assez clair, au moins dans la tête du program-
meur et sans doute aussi du lecteur, que c’est la valeur du champ position
qui détermine la manière d’interpréter le contenu de cette union. Mais, faut-il
le répéter, rien dans la syntaxe ne l’exprime.
La fonction print_person rend cette hypothèse plus explicite grâce à
l’instruction switch. Il est d’ailleurs extrêmement fréquent de voir associés à
une union l’utilisation d’un type énuméré et de son corollaire quasi-inévitable
switch. En outre noter que C requiert une qualification complète du chemin
conduisant à la variante, comme dans
pers.info.faculty_info.CNU_section
Ici .info sélectionne le champ union de pers, .faculty_info sélectionne
l’interprétation du contenu de cette union, et .CNU_section sélectionne un
champ dans la structure correspondante. Ouf !
Enfin, la fonction main contient un cas de test. Notez la manière dont l’objet
a_student est initialisé. Nous utilisons la possibilité de nommer les champs
C99 propres à C99, mais pour ce qui concerne la structure Student qui est simple,
nous nous contentons de l’initialisation d’agrégat ordinaire de C90.
Programme 4.4: Structure avec variante (en C99)
/***** Fichier: struct_variant.c --- ATTENTION: C99 *****/

#include <stdio.h>

5 /* Informations specifiques a un membre du corps enseignant */


struct Faculty
{
enum {PR, MDC, ATER, MONITOR} grade;
char CNU_section[10];
10 };

/* Informations specifiques a un personnel administratif */


struct Admin
{
15 enum {A, B, C, D} category; // categorie administrative
char fonction[20]; // fonction exercee
};

/* Informations specifiques a un etudiant */


20 struct Student
{
enum {Y1 = 1, Y2, Y3} year; // annee d’etude
_Bool repeater; // redoublant ?
};
25
/* Inforamtions pour une eprsonne de l’une quelconque
* des categorie precedentes
*/
struct Person
30 {
char first_name[50]; // nom
char last_name[50]; // prenom
enum {FACULTY, ADMIN, STUDENT} position; // statut

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
4.3. Unions 99

35 union {
struct Faculty faculty_info;
struct Admin admin_info;
struct Student student_info;
} info; // informations specifiques selon statut
40 };

/* Impression des informations */


void print_person(const struct Person pers)
{
45 printf("%s %s\n", pers.first_name, pers.last_name);

switch(pers.position)
{
case FACULTY :
50 printf("\tenseignant : %s\n",
pers.info.faculty_info.CNU_section);
break;
case ADMIN:
printf("\tadministratif : %s\n",
55 pers.info.admin_info.fonction);
break;
case STUDENT:
printf("\tetudiant : annee %d %s\n",
pers.info.student_info.year,
60 pers.info.student_info.repeater?"redoublant":"");
break;
}
}

65 /* Test simple */
int main()
{
struct Person a_student = {
.last_name = "Glandu", .first_name = "Anthelme",
70 .position = STUDENT,
.info.student_info = {Y2, 1},
};

print_person(a_student);
75 return 0;
}

Voic le résultat de l’exécution de ce programme :


% struct_variant
Anthelme Glandu
etudiant : annee 2 redoublant
%

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



100 4. Tableaux, structures et unions

4.4 Exercices du chapitre 4

Exercice 4.1 (Manipulation de bits) Modifier le programme 3.6 afin de traiter


des tableaux de bits de longueur maximum NBITS pouvant être supérieur au
nombre de bits dans un char (CHAR_BIT défini dans <limits.h>).

Exercice 4.2 (Calcul matriciel simple) Réaliser quelques opérations de calcul


matriciel simple : addition et produit de deux matrices rectangulaires, impres-
sion d’une matrice, etc.
Utilisation des facilités de C99
C99 Il peut être agréable de profiter des « facilités » des tableaux de taille variable
de C99 (voir 4.1.4). Par exemple le prototype de la fonction d’addition peut être
le suivant :
void matrix_sum(int m, int n,
double mat1[m][n], double mat2[m][n],
double mres[m][n]);

Soyez cependant conscient que le fait de spécifier les dimensions dans le pro-
totype n’entraine pas de vérification de ces dimensions et qu’il vous appartient
toujours d’assurer que vous passez des matrices de taille correcte (et compatible
avec les dimensions indiquées). En particulier, c’est un excellent exercice de
faire tourner votre programme en passant volontairement des tableaux de taille
incorrecte. Essayez même de l’exécuter plusieurs fois avec les mêmes données
(incorrectes).

Exercice 4.3 (Recherche en table) Un enseignant anglo-saxon note habituelle-


ment ses examens en pourcentage (donc de 0 à 100), mais son administration
peut lui demander des notes représentées pas les lettres de A à E. La table de
correspondance est la suivante :

% Note
0–60 E
61–70 D
71–80 C
81–90 B
91–100 A

Écrire un programme qui imprime la table de correspondance entre les notes


de 0 à 100 et les lettres.
Améliorer ce programme pour ajouter un + ou un - après la lettre en fonction
du dernier chiffre du pourcentage selon la tableau suivant :

Chiffre Modifieur
1–3 -
4–7
8–0 +

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
4.4. Exercices du chapitre 4 101

Ce tableau n’est valable que pour les notes de A à D, E ne peut avoir de modi-
fieur !

Exercice 4.4 (Opérations sur les dates) On souhaite représenter les dates par
une structure à trois champs entiers représentant respectivement l’année, le
mois, et le jour du mois afin de faire des opérations arithmétiques.
Les opérations suivantes doivent être réalisées (d, d1 et d2 sont des dates et n
un entier) :
date_valid(d)
Retourne un booléen indiquant si la date est valide : l’année doit être
supérieure ou égale à 1900 (Posix ne gère pas de dates inférieures), le
mois doit être compris entre 1 et 12, et le jour entre 1 et 28, 29, 30 ou 31
selon le mois et l’année...
is_leap(y)
Retourne un booléen indiquant si l’année y est bissextile ; une année est
bissextile si elle est divisble par 4, à moins que ce soit une fin de siècle
auquel cas elle doit être divisble par 400 (1900, 2100 ne sont pas bissextiles,
2000 l’est).
today()
Retourne la date du jour. On utilisera les fonctions time et localtime
de <time.h> pour obtenir cette information.
print_date(d)
Imprime une version humainement lisible de la date comme 23/01/2008.
compare_date(d1, d2)
Retourne 0 si les deux dates sont égales, une valeur négative si la première
est inférieure à la seconde, une valeur positive sinon.
add_date(d, n)
Retourne la date n jours après d ; ainsi add_date(today(), 1) re-
tourne demain.
sub_date(d, n)
Retourne la date n jours avant d ; ainsi sub_date(today(), 1) re-
tourne la veille.
diff_date(d1, d2)
Retourne le nombre algébrique de jours entre d1 et d2. Ainsi la différence
entre aujourd’hui et demain est -1 alors qu’entre demain et aujourd’hui,
c’est +1.
Remarque
Pour la réalisation des fonctions arithmétiques add_date, sub_date et
diff_date n’essayez pas d’être trop malin ! Utilisez des algorithmes simples,
par exemple des boucles jour par jour, en gérant correctement les passages de
mois et d’année.

Exercice 4.5 (Gestion de liste de contacts) Écrire une gestion d’agenda, c’est-
à-dire d’une collection d’individus avec leur nom, prénom, adresse et numéro
de téléphone. Les fonctions suivantes doivent au minimum pouvoir être effec-
tuées :
– recherche sur le nom et le prénom,

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



102 4. Tableaux, structures et unions

– affichage de la liste complète,


– introduction d’un nouveau contact à condition qu’il ne figure pas déjà dans
la base,
– destruction d’un enregistrement étant donnés son nom et son prénom.
Dans cette première version on se contentera d’une structure de données simple
et en mémoire. D’autres exercices viendront plus tard compléter celui-ci.
Remarque
Pour cette exercice, vous utiliserez les fonctions de manipulation de chaînes de
caractères de la bibliothèque standard (<string.h>) en particulier la fonction
de comparaison de chaînes strcmp.

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
Chapitre 5

Pointeurs

U n pointeur est un objet (variable ou constante) dont la valeur est l’adresse


d’un autre objet. Cette notion est bien connue dans des langages comme
Pascal ou Ada (types access). Cependant les pointeurs de C sont bien plus gé-
néraux que ceux de ces langages, dans la mesure où il est possible de prendre
l’adresse de tout objet et pas seulement des objets alloués dynamiquement. En
contre-partie de cette généralité, la manipulation de pointeurs en C est déli-
cate et souvent source de graves erreurs. Maitriser C, c’est d’abord maitriser
l’utilisation des pointeurs.

5.1 Déclarations et opérations sur pointeurs


5.1.1 Pointeurs simples
Un pointeur simple contient l’adresse d’un objet qui n’est pas lui-même un
pointeur. L’objet pointé peut être un scalaire, une structure, une union et aussi,
comme nous le verrons plus tard, une fonction (7.3.3). Pour une raison qui
apparaitra en 5.3.2, ce ne peut pas être un tableau.

Déclaration d’un pointeur simple

Les pointeurs de C sont typés : un pointeur sur un entier n’a pas le même
type qu’un pointeur sur un double ou qu’un pointeur sur une structure ; des
pointeurs sur des structures de types différents n’ont pas le même type. On ne
peut donc pas mélanger des types de pointeurs différents dans les expressions.
La syntaxe de la définition et de la déclaration de pointeurs doit indiquer le
type de l’objet pointé :
type-pointé *identificateur;
Par exemple :
int *pi;
double *px;
struct Person *ptr_pers;
union Real_Int *pu;

Attention: factorisation des déclarations de pointeurs


Lorsque l’on veut déclarer plusieurs pointeurs en une seule déclaration
(ce qui est par ailleurs de style souvent douteux), il importe de remarquer

V 2.1 – 7 février 2010 103 c Jean-Paul R IGAULT



104 5. Pointeurs

Figure 5.1 Pointeur simple

que l’étoile est attachée à l’identificateur (même s’il peut y avoir des espaces
entre cette étoile et le nom de l’objet). Ainsi
int *pi, *pj;

déclare bien deux pointeurs sur int, alors que


int * pi, pj;

déclare un pointeur sur entier (pi) et un simple entier (pj).

Initialisation de pointeurs; opérateur adresse (&)

Une déclaration de pointeur peut bien sûr comporter une initialisation.


L’opérateur & unaire permet de prendre l’adresse d’un objet. Plus précisément,
pour un objet a de type T, l’expression &a est un pointeur — constant 1 — sur
a. Par exemple, dans
int i = 4, j;
int *pi = &i;
pi est un pointeur sur entier qui contient l’adresse de l’objet entier i. La fi-
gure 5.1 illustre la situation.
L’opérateur adresse ne peut s’appliquer qu’à une référence sur un objet, et
pas à une valeur : &4, &(x + 2) ou encore &sin(3.14) sont des erreurs.
Dans le fichier d’entête standard <stddef.h> est définie la constante
NULL : on peut donner cette valeur à tout pointeur, quel que soit son type.
On peut également comparer tout pointeur à cette valeur. Par convention, un
pointeur qui a la valeur NULL ne pointe sur aucun objet. Dans une expression
entre pointeurs, la valeur entière 0 est également convertie implicitement en
NULL 2 . On peut donc écrire, par exemple,
1. car l’adresse d’un objet est une constante et ne peut pas être modifiée par le programme
2. En fait, en ansi C, NULL est défini ainsi: #define NULL 0

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
5.1. Déclarations et opérations sur pointeurs 105

int *pi = 0;
au lieu de
int *pi = NULL;

Opérateur d’indirection (*)

L’opérateur unaire * est l’inverse de &. Il s’applique à un pointeur et rend


une référence sur l’objet pointé. Ainsi, après les définition de i, j et pi précé-
dentes
j = *pi; /* j vaut 4 */
*pi = 5; /* i vaut maintenant 5 */
(*pi)++; /* i vaut maintenant 6 */
Noter les parenthèses indispensables à cause de la précédence des opérateurs
(voir 3.4.1 et 5.1.3).
Il est clair que l’opérateur d’indirection ne doit être appliqué à un poin-
teur que si ce dernier pointe effectivement sur un objet. Sinon, l’exécution du
programme sera sanctionnée (le plus souvent par un core dumped !) En par-
ticulier, appliquer l’opérateur d’indirection à un pointeur qui a la valeur NULL
provoque inévitablement une telle catastrophe.
Remarque : partie gauche d’affectation et évaluation unique pour les
affectations composées
Avec les pointeurs, la partie gauche d’une affectation peut devenir une
expression assez complexe. En voici quelques exemples :
int *p, *p1, *p2;
...
*p++ = 3; /* lire: *(p++) = 3 */
*(a > b ? p1 : p2) = 4;
On note la précédence des opérateurs dans le premier exemple ; dans le
second, selon le résultat de la comparaison a < b, c’est l’objet pointé par
p1 ou celui pointé par p2 qui prend la valeur 4.
Dans le cas d’une affectation composée, on voit l’intérêt de la remarque
sur l’évaluation unique (3.3.4). Considérons en effet
*(a < random() ? p1 : p2) += 2;
où random est une fonction qui retourne une valeur aléatoire. Cette ins-
truction évalue une seule fois random, et ajoute 2 soit à l’objet pointé par
p1, soit à celui pointé par p2. Il est clair que ceci est un comportement
radicalement différent de celui de
*(a < random() ? p1 : p2) =
*(a < random() ? p1 : p2) + 2;

5.1.2 Pointeurs multiples


Un pointeur est un objet qui a lui même une adresse : on peut donc lui ap-
pliquer l’opérateur &, ce qui donne une valeur de type « pointeur sur pointeur ».
Ainsi dans

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



106 5. Pointeurs

Figure 5.2 Pointeur double

int i = 4;
int *pi = &i;
int **ppi = &pi;
&pi est une constante de type « pointeur sur pointeur sur entier » ou encore
« double pointeur sur int », alors que ppi est une variable de ce même type.
La situation est schématisée dans la figure 5.2.
Sur un pointeur double comme ppi on peut appliquer une ou deux fois
l’opérateur d’indirection : *ppi est une référence sur un pointeur sur entier
(int *), alors que **ppi est une référence sur un entier.

5.1.3 Opérations arithmétiques sur pointeurs


Comparaison de pointeurs

Des pointeurs de même type peuvent être comparés pour l’égalité (==) ou
l’inégalité (!=) ; ces mêmes opérateurs permettent de comparer un pointeur,
quel que soit son type, à la valeur NULL.
On peut aussi comparer les pointeurs grâces aux opérateurs <, <=, > et >=.
Cette comparaison n’a pas vraiment de sensla plupart du temps. Cependant
elle a un sens lorsque les deux pointeurs pointent sur un même agrégat, par
exemple un tableau, comme dans l’exemple suivant :
int t[100];
int *p1 = &t[0];
int *p2 = &t[5];
...
if (p1 < p2) {...} /* OUI */
En revanche, dans le cas suivant :
int t1[100];
int *p1 = &t1[0];
int t2[10];

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
5.1. Déclarations et opérations sur pointeurs 107

int *p2 = &t2[5];


...
if (p1 < p2) {...} /* ??? */
le résultat dépend de l’implémentation. Et quel peut bien être l’intérêt de savoir
si &t1[0] est inférieure ou supérieure à celle de &t2[5] ?

Addition d’un entier à un pointeur

Pour un pointeur p sur un type T (ou une expression de type pointeur sur
T) et une expression entière n, l’expression p+n a comme valeur un pointeur
sur T qui pointe sur le nème objet de type T suivant si n > 0 (resp. précédant
si n < 0) celui pointé par p.
Illustrons cette opération par l’extrait de programme suivant :
struct Person *px;
struct Person tab[100];
...
px = &tab[5];
Ici px pointe sur la structure Person qui est la composante d’indice 5 du ta-
bleau tab. Dans ces conditions, px+3 pointe sur le troisième élément de type
struct Person suivant tab[5], c’est-à-dire tab[8]. Et px-2, quant à lui,
pointe sur tab[3].
On voit donc que l’addition d’un entier à un pointeur n’est pas la simple
addition à la valeur de l’adresse mémoire. Il y a en fait une mise à l’échelle en
fonction de la taille de l’objet pointé.
Notons, une fois encore, qu’il n’y a aucune vérification de débordement
d’indice : dans l’exemple précédent, px-10 est certainement une erreur, mais
elle n’est détectée ni à la compilation, ni à l’exécution — sauf option spéciale
de mise au point.
Bien entendu, les cas particuliers d’addition et de soustraction entières re-
présentés par les affectations composées (+= et -=) et les incrémentations et
décrémentations (++ et --) sont applicables aux pointeurs.

Soustraction de deux pointeurs (de même type)

Si p1 et p2 sont deux pointeurs pointant sur des objets de même type T,


l’expression p2-p1 rend une valeur entière (int) qui est le nombre algébrique
d’objets de type T contenu entre celui pointé par p2 et celui pointé par p1. Ce
nombre est bien sûr nul si p2 == p1, positif si p2 > p1, négatif si p2 < p1.
Comme pour la comparaison de pointeurs mentionnée ci-dessus, une telle
différence n’a vraiment de sens que si p1 et p2 sont non seulement de même
type, mais pointent sur des éléments d’un même tableau : dans ce cas en effet
on est sûr qu’il y a un nombre entier exact d’éléments de même type entre les
deux pointeurs :
double t[10];
int u[3];
double v[5];
...
double *p1 = &t[3];
double *p2 = &t[7];

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



108 5. Pointeurs

int n = p2 - p1; // OUI : n == 4, le nombre de double entre p2 et p1


...
n = &v[2] - p1; // cela compile, mais qu’est-ce que cela signifie ?

Attention : addition de deux pointeurs


L’opération d’addition de deux pointeurs, même s’ils sont de type iden-
tique n’a pas de sens. Le compilateur ansi C émet d’ailleurs un message
d’erreur.
Quant aux programmeurs qui voudraient calculer un pointeur sur un objet
qui est exactement au milieu des objets pointés par les deux pointeurs p1
et p2, ils devront d’abord se rendre compte que la notion d’être exactement
« au milieu » n’est pas complètement définie : elle dépend de la parité de
p2-p1. Une fois le choix fait, ils pourront essayer des expressions de la
forme
p1 + (p2 - p1)/2

qui ont un sens, car il s’agit de l’addition d’un entier à un pointeur.

5.1.4 Conversions de pointeurs


Pointeurs génériques : void*

Par convention 3 , un pointeur sur void est un pointeur « brut », une adresse
machine. Tout pointeur peut être converti en void* sans perte d’information. Ceci
signifie que si le void* est à nouveau converti dans le type initial, il retrouve
la même valeur (i.e., pointe sur le même objet).
Ces conversions, dans les deux sens, peuvent être implicites ou bien sûr
explicites (cast) comme dans les exemples suivants :
int i = 4;
double x = 3.14;
int *pi = &i;
double *px = &x;
void *pv;
...
pv = pi;
pi = pv; /* OK: pi pointe sur i */
...
pv = (void *)px;
px = (double *)pv; /* OK: px pointe sur x */
Mais attention aux incohérences :
pv = pi; px = pv; /* Catastrophe annoncée !!!! */
Dans le dernier exemple, pv prend la valeur d’un pointeur sur int puis est
converti en pointeur sur double. Si l’on essaie de prendre une référence sur
l’objet maintenant pointé par px (*px), cela revient à interpréter l’entier pointé
par pi comme un double, mais sans conversion ! C’est un problème analogue
à celui des unions (4.3.2) : le programmeur a la responsabilité de garder la
trace du type exact pointé par un pointeur sur void. Il est à noter que ce
type d’erreur est indétectable dans le cas général, à la compilation comme à
l’exécution.
3. Ce ne peut être qu’une convention, puisqu’il n’y a aucun objet de type void.

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
5.1. Déclarations et opérations sur pointeurs 109

Enfin, et pour des raisons évidentes, les seules opérations permises sur un
void* sont l’affectation et les tests d’égalité et d’inégalité, ainsi que la prise
de l’adresse du pointeur lui-même. Les opérations d’indirection (*), l’addition
et la soustraction d’un entier, la soustraction de deux pointeurs, l’opérateur
-> (5.2.1) ou encore l’indexation (5.3.2) sont interdites pour les void*, car elle
nécessitent toutes la connaissance du type, ou au moins de la taille, de l’objet
pointé.

5.1.5 Pointeurs et constantes


Considérons le cas d’un pointeur simple. Il y a deux notions de « constante »
possibles pour un tel pointeur :
– le pointeur lui même est constant,
– l’objet pointé est constant.
Les déclarations correspondantes sont les suivantes :
int i;
...
int *const pconst = &i; /* pointeur constant */
const int *p_sur_const = &i; /* pointeur sur constante */
Dans le premier cas, c’est la valeur de pconst qui est constante ; pconst ne
peut donc pas apparaitre à gauche d’une affectation. En revanche, l’objet pointé
— qui sera donc toujours le même — n’est pas constant, et *pconst peut
apparaitre à gauche d’une affectation. Noter que l’initialisation est ici nécessaire
(c’est la seule chance de donner une valeur à pconst).
Dans le second cas, c’est l’objet pointé qui est constant : *p_sur_const
ne peut donc apparaitre à gauche d’une affectation, mais p_sur_const en
revanche pourra pointer sur plusieurs objets au cours de sa vie. L’initialisation
n’est donc pas indispensable ici.
Dire, dans le cas de p_sur_const, que « l’objet pointé » est constant est
un raccourci, voire un abus de langage. En fait, pour être plus précis, l’objet ne
peut être modifié lorsqu’il est accédé au travers du pointeur ; il peut cependant
exister d’autres voies d’accès qui permettent de modifier l’objet pointé. C’est
clair dans l’exemple précédent, puisque p_sur_const pointe sur i qui est
une variable. Donc l’objet en question est modifiable qand il est accédé par le
nom i mais ne l’est pas quand on utilise *p_sur_const.
Un pointeur simple peut à la fois être constant et pointer sur une constante :
la déclaration devient alors
const int *const pconst_sur_const = &i;
Ni pconst_sur_const, ni *pconst_sur_const ne peuvent apparaitre en
partie gauche d’une affectation. Là encore l’initialisation est nécessaire.
Pour les pointeurs multiples, la combinatoire des possibilités augmente. Par
exemple pour un double pointeur sur entier, on a les possibilités suivantes :
int **p; double pointeur variable
const int **p; **p est constant
int **const p; p est constant
const int **const p; p et **p sont constants
int *const *p; *p est constant
const int *const *p; *p et **p sont constants
etc...

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



110 5. Pointeurs

On laisse au lecteur le soin de déterminer les cas où les initialisations sont


indispensables.
Le compilateur ansi C est extrêmenent vigilant lors des affectations et ini-
tialisations mettant en jeu des pointeurs sur constante. Il impose la règle natu-
relle suivante : il est toujours possible de restreindre l’accès à un objet, mais jamais
de l’étendre. On peut donc décider de faire implicitement une constante à partir
d’une variable, mais pas l’inverse. Les exemples suivants résument les possibi-
lités et les impossibilités dans le cas de pointeurs simples :
int i;
const int ic = 4;
int *p = &i; OUI: pointeur sur variable
int *p = &ic; NON: objet pointé constant
const int *p = &ic; OUI: pointeur sur une constante
const int *p = &i; OUI: i non modifiable à travers p
int *const p = &i; OUI: pointeur constant
int *const p = &ic; NON: pointeur sur une variable
const int *const p = &i; OUI: i non modifiable à travers p
const int *const p = &ic; OUI

int *pp;
int *const pconst = &i;
const int *p_sur_const;
const int *pconst_sur_const = &ic;
pp = pconst; OUI:
pconst = pp; NON: affectation interdite
p_sur_const = pp; OUI: restriction d’accés
pp = p_sur_const; NON: élargissement d’accès
p_sur_const = pconst; OUI
p_sur_const = pconst_sur_const; OUI
Noter que dans la première série d’exemples, &i est de type int *const,
alors que &ic est de type const int *const.
Un cas particulier d’initialisation est celui du passage d’arguments à une
fonction étudié en 7.3.2.

5.2 Pointeurs sur structures et unions


5.2.1 Opérateur de sélection « flêche »
C permet bien entendu d’avoir des pointeurs sur des structures ou des
unions :
struct Person somebody;
struct Person *ps = &somebody;
...
union Real_Int ri;
union Real_Int *pu = &ri;
Si l’on veut sélectionner un champ de structure ou d’union à travers le poin-
teur on peut utiliser l’opérateur de sélection habituel (.), mais ceci oblige à
appliquer auparavant l’opérateur d’indirection :
(*ps).position = ADMIN;
(*pu).int_view = 3;

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
5.2. Pointeurs sur structures et unions 111

Compte tenu de la précédence des opérateurs (3.4.1), les parenthèses sont in-
dispensables et l’écriture devient vite pénible. C’est pourquoi C introduit l’opé-
rateur de sélection de champ à travers un pointeur, dénoté ->. L’expression
p->champ désigne une référence sur le champ de la structure ou de l’union
pointée par p ; elle est strictement équivalente à l’expression (*p).champ, mais
moins lourde.
Avec cet opérateur, les exemples précédents s’écrivent de manière plus lè-
gère :
ps->position = ADMIN;
pu->int_view = 3;
Nous verrons dans la sous-section suivante (5.2.2) l’origine et tout l’intérêt de
cette notation.

5.2.2 Types récursifs


Un type récursif est un type qui contient un composant — un champ — du
même type que lui-même. Au sens strict ceci est interdit en C : un champ de
structure ou d’union ne peut pas être du type de cette structure ou union. Ainsi
ceci
struct Recur
{
int i;
struct Recur r;
};
est interdit. La raison principale est que C insiste pour connaitre la taille des
objets à la compilation, et qu’à l’évidence ceci est ici impossible. En revanche,
la taille d’un pointeur est connue à la compilation. Il est alors possible qu’un
champ d’une structure ou d’une union soit de type pointeur sur cette structure
ou union. L’exemple précédent peut donc s’écrire correctement
struct Recur
{
int i;
struct Recur *pr;
};

Exemple: liste simple d’entiers

L’un des exemples les plus évidents de type récursif est celui des listes
simplement chaînées. Le fichier d’inclusion simple_list.h en 5.1 présente
la définition d’un tel type : une liste simplement chaînée de valeurs entières.
Ce fichier contient aussi le prototype de la fonction insert, dont le corps est
présenté plus loin.
La structure List_Cell 4 comporte donc deux champs : une valeur entière
(val) et un pointeur (next) sur un objet de même type soit List_Cell* .
Ceci va permettre de chaîner linéairement de tels objets comme schématisé sur
4. On voit que List_Cell est à la fois le tag et le nom du type défini par typedef. Ceci
est autorisé, comme nous le verrons en 8.1. Noter aussi que le tag est ici obligatoire car le nom
du type défini par typedef n’est pas encore connu à l’intérieur de la définition de structure.

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



112 5. Pointeurs

Programme 5.1 – Liste d’entiers : définition du type


#ifndef _SIMPLE_LIST_H_
#define _SIMPLE_LIST_H_

/***** Fichier: Simple_List/simple_list.h *****/


5
typedef struct List_Cell
{
int val;
struct List_Cell *next;
10 } List_Cell;

struct List_Cell *insert(List_Cell *, int);

#endif

Figure 5.3 Liste simplement chaînée : simple_list.h

la figure 5.3. Par convention — mais cette convention est assez universelle —
la liste s’arrête quand le pointeur next vaut NULL. La liste tout entière est
représentable par un pointeur (type List_Cell *) sur son premier élément
(phead).
Remarque: origine de la notation « flêche »
Sur la figure 5.3, on voit l’origine et même l’élégance de la notation ->. En
effet, dans un expression comme
phead->next->next->val

qui désigne la valeur portée par le troisième élément de la liste (le second
ave la valeur 3), les -> correspondent exactement aux flêches du schéma.
La fonction insert est définie dans un autre fichier présenté en 5.2. On doit
bien entendu inclure le fichier simple_list.h (programme 5.1) afin d’impor-
ter la définition du type List_Cell. La directive #include effectue ce travail :
noter l’utilisation des « doubles quotes » ("...") à la place des « piquants »

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
5.2. Pointeurs sur structures et unions 113

Figure 5.4 Insertion dans une liste avec un simple pointeur

(<...>) qui indiquent ici de chercher ce fichier d’abord dans le répertoire cou-
rant, avant d’examiner les répertoires standards du système (voir 6.2.2).
La fonction insert insère la valeur entière v de son deuxième argument
dans la liste dont la tête est pointée par son premier argument. Le point d’in-
sertion est devant le premier élément de la liste dont la valeur est supérieure ou
égale à v. Ainsi, si la liste était rangée par ordre croissant avant insertion, elle
le demeure après. La liste peut comporter des répétitions. La fonction retourne
un pointeur 5 sur la nouvelle tête de liste. En effet, au cas où v est inférieur à
toutes les valeurs de la liste, c’est la nouvelle cellule qui devient la tête de liste.
Le parcours de liste et l’insertion, schématisés en 5.4, mettent en jeu trois
pointeurs :
1. pcurr visite les cellules l’une après l’autre ;
2. pprev pointe toujours sur la cellule précédant immédiatement celle poin-
tée par pcurr, sauf au début — quand pcurr vaut phead, où il est NULL ;
3. enfin pnew pointe sur une nouvelle cellule de liste qui est allouée dyna-
miquement pour contenir la valeur v.
L’allocation dynamique est effectué par l’appel de la fonction malloc. Cette
fonction reçoit en argument la taille d’un objet, obtient du système l’allocation
d’une zone mémoire suffisamment grande pour contenir un tel objet et retourne
un pointeur sur cette zone (voir 9.1.4). Nous ignorons ici le traitement du cas
d’erreur où la mémoire serait indisponible : malloc retournerait alors la valeur
NULL.
Le parcours de liste s’interrompt soit en fin de liste (pcurr devient NULL),
soit dès que la valeur contenue dans la cellule courante (pcurr->val) devient
supérieure ou égale à v. On note que tout le travail est effectué dans la partie
5. Pour une explication de la déclaration de cette fonction, et en particulier de son type de
retour, voir 7.3.1.

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



114 5. Pointeurs

Programme 5.2 – Liste d’entiers : insertion par ordre croissant


/***** Fichier: Simple_List/simple_list.c *****/

#include <stdlib.h>

5 #include "simple_list.h"

List_Cell *insert(List_Cell *phead, int v)


{
List_Cell *pcurr;
10 List_Cell *pprev;
List_Cell *pnew;

pnew = malloc(sizeof(List_Cell));
pnew->val = v;
15
for (pcurr = phead, pprev = NULL;
pcurr != NULL && v > pcurr->val;
pprev = pcurr, pcurr = pcurr->next)
{
20 /* Vide */
}

/* Ici, soit pcurr est NULL, soit pcurr pointe sur


le premier element superieur ou egal a v
25 */

if (pprev != NULL)
{
/* La liste n’etait pas vide (phead != NULL) */
30 pnew->next = pcurr;
pprev->next = pnew;
return phead;
}
else if (pcurr != NULL)
35 {
/* La nouvelle cellule devient la tete de liste */
pnew->next = pcurr;
return pnew;
}
40 else
{
/* La liste etait vide (phead == NULL) */
pnew->next = NULL;
return pnew;
45 }
/*NOTREACHED*/
}

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
5.2. Pointeurs sur structures et unions 115

contrôle de la boucle for (le corps est vide) : c’est évidemment une question
de style et de goût.
Lorsque la boucle s’arrête trois cas sont possibles :
1. si pprev n’est pas NULL on insère pnew 6 juste derrière la cellule pointée
par pprev — noter que pcurr peut être NULL si le nouvel élément de-
vient le dernier de la liste ; la tête de liste est inchangée (égale à phead) ;
2. si pprev est NULL, mais pas pcurr, c’est que pcurr pointe sur le pre-
mier élément de la liste (pcurr est égal à phead) et que pnew devient la
nouvelle tête de liste ;
3. enfin si pprev et pcurr sont tous les deux NULL, c’est que phead l’était
aussi et pnew devient la tête de liste — et aussi la seule cellule.
Enfin, le programme 5.3 sert de test pour la fonction insert. Il lit une liste
de nombres entiers, les insère un à un et imprime le résultat qui doit être une
liste triée. Noter ici que le source du programme tient sur deux fichiers ; pour
obtenir un exécutable, on peut utiliser la commande
% gcc -o simple_list simple_list_main.c simple_list.c
mais il est conseillé d’utiliser make (10.2.2). Ici, on a besoin d’un fichier de nom
makefile (ou Makefile) dans le même répertoire que les fichiers-source. En
voici le contenu (en supposant que vous avez défini les variables d’environne-
ment CC et CFLAGS) :
# Makefile pour la liste simplement chainée

# Cible finale : exécutable ’simple_list’


simple_list : simple_list_main.o simple_list.o
T A B $(CC) -o simple_list simple_list_main.o simple_list.o

# Dépendances individuelles
simple_list_main.o simple_list.o : simple_list.h
# Nettoyage
clean :
T A B -rm *.o simple_list

Les tab représentent effectivement le caractère tab du clavier. Il suffit alors


d’invoquer make depuis le shell puis d’exécuter le programme produit :
% make
gcc -g -std=c99 -Wall -c simple_list_main.c simple_list.c
gcc -g -std=c99 -Wall -c simple_list.c
gcc -o simple_list simple_list_main.o simple_list.o
% simple_list
Entrez une suite d’entiers terminees par EOF
1 2 5 7 0 6 4 2 6 9 -1 -3
Liste triee:
-3 -1 0 1 2 2 4 5 6 6 7 9
%

Allocation dynamique ou variable locale ?


Dans le programme précédent les cellules de la liste sont allouées dyna-
miquement, grâce à l’appel de malloc. Ceci est inévitable. Comme on ne

6. abus de langage pour « la cellule pointée par pnew » !

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



116 5. Pointeurs

Programme 5.3 – Liste d’entiers : programme principal


/***** Fichier: Simple_List/simple_list_main.c *****/

#include <stdio.h>

5 #include "simple_list.h"

int main()
{
List_Cell *phead = NULL;
10 List_Cell *p;
int n;

printf("Entrez une suite d’entiers terminees par EOF\n");


while (scanf("%d", &n) != EOF)
15 phead = insert(phead, n);

printf("Liste triee:\n");
for (p = phead; p != NULL; p = p->next)
printf("%d ", p->val);
20 putchar(’\n’);

return 0;
}

connait pas à l’avance le nombre (maximal) de cellules nécessaires, il n’est


pas possible de les allouer, par exemple, dans un tableau dont la taille de-
vrait être connue à la compilation ; il faut donc les créer au fur et à mesure
des besoins. Cette création ne peut pas être sous la forme d’une variable
locale (automatique — voir 8.3.1), dans la fonction insert, car une telle
variable disparaitrait dès que la fonction se terminerait alors que les cel-
lules et la liste doivent perdurer (aussi longtemps que le programmeur le
désire).

Parcours de liste et insertion avec double pointeur

La fonction d’insertion précédente utilise deux pointeurs pour parcourir


la liste : sur la cellule courante et sur la cellule immédiatement précédente.
On peut se demander s’il n’est pas possible d’utiliser un unique pointeur. La
réponse est positive, comme le montre le programme 5.4.
Le pointeur unique est en fait un double pointeur pp qui pointe tour à tour,
non pas sur chaque cellule, mais sur le champ next de la cellule. Il faut aussi
modifier la structure de données elle même : on introduit une cellule de liste
fixe qui sert de tête de liste et dont le champ val n’est pas utilisé. Avec cette
hypothèse, la tête de liste est fixe et la fonction insert n’a plus besoin de
retourner une valeur. En outre, pour représenter la liste vide, il suffit de faire
boucler la tête de liste sur elle-même (figure 5.5(a)).
On constate que le code est plus court, sinon plus simple à comprendre.
Il est aussi plus homogène puisqu’il ne traite aucun cas particulier comme le
faisait la programmation précédente. Bien sûr, sa lecture et plus encore son écri-

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
5.2. Pointeurs sur structures et unions 117

Programme 5.4 – Insertion dans une liste avec un seul (double) pointeur
/***** Fichier: Simple_List/simple_list_double_ptr.c *****/

#include <stdlib.h>
#include <stdio.h>
5
typedef struct List_Cell
{
int val;
struct List_Cell *next;
10 } List_Cell;

void insert(List_Cell *phead, int v)


{
List_Cell **pp;
15 List_Cell *pnew;

pnew = malloc(sizeof(List_Cell));
pnew->val = v;

20 for (pp = &phead->next;


*pp != NULL && v > (*pp)->val;
pp = &(*pp)->next)
{
/* Vide */
25 }

pnew->next = *pp;
*pp = pnew;
}
30
int main()
{
List_Cell head = {0, NULL};
List_Cell *p;
35 int n;

printf("Entrez une suite d’entiers terminees par EOF\n");


while (scanf("%d", &n) != EOF)
insert(&head, n);
40
printf("Liste triee :\n");
for (p = head.next; p != NULL; p = p->next)
printf("%d ", p->val);
putchar(’\n’);
45
return 0;
}

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



118 5. Pointeurs

Figure 5.5 Insertion dans une liste avec un double pointeur

(a) Représentation de la liste vide

(b) Insertion d’une cellule

ture nécessitent un peu d’habitude de C. La figure 5.5(b) schématise le parcours


de boucle.

5.3 Pointeurs et tableaux


Cette section étudie les relations entre tableaux et pointeurs. Tout d’abord,
C permet de définir des tableaux de pointeurs, ce qui fait l’objet de la première
sous-section. Compte tenu de la forme particulière de la déclaration de tels
tableaux, nous sommes conduits à donner aussi un règle pour la lecture des
déclarations de C.
La deuxième sous-section traite d’un des problèmes très particuliers du lan-
gage C : l’équivalence — partielle — entre tableaux et pointeurs.

5.3.1 Tableaux de pointeurs ; lecture des déclarations de C


Dans les déclarations suivantes
int *ptab[10];
int **p2tab[100];

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
5.3. Pointeurs et tableaux 119

ptab est un tableau de 10 pointeurs sur entiers (int) et p2tab un tableau de


100 doubles pointeurs sur entiers. Pourquoi en est-il ainsi ? Comment interpré-
ter ces déclarations?
Un petit retour en arrière s’impose. Lorsque nous déclarons un entier par
int i;
nous disons simplement que l’objet dénoté i, quand il est rencontré dans une
expression, a une valeur de type int. De même une déclaration comme
int t[10];
indique que si l’on rencontre t[e] dans une expression, où e est une expres-
sion entière, alors cette forme désigne un int. Toujours de manière analogue,
la déclaration
int *pi;
signifie que dans une expression, *pi est un int. Comme * est l’opérateur
d’indirection, ceci revient bien à dire que pi est un pointeur sur int.
Revenons maintenant aux déclarations
int *ptab[10];
int **p2tab[100];
Compte tenu de cette règle d’interprétation, la première déclaration signifie
que *ptab[e], où e est une expression, est un int. A cause de la précédence
des opérateurs (3.4.1), *ptab[e] doit se lire comme *(ptab[e]). Ceci signifie
que ptab[e] est un pointeur sur int, puisqu’on peut lui appliquer l’opérateur
d’indirection. Et finalement ptab est lui-même un tableau de pointeurs sur
int, puisqu’on peut lui appliquer l’opérateur d’indexation.
Cette règle de lecture des déclarations de C est toujours valable et elle sera
précieuse lorsque nous étudierons les déclarations de pointeurs sur fonctions
(7.3.3).

5.3.2 Relations entre tableaux et pointeurs


Indexation des pointeurs

Revenons sur l’opération d’addition d’un entier à un pointeur (5.1.3) dans


le cadre de cet extrait de programme :
int t[10];
int *p = &t[0];
Le pointeur p pointe donc sur le premier élément du tableau t. Conformément
à la définition de l’addition d’un entier à un pointeur, pour tout entier i (supé-
rieur ou égal à 0 et inférieur à 10), p+i est un pointeur sur l’élément d’indice
i de t. Donc p+i == &t[i]. Donc encore, *(p+i) et t[i] référencent le
même objet.
A cause de ces remarques, le langage C permet d’appliquer l’opération d’in-
dexation à un pointeur. Pour tout pointeur p et toute expression i, p[i] est
défini comme étant équivalent à *(p+i). Ceci permet donc de manipuler des
pointeurs comme des tableaux mono-dimensionnels. Par exemple, après
struct Person *ppers = malloc(10*sizeof(struct Personne));

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



120 5. Pointeurs

Figure 5.6 Relation entre pointeurs et tableaux mono-dimensionnels en C

le pointeur ppers pointe sur une zone de mémoire pouvant accueillir un ta-
bleau de 10 structures Person. On peut désigner les différentes composantes
sous la forme ppers[0], ppers[1]... On pourra donc écrire des instructions
comme
ppers[i].age = 18;

Relation entre tableaux et pointeurs

Toujours dans le cadre de l’exemple précédent où p pointe sur le premier


élément du tableau t, on peut donc désigner les éléments de t par t[0],
t[1]... bien sûr, mais aussi par p[0], p[1]..., et encore par *(p+0), *(p+1)....
Si l’on peut traiter un pointeur comme un tableau, on doit par cohérence traiter
un tableau comme un pointeur ! Ceci revient à dire que l’on doit aussi pou-
voir désigner les éléments de t par *(t+0), *(t+1)... ou encore que l’on doit
considérer un nom de tableau comme un pointeur sur sa première composante.
C fait donc hardiment cette assimilation des noms de tableaux à des poin-
teurs. Ainsi, l’extrait précédent aurait-il pu s’écrire
int t[10];
int *p = t; /* t == &t[0] */
La figure 5.6 illustre cette relation très particulière entre pointeurs et tableaux
en C.
Comme les chaînes de caractères ne sont que des tableaux particuliers,
C est aussi amené à considérer qu’une chaîne de caractères littérale comme
"bonjour\n" est aussi de type pointeur (sur caractère, donc char *).

Différence entre tableaux et pointeurs

L’équivalence entre tableau et pointeur n’est cependant pas totale :


– Un pointeur comme p est une variable ; en revanche, t est un pointeur
sur un objet dont le compilateur est maitre de l’adresse, pas le program-

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
5.3. Pointeurs et tableaux 121

meur. Un nom de tableau est donc un pointeur constant (t est de type


int *const).
– D’autre part, une définition de tableau comme celle de t réserve effective-
ment la place pour toutes les composantes (ici 10), alors que la définition
de p ne réserve que la place d’un pointeur.
La deuxième remarque est fondamentale : on peut manipuler des pointeurs
comme des tableaux, mais après s’être assuré qu’ils pointent effectivement sur
des tableaux ! Une conséquence de cette remarque est que sizeof(t) est la
taille du tableau tout entier, alors que sizeof(p) est la taille d’un simple
pointeur. Ainsi,
sizeof(t) == 10*sizeof(int)
sizeof(p) == sizeof(int *)
Sur une machine 32 bits, sizeof(t) vaut 40, alors que sizeof(p) ne vaut
que 4 !
Une autre conséquence est que C ne sait pas distinguer un pointeur sur un
objet individuel d’un pointeur sur un tableau d’objets !

Cas des tableaux multi-dimensionnels

Si un nom tableau mono-dimensionnel a comme type celui de pointeur sur


le type de ses composantes, qu’en est-il d’un tableau bi-dimensionnel comme
par exemple Days déjà rencontré en 4.1.3 ?
const char Days[][10] = {
"lundi", "mardi", "mercredi", "jeudi", "vendredi",
"samedi", "dimanche",
};
La réponse est évidente : Days est de type « double pointeur sur charactère »
(char**). L’expression Days[i] devient possible (équivalente à *(Days+i),
et donc de type char* : elle désigne tout naturellement la ligne d’indice i de
Days, c’est-à-dire un tableau mono-dimensionnel.
Considérons la définition suivante :
const char *Jours[] = {
"lundi", "mardi", "mercredi", "jeudi", "vendredi",
"samedi", "dimanche",
};
Cette fois, Jours est un tableau de 10 pointeurs sur caractères. L’initialisation est
correcte, puisque nous avons déjà remarqué que les chaînes comme "lundi"
étaient des pointeurs sur caractères.
Les noms Days et Jours sont des expressions de même type, à savoir
char**. Ils sont tous les deux simplement indexables : Days[1], comme
Jours[1], pointe sur la chaîne "mardi". Ils sont également tous les deux
doublement indexables : Days[1][3], comme Jours[1][3], est une réfé-
rence sur le caractère ’d’ de "mardi".
Il y a cependant une différence : Days occupe en mémoire une taille de 70
caractères (sizeof(Days) vaut 70), alors que Jours n’occupe que la place de
7 pointeurs (par exemple sizeof(Jours) vaut 28 sur ma station de travail).
La figure 5.7 montre les représentations différentes en mémoire des deux objets
Days et Jours.

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



122 5. Pointeurs

Figure 5.7 Tableau multi-dimensionnel et tableau de pointeurs

(a) Une matrice de caractères

(b) Un tableau de pointeurs sur caractère

Attention : double indexation de pointeurs


Une autre différence entre tableaux et pointeurs concerne la double (ou
plus) indexation. La double indexation d’un double pointeur « pur » —
c’est-à-dire qui n’est pas lui même soit un tableau de pointeurs, soit un
tableau bi-dimensionnel — n’a pas de sens : dans
char **pp = Days; /* probleme identique avec Jours */

comment pourrait-on calculer pp[i][j] ? Le calcul d’indice nécessite de


connaitre le nombre de « colonnes » du « tableau » à la compilation et cette
information n’est pas disponible pour le pointeur pp.

5.4 Exercices du chapitre 5

Exercice 5.1 (Liste triée simplement chaînée) Compléter le type « liste d’en-
tiers triée par ordre croissant » en définissant les fonctions suivantes :
– chercher si une valeur est dans la liste ;
– retourner le nième élément, s’il existe ;

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
5.4. Exercices du chapitre 5 123

– détruire une valeur dans la liste, si elle s’y trouve (une seule occurrence et
toutes les occurences) ;
– fusionner une liste avec une autre, c’est-à-dire ajouter à la première liste une
copie des valeurs de la seconde ; bien entendu la liste finale doit aussi être
triée ;
– dupliquer une liste, c’est-à-dire retourner une nouvelle liste dont les valeurs
sont des copies des valeurs de la première liste ;
– imprimer une liste.

Exercice 5.2 (Liste triée doublement chaînée) Même exercice que précédem-
ment, mais avec des listes doublement chainées. La cellule de liste a donc la
forme suivante :
struct DList_Cell
{
int val;
struct DList_Cell *prev; /* pointeur arriere */
struct DList_Cell *next; /* pointeur avant */
};

Par ailleurs, l’implémentation des listes avec une fin marquée par un pointeur
nul comme dans l’exercice 5.1 est déjà maladroite pour des listes simplement
chaînée mais elle est carrément débile pour des listes doubles. Vous utiliserez
donc l’implémentation décrite dans la figure 5.8. Ici la liste contient toujours
une cellule de garde (dont la valeur ne fait pas partie de la liste). Lorsque
la liste est vide, les deux pointeurs de cette cellule pointent sur elle-même
(figure 5.8(a)). Sinon, les cellules sont chaînées naturellement, et la liste re-
boucle sur la cellule de garde. Si phead est l’adresse de la cellule de garde
(figure 5.8(b)), la première cellule « utile » est donc pointée par phead->next
et la liste se termine à la cellule phead exclue.
Bien entendu, un léger inconvénient par rapport à l’implantation directe de
l’exercice 5.1 est que vous avez maintenant besoin de définir une fonction d’ini-
tialisation de liste pour créer la cellule de garde et vous ne devez pas oublier
d’invoquer cette fonction avant de manipuler le liste.
Remarque sur le test de ce programme
Un bon moyen pour (commencer à) tester ce programme est de prendre comme
programme de test exactement le même que pour l’exercice 5.1, de récupérer
les sorties des deux programmes dans des fichiers et de vérifier que ces fichiers
sont identiques, par exemple en utilisant les commandes cmp ou diff du shell.

Exercice 5.3 (Manipulation de chaînes de caractères) Les programmes 3.2 et


3.3 montrent comment on peut manipuler les chaînes de caractères en C, sous
l’hypothèse (forte) que les dites chaînes se terminent par le caractère nul. La
bibliothèque standard de C fournit un certain nombre de fonctions de manipu-
lation de chaînes qui reposent toutes sur cette hypothèses. Ces fonctions sont
déclarées dans le fichier d’entête <string.h> et documentées dans la page de
manuel correspondante (pour vous familiariser avec elles, faites man string).
L’exercice consiste à utiliser quelques unes de ces fonctions pour réaliser des
opérations de sous-chaînes, opérations peu représentées dans la bibliothèque stan-
dard.

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



124 5. Pointeurs

Figure 5.8 Liste doublement chaînée avec cellule de garde et rebouclage

(a) Représentation de la liste vide

(b) Cas général

Dans le même esprit (et sous la même hypothèse), écrire (et testez !) les fonc-
tions suivantes (s, s1 et s2 désignent des chaînes, i et n des entiers, c un
caractère) :
string_trim(s)
Enlève les espaces initiaux et finaux de la chaîne.
substring(s, i, n)
Retourne la sous-chaîne de s de longueur maximale n commençant à
l’indice i ; si n est trop grand, on ne retourne que ce qui est possible.
substring_search(s1, s2)
Recherche la première occurrence de la chaîne s1 dans s2 et retourne
l’index correspondant dans s2 ou -1 (si s1 n’est pas une sous-chaîne de
s2).
insert_substring(s1, s2, i)
Insère la chaîne s1 devant le caractère d’indice n de s2 ; ne fait rien si n
est invalide.

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
5.4. Exercices du chapitre 5 125

delete_substring(s, i, n)
Détruit la sous-chaîne de s de longueur maximale n commençant à l’in-
dice i ; si n est trop grand, on ne détruit que ce qui est possible.
replace_substring(s1, i, n, s2)
Remplace la sous-chaîne de s1 de longueur maximale n commençant à
l’indice i par la chaîne s2; si n est trop grand, on ne remplace que ce qui
est possible.
tokenize_string(s, c, ts[], n, empty_fields)
Étant donnée une string s composée de champs séparés par le caractère
c, copie ces champs un par un dans les éléments du tableau de chaînes
ts et retourne le nombre de champs ; n est la taille maximale du tableau
ts.
Si le booléen empty_fields est vrai et si deux séparateurs c sont ad-
jacents, la chaîne correspondante de ts doit être la chaîne vide. Ainsi
après
char *ts[10];
char s[] = "/bin:/usr/bin::/usr/local/bin:/home/jpr/bin";
int n = tokenize_string(s, ’:’, ts[], 10, b);

si b est vrai, n vaut 5, et ts[0] est "/bin", ts[1] est "/usr/bin",


ts[2] est la chaîne vide, ts[3] est "/usr/local/bin", etc. En
revanche is b est faux, il n’y a pas de champ vide (ts[2] est
"/usr/local/bin", etc.
Remarque 1
Commencez par définir précisément les prototypes (type des arguments et de
la valeur de retour de ces fonctions), et placez les dans un fichier d’entête
substring.h. Puis, dans substring.c, incluez substring.h et définis-
sez chacune des fonctions. Compilez et testez vos fonctions au fur et à mesure :
inutile d’essayer de tout écrire d’abord et de se retrouver devant des pages de
messages d’erreur de compilation, ou alors face à un « crash » (memory fault,
segmentation violation, etc.) dont on ne sait où il a eu lieu. Soyez à peu près
sûrs que vous allez effectivement « crasher » dans vos premières versions de
certaines fonctions.
Remarque 2
Pour certaines de ces fonctions, vous serez sans doute obligés de faire des co-
pies de chaînes de caractères et, comme leur longueur n’est connue qu’à l’exé-
cution, d’utiliser malloc. Quel inconvénient(s) voyez vous à cela ? Pourquoi
la bibliothèque standard ne définit-elle pas plus de fonctions du type de celles
que vous venez de réaliser ?
Remarque 3 : compatibilité ISO
Il y a beaucoup de fonctions disponibles dans <string.h> et elle ne sont mal-
heureusement pas toutes standardisées. N’utilisez que les fonctions conformes
à la norme iso. Elle sont indiquées comme conforme à iso 9899 dans les pages
du manuel (man).

Exercice 5.4 (Tri d’un tableau de pointeurs) Modifier le tri par insertion du
programme 2.6 pour trier des chaînes de caractères.

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



126 5. Pointeurs

Figure 5.9 Structure d’un arbre binaire

Exercice 5.5 (Commande fgrep) Utilisez les fonctions de l’exercice 5.3 pour
écrire une version simplifiée de la commande fgrep. Cette commande reçoit
une chaîne de caractère en argument, lit l’entrée standard et en affiche toutes
les lignes qui contiennent la chaîne.
Remarque
En quoi la commande fgrep fournie par le système est-elle supérieure à la
votre ?

Exercice 5.6 (Arbre binaire) On définit un nœud d’arbre binaire par


struct Binary_Tree_Cell
{
int val;
struct Binary_Tree__Cell *left;
struct Binary_Tree__Cell *right;
};

L’arbre complet est bien entendu défini par un pointeur sur sa racine et a la
forme décrite dans la figure 5.9.
Ecrire la procédure d’insertion suivante pour un entier n donné : si la valeur
portée par la racine est inférieure (resp. supérieure) à n, insérer la valeur n dans
le sous-arbre de gauche (resp. de droite) ; si les deux valeurs sont égales, ne rien
insérer ; cette procédure est appliquée récursivement sur les sous-arbres.
Ensuite écrire une fonction de recherche qui explore l’arbre pour trouver une
valeur donnée et retourne un pointeur sur la cellule correspondante (ou bien
le pointeur nul si la valeur n’est pas dans l’arbre). Enfin, écrivez une fonction
d’impression de l’arbre « en profondeur d’abord » : à tire d’exemple, l’arbre de
la figure 5.10 devra être imprimé ainsi :
10
-6
--3
---EMPTY

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
5.4. Exercices du chapitre 5 127

Figure 5.10 Un exemple d’arbre binaire

---EMPTY
--9
---7
----EMPTY
----EMPTY
---EMPTY
-12
--EMPTY
--14
---EMPTY
---17
----EMPTY
----EMPTY

Le nombre de tirets exprime la profondeur.

Exercice 5.7 (Recherche dichotomique) Écrire un programme de recherche di-


chotomique d’un entier donné dans une liste triée d’entiers. La fonction retourne
l’indice de la valeur dans la liste, ou -1 si la recherche échoue.
Recherche dichotomique
Soit a la valeur à rechercher dans la liste triée u0 ≤ u1 ≤ ... ≤ un ; si a < un/2
(resp. a > un/2 ) alors a doit être recherché dans la liste u0 , u1 , ..., un/2−1 (resp.
un/2+1 , ..., un ) ; si a = un/2 alors le résultat est bien évidemment n/2.
Remarque 1
On traitera correctement les problèmes liés à la parité de n.
Remarque 2
On pourra écrire une version récursive et/ou une version non-récursive de ce
programme.

Exercice 5.8 (Pathologie des pointeurs) Les pointeurs fournissent des possibi-
lités très puissantes mais sont aussi une des principales cause d’erreur dans les
programmes C. Parmi les erreurs fréquentes :
– Utiliser l’objet pointé par un pointeur sans que ce pointeur ait été initialisé
(ou après qu’il ait été initialisé à NULL).
– Se tromper lors de l’arithmétique des pointeurs en débordant les limites et
en pointant ainsi sur des objets de type différent de ce que l’on croit.

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



128 5. Pointeurs

– Ne pas libérer la mémoire allouée par malloc lorsque l’on n’en a plus besoin
(« fuite de mémoire »).
En particulier, lorsque l’on utilise une fonction (de bibliothèque standard ou
pas) dont certains des paramètres sont des pointeurs, on doit toujours se po-
ser les questions suivantes (et obtenir la réponse) : le paramètre peut-il être un
simple pointeur (possiblement non initialisé) ? ou doit-il être un pointeur sur
un objet effectivement alloué ? et dans ce cas, est-ce un pointeur sur un objet
individuel ou sur un tableau ? Si la fonction retourne un (ou plusieurs) poin-
teurs, il faut également toujours se demander si les objets correspondants ont
été alloués et comment (i.e., par malloc ou autre ?).
Testez la robustesse des fonctions que vous avez réalisées lors de l’exercice 5.3
par rapport à ces problèmes.

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
Chapitre 6

Le préprocesseur ANSI C

L e préprocesseur constitue la première phase de la compilation d’un pro-


gramme C. Il effectue sur le source, fichier par fichier, un certain nombre de
transformation textuelles. Présent depuis l’origine en C, il a subit un nettoyage
complet lors de la normalisation d’ansi C. Le nouveau préprocesseur est donc
largement incompatible avec l’ancien (dit « traditionnel »). Ce dernier n’existe
pratiquement plus et nous ne parlerons ici que du préprocesseur ansi C et de
ses extensions en C99.
Ce chapitre ne prétend pas être une étude exhaustive de toutes les possi-
bilités du préprocesseur ansi C : ce serait trop long et à mon avis sans grand
intérêt. Les possibilités sont trop nombreuses, souvent obscures, parfois inadé-
quates. On doit réserver l’utilisation du préprocesseur à des opérations où il est
incontournable, à savoir :
– définition de constante,
– inclusion de fichiers,
– compilation conditionnelle,
– et, en C90 uniquement, en prenant des précautions, définition de fonc-
tions « en ligne ».

6.1 Prétraitement des programmes C


La norme ansi C décrit précisément les transformations qui doivent être
effectuées sur les programmes source avant qu’ils soient compilés. Ce sont dans
l’ordre :
1. les trigraphs sont substitués, que nous ignorons délibérément ;
2. les lignes-suites sont traitées, ce qui revient à supprimer les séquences où
un \ est suivi d’une fin de ligne ;
3. le source est découpé en éléments lexicaux (tokens), c’est-à-dire en mots
séparés par des espaces ; les commentaires sont remplacés par un blanc ;
4. les directives du préprocesseur sont exécutées ;
5. les séquences d’échappement (3.1.6) dans les littéraux (caractères et les
chaînes) sont remplacés par leurs équivalents ;
6. les chaines de caractères littérales adjacentes sont concaténées ;
7. la compilation puis l’édition de liens peuvent alors avoir lieu.

V 2.1 – 7 février 2010 129 c Jean-Paul R IGAULT



130 6. Le préprocesseur ANSI C

La norme ne précise pas comment sont réalisés ces différents traitements.


Dans la plupart des implémentations, les quatre premières phases sont de la
responsabilité du préprocesseur C, un programme qui effectue des transforma-
tions textuelles sur le source.
Outre les transformations citées ci-dessus, le préprocesseur permet à l’utili-
sateur de spécifier certaines transformations particulières :
– l’inclusion de fichiers (source),
– le remplacement de mots (i.e. de tokens — voir 3.1.2),
– la compilation conditionnelle.
L’étude de ces transformations est l’objet de ce chapitre.
Les directives au préprocesseur tiennent sur une ligne (logique 1 ) dont le
premier caractère qui n’est ni un blanc, ni une tabulation horizontale, doit être
un dièse (#). Noter qu’aucun point-virgule ne termine les directives au prépro-
cesseur.

6.2 Inclusion de fichier


6.2.1 Effet de l’inclusion de fichier
La directive d’inclusion de fichier a l’une des formes suivantes :
#include "nom-de-fichier"
#include <nom-de-fichier>
Le nom-de-fichier ne doit pas contenir l’un des caractères (ou l’une des
séquences) suivants, sinon le résultat risque d’être indéfini : > (au moins pour
la forme en <...>), fin de ligne, ’, ", \, /*.
L’effet de cette directive est simple : tout se passe comme si la ligne corres-
pondante était remplacée par le contenu du fichier. Les fichiers inclus peuvent
eux-mêmes comporter des directives #include : l’inclusion sera alors récur-
sive (voir cependant 6.4.2).

6.2.2 Recherche du fichier à inclure


Les deux formes de la directive d’inclusion diffèrent par la méthode de
recherche du fichier à inclure. Cette méthode dépend de l’implémentation. Nous
donnons ici les règles utilisées sous Unix.
Dans les deux formes, si le nom-de-fichier commence par un /, c’est un
chemin absolu qui désigne sans ambiguïté le fichier à inclure.
Sinon la forme
#include <nom-de-fichier>
cherche le fichier dans un liste de répertoires dépendant de l’installation du
système, mais qui peut être modifiée par l’option -I de la commande de com-
pilation C. Le nom-de-fichier est alors un nom relatif à ces répertoires.
Sur la plupart des systèmes Unix, la liste par défaut est réduite au répertoire
/usr/include. Par exemple :
#include <stdio.h> /* defaut: /usr/include/stdio.h */
#include <sys/signal.h> /* defaut: /usr/include/sys/signal.h */

1. qui peut être constituée de plusieurs lignes physiques, si l’on précède chaque fin de ligne
du caractère \

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
6.3. Définition de macros 131

La forme
#include "nom-de-fichier"
quant à elle, commence par chercher dans le répertoire où se trouve le fichier source
qui comporte cette directive d’inclusion 2 . Si le fichier n’est pas trouvé, le fonc-
tionnement est identique à la forme avec des « piquants » (<...>).

6.2.3 Utilisation de l’inclusion de fichier


On peut utiliser l’inclusion de fichier pour inclure n’importe quel suite
d’instructions, de déclarations ou de commentaires. Cependant, la seule uti-
lisation réellement intéressante est l’importation de fichiers d’entête (header-
file). Ces fichiers contiennent des définitions de types, de macros (voir section
suivante), d’objets externes (voir 8.3.2) ou encore des prototypes de fonctions
partagées par plusieurs fichiers source. L’utilisation des fichiers d’entête per-
met alors de n’écrire qu’une seule fois ces définitions ou déclarations partagées
(voir 8.4.1).

6.3 Définition de macros


La directive #define est hélas la directive la plus importante du prépro-
cesseur C. Elle permet de réaliser le remplacement de tokens.

6.3.1 Macros sans arguments : définition de constantes


La forme la plus simple de définition de macro
#define ident chaîne-définition
permet de remplacer l’identificateur ident par toute la chaîne-définition.
Noter que cette chaîne commence au premier blanc rencontré après ident.
L’identificateur ident n’est pas remplacé s’il est à l’intérieur d’une chaîne de
caractères littérale.
Par exemple, le fichier source suivant
#define NBUF 512
#define NBUF2 NBUF*2
#define MESSAGE "NBUF = "
int t[NBUF];
int u[NBUF2];

int main()
{
printf(MESSAGE "%d\n", NBUF);
}
donne après passage à travers le préprocesseur
int t[512 ];
int u[512 *2 ];

int main()

2. et pas le répertoire courant comme on le croit trop souvent !

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



132 6. Le préprocesseur ANSI C

{
printf("NBUF = " "%d\n", 512 );
}
A noter que la ligne contenant printf deviendra
printf("NBUF = %d\n");
après concaténation des chaînes littérales adjacentes.
On remarque aussi que la macro NBUF est correctement expansée dans la
définition de NBUF2.

L’utilisation la plus fréquente de cette forme de définition de macros est


la définition de constantes, en particulier celles servant à dimensionner des
tableaux. Nous avons en effet déjà remarqué (4.1.1) qu’en C90 il n’était pas
possible de le faire avec une const int et que ce n’était que partiellement
possible avec C99 (4.1.4).
Attention : une macro n’est pas vraiment une définition de constante
Une macro telle que #define N 10 ne définit pas vraiment une
constante, mais simplement une substitution textuelle. Certaines substi-
tutions conduisent donc à des expressions incorrectes : par exemple,
l’adresse de N, &N, n’a pas de sens, car c’est équivalent à &10 et la valeur
10 n’a pas d’adresse. En revanche une const int N = 10 est un véri-
table objet de C qui a un type (const int évidemment) et, si nécessaire,
une adresse (&N).

6.3.2 Macros à arguments : fonctions en ligne


La deuxième forme de #define permet de donner des arguments à la
macro. Par exemple après
#define MAX(a, b) ((a) > (b) ? (a) : (b))
toutes les occurrences de l’identificateur MAX 3 suivi d’une paire de parenthèses
entourant deux suites de mots séparées par une virgule (ouf !) seront rempla-
cées. Ainsi
x = MAX(y + 4, z - 1);
deviendra
x = ((y + 4) > (z - 1) ? (y + 4) : (z - 1));
On voit donc que ce type de définition permet d’obtenir des « sortes » de fonc-
tions qui sont expansées en ligne, plutôt que d’être invoquées avec une sé-
quence d’appel normale. En général, on utilise cette possibilité pour des raisons
d’efficacité.
C99 Notez que C99 a introduit la possibilité d’avoir des macros avec un nombre
variable d’argument (variadic macro — voir 11.1.2).
Attention : une macro n’est pas une fonction !
Les différences entre macro et fonction sont nombreuses et doivent inciter
à la prudence dans l’utilisation des macros à arguments. Illustrons ces
différences à l’aide de la macro MAX ci-dessus et d’une vraie fonction Max :
3. Noter qu’il n’y pas d’espace entre MAX et la parenthèse ouvrante (pourquoi ?).

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
6.3. Définition de macros 133

int Max(int a, int b)


{
return a > b ? a : b;
}

Tout d’abord la fonction est typée, et le compilateur vérifie le bon usage


des types. Il n’en est pas de même pour une macro : MAX est valable aussi
bien pour des entiers que des réels par exemple. C’est une puissance sup-
plémentaire, mais c’est aussi, parfois, une perte de sécurité.
Ensuite, un appel de fonction est syntaxiquement équivalent à une valeur
de son type de retour :
x = 2 + Max(y, z);

se lit bien
x = 2 + (Max(y, z));

Avec la macro, l’analyse de l’expression n’est faite qu’après le rempla-


cement textuel, ce qui peut provoquer quelques suprises. Supposons par
exemple qu’au lieu de définir la macro MAX avec ce qui parait être un
sur-parenthésage, nous ayons écrit
#define BAD_MAX(a, b) a > b ? a : b

alors
x = 2 + BAD_MAX(y, z);

est expansé en
x = 2 + y > z ? y : z;

qui, compte tenu de la précédence des opérateurs (3.4.1), se lit


x = (2 + y) > z ? y : z;

ce qui, pour le moins, n’est pas très naturel. Le sur-parenthésage de la


définition de MAX évite ce genre de surprise.
Enfin la troisième différence est qu’une fonction n’évalue ses arguments
qu’une seule fois ; comparer
int y = 0;
int z = 0;
x = Max(y++, ++z);

où les incrémentations n’ont lieu qu’une fois (y et z valent 1 après l’appel)


avec
int y = 0;
int z = 0;
x = MAX(y++, ++z);

qui va être expansé en


x = ((y++) > (++z) ? (y++) : (++z));

avec comme conséquence qu’une des incrémentations (savoir laquelle dé-


pend du résultat de la comparaison) sera effectuée deux fois !
Pour toutes les raisons invoquée ci-dessus, C99 a emprunté à C++la notion C99
de fonction inline (mot-clé inline). Voir 7.2.4 pour les détails.

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



134 6. Le préprocesseur ANSI C

6.3.3 Définition récursive de macros


ansi C ne permet pas — au moins directement — les définitions récursives.
Ainsi
#define A A*2
i = A*A;
donne l’expansion
i = A*2 *A*2;

6.3.4 Concaténation et « stringification »


Concaténation : ##

ansi C introduit dans le préprocesseur l’opérateur de concaténation de sym-


boles désigné par ##. Ainsi dans
#define cat(a, b) a ## b
...
int cat(toto, _old) = toto;
la dernière ligne sera transformée en
int toto_old = toto;
et toto_old sera considéré comme un identificateur par le compilateur.

« Stringification » : #

Dans une macro avec arguments, un argument formel n’est pas remplacé
si dans la chaine définissante il figure à l’intérieur d’une chaîne littérale. Par
exemple, dans
#define message(a) "le message est: a"
...
printf(message(bonjour));
la dernière ligne est expansée en
printf("le message est: a");
ce qui n’est pas forcément ce qu’on veut.
L’opérateur unaire # appliqué à un argument de macro en effectue la strin-
gification 4 c’est-à-dire transforme cet argument en chaîne littérale. Ainsi, avec
#define message(a) "le message est: " #a
...
printf(message(bonjour));
la dernière ligne sera expansée en
printf("le message est: " "bonjour");
qui deviendra
printf("le message est: bonjour");
après concaténation des chaînes littérales adjacentes.
4. Je manque d’imagination pour trouver une traduction française !

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
6.4. Compilation conditionnelle 135

6.3.5 Annulation de définition : #undef


La portée d’une directive #define va jusqu’à la fin du fichier source cou-
rant. On peut annuler une définition à l’aide de la directive #undef :
#define NBUF 100
...
int t[NBUF];
...
#undef NBUF
const int NBUF = 10;
...

6.3.6 Macros réservées


En principe, les noms de macros encadrés de deux « soulignés » (__) sont
réservés par ansi C. En particulier les identificateurs suivants sont prédéfinis :
__LINE__ numéro de ligne source courante (un int)
__FILE__ nom du fichier source courant
__DATE__ la date du jour
__TIME__ l’heure
__STDC__ vaut 1 si le préprocesseur est à la norme ansi C
En C99 est aussi définie la macro __STDC_VERSION__ qui a la valeur (en-
tière longue) 199901L.
Voici un exemple d’utilisation inspiré de la macro assert définie dans le
fichier <assert.h>.
#define ASSERT(cond) \
if (!(cond))\
{\
printf("%s %s: ", __DATE__, __TIME__);\
printf("%s, line %d: ", __FILE__, __LINE__);\
printf("assertion failed: " #cond "\n");\
exit(1);\
}
Si ASSERT est invoqué avec une condition fausse (noter une fois de plus le
sur-parenthésage indispensable), on obtient un message comme
Jan 14 2008 15:25:02: cpp-test1.c, line 22: assertion failed:
a < b

6.4 Compilation conditionnelle


6.4.1 Les directives de compilation conditionnelle
Les directives de compilation conditionnelles permettent de choisir des
lignes sources en fonction de certaines expressions. La forme canonique est
la suivante :
#if expression-statique
lignes-si-vrai
#else

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



136 6. Le préprocesseur ANSI C

lignes-si-faux
#endif
On évalue l’expression-statique qui est constituée à l’aide des opéra-
teurs habituel de C et sa valeur de vérité détermine les lignes source que l’on
considère. Cette valeur de vérité doit bien entendu être évaluable à la compila-
tion.
#define NBUF 512
#define NBUF2 NBUF*2
...
#if NBUF > 512 && NBUF2 > 1024
int t[NBUF];
int u[NBUF2];
#else
int t[512];
int u[1024];
#endif
Bien entendu, la clause #else est optionnelle. En outre la clause #elif
permet d’enchaîner les conditions :
#if NBUF < 512
...
#elif NBUF < 1024
...
#elif NBUF < 2048
...
#else
...
#endif
La encore, la clause #else est optionnelle.

Le préprocesseur fournit un opérateur permettant de déterminer si un sym-


bole a été défini (c’est-à-dire a fait l’objet d’un #define non encore annulé par
un #undef). C’est l’opérateur defined :
#if defined(LINUX) || defined(MSWINDOWS) || defined(MACOS)
... /* code commun a ces systemes */
#endif
La forme suivante
#if defined(__STDC__)
double cos(double);
#else
double cos();
#endif
est si fréquente qu’elle peut aussi s’écrire
#ifdef __STDC__
double cos(double);
#else
double cos();
#endif
De la même manière,

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
6.5. Autres directives du préprocesseur 137

#if !defined(getchar)
int getchar(void);
#endif
s’écrit aussi
#ifndef getchar
int getchar(void);
#endif

6.4.2 Inclusion unique de fichiers


L’une des utilisations systématiques de la compilation conditionnelles est de
gérer correctement l’inclusion unique de fichiers d’entête (header-file). Suppo-
sons que le fichier main.c, inclut A.h et B.h
/**** fichier main.c ****/
#include "A.h"
#include "B.h"
...
Supposons aussi que A.h et B.h aient chacun besoin des définitions conte-
nues dans un troisième fichier d’entête, Common.h ; ils vont tout naturellement
inclure chacun ce dernier fichier. La conséquence est que ce fichier va être in-
clus deux fois dans main.c. Ceci peut être grave car certaines de définitions
de Common.h peuvent provoquer des erreurs si elles sont rencontrées à plu-
sieurs exemplaires dans le même fichier source — c’est le cas par exemple des
définitions de type.
Comment éviter ce phénomène de double inclusion ? En protégeant le fi-
chier Common.h de la manière suivante :
/**** fichier Common.h ****/
#ifndef _Common_h_
#define _Common_h_
...
/* contenu du fichier Common.h */
...
#endif /* _Common_h_ */
Par le jeu de la compilation conditionnelle, le contenu utile du fichier ne sera
effectivement inclus que lors de la première directive #include. Bien entendu,
l’identificateur _Common_h_ doit être choisi de manière unique. On utilise très
souvent, comme ici, un identificateur qui rappelle le nom du fichier.
Cette technique doit être utilisée systématiquement sur tous les fichiers d’en-
tête, à moins qu’une raison péremptoire soit donnée de ne pas le faire.

6.5 Autres directives du préprocesseur


Signalons d’abord qu’une ligne réduite à un dièse et des blancs et/ou tabu-
lations hoirizontales est ignorée.
Par ailleurs, trois autres directives complètent la description du préproces-
seur C.

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



138 6. Le préprocesseur ANSI C

6.5.1 Numéro de ligne : #line


Cette directive qui prend l’une des formes
#line constante "nom-de-fichier"
#line constante
fait croire au compilateur que la prochaine ligne source a comme numéro la
constante (qui est soumise à macro-expansion si nécessaire).
Cette directive n’est en général pas utilisée directement par le program-
meur, mais plutôt par les compilateurs et préprocesseurs de toute nature afin
d’assurer la « remontée dans le source » des messages d’erreur.

6.5.2 Messages d’erreur : #error


Cette directive qui a la forme
#error ce que vous voulez
écrit le ce que vous voulez sur l’erreur standard et interrompt la compila-
tion :
#ifndef __STDC__
#error Abandon: compilateur non ANSI
#endif
provoque éventuellement l’arrêt de la chaîne de compilation avec le message
cpp-test1.c:15: #error Abandon: compilateur non ANSI

6.5.3 Commentaire exécutable : #pragma


Enfin, la dernière directive a la forme
#pragma n’importe-quoi
et, en C90, son effet est entièrement laissé à l’implémentation ! En fait l’idée est
de permettre de donner des commentaires qui on un sens pour un compilateur
particulier et qui peuvent lui suggérer un comportement spécial. En tout état de
cause, ce comportement spécial ne devrait pas altérer la sémantique profonde
du programme !
À titre d’exemple, un environnement peut proposer la vérification option-
nelle des débordements d’indices. Afin que le compilateur génère le code cor-
respondant, on lui précise par un #pragma. On peut imaginer, par exemple, ce
type de syntaxe :
#pragma CHECK_INDEX_RANGE ON
int t[10];
...
t[i] = t[j];
...
#pragma CHECK_INDEX_RANGE OFF
La norme exige qu’un #pragma non reconnu par un compilateur soit sim-
plement ignoré, afin de permettre la portabilité des sources.
C99 C99 introduit des pragmas pré-définis qui ont trait à la manipulation des
nombres réels et complexes (voir 9.2.4).

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
6.6. Exercices du chapitre 6 139

6.6 Exercices du chapitre 6

À la rigueur un exo de comparaison entre fonctions inline et macros avec


des problèmes pathologiques de macros (évaluation multiple, non atomicité
de l’éval des paramètres...).

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



140 6. Le préprocesseur ANSI C

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
Chapitre 7

Fonctions

O n décrit ici en détail la notion de fonction que nous avons déjà largement
utilisée dans les chapitres précédents. La fonction est l’un des mécanismes
principaux de modularité en C ; c’est aussi l’une des bases de la programmation
structurée.
En ansi C, chaque fonction a un type, déterminé à la fois par le nombre et
le type de ses arguments et par celui de sa valeur de retour. Comme pour tout
objet, ce type doit être connu avant toute utilisation. Trois contextes différents
sont associés à la notion de fonction :
1. la déclaration d’une fonction, qui en définit le type et qui est mise en œuvre
en ansi C, grâce à la notion de prototype ;
2. la définition d’une fonction, qui reprend son type, mais surtout en définit
le corps, c’est-à-dire la liste des instructions exécutées à chaque appel ;
3. l’appel — ou l’invocation — de la fonction.

7.1 Arguments et valeur de retour


7.1.1 Type de retour d’une fonction
Un appel de fonction est syntaxiquement équivalent à une valeur de son
type de retour. Ainsi, dans
double cos(double);

double x = 1.0 + cos(2*PI*t);


l’invocation cos(2*PI*t) est équivalente à une valeur de type double.
En ansi C, les types de retour possibles d’une fonction sont les suivants :
– Le type void : la fonction ne retourne pas de valeur — dans d’autres
langages on dirait que c’est une procédure ;
– Tout type scalaire, entier ou réel, y compris les énumérations et les poin-
teurs ;
– Les structures et les unions.
A l’exception de void, ce sont en fait les types « affectables ». Les tableaux
n’y figurent donc pas : une fonction ne peut donc retourner un tableau tout
entier ; en revanche, elle peut retourner un pointeur sur le (premier élément
du) tableau.

V 2.1 – 7 février 2010 141 c Jean-Paul R IGAULT



142 7. Fonctions

7.1.2 Type des arguments d’une fonction


Un fonction peut ne pas avoir d’argument du tout (voir 7.2.1). Quand elle
en a, chacun des arguments peut être d’un des types suivants :
– Tout type scalaire, entier ou réel, y compris les énumérations et les poin-
teurs ;
– Les structures et les unions.
Ici encore, ce sont les types « affectables ».
Un appel de fonction retournant une valeur, il ne peut jamais être une lvalue,
c’est-à-dire une référence.

7.1.3 Mode de passage des arguments


Passage par valeur

En C traditionnel comme en ansi C, il n’y a qu’un seul mode de passage


d’argument : le passage par valeur. En fait on devrait plutôt parler de passage
par copie de la valeur. Le mécanisme exact est le suivant. Soit f une fonction à
un argument de type T dont la définition ressemble à :
void f(T t)
{
...
}
Soit un appel de cette fonction :
f(tt);
L’expression tt (l’argument effectif) est d’abord évaluée ; si nécessaire elle est
convertie dans le type T 1 ) ; la valeur obtenue sert à initialiser une variable locale
au corps de la fonction f correspondant à l’argument formel t. Donc si l’argu-
ment formel t est modifé dans le corps de la fonction, cette modification reste
locale, et n’a aucune influence sur la valeur de l’argument effectif.

Passage des tableaux et des chaînes de caractères

Il n’y a aucune exception à cette règle de passage par valeur. Cependant,


à cause de l’assimilation des tableaux aux pointeurs (5.3.2), les tableaux pa-
raissent être passés par adresse comme dans
void raz(int n, int t[]);

int tab[100];
raz(100, tab);
Il n’y a pas de contradiction : un nom de tableau est une constante de type
pointeur, et ce pointeur est passé par valeur. En fait, le prototype de raz peut
tout aussi bien s’écrire :
void raz(int n, int *t);
et l’appel
raz(100, &tab[0]);

1. Si cette conversion est impossible, il y a erreur de compilation.

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
7.2. Déclaration, définition et appel de fonction 143

Cette même remarque s’applique également aux chaînes de caractères lit-


térales qui paraissent passées par adresse : en fait ce sont aussi des pointeurs
(sur leur premier caractère).
void string_copy(char *dest, const char *orig);

char str[100];
string_copy(str, "bonjour");
Rappelons que la fonction doit avoir un moyen de déterminer le nombre
d’éléments du tableau. C’est pourquoi, dans le cas de la fonction raz, ce
nombre (100) est passé en argument. Ce n’est pas nécessaire pour les chaînes
de caractères puisque la convention de les terminer par le caractère nul permet
d’en connaitre la longueur.

Passage par valeur des structures et unions

Le passage par valeur s’applique aussi quand une structure ou une union
est en argument :
void print_person(struct Person who);

struct Person somebody;


print_person(somebody);
Ce passage par valeur implique donc une recopie champ par champ de l’ar-
gument effectif somebody dans l’argument formel who. Si la structure est de
grande taille, cela peut être inefficace. Nous examinerons en 7.3.2 un moyen
d’avoir le même effet mais plus efficacement.

7.2 Déclaration, définition et appel de fonction


7.2.1 Déclaration du type d’une fonction : prototype
Le prototype d’une fonction donne le nom de la fonction, le nombre et le
type de chacun des arguments, et le type de la valeur de retour. Il permet
d’utiliser la fonction alors qu’elle n’a pas encore été définie. Sous sa forme la
plus simple — on pourra trouver en 7.3.3 des prototypes plus complexes —, un
prototype a la syntaxe suivante :
type-retour nom-fonction(liste-args);
Si la liste d’argument est vide, liste-args doit être réduite au mot-clé void
comme dans
int getchar(void);
La liste est une suite de noms de types séparés par des virgules comme dans
double cos(double);
int strcmp(const char *, const char *);
Les noms des arguments formels peuvent être présents comme dans
char *strcpy(char *dest, const char *orig);

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



144 7. Fonctions

Ils ne jouent alors pas d’autre rôle que celui de faciliter la lisibilité en auto-
documentant le source ; pour le reste, ils sont ignorés.
On peut avoir dans un fichier source plusieurs prototypes pour la même
fonction, du moment qu’ils sont identiques (aux noms éventuels des arguments
formels près).

Liste variable d’arguments

On a parfois besoin de fonction pour lesquelles le nombre et le type des


arguments est inconnu, totalement ou partiellement. Un cas typique est celui
de printf : nous savons que le premier argument, le format, est une chaîne
de caractères, d’ailleurs constante ; en revanche les autres arguments sont en
nombre et types variant à chaque appel. En ansi C, son prototype est
int printf(const char *, ...);
L’ellipse (...) indique au compilateur de ne faire de vérification ni de type
ni de nombre sur les arguments éventuels correspondants. En revanche, il y
aura bien ici vérification du type du premier argument. On ne peut élider ainsi
qu’un segment terminal de la liste d’argument. Ceci
int f(int, int, ..., double);
int g(int, ..., double, ..., int);

est interdit.
Pour écrire le corps d’une fonction dont la liste d’arguments est élidée, il
faut pouvoir trouver quelque part une information sur le type des arguments.
Dans le cas de printf, ce sont les spécifications de format, contenues dans
le premier argument, qui jouent ce rôle. Une fois ces types connus, les macros
standards d’environnement définies dans le fichier <stdarg.h> (voir 9.6.1)
permettent de décomposer ces listes variables d’argument.
Compatibilité avec C traditionnel et C++
Pour des raisons de compatibilité arrière, en ansi C, un prototype comme
int f();

est équivalent à
int f(...);

et non pas à int f(void). Ceci est une cause d’incompatibilité avec C++
où int f() est le prototype d’une fonction sans argument.

Type de retour implicite d’une fonction en C99


La règle encore valable en C90 selon laquelle une fonction dont le type
de retour n’était pas connu était supposée retourner un entier (int) n’est
C99 plus vraie en C99. Dans cette dernière version, l’absence de déclaration du
type de retour est une erreur.

7.2.2 Définition d’une fonction


La définition d’une fonction comporte deux parties :

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
7.2. Déclaration, définition et appel de fonction 145

– L’entête de la fonction qui est une simple reprise du prototype : cette


fois-ci cependant, il est utile, voire nécessaire, de donner un nom aux
arguments formels si on veut pouvoir les référencer dans le corps ;
– Le corps de la fonction qui est un simple bloc.
La définition d’une fonction ne peut pas être située dans un bloc. Elle est
nécessairement au niveau 0 de la hiérarchie de bloc. Il n’y a donc pas d’imbri-
cation de fonctions, pas de fonction locale en C.
Voici quelques exemples de définition de fonctions ::
void raz(int n, int t[]) /* entete */
{ /* corps */
int i;

for (i = 0, i < n; i++)


t[i] = 0;
}

void string_copy(char *dest, const char *orig)


{
while ((*dest++ = *orig++) != ’\0’)
{}
}

7.2.3 Appel d’une fonction


L’appel de fonction est tout à fait naturel. On doit fournir des arguments
effectifs en nombre et type compatibles avec le prototype de la fonction. Si
les types ne sont pas rigoureusement identiques, le compilateur effectuera les
conversions correspondantes si celles-ci sont possibles ; par exemple,
void f(short);
void g(void *);

int i;
struct Person somebody;
f(i); /* f((int)i): troncature */
g(&somebody); /* g((void *)&somebody) */

Attention : il y a virgule et virgule


La virgule qui sépare les arguments formels lors de l’appel n’est pas l’opé-
rateur d’évaluation séquentielle (3.3.7). En fait l’ordre d’évaluation des ar-
guments n’est pas défini en ansi C. On devra donc se méfier des effets de
bord dans les arguments effectifs. Ainsi, ceci
int f(int);
int Max(int, int);
int i, j;

j = Max(i++, f(i));

est non portable : on ne sait pas si f est invoqué après que i ait été incré-
menté, ou avant.

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



146 7. Fonctions

7.2.4 Fonctions « en ligne » de C99


Nous avons vu (6.3.2) que les macros du préprocesseur (#define) permet-
taient de définir des fonctions dont l’appel était remplacé par l’expansion en
ligne. Ceci permet de faire l’économie de la séquence d’appel/retour de fonc-
tion, particulièrement bienvenue dans le cas de fonctions dont le corps est bref.
Malheureusement, nous avons vu aussi que les macros faisaient de bien piètres
C99 fonctions. Pour pallier cet inconvénient, C99 a emprunté à C++ la notion de
fonction inline.
Déclarer une fonction inline comme
static inline int min(int a, int b)
{
return a < b ? a : b;
}
c’est une sugggestion au compilateur de faire l’exapansion en ligne. Notez le
mot « suggestion » : le compilateur est libre de ne pas obéir, car il sait, en gé-
néral mieux que le programmeur, évaluer le compromis entre le gain en temps
d’exécution et l’augmentation de la taille mémoire du code.
Combinaison de inline et static
Une fonction inline devrait toujours être déclarée static. En outre si
cette fonction doit être utilisée par plusieurs unités de compilation, elle
doit être dans un fichier d’entête (.h) inclus dans ces unités. Voir 8.4.2
pour plus de détails.

7.3 Fonctions et pointeurs


7.3.1 Fonction retournant un pointeur
Nous avons déjà rencontré des fonctions qui retournaient des pointeurs
comme la fonction insert du programme 5.2 :
List_Cell *insert(List_Cell *, int);
L’interprétation d’une telle déclaration utilise la règle introduite en 5.3.1. Le
prototype signifie simplement que, dans une expression,
*insert(lc, i);
est une List_Cell ; compte tenu de la précédence des opérateurs (3.4.1),
insert(lc, i) est donc un pointeur sur List_Cell et finalement insert
est une fonction qui retourne un pointeur sur List_Cell — et qui a deux
arguments, un pointeur sur List_Cell et un int.
Attention : pointeurs en l’air
Lorsqu’une fonction retourne un pointeur, l’objet pointé doit avoir une
durée de vie supérieure à celle du corps de la fonction, sinon on court à la
catastrophe. Ainsi dans
int *f(void)
{
int tmp = 1;
...
return &tmp;
}

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
7.3. Fonctions et pointeurs 147

la sanction sera sévère : l’objet local tmp n’existe plus après le retour de la
fonction ! La plupart des compilateurs donneront un « warning » dans des
cas aussi simples, mais ce n’est pas toujours possible en général.

7.3.2 Pointeurs en argument


Il y a deux raisons pour lesquelles on peut vouloir passer un argument sous
forme d’une adresse, c’est à dire d’un pointeur :
– on veut modifier l’argument effectif dans le corps de la fonction;
– on veut éviter de passer par valeur des objets très gros, ce qui conduirait
à de nombreuses copies en mémoire.

Modification d’un argument effectif dans le corps d’une fonction

On veut réaliser une fonction qui échange deux entiers. Les entiers ne
peuvent pas être passés par valeur car leur changement de valeur dans le corps
de la fonction serait sans effet sur les arguments effectifs :
void bad_swap(int i, int j)
{
int temp = i;
i = j; // les variables locales sont bien echangees...
j = temp; // mais pas les parametres effectifs !
}
On doit donc utiliser des pointeurs :
void swap(int *pi, int *pj)
{
int temp = *pi;
*pi = *pj;
*pj = temp;
}
et lors de l’appel, on doit faire attention à prendre les adresses des objets à
échanger :
int i, j;
swap(&i, &j);
Si c’est un pointeur qui doit être modifié, il faut utiliser un double pointeur
comme dans la fonction alloc_mem suivante 2 :
void alloc_mem(size_t sz, void **ptr)
{
if ((*ptr = malloc(sz)) == NULL)
{
fprinft{stderr, "Alloc_Mem: memoire saturee\n");
exit(1);
}
}

2. Stylistiquement, la fonction alloc_mem serait plus élégante si son prototype était


void *alloc_mem(size_t), mais c’est juste pour l’exemple.

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



148 7. Fonctions

Passage de grands objets par adresse

On peut passer des structures (ou des unions) par valeur


void print_person(struct Person};
mais cela risque d’entrainer une lourde copie de mémoire à mémoire. Si l’on
passe brutalement un pointeur, comme dans
void print_person(struct Person *);
on évite ces copies, mais en contre-partie l’argument effectif pourra être modifié
dans la fonction, ce qui réduit la sécurité de programmation.
La bonne solution est évidemment de passer un pointeur sur une constante :
void print_person(const struct Person *);
ce qui empêche la modification de l’objet pointé dans le corps de la fonction.

Noter que le compilateur effectue dans le cadre des appels de fonctions les
mêmes vérifications que celles évoquées en 5.1.5. Ainsi, dans
void f(struct Person *);

void print_person(const struct Person *somebody)


{
...
f(somebody);
...
}
l’appel de f provoque une erreur : en effet l’argument de f n’est pas un poin-
teur sur une constante, mais sur une variable, et donc l’accés à l’objet pointé
par somebody serait élargi dans le corps de f.
Chaque fois que l’on passe un pointeur en argument, on doit se demander
si l’objet pointé pourra ou non être modifié dans le corps de la fonction. Si la
réponse est non, ne pas hésiter à passer un pointeur sur une constante. C’est
plus qu’une clause de style : cela renforce la sécurité de la programmation et
c’est un indice de qualité.

7.3.3 Pointeurs sur fonctions


Type « pointeur sur fonction »

Supposons que l’on souhaite passer une fonction en argument à une autre
fonction. Par exemple, on a écrit un programme d’intégration numérique qui
calcule
Z b
f ( x )dx
a

pour toute fonction f (raisonnable !) à argument et résultat réels. La fonction


d’intégration, disons integr va donc avoir trois arguments, deux réels et la
fonction. En C on passe en fait un pointeur sur fonction :
double integr(double a, double b, double (*pf)(double));
Interprétons la déclaration de pf avec notre règle habituelle (5.3.1). La déclara-
tion

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
7.3. Fonctions et pointeurs 149

double (*pf)(double)
signifie qu’une expression comme
(*pf)(3.14)
est un réel double ; donc *pf est une fonction à un argument double qui
retourne un double ; et finalement, pf est bien un pointeur sur une telle
fonction.

Utilisation d’un pointeur sur fonction

Tout d’abord, un nom de fonction rencontré seul — c’est-à-dire sans liste


d’arguments associée — est de type « pointeur sur fonction » ; ce pointeur est
d’ailleurs constant puisque la fonction a une adresse qui est choisie par le com-
pilateur et l’éditeur de liens et qui n’est pas modifiable. Ainsi, notre fonction
integr peut être invoquée correctement par
double cos(double);

r = integr(0.0, PI, cos);


ce qui effectue le calcul de
Z π
cos x dx
0

Dans le corps de la fonction integr, il est possible d’invoquer la fonction


pointée sous la forme complète
(*pf)(x)
mais ansi C autorise aussi la forme raccourcie et syntaxiquement moins obs-
cure
pf(x)

Un pointeur sur fonction est un type (scalaire) comme un autre et, comme
tel, peut être soumis à n’importe quelle construction de type. On peut en dé-
finir des tableaux, il peut être le type de retour d’une fonction, il peut être un
champ de structure ou d’union. Le programme 7.1 illustre quelques unes de
ces possibilités.
Dans ce programme, on trouve quelques types intéressants 3 :
– funct_tab est un tableau de (3) pointeurs sur fonction à argument
double et résultat double ;
– funct_names est un tableau de (3) structures ; le premier champ de cette
structure est une chaîne de caractères (banal !), et le second un double
pointeur sur fonction (à argument double et résultat double) ;
– La fonction select n’a pas d’argument et retourne un pointeur sur fonc-
tion (à argument double et résultat double) choisie au hasard ;
– La fonction main choisit une fonction à exécuter grâce à la fonction
select ; elle applique cette fonction à la valeur π, puis cherche dans la
table funct_names le nom de la fonction qui a été choisie en comparant
la valeur du pointeur pcurrf au contenu du champ ppf.
3. On peut essayer de réécrire ce programme avec des typedef pour le rendre plus lisible.

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



150 7. Fonctions

Programme 7.1 – Les pointeurs sur fonction dans tous leurs états
/***** Fichier: ptr_fonct.c *****/

#include <stdlib.h>
#include <stdio.h>
5 #include <math.h>

double (*funct_tab[])(double) = {sin, cos, tan};

struct Funct_Name
10 {
const char *name;
double (**ppf)(double); /* double pointeur sur fonction */
} funct_names[] = {
{"sinus", &funct_tab[0]},
15 {"cosinus", &funct_tab[1]},
{"tangente", &funct_tab[2]},
};
const int N_FUNCT_NAMES =
sizeof(funct_names)/sizeof(struct Funct_Name);
20
double (*select(void))(double)
{
extern long int random(void);

25 return funct_tab[random() % 3];


}

const double PI = 3.141592;

30 int main()
{
double (*pcurrf)(double);
double res;
int i;
35
while (1)
{
pcurrf = select();
res = pcurrf(PI);
40 for (i = 0; i < N_FUNCT_NAMES; i++)
{
if (*funct_names[i].ppf == pcurrf)
{
printf("Fonction: %s Resultat: %g\n",
45 funct_names[i].name, res);
break;
}
}
}
50 }

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
7.4. La fonction main 151

Voici un exemple d’exécution :


% ptr_fonct
Fonction: sinus Resultat: 6.5359e-07
Fonction: sinus Resultat: 6.5359e-07
Fonction: cosinus Resultat: -1
Fonction: tangente Resultat: -6.5359e-07
Fonction: cosinus Resultat: -1
Fonction: sinus Resultat: 6.5359e-07
Fonction: sinus Resultat: 6.5359e-07
Fonction: cosinus Resultat: -1
Fonction: cosinus Resultat: -1
Fonction: cosinus Resultat: -1
Fonction: sinus Resultat: 6.5359e-07
Fonction: tangente Resultat: -6.5359e-07
ˆC
%

7.4 La fonction main


La fonction main joue un rôle très particulier puisque c’est par elle que
commence l’exécution d’un programme 4 . La norme interdit d’appeler explici-
tement la fonction main. Le type de retour de main doit être int. Une instruc-
tion comme
return c;
dans main termine le programme en transmettant le code de retour c au pro-
cessus père (par exemple le shell ; elle est équivalente à exit(c).
On peut définir main avec de 0 à 3 arguments. La totale sous Unix est
main(int argc, char *argv[], char *envp[])

argc le nombre d’arguments positionels
argv un tableau de pointeurs sur caractère de dimension argc
envp un tableau de chaînes de caractères qui se
termine au premier i tel que envp[i] == NULL
Les chaînes de caractères argv[0], argv[1], ..., argv[argc - 1] corres-
pondent aux arguments de la ligne de commande à la manière des arguments
positionnels $0, $1, ... du shell. En particulier, argc vaut toujours au moins
1, et argv[0] est le nom de la commande. Il est garanti que argv[argc] est
égal au pointeur NULL.
Le troisième argument est rarement utilisé ; c’est aussi un tableau de chaînes
de caractères dont chacune est analogue à chaîne représentant une initialisation
de variable d’environnement du shell, par exemple
"PATH=/usr/ucb:/bin:/usr/bin"
Ceci permet de passer à main un environnement particulier.
A titre d’exemple, le programme 7.2 réalise la commande echo du shell
avec quelques modifications. On réaffiche les arguments que l’on a reçu. La
commande accepte trois options :
4. au moins du point de vue du programmeur

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



152 7. Fonctions

-n pas de fin de ligne


-l les majuscules sont transformées en minuscules
-u les minuscules sont transformées en majuscules
Les fonctions de transformation en majuscules et minuscules utilisent les fonc-
tions de bibliothèque standard toupper et tolower dont le prototype est dans
le fichier d’entête <ctype.h> (voir 9.4.1).

Programme 7.2: La commande echo modifiée


/***** Fichier: newecho.c *****/

#include <stdio.h>
#include <ctype.h>
5
int no_new_line = 0;
int upper_case = 0;
int lower_case = 0;

10 void to_upper(char str[])


{
int i;
for (i = 0; str[i] != ’\0’; i++)
str[i] = toupper(str[i]);
15 }

void to_lower(char str[])


{
int i;
20 for (i = 0; str[i] != ’\0’; i++)
str[i] = tolower(str[i]);
}
int main(int argc, char *argv[])
{
25 int nopt;
int narg;

for (nopt = 1; nopt < argc && argv[nopt][0] == ’-’; nopt++)


{
30 switch (argv[nopt][1])
{
case ’n’:
no_new_line++;
break;
35 case ’u’:
upper_case++;
break;
case ’l’:
lower_case++;
40 break;
default:
fprintf(stderr, "%s: option inconnue: %s\n",
argv[0], argv[nopt]);
break;
45 }
}

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
7.5. Exercices du chapitre 7 153

for (narg = nopt; narg < argc; narg++)


{
50 if (upper_case)
to_upper(argv[narg]);
else if (lower_case)
to_lower(argv[narg]);
printf("%s ", argv[narg]);
55 }
if (!no_new_line)
putchar(’\n’);
return 0;
}

7.5 Exercices du chapitre 7

Exercice 7.1 (Intégration par la méthode des trapèzes) Écrire une fonction
d’intégration par la méthode des trapèzes invocable de la manière suivante :
s = integr(a, b, f, delta_x);

où f est une fonction à argument double et résultat double et où s, a, b et


delta_x sont des double. Cette fonction doit retourner une approximation
de
Z b
f ( x ) dx
a

en intégrant f par la méthodes des trapèzes avec un pas de delta_x.

Exercice 7.2 (Efficacité des fonctions inline) Il n’est pas sûr que l’optimisation
-O ne soit pas meilleure... quand elle est possible.

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



154 7. Fonctions

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
Chapitre 8

Structure des programmes

C e chapitre décrit la manière de structurer de grands programmes en plu-


sieurs « unités de compilation », c’est-à-dire plusieurs fichiers-sources (.c)
compilables séparément. Le rôle et le contenu des fichiers d’entête (.h) est
également mis en évidence, comme moyen de partager des déclarations entre
plusieurs unités de compilation. Enfin, on montre comment l’utilisation de ces
mécanismes permet de découper un programmes en modules regroupant des
éléments sémantiquement proches.

8.1 Espaces de nommage


ansi C classe les identificateurs en quatre espaces de nommage indépen-
dants décrits ci-après :
1. E1 : les noms d’objets (variables et constantes), de fonctions, de types
(définis par typedef) et les valeurs de tous les types énumérés (enum) ;
2. E2 : les étiquettes (pour goto );
3. E3 : les tags de structures, unions et énumérations ;
4. E4 : les noms de champs pour chaque structure ou union individuelle-
ment.
Ceci signifie par exemple qu’un identificateur, disons ident, peut être à la
fois le nom d’une variable, une étiquette, un tag et un nom de champ de struc-
ture sans qu’il y ait d’ambiguïté. En revanche, ident ne peut pas désigner à la
fois une variable et une fonction, ni un type et une constante énumérée. Pour
le dernier espace (E4), « individuellement » signifie que les noms de champs
d’une structure ou d’une union sont dans un espace différent de celui d’une
structure ou union d’un autre type. Le programme 8.1 donne un exemple assez
« maximaliste » illustrant l’indépendance de ces espaces.
Dans ces espaces de nommage, nous ne prenons pas en compte celui des
noms de macros du préprocesseur, définis par #define. Ces noms de macros
ont une portée qui englobe les espaces ci-dessus puisqu’il s’agit de remplace-
ment textuel. Cet espace du préprocesseur prend le pas sur ces quatre espaces
et la portée d’un nom de macro s’étend de son point de définition jusqu’à la
fin du fichier source courant ou jusqu’à une directive #undef pour cette macro
(voir 6.3.5).

V 2.1 – 7 février 2010 155 c Jean-Paul R IGAULT



156 8. Structure des programmes

Programme 8.1 – Espaces de nommage en C


/***** Fichier: espace_noms.c *****/

int var; /* E1 */

5 typedef struct st /* E3 */
{
int st; /* E4 */
} st; /* E1 */

10 struct var /* E3 */
{
double st; /* E4 */
double var; /* E4 */
};
15
enum e /* E3 */
{
A, /* E1 */
B /* E1 */
20 } x; /* E1 */

struct A /* E3 */
{
int A; /* E4 */
25 };

int main() /* E1 */
{
goto var; /* E2 */
30
var: /* E2 */
st: /* E2 */
A: /* E2 */
B: /* E2 */
35 goto A; /* E2 */

8.2 Structure des programmes C


8.2.1 Compilation séparée
La compilation séparée permet au texte d’un programme C d’être dispersé
dans plusieurs fichiers-sources. Ces fichiers-sources sont de deux types : les fi-
chiers directement compilés (dont l’extension est .c) et les fichiers inclus (dont
l’extension est traditionnellement .h) 1 .
Une fichier .c constitue une unité de compilation. En fait la dite unité est
constitué du contenu du fichier .c et de tous les fichiers qu’il inclus, directe-
1. En fait la séparation entre fichiers .c et .h est une pure tradition et n’a absolument
aucun caractère normatif. La norme de C ne connait en fait que la notion d’unité de compilation.
Cependant la tradition est très bien établie et il convient de la respecter.

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
8.2. Structure des programmes C 157

Figure 8.1 Compilation séparée et unités de compilations

ment ou indirectement (voir la directive #include en 6.2). La figure 8.1 sché-


matise ce concept.
Chaque unité (il y en a trois dans la figure 8.1) est compilée séparément
par une exécution du compilateur : ceci comporte l’exécution du préprocesseur
(6), l’analyse lexicale et syntaxique, la vérification des types et la génération de

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



158 8. Structure des programmes

code. Chaque unité de compilation produit en résultat un fichier-objet binaire


(extension .o).
Toutes les exécutions sont indépendantes et en conséquence chaque unité
doit comporter toute l’information nécessaire au compilateur pour pouvoir ef-
fectuer sa tâche.
Puis les fichiers-objets obtenus sont envoyés à l’éditeur de liens qui les re-
groupe en un fichier binaire exécutable unique en les combinant avec biblio-
thèques nécessaires.

8.2.2 Unité de compilation


Une unité de compilation constitue également une unité de visibilité
(voir 8.3.2) et aussi, le plus souvent un module logique (voir 8.4).
Chacune de ces unités de compilation est constituée d’une suite d’éléments
pris parmi les suivants :
– directives au préprocesseur ;
– définition de types (enum, structures, unions, typedef) ;
– déclarations ou définitions
– de variables ou de constantes,
– de fonctions.
Nous avons déjà signalé la différence qu’il y avait entre une définition et
une déclaration de fonction (chapitre 7). Il existe la même différence entre une
simple déclaration de variable ou de constante, qui se contente d’en donner le
nom et le type, et sa définition où en outre on procède à l’allocation de l’espace
mémoire et éventuellement à son initialisation.
Pour chaque objet, il peut y avoir autant de déclarations que l’on souhaite
— du moment qu’elles sont compatibles — mais il doit y avoir une et une seule
définition de l’objet dans tout le programme. C’est ce qu’ansi C appelle la règle
de définition unique (odr ou One definition Rule).

8.3 Durée de vie et règles de visibilité


Cette section est consacrée essentiellement à la visibilité et la durée de vie
des variables et constantes ; les règles correspondantes concernant les fonctions
seront vues en 8.3.4.

8.3.1 Durée de vie


Le langage C définit trois classes de durée de vie des objets :
– les objets statiques dont la durée de vie est la durée totale d’exécution
du programme ; ces objets sont créés (alloués et initialisés) au début de
l’exécution et détruits à la fin ;
– les objets automatiques dont la durée de vie est celle d’un bloc ;
– les objets à allocation dynamique explicite 2 qui sont alloués et désalloués
par le programmeur — en général à l’aide des fonctions de la famille de
malloc et free (voir 9.1.4).
2. Ces objets ne sont pas vraiment dans le langage, puisqu’ils sont alloués et désalloués par
des fonctions de bibliothèque, mais on les considère ici car leur rôle pratique est important.

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
8.3. Durée de vie et règles de visibilité 159

8.3.2 Visibilité des objets


La visibilité des objets est régie par un ensemble de règles qui permettent
de relier de manière unique un identificateur à sa déclaration. Il existe en C
deux classes de visibilité 3 :
– la visibilité de bloc, où la déclaration de l’objet doit se trouver dans le
bloc où il est utilisé ; on parle d’objet local ;
– la visibilité de fichier 4 , où l’objet est visible de sa déclaration jusqu’à la
fin de l’unité de compilation où se trouve cette déclaration ; la déclaration
de l’objet doit donc être hors bloc ; on parle alors d’objet global — bien
que cette globalité soit restreinte à l’unité de compilation courante.
Dans un bloc, les objets locaux prennent le pas sur les objets globaux de
même nom et sur les objets locaux des blocs englobants, comme illustré dans
l’extrait de programme 8.2.

Programme 8.2 – Visibilité des objets locaux


/***** Fichier: visib_local.c *****/

int g = 1; /* Objet global */

5 void f(int i) /* Definition de fonction */


{
double g; /* Masque le g global */
short s; /* Objet local */
/* ... */
10 s = 3; /* Le s local */
/* ... */
{ /* Bloc imbrique */
long s; /* Masque le s precedent */
/* ... */
15 }
/* ... */
s = 4; /* On retrouve le premier s */
}

Objet locaux

Les objets locaux se subdivisent en deux catégories :


– ceux qui sont statiques, c’est-à-dire à durée de vie permanente, et dont la
déclaration (locale) est préfixée par le mot-clé static ; on parle parfois
d’objets rémanents ;
– ceux qui sont automatiques, c’est-à-dire à durée de vie de bloc ; aussi
leur déclaration a la forme simple habituelle, éventuellement précédée
du mot-clé auto ou du mot-clé register.
L’extrait suivant illustre ces deux types d’objets locaux :
int f(int i)

3. En fait trois, car on a aussi la visibilité de fonction applicable aux étiquettes (voir 8.3.6)
4. Ce terme de visibilité de « fichier » est passablement impropre : il vaudrait mieux parler
de visibilité d’unité de compilation.

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



160 8. Structure des programmes

{
int a; /* Objet local automatique */
static int n; /* Objet local statique (remanent) */
register int r; /* Objet local automatique */
auto int b; /* Objet local automatique */
...
}
Un objet rémanent (statique local) comme n conserve sa valeur d’une entrée
du bloc à l’autre. Ainsi la fonction suivante conserve dans une variable locale
rémanente le nombre de fois où elle a été appelée :
int f(int i)
{
static int nb_calls = 0;

nb_calls++;
...
}

Modifieurs de type register et auto


Le modifieur de type register suggère au compilateur de placer la va-
riable correspondante dans un registre machine (quoi que cela signifie...).
Il ne peut s’appliquer qu’à une variable automatique. Le compilateur est
libre d’ignorer cette suggestion qui ne devrait d’ailleurs pas faire partie
du langage et qui est battue en brêche par la plupart des optimiseurs
de code modernes. Sauf cas exceptionnel, on ignorera donc register.
Quant au mot-clé auto, il ne sert absolument à rien ! Mais, comme ces
deux mots sont réservés, vous ne pouvez pas les utiliser pour autre chose
(par exemple, nom de variable, de fonction, de type...).

Objets globaux

Les objets globaux tombent eux-mêmes dans deux catégories :


– ceux dont la visibilité est uniquement limitée au fichier (en fait à l’unité
de compilation) où ils sont définis ; on parle alors d’objets privés (au mo-
dule) ;
– ceux qui sont potentiellement visibles dans tout le programme ; on parle
alors d’objets externes.
Les objets privés sont des objets globaux dont la définition est précédée du
mot-clé static comme dans
static int g = 3;

void f()
{
...
}
Des objets privés de même nom, mais dans des fichiers source différents
n’entrent pas en conflit.
Comme les objets externes sont potentiellement visibles dans tout le pro-
gramme, à cause de la compilation séparée, chaque fichier source doit en
connaitre le type ; ceci est réalisé par une déclaration d’externe comme par
exemple :

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
8.3. Durée de vie et règles de visibilité 161

extern char Days[7][10];


Il s’agit bien ici d’une déclaration et non pas d’une définition. Il peut y avoir de
nombreuses déclarations de cette forme, mais il ne peut y avoir, dans tous les
fichiers source d’un programme, qu’une seule définition et seule cette définition
pourra effectuer l’initialisation ; elle aura la forme suivante, déjà rencontrée :
char Days[7][10] = {...};

8.3.3 Initialisation des objets


L’initialisation d’un objet ne peut avoir lieu que lors de sa définition.
Les objets statiques — privés, rémanents ou externes — sont initialisés par
défaut à 0 (zéro !). S’ils sont initialisés explicitement, ils doivent l’être par une
expression statique, c’est-à-dire évaluable à la compilation. L’initialisation à
zéro des objets statiques n’a pas forcément de sens. Il est donc de bon style
d’initialiser explicitement ce qui doit l’être.
Les objets automatiques ne sont pas initialisés par défaut. S’ils sont initiali-
sés explicitement, ils peuvent l’être par une expression évaluable à l’exécution :.
Pour les objets à allocation dynamique, malloc n’effectue aucune initialisa-
tion, mais certaines fonctions de sa famille comme calloc le font (voir 9.1.4).
Lorsqu’un objet statique est initialisé explicitement, cette initialisation n’a
lieu qu’une fois en début de programme. En revanche pour un objet (local)
automatique, l’initialisation a lieu à chaque entrée dans le bloc 5 :
int f(int i)
{
static int s = 3; /* Local statique (remanent) */
int j = 3 * i; /* Local automatique */
...
}
Noter que l’objet s n’est ici initialisé qu’une seule fois, au début du programme.
L’expression d’initialisation doit donc être statique, c’est-à-dire évaluable à la
compilation. En revanche, pour un objet automatique comme j, l’initialisation
est exécutée à chaque entrée dans le bloc et peut donc utiliser une expression
évaluable à l’exécution.

Visibilité des objets à allocation dynamique

Les objets à allocation dynamique explicite sont manipulés à travers des


pointeurs. Leur règles de visibilité sont donc celles de ces pointeurs.

8.3.4 Visibilité des fonctions


Les fonctions ont bien entendu une durée de vie permanente. Elles sont
externes par défaut, mais il est possible de les rendre privées en préfixant leur
définition par le mot-clé static :
static int f(int i)
{
...
}
5. En fait un nouvel objet est créé — et initialisé — à chaque entrée dans le bloc.

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



162 8. Structure des programmes

Il peut donc exister plusieurs fonctions de même nom dans le programme, du


moment qu’elles sont toutes déclarées static et définies dans des unités de
compilation différentes.

8.3.5 Synthèse : relation entre durée de vie et visibilité


Le tableau 8.1 résume les différentes relations qui existent entre la durée de
vie et les règles de visibilité des objets.

Table 8.1 Relations entre durée de vie et visibilité


Objets statiques Objets dynamiques automa-
tiques
Visibilité de bloc (locale) Objet rémanent static Objet local automatique
auto, register
Visibilité de fichier (globale) Objet privé static IMPOSSIBLE
Visibilité dans tout le pro- Objet externe extern IMPOSSIBLE
gramme (potentiellement)
(externe)

La figure 8.2 illustre quelques unes des situations possibles.


– La variable v est externe. Elle est définie (et initialisée à 2) à la ligne 1
de fic1.c. Elle est visible globalement dans tout ce fichier. Comme elle
fait l’objet d’une déclaration d’externe à la ligne 1 de fic2.c, elle est
également visible globalement dans tout ce dernier fichier.
– La variable vv est également externe. Elle est définie à la ligne 2 de
fic1.c. Elle est donc visible globalement à partir de cette ligne et dans
tout ce fichier. N’ayant pas d’initialisation explicite, elle est initialisée à
0, comme tout objet à allocation statique. Comme elle fait l’objet d’une
déclaration d’externe locale à la fonction f3 de fic2.c (ligne 7), elle est
visible dans cette fonction. Et elle est également visible à partir de sa dé-
claration d’externe globale (ligne 18) du même fichier, jusqu’à la fin. En
revanche elle n’est pas visible à l’intérieur de la fonction f4.
– Les paramètres x, y de f1 (ligne 8 de fic1.c), x de f2 (ligne 17 de
fic1.c), x de f3 (ligne 3 de fic2.c) ainsi que les variables z (ligne 11
de fic1.c) et i (ligne 19 de fic1.c) sont des variables locales auto-
matiques. Leur portée est le bloc de la fonction dans laquelle elles sont
définies. Ces variables automatiques ne sont pas initialisées par défaut.
– La variable v de f2 (ligne 20 de fic1.c) est locale automatique. Sa portée
est le bloc de f2. À l’intérieur de cette fonction elle masque la variable
globale externe v.
– Les deux variable nommées z sont toutes les deux globales statiques.
Comme elles sont définies dans deux unités de compilation différentes
(ligne 15 de fic1.c et 17 de fic2.c), elles désignent des objets distincts,
de types d’ailleurs également différents. N’ayant pas d’initialisation expli-
cite, elles sont initialisés à 0. Elles sont visibles de leur point de définition
jusqu’à la fin du fichier où elles sont définies. Elles ne peuvent pas être
l’objet d’une déclaration d’externe.
– Les fonctions f1, f2 et g sont externes. Elles sont visibles de leur point de

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
8.3. Durée de vie et règles de visibilité 163

Figure 8.2 Visibilité et durée de vie : quelques exemples

Programme 8.4 – Visibilité et


Programme 8.3 – Visibilité et durée de vie : fic2.c
durée de vie : fic1.c extern int v;
int v = 2, vv;
static int f3(int x)
int main() {
{ double cos(double);
/* ... */ static int w;
} extern int vv;
/* ... */
void f1(int x, int y) }
{
extern double g(); static void f4(void)
int z; {
/* ... */ /* ... */
} }

static double z;
static int z[10];
double f2(int x) extern int vv;
{ extern int f1(int, int);
register int i;
int v; double g(void)
/* ... */ {
} /* ... */
}

définition jusqu’à la fin du fichier où elles sont définies. En outre f1 est


également visible à partir de la ligne 19 du fichier fic2.c. La fonction g
a aussi une déclaration d’externe (ligne 10 de fic1.c) qui la rend visible
dans le bloc de la fonction f1.
– Enfin les fonctions f3 et f4 de fic2.c (lignes 3 et 11) sont statiques.
Elles ne sont donc visibles qu’à partir de leur point de définition jusqu’à
la fin du fichier courant. Elles ne peuvent pas être l’objet d’une déclaration
d’externe.

8.3.6 Autres règles de visibilité


Les étiquettes ont une visibilité limitée à la fonction où elles sont définies.
Les noms de types (obtenus par typedef), les tags de structure, d’union, d’énu-
mération, les constantes symboliques des énumérations ont une visibilité globale
si leur définition l’est et locale si leur définition se trouve dans un bloc.
Le programme 8.5, bien qu’un peu pathologique, illustre ce dernier type de
visibilité.

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



164 8. Structure des programmes

Programme 8.5 – Visibilité des noms de type en ansi C


typedef struct A /* Global */
{
short a;
struct B /* Global */
5 {
int b;
} b;
} A;

10 struct B b;

int main()
{
typedef struct A /* Local */
15 {
int a;
} A;

enum e {X, Y, Z}; /* Local */


20 A a;
}

void f()
{
25 typedef struct A /* Local */
{
double a;
} A;
A a;
30 struct A b;

enum e {X, Y, Z}; /* Local */


}

8.4 Programmation modulaire en C


8.4.1 Notion de module
Dans le sens général, un module est un regroupement (syntaxique) de types,
de variables, de constantes et de fonctions logiquement (ou sémantiquement)
liées. On distingue classiquement deux parties dans un module :
– l’interface du module, qui contient l’ensemble des informations dont ont
besoin les fonctions qui veulent utiliser le module ;
– le corps du module qui contient l’implémentation des différentes fonctions
et données.
Il doit être possible de changer le corps — l’implémentation — du mo-
dule sans changer son interface, c’est-à-dire la manière de l’utiliser. Dans ces
conditions, les programmes utilisant le module n’auront pas besoin d’être re-
compilés ; seule l’édition de liens devra être refaite.

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
8.4. Programmation modulaire en C 165

Figure 8.3 Organisation d’un module en C

En C, il est possible avec une certaine discipline de réaliser ce type de pro-


grammation modulaire. L’interface est constituée par un fichier d’entête com-
portant les définitions de type utilisées, les déclarations d’objets externes et les
prototypes des fonctions externes de manipulation du module. Ce fichier sera
inclus par tous les fichiers-sources qui veulent utiliser le module.
La figure 8.3 schématise cet organisation. Elle décrit un module de nom
mod. Son interface est un fichier d’entête mod.h et son implémentation une
unité de compilation mod.c. Bien entendu, l’interface elle-même peut avoir be-
soin de déclarations ou définitions empruntés à d’autres modules, elle peut
donc en inclure les fichiers d’entête (.h) correspondants. De même, l’implé-
mentation mod.c peut avoir besoin directement d’autres ressources qui ne sont
pas dans mod.h ni dans les fichiers que ce dernier inclus. Enfin le « code client »
(client.c) qui utilise le module mod ne doit avoir à inclure que l’interface de
ce module (mod.h). Bien entendu, à l’édition de liens, les deux fichiers objets
client.o et mod.o doivent être fournis ainsi que ceux correspondant aux
autres modules qu’ils utilisent.
L’art du concepteur de modules est d’établir les dépendances minimales
entre ces différents composants. Le code client dépend de tout qui est dans
l’interface (directement ou inclus). Toute modification directe ou indirecte de
cette interface risque donc d’induire des modifications et en tout état de cause
une recompilation du code client. Il faut donc ne mettre dans l’interface que ce
qui est nécessaire et suffisant au code client pour utiliser le module.

8.4.2 Objets globaux trouvés dans une interface de module


Noter qu’un fichier d’entête comme mod.h qui constitue l’interface d’un
module ne doit jamais contenir de définition d’objets externes (variables,
constantes, ou fonctions).
Il peut (doit) en revanche en contenir des déclarations. En revanche un fi-

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



166 8. Structure des programmes

chier d’interface peut contenir des définition de constantes et variables globales


ainsi que des définitions de fonctions du moment que ces objets sont privés
(static) à l’unité de compilation. Tout se passera comme si chaque unité de
compilation avait sa propre copie des objets en question, mais toutes ces copies
seront de même type.
En particulier, les fonctions inline (7.2.4) doivent toujours être déclarée
aussi statiques et être définies dans un fichier d’entête .h inclut par toutes les
unités de compilation qui souhaitent en partager la définition
#ifndef _Utilitaires_h_
#define _Utilitaires_h_

/***** Fichier: utilitaires.h *****/

/*
* Quelques fonctions inline d’usage general et
* destinees a etre utilisees dans de nombreux modules.
*/

static inline int max(int a, int b)


{
return a > b ? a : b;
}

static inline int min(int a, int b)


{
return a < b ? a : b;
}

static inline int abs(int a)


{
return a < 0 : -a : a;
}

#endif
Enfin, noter que comme tout fichier .h, mais encore plus que pour les autres
un fichier d’interface doit être protégé en inclusion unique (6.4.2).

8.4.3 Exemple d’organisation modulaire


Par exemple, le fichier Stack.h (programme 8.6) décrit l’interface d’un mo-
dule « pile d’entiers », géré selon une stratégie « dernier entré, premier sortie ».
Cette interface se réduit au prototype des quatre fonctions de base : empiler,
dépiler, tester si la pile est pleine, tester si elle est vide, ainsi qu’une fonction
d’initialisation.
Le corps du module est dans le fichier Stack.c (programme 8.7) : la pile
est réalisée à l’aide d’un tableau qui est alloué dynamiquement (par malloc)
puisque l’on veut fixer la taille de la pile à l’exécution. On remarque que, à
l’exception des cinq fonctions externes de l’interface, tous les autres objets sont
privés.
Enfin le programme 8.8 utilise ce module de pile en empilant puis dépilant
une suite de nombres. En voici un exemple d’exécution :

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
8.5. Exercices du chapitre 8 167

Programme 8.6 – Spécification du module « pile d’entiers »


/***** Fichier: Stack.h *****/

/* Interface du module Stack */

5 #ifndef _Stack_h_
#define _Stack_h_

void stack_init(int stack_size); /* Alloue et intialise */

10 void stack_push(int elem); /* Placer un element */


int stack_pop(void); /* Retirer un element */
int stack_is_full(void); /* Pile pleine ? */
int stack_is_empty(void); /* Pile vide ? */

15 #endif

% Stack
Taille de la pile? 10
Entrez un nombre: 1
Entrez un nombre: 2
Entrez un nombre: 3
Entrez un nombre: 4
Entrez un nombre: 5
Entrez un nombre: 6
Entrez un nombre: 7
Entrez un nombre: 8
Entrez un nombre: 9
Entrez un nombre: 10
Resultat: 10 9 8 7 6 5 4 3 2 1
%

8.5 Exercices du chapitre 8

Exercice 8.1 (Modification de l’implémentation des piles) Changer le corps du


module « pile d’entiers » en utilisant une liste à la place d’un tableau.

Exercice 8.2 (File Ecrire un module fifo (c’est-à-dire une file « premier
FIFO )
entré, premier sorti ») à la manière du module « pile d’entiers ».

Exercice 8.3 (Du module au type abstrait) ADT Stack et Fifo.

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



168 8. Structure des programmes

Programme 8.7 – Corps du module « pile d’entiers »


/***** Fichier: Stack.c *****/

/* Implemetnation du module Stack */

5 #include <stdio.h>
#include <stdlib.h>
#include "Stack.h"

static int *the_stack; /* La pile elle-meme */


10 static int stack_top = 0; /* Indice du sommet */
static int stack_size; /* Taille amximale */

static void error(const char *msg)


{
15 fprintf(stderr, "Stack: ERREUR: %s\n", msg);
exit(1);
}

void stack_init(int size)


20 {
if (size < 0)
error("taille");
if ((the_stack = malloc(size*sizeof(int))) == NULL)
error("malloc");
25 stack_size = size;
}

void stack_push(int elem)


{
30 if (!stack_is_full())
the_stack[stack_top++] = elem;
else
error("pile pleine");
}
35
int stack_pop(void)
{
if (!stack_is_empty())
return the_stack[--stack_top];
40 else error("pile vide");
/*NOTREACHED*/
return 0; // pour eviter un warning...
}

45 int stack_is_full(void)
{
return stack_top == stack_size;
}

50 int stack_is_empty(void)
{
return stack_top == 0;
}

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
8.5. Exercices du chapitre 8 169

Programme 8.8 – Utilisation du module « pile d’entiers »


/***** Fichier: Stack_main.c *****/

/* Utilisation du module Stack */

5 #include <stdio.h>
#include "Stack.h"

int main()
{
10 int n = 10;

printf("Taille de la pile = %d\n", n);


stack_init(n);

15 for (int i = 0; !stack_is_full(); ++i)


stack_push(i);

printf("Resultat: ");
while (!stack_is_empty())
20 printf("%d ", stack_pop());
putchar(’\n’);

return 0;
}

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



170 8. Structure des programmes

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
Chapitre 9

La bibliothèque standard

O n ne prétend pas ici donner une description complète de toutes les fonc-
tions de la bibliothèque standard de C. Pour cela, il vaut mieux se reporter
aux pages du manuel (commande man) ou a un site en ligne : celui de Wikipe-
dia est un bon point de départ (cherchez "c standard library manual").
Le livre de Prata [3] contient également une description très complète de la
bibliothèque standard.

9.1 Éléments généraux


9.1.1 Assertions : <assert.h>
Définition de la macro assert : voir 3.3.3 et 6.3.6. À noter que si l’on définit
la macro NDEBUG
#define NDEBUG
les assertions disparaissent (aucun code n’est généré).

9.1.2 Codes d’erreur : <errno.h>


Ce fichier définit de possibles codes d’erreurs positionnés par les fonctions
de la bibliothèque. Seuls trois codes sont définis dans le standard et il n’est pas
requis que les fonctions les positionnent ! En C pur, ce fichier est donc de peu
d’intérêt.
En revanche, en programmation-système sous Unix ce fichier est très impor-
tant car il contient les codes d’erreur positionnées par les primitives système de
la norme Posix

9.1.3 Définitions communes : <stddef.h>


Ce fichier contient un certain nombre de définitions utiles, comme celle du
type size_t (le type de retour de l’opérateur sizeof) ou de la valeur NULL
du pointeur de même propriété.
En général, ce fichier <stddef.h> est inclus dans <stdlib.h> et donc n’a
pas à être inclus directement.

V 2.1 – 7 février 2010 171 c Jean-Paul R IGAULT



172 9. La bibliothèque standard

9.1.4 Utilitaires généraux : <stdlib.h>


Ce fichier contient un grand nombre d’éléments et d’utilitaires généraux. Il
est rare que l’on n’en ait pas besoin dans un programme. Parmi ces éléments
– ceux de <stddef.h> ;
– les constantes EXIT_SUCCESS (0) et EXIT_FAILURE (1) utilisables
comme paramètre de la fonction exit ou comme valeur de retour de
la fonction main ;
– des fonctions comme atoi, atof, etc. pour des conversions entre chaines
de caractères et types numériques ;
– des générateurs de nombres au hasard comme rand, srand, etc.
– les fonctions d’allocation mémoire dynamique de la famille malloc,
calloc et free ;
– les fonctions de terminaison du programme comme exit et abort ;
– et bien d’autres choses encore...

9.2 Éléments numériques


9.2.1 Booléens : <stdbool.h>
C99 C99 définit le mot réservé _Bool qui est une énumération avec deux valeurs
0 et 1. Si, en plus, vous incluez le fichier <stdbool.h>, alors _Bool est rendu
synonyme de bool et vous gagnez aussi le deux nouvelles macros true et
false.
Il est fortement conseillé de le faire. Outre une meilleure lisibilité, cela as-
sure la compatibilité avec C++.

9.2.2 Types entiers : <stdint.h>


Ce fichier définit une ensemble de types entiers signés ou non dont le
nombre de bits de la représentation est garanti : int8_t, int32_t, uint64_t,
etc.

9.2.3 Conversion des types entiers : <inttypes.h>


C99 Ce fichier définit un ensemble de macros pour faciliter la conversion entre
les types entiers à nombre de bits garantis de <stdint.h>.

9.2.4 Environnement pour calcul en nombres réels : <fenv.h>


C99 Ce fichier définit un certain nombre de types et de fonctions permettant de
manipuler des « exceptions » lors des calculs en nombres flottants. Le pragma
#pragma STD FENV_ACCESS on
active le mécanisme d’exception (et le même pragma avec off le désactive).
Les exceptions en question capturent des erreurs de calcul comme le dépas-
sement de capacité, la division par zéro, les valeurs illégales, etc. On peut aussi
contrôler la manière dont s’effectuent les arrondis.
Tout ceci est réservé aux spécialistes du calcul scientifique...

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
9.3. Fonctions mathématiques 173

9.2.5 Nombres complexes : <complex.h>


Ce fichier déclare le type complex comme synonyme de _Complex et les C99
opérations sur ce type (y compris les fonctions trigonométriques). Noter que
ni _Complex ni a fortiori complex ne sont des types de base : ils font juste
référence à une structure à 2 champs, la partie réelle et la partie imaginaire ;
en outre les opérations arithmétiques sont réalisées sous forme de fonctions et
non d’opérateurs (comme + ou *).

9.3 Fonctions mathématiques


9.3.1 Bibliothèque mathématique de base : <math.h>
Le fichier <math.h> contient la déclaration d’un grand nombre de fonc-
tions mathématiques (fonctions trigonométriques, de Bessel, logarithmes et
exponentielles, arrondis, comparaisons entre réels, etc.).
Si vous utilisez ces fonctions vous devez éditer les liens avec la biblio-
thèque mathématique en fournissant l’option -lm à votre commande d’édition
de liens :
gcc -o prpg prog1.o prog2.o prog3.o -lm

9.3.2 Bibliothèque mathématique générique : <tgmath.h>


Ce fichier définit un ensemble de macros qui permettent d’utiliser les fonc- C99
tions de <math.h> et <complex.h> en utilisant un nom unique pour chaque
fonction. Ainsi, après inclusion de ce fichier, l’expression sin(x) invoquera la
fonction sinus adaptée au type de x : csin s’il s’agit d’un complexe, sinf si
c’est un float, etc.
Il s’agit là d’une manière rustique de compenser l’absence de surcharge des
fonctions et opérateurs en C !

9.4 Caractères et chaînes de caractères


9.4.1 Manipulation de caractères : <ctype.h>
Ce très utile fichier contient des fonctions permettant de déterminer la na-
ture d’un caractère : espace (isspace), lettre (isalpha) ou chiffre (isdigit),
ponctuation (ispuct), etc. Il permet aussi certaines transformations comme le
passage en majuscules ou minuscules (toupper, tolower)... Le fonctionne-
ment de ces fonctions dépend des conventions locales (voir 9.4.5 plus loin) et
n’est correct qu’avec un encodage de caractères sur 8 bits.

9.4.2 Manipulation de chaînes de caractères : <string.h>


Indispensable, ce fichier déclare les fonctions de manipulations de chaînes
de caractères (encodés sur 8 bits) : copie (strcpy, strcat), comparaison lexi-
cographique (strcmp), etc. Les chaînes doivent être bien évidemment termi-
nées par le caractère nul (voir figure 3.1).

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



174 9. La bibliothèque standard

Ce fichier déclare aussi des fonctions de manipulation « brutales » de zones


de mémoire comme la copie (memcpy), la comparaison (memcmp), etc. Voici
par exemple une manière trés efficace (beaucoup plus efficace qu’une boucle
élément par élément) de copier un tableau de double dans unn autre (de même
taille) :
double t1[100];
double t2[100];

// initialisation des elements de t2

memcpy(t1, t2, 100 * sizeof(1)); // copie t2 dans t1

9.4.3 Manipulation de caractères étendus: <wctype.h>


C99 Ce fichier est l’équivalent de <ctype.h> mais pour les caractères encodés
sur plus d’un octet (type wchar_t), par exemple Unicode.

9.4.4 Manipulation de chaînes de caractères étendus : <wchar.h>


C99 Ce fichier est l’équivalent de <string.h> mais pour les caractères encodés
sur plus d’un octet (type wchar_t), par exemple Unicode.
Il contient également des opération de conversions entre les différents en-
codages ainsi que l’équivalent des opérations d’E/S de <stdio.h>) pour les
caractères wchar_t.

9.4.5 Localisation : <locale.h>


Ce fichier déclare un ensemble d’éléments permettant la localisation des
programmes, c’est-à-dire le respect d’un certain nombre de convention locales :
point décimal, symbole monétaire, format de la date et de l’heure, jeu de ca-
ractères, etc. La manipulation des locales est un monde en soi, qu’il ne nous
appartient pas de décrire ici !

9.5 Entrées-sorties : <stdio.h>


L’un des plus utiles de la bibliothèque standard, ce fichier déclare les fonc-
tions d’entrées-sorties :
– ouverture et fermeture de fichier : fopen, fclose...
– accès direct au fichier : fseek...
– E/S en mode caractère : getchar, putchar...
– E/S en binaire : fread, fwrite...
– E/S formattées : printf, scanf...

9.6 Divers

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
9.6. Divers 175

9.6.1 Fonctions à nombre variable d’arguments : <stdarg.h>


Ce fichier définit des macros permettant d’extraire chacun des arguments
d’une liste variable d’arguments. la page de manuel (man vararg) est très bien
faite.

9.6.2 Date et heure : <time.h>


Ce fichier permet de consulter, d’imprimer, de transformer des dates et
heures (entre gmt et l’heure locale). Bien utile...

9.6.3 Traitement d’événements : <signal.h>


Oubliez celui-là ! Il vaut mieux étudier le problème de la gestion d’événe-
ments dans un cadre système (e.g., Posix).

9.6.4 Points de reprise : <setjmp.h>


Permet de positionner des points de reprise pour y revenir plus tard. Cela
permet de simuler un mécanisme d’exception rustique comme dans l’extrait de
programme suivant :
#include <stdio.h>
#include <setjmp.h>
jmp_buf env;

double sqrt(double x)
{
if (x < 0)
longjmp(env, 1);

// ... algorithme de calcul de la racine carree ...


return ...;
}

int main()
{
double t[] = {1.5, 3.5, -2.5, 4.0};
const int N = sizeof(t) / sizeof(t[0]);

for (int i = 0; i < N; ++i)


{
if (setjmp(env) != 0)
fprintf("valeur = %lg est negative!\n", t[i]);
printf("sqrt(%lg) = %lg\n", t[i], sqrt(t[i]));
}
}
Lisez donc la page de manuel (man setjmp) pour comprendre ce que fait ce
programme... Inutile de dire qu’il s’agit là d’un horreur absolue par rapport à
un vrai mécanisme d’exception comme en Java ou C++.!

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



176 9. La bibliothèque standard

9.6.5 Notation alternative de certains opérateurs : <iso646.h>


Ce fichier est utile si on veut utiliser les mot-clés définis par C++ pour cer-
tains opérateurs. Il définit les macros équivalentes pour les opérateurs suivants :

Macro Opérateur
and &&
and_eq &=
bitand &
bitor |
compl ~
not_eq !=
or ||
or_eq |=
xor ^
xor_eq ^=

Préférer
if (not p and i < N and p[i] not_eq 0) ...
à
if (!p && i < N && p[i] != 0) ...
est évidemment une question de gout et de couleur ! Ma propre préférence va
en fait à
if (p != 0 and i < N and p[i] != 0) ...

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
Chapitre 10

Environnement de développement

Pour développer du logiciel avec un langage on a besoin d’outils pour sup-


porter les différentes activités :
– un compilateur du langage, capable de produire du code pour le proces-
seur et le système d’exploitation visé,
– un éditeur de texte, de préférence connaissant la syntaxe du langage et
capable de la faire ressortir par le jeu des couleurs et/ou des polices,
– un gestionnaire de configuration permettant d’enchaîner harmonieusement
et efficacement les différentes étapes de la production de code,
– des outils de mise au point (« debuggers »),
– des outils de gestion et de génération de documentation.

10.1 Compilateurs
Qu’ils soient libres ou propriétaires, il existe un grand nombre de compila-
teurs pour le langage C. Wikipedia 1 en liste la plupart.
Il est à noter que le célèbre Visual C++ de Microsoft 2 n’est pas, dans ses
versions récentes tout au moins, un compilateur C99.
La suite gcc 3 , le gnu c compiler, est universellement répandue. Elle pos-
sède un certain nombre d’avantages :
– elle est libre, sous licence gpl 4 ;
– il s’agit non pas d’un simple compilateur d’un langage donné, mais d’un
multi-compilateurs, supportant plusieurs langages différents : C et C++,
Objective C et C++, Ada, Fortran, Java...
– la suite gcc évolue de manière très efficace et réactive ce qui lui permet
de suivre de très près, voire d’anticiper, le travail de normalisation de ces
différents langages ;
– elle produit du code pour virtuellement tout processeur et la plupart des
systèmes disponibles sur le marché ;
– sa qualité globale n’est plus à démontrer (même si certains compilateurs
propriétaires peuvent être supérieurs sur certains points particuliers : e.g.,
la suite de compilateurs Intel et l’architecture multi-cœurs) ;
1. http://en.wikipedia.org/wiki/List_of_compilers
2. http://msdn.microsoft.com/en-us/library/60k1461a(VS.80).aspx
3. http://gcc.gnu.org/
4. http://www.gnu.org/licenses/gpl.html

V 2.1 – 7 février 2010 177 c Jean-Paul R IGAULT



178 10. Environnement de développement

– c’est la suite favorite du monde du logiciel libre, disponible par défaut


sur tout les Linux et autres UnixBSD ;
– c’est le compilateur par défaut qui est derrière la plupart des environne-
ments intégrés de développement (IDEs) multi-plateformes actuellement
disponibles : Eclipse, Code::Blocks, CodeLite, etc.

10.2 Développement traditionnel sous U NIX


C’est évidemment sous Unix que l’on a commencé à développer en C. Au
départ on utilisait des outils assez disparates pour supporter les différentes
activités. Assez rapidement, des passerelles ont été établies entre ces outils,
allant même, dans certains cas, jusqu’à une véritable intégration.

10.2.1 Édition de source


Plus personne n’utilise un éditeur ligne comme ed pour écrire du code ; on
souhaite au moins pouvoir afficher et travailler directement sur toute un page
de code. Le successeur à « 2 dimensions » d’ed, vi, remplit bien ce rôle, surtout
sous sa forme récente vim qui permet d’éditer plusieurs fichiers simultanément
et de décorer la syntaxe.
Cependant, même si vim a son fan club, de même que gedit ou kate, les
rois des éditeurs dans ce domaine sont ceux de la famille Emacs, soit emacs
lui-même (gnu emacs pour être correct), soit son clone xemacs. Ces éditeurs
disposent depuis fort longtemps d’une suite impressionnante de propriétés :
– édition et affichage simultanés de plusieurs fichiers,
– affichage simultané de parties du même fichier avec synchronisation au-
tomatique des vues,
– intégration parfaite avec le système de fenêtrage natif : X11 sous
Unix/Linux, MS Windows, aquamacs sous Mac OS X...
– extensibilité, avec un puissant dialecte de Lisp qui permet d’ajouter n’im-
porte quelle fonction d’édition, mais aussi de communiquer avec quasi-
ment n’importe quelle application extérieure.
C’est cette dernière propriété qui donne sa pleine puissance à Emacs. Elle a
permis de développer un ensemble considérable d’outils facilitant le vie du
programmeur :
– modes langages, avec coloration syntaxique, aide à la saisie, complétion
de code, etc. pour virtuellement n’importe quel langage connu ou moins
connu,
– intégration de l’outil make avec remontée automatique des messages d’er-
reurs de compilation vers les lignes correspondantes des fichiers-source,
– intégration du « debugger » gdb,
– intégration des outils de gestion de documentation comme doxygen...
On peut dire que emacs est, depuis fort longtemps, un véritable IDE. La fi-
gure 10.1 montre cet aspect d’intégration. Trois fichiers source sont en cours
d’édition, dont deux affichés (simple_list.h et simple_list_main.c ; la
troisième fenêtre, tout en bas montre l’exécution de la commande make. Si des
erreurs de compilation étaient intervenues, une simple commande à l’éditeur
aurait permis d’afficher le fichier source correspondant, à la ligne en erreur.

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
10.2. Développement traditionnel sous Unix 179

Figure 10.1 XEmacs à l’œuvre

Pendant longtemps, les éditeurs de type Emacs ont eu la réputation d’être


complexes à utiliser et de nécessiter l’apprentissage d’un grand nombre de rac-
courcis clavier à grand renfort de touches control ou escape. Ce n’était pas
faux mais il faut reconnaitre que, de nos jours, même si la maitrise totale de
toutes les fonctionnalités d’Emacs reste évidemment difficile, grâce à la sou-
ris, aux touches spéciales du claviers, aux boutons et aux menus, l’utilisation
quotidienne en est devenue très aisée.

10.2.2 L’outil make


L’utilitaire make est une des perles qu’a produite Unix. Cette commande
exploite les dépendances chronologiques entre fichiers source (fondées sur la
date de dernière modification) pour déclencher les actions nécessaires à la re-
construction d’un produit final, comme par exemple un binaire exécutable. Son
rôle n’est certes pas limitée à la compilation et l’édition de liens en C, mais dans
ce cas comme ailleurs elle y est précieuse.
Les dépendances entre fichiers doivent être fournies dans un fichier nommé
makefile (ou Makefile ou MAKEFILE) dans le répertoire où vous lancez
make. Cependant make peut déduire certaines dépendances grâce à ses règles

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



180 10. Environnement de développement

par défaut. Par exemple, si vous programme tient dans un seul fichier source,
disons prog.c et que vous avez correctement définit les variables d’environ-
nement CC et CFLAGS, vous n’avez même pas besoin de makefile ; tapez sim-
plement
% make prog
gcc -g -Wall -std=c99 -o prog prog.c
%
Par ailleurs, si vous n’êtes pas sûr de vos dépendances entre fichiers, le
compilateur gcc vous aidera : utilisez l’option -MM sur la liste des unités de
compilation (fichiers *.c) et il vous fournira ces dépendances sous une forme
telle que vous n’aurez plus qu’à la couper-coller dans votre makefile :
% gcc -MM *.c
random_list.o: random_list.c
simple_list.o: simple_list.c simple_list.h
simple_list_double_ptr.o: simple_list_double_ptr.c
simple_list_main.o: simple_list_main.c simple_list.h
%

10.2.3 Mise au point avec gdb et compagnie


Le gnu debugger gdb est l’outil de mise au point le plus utilisé sous Unix
ou Linux. Il permet la mise au point au niveau source, sous réserve que vous
ayez utilisé l’option -g lors de la compilation. Il permet de placer des points
d’arrêt, de progresser pas à pas ou en continu, de visualiser des variables, des
flôts de contrôle (« threads »), la pile d’appel, etc.
On peut l’utiliser en mode ligne, mais il est plus agréable et efficace le le
faire à travers l’une de ses interfaces graphiques comme emacs, xxgdb, ddd ou
l’un des multiples environnements intégrés qui l’utilisent (voir section 10.3). La
figure 10.2 présente son utilisation à traver ddd. Elle montre deux fenêtres, celle
de ddd proprement dite et, en bas, la fenêtre d’exécution du programme (une
fenêtre xterm normale). Dans le fenêtre de ddd se trouvent trois sous-fenêtres :
tout en bas, celle qui permet de donner des ordres au debugger ; au dessus,
la visualisation du fichier source en cours — où l’on voit que l’exécution est
bloquée sur un point d’arrêt sur une ligne invoquant la fonction malloc. La
troisième fenêtre est une originalité de ddd : ce « debugger » permet en effet
de dessiner les structures de données avec pointeurs (ici une simple liste) de
manière « naturelle » : sympa dans les cas simples, mais gadget dans les cas
plus complexes ?
Il existe bien d’autres outils pour la mise au point. Les « profilers » comme
gprof vous permettent de mesurer lors de l’exécution le nombre d’appels et le
temps passé dans chaque fonction. Vous pourrez ainsi savoir ce qui vous coute
le plus cher en temps et donc nécessite le plus gros de vos efforts d’optimisa-
tion.
Un autre type d’outils a une importance considérable en particulier dans les
programmes utilisant l’allocation de mémoire dynamique (malloc and Co). Ce
sont les détecteurs de fuite de mémoire. Les plus connus sont Purify de Rational
(maintenant IBM), très efficace et puissant, mais cher, et valgrind qui lui est
suffisamment puissant, facile d’utilisation et... libre.

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
10.2. Développement traditionnel sous Unix 181

Figure 10.2 ddd (et gdb) à l’œuvre

Pour utilisr valgrind, aucune option de compilation ou d’édition de liens


n’est nécessaire. Vous invoquez juste l’utilitaire avec le nom de votre exécu-
table :
% valgrind simple_list
==4576== Memcheck, a memory error detector.
==4576== Copyright (C) 2002-2008, and GNU GPL’d, by Julian Seward et al.
==4576== Using LibVEX rev 1884, a library for dynamic binary translation.
==4576== Copyright (C) 2004-2008, and GNU GPL’d, by OpenWorks LLP.
==4576== Using valgrind-3.4.1, a dynamic binary instrumentation framework.
==4576== Copyright (C) 2000-2008, and GNU GPL’d, by Julian Seward et al.
==4576== For more details, rerun with: -v
==4576==
Entrez une suite d’entiers terminees par EOF
1 3 2
Liste triee:
1 2 3
==4576==
==4576== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 6 from 2)
==4576== malloc/free: in use at exit: 2,156 bytes in 3 blocks.
==4576== malloc/free: 4 allocs, 1 frees, 2,172 bytes allocated.
==4576== For counts of detected errors, rerun with: -v

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



182 10. Environnement de développement

==4576== searching for pointers to 3 not-freed blocks.


==4576== checked 71,744 bytes.
==4576==
==4576== LEAK SUMMARY:
==4576== definitely lost: 32 bytes in 2 blocks.
==4576== possibly lost: 0 bytes in 0 blocks.
==4576== still reachable: 2,124 bytes in 1 blocks.
==4576== suppressed: 0 bytes in 0 blocks.
==4576== Rerun with --leak-check=full to see details of leaked memory.
%
Pendant l’exécution qui se déroule normalement (lignes marquée en bleue 5 ),
les fonctions de la famille malloc sont instrumentées. À la fin un rapport
est émis. On constate un déséquilibre entre le nombre d’allocation (4) et de
déallocation (1) ; on nous indique aussi que 2 blocs totalisant 32 octets sont non
récupérables. Rien de dramatique ici puisque le programme se termine et que
donc l’ensemble de son espace mémoire est retourné au système. Mais dans des
cas plus complexes, cela pourrait être l’indication d’une catastrophe à venir...
Sachez que valgrind possède d’autres options et bien d’autres possibilités.
N’hésitez pas à l’utiliser et apprenez à interpréter ses rapports.

10.2.4 Outils de documentation


L’une des qualités majeures que l’on peut attendre de la documentation
interne d’un programme est d’être en permanente synchronisation avec le code lui-
même. Cela est difficile à imposer mais certains ont pensé qu’on pouvait au
moins l’inciter en plaçant cette documentation dans les commentaires du code.
Ainsi en modifiant le code, peut-être penserait-on aussi à mettre à jour la doc 6 .
Parmi les outils permettant de gérer une telle documentation dans les com-
mentaires, les plus connus sont Javadoc (malheureusement limité au langage...
Java) et doxygen 7 . Ce dernier permet de documenter du code source en C,
C++, Java, Objective C, Python, idl, Fortran, vhdl, php et C#. Il produit
une documentation dans une foultitude de langues différentes (dont le Fran-
çais !) et dans de nombreux formats (html, LATEX, rtf (i.e., MS Word), pdf, man
d’Unix...). Il est disponible sous Unix/Linux, MS Windows et Mac OS X. Et il
est libre. Bref un outil universel et précieux. Apprenez à l’utiliser...

10.3 Environnements intégrés de développement


Un environnement intégré de développement (IDE) regroupe en une seule
application tous les outils supports des activités de développement de logiciel
et les dote d’un ensemble cohérent et homogène d’interfaces utilisateur. Un
IDE peu être limité à un type de plateforme (i.e., système d’exploitation ) ou
s’exécuter sur plusieurs plateformes. Il existe de nombreux IDE pour C, com-
merciaux ou non. Nous nous limitons ici aux non-commerciaux, souvent aussi,
sinon plus, puissants que leurs concurrents payants.
5. Les couleurs sont dues à l’auteur, non à valgrind, malheureusement !
6. Pour être honnête, cette idée n’a pas que des partisans inconditionnels et l’expérience
permet de dire que c’est sans doute à juste titre !
7. http://www.doxygen.org

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
10.3. Environnements intégrés de développement 183

Figure 10.3 Eclipse en mode édition/compilation de C/C++

10.3.1 Environnements multi-plateformes


Dans le domaine des IDEs multi-plateforme eclipse 8 est la Rolls, mais à un
prix ridicule, puisque nul. Supporté par IBM et une large communauté d’utili-
sateurs et de développeurs, c’est bien plus qu’un simple IDE multi-plateformes,
multi-langages, multi-activités, multi-tout. C’est un environnement de dévelop-
pement d’applications extensible (grâce à Java) et adaptable à virtuellement
n’importe quelle activité interactive. Sa communauté l’a doté de capacités de
développement de logiciel (dans de nombreux langages) mais aussi de modéli-
sation, de documentation, de développement graphique, etc.
Équipée du « greffon » (« plugin ») CDT, eclipse est un véritable environ-
nement de développement en C (et C++) fonctionnant sous Unix/Linux, MS
Windows et Mac OS X. Par défaut, les outils utilisés sont gcc, make et gdb,
mais ceci est largement paramétrable. La figure 10.3 montre eclipse à l’œuvre
en mode édition et compilation, alors que 10.4 en montre la vue de mise au
point (interface avec gdb).
Bien sûr, eclipse n’est pas le seul IDE multi-plateformes pour C. Il en existe
d’autres, souvent plus légers d’utilisation, comme Code::Blocks 9 , figure 10.5,
ou CodeLite 10 , figure 10.6, qui eux aussi s’interfacent par défaut avec les outils
gnu (gcc, make, gdb...).
8. http://www.eclipse.org
9. http://http://www.codeblocks.org/
10. http://codelite.org/

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



184 10. Environnement de développement

Figure 10.4 Eclipse en mode mise au point de C/C++

10.3.2 Environnements spécifiques à U NIX


Il existe aussi certains IDEs spécifiques à certaines versions d’Unix. C’est le
cas de kdevelop 11 , l’environnement de développement de kde, disponible sous
Linux uniquement, de anjuta 12 un IDE issu du projet gnome et donc également
dédié à Linux, ou encore XCode 13 , l’IDE propre à Apple pour Mac OS X.

10.3.3 Environnements spécifiques à MS W INDOWS


Le plus connu et l’un des meilleurs IDEs toutes plateformes confondues
est certainement Visual Studio. Son intégration aux bibliothèques standard
et à celles de Microsoft, ses modes d’édition, la qualité exceptionnelle de
son « debugger » en feraient un outil parfait si il tournait aussi sur d’autres
11. http://www.kdevelop.org/
12. http://projects.gnome.org/anjuta/
13. http://developer.apple.com/tools/xcode/

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
10.4. Remarque finale 185

Figure 10.5 Encore un IDE multiplateformes, Code::Blocks

plates-formes, si il n’était pas pollué (parfois insensiblement) par les extensions


propres à Microsoft, si il respectait les normes internationales, comme C99...
La figure 10.7 montre Visual Studio en mode édition et compilation et la
figure 10.8 en mode de mise au point.

10.4 Remarque finale


De mon point de vue l’utilisation d’un éditeur puissant et intégrant l’invo-
cation de make comme emacs associé à un debugger de type ddd ou xxgdb est
ce que l’on peut faire de plus productif pour des projets de développement de
petits à moyens. La mise en œuvre est simple et nécessite peu de ressources et
l’apprentissage est relativement rapide.
Dès que l’on recourt à un IDE, le coût d’entrée augmente. Il faut apprendre
à l’utiliser, comprendre la notion de projet qui lui est propre, les différentes
activités qu’il permet. Ensuite, il ne faut pas se leurrer : la puissance ou l’agré-
ment des IDEs se paye souvent par une complexité importante et surtout une
hyper-consommation de ressources. De ce point de vue, sachez que eclipse et
Visual Studio sont vraiment très gourmands !

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



186 10. Environnement de développement

Figure 10.6 Un IDE multiplateformes léger, CodeLite

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
10.4. Remarque finale 187

Figure 10.7 Visual Studio 2008 en mode édition/compilation de C/C++

Figure 10.8 Visual Studio 2008 en mode mise au point de C/C++

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



188 10. Environnement de développement

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
Chapitre 11

Extensions avancées de C99

O n mentionne ici quelques extensions propres à C99 et dont la plupart


n’ont pas eu à être utilisées dans le reste du texte.
Cette section est à compléter.

11.1 Préprocesseur
11.1.1 Macros prédéfinies
11.1.2 Macros à nombre variable d’arguments

11.2 langage de base


11.2.1 Nom de la fonction courante : __func__
11.2.2 Pointeurs restreints : restrict
11.2.3 Tableaux
Tableaux de taille variable

Tableaux incomplets

11.3 Bibliothèque standard


11.3.1 Fichiers d’entête
11.3.2 Type booléen
11.3.3 Types entiers étendus
11.3.4 Nombres complexes
11.3.5 Manipulation des caractères multiples
11.3.6 Réels en virgule flottante

V 2.1 – 7 février 2010 189 c Jean-Paul R IGAULT



190 11. Extensions avancées de C99

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
Chapitre 12

C traditionnel, ISO C90, ISO C99 et


C++

C e chapitre liste brièvement les différences entre C dit « traditionnel », C90,


C99 et C++.
Ce chapitre est à compléter.

12.1 D’ISO C90 à C traditionnel

12.2 D’ISO C90 à ISO C99


– inline functions
– variable declaration no longer restricted to file scope or the start of a
compound statement
– several new data types, including long long int, optional extended integer
types, an explicit boolean data type, and a complex type to represent
complex numbers
– variable-length arrays
– support for one-line comments beginning with //, as in BCPL or C++
– new library functions, such as snprintf
– new header files, such as stdbool.h and inttypes.h
– type-generic math functions (tgmath.h)
– improved support for IEEE floating point
– designated initializers
– compound literals
– support for variadic macros (macros of variable arity)
– restrict qualification to allow more aggressive code optimization

12.3 D’ISO C90 ou ISO C99 à C++

V 2.1 – 7 février 2010 191 c Jean-Paul R IGAULT



192 12. C traditionnel, iso C90, iso C99 et C++

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
Bibliographie

[1] von Hagen, W. The Definite Guide to GCC, 2e édition. Apress, 2006.
[2] ansi Committee x3j11. Rationale for the ansi C Programming language.
Silicon Press, 1990.
[3] Prata, S. C Primer Plus, 5e édition. Sams Publishing, 2005.
[4] Prata, S. C++ Primer Plus, 4e édition. Sams Publishing, 2001.
[5] Flipo, D. Documentation sur le module frenchb de babel. Site Web,
http://daniel.flipo.free.fr/frenchb/frenchb2-doc.pdf,
2007.
[6] Joseph, N. Divers articles sur C et C++. Site Web,
http://nicolasj.developpez.com/.
[7] Braams, J. Babel, a multilingual package for use
with LATEX’s standard document classes. Site Web,
http://css.ait.iastate.edu/Tex/Sp/babel.pdf, 2005.
[8] Kochan, S. G. Programming in C, 3e édition. Sams Publishing, 2005.
[9] Tribble, D. Incompatibilities between iso C and iso C++. Site Web,
http://david.tribble.com/text/cdiffs.htm.
[10] Lamport, L. LATEX, a Document Preparation System. Addison-Wesley, 1986.
[11] Lippman, S. B., Lajoie, J. C++ Primer, 3e édition. Addison Wesley, 1998.
[12] Delannoy, C. Langage C, 4e édition. Eyrolles, 1999.
[13] Dijkstra, E. W. Go to statement considered harmful. Communications of
the ACM 11, 3 (March 1968), 147–148. Version annotée par David Tribble
en http://david.tribble.com/text/goto.html.
[14] Institut für Theoretische Informatik at TH-
Darmstadt. xindy, a flexible indexing system. Site Web,
http://xindy.sourceforge.net/.
[15] Valgrind Developpers. Valgrind. Site Web, http://valgrind.org/.
[16] Kernighan, B. W., Ritchie, D. M. The C Programming Language, 1re édi-
tion. Software Series. Prentice Hall, 1978.
[17] Kernighan, B. W., Ritchie, D. M. The C Programming Language (ansi C
edition, 2e édition. Software Series. Prentice Hall, 1988.
[18] Kernighan, B. W., Ritchie, D. M. Le langage C : norme ansi, 2e édition.
Sciences Sup. Dunod, 2004.
[19] gnu Project. Gdb: The gnu project debugger. Site Web,
http://sourceware.org/gdb/.

V 2.1 – 7 février 2010 193 c Jean-Paul R IGAULT



194 BIBLIOGRAPHIE

[20] gnu Project. Ddd: Data display debugger. Site Web,


http://www.gnu.org/software/ddd/.
[21] gnu Project. Gprof: The Gnu profiler. Site Web,
http://www.gnu.org/software/binutils/.
[22] gnu Project. Status of C99 features in GCC. Site Web,
http://gcc.gnu.org/c99status.html.
[23] gnu Project. Programmation et langages. Site Web,
http://gcc.gnu.org/.
[24] Stroustrup, B. The C++ Programming Language, 3e édition. Addison-
Wesley, 1991.
[25] Stroustrup, B. Programming: Principles and Practice Using C++. Addison-
Wesley, 2009.
[26] Mittelbach, F., Goossens, M., Braams, J., Carlisle, D., Rowley, C. The
LATEX Companion, 2e édition. Addison Wesley, 2004.
[27] Braquelaire, A. Méthodologie de la programmation en C : norme C 99 — api
Posix, 4e édition. Dunod, 2005.
[28] Forum interactif. Programmation et langages. Site Web,
http://www.developpez.net/forums/.
[29] Multiples auteurs. Diverses ressources sur la programmation. Site Web,
http://www.developpez.com/.

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
Glossaire

O n tente ici de donner la (une) définition d’un certain nombre de termes et


de concepts généraux liés aux langages de programmation et au processus
de leur traduction en langage-machine. Elle peu sans dommage être ignorée par
l’informaticien expérimenté.
affectation
L’une des instructions 1 fondamentales de la programmation procédura-
le. L’affectation permet de modifier la valeur d’un objet (qui doit être
variable bien sûr). Voir aussi référence et valeur.
agrégat
Un type dont les objets sont composés d’autres objets. En C, il y a trois
sortes d’agrégats: les tableaux, les structures et les unions.
analyse lexicale
La première phase de la compilation, où l’on reconnait les éléments lexi-
caux (token), c’est-à-dire les « mots » d’un langage de programmation
(identificateurs, opérateurs, constantes...).
analyse statique (ou de sémantique statique)
Elle est effectuée par le compilateur après l’analyse syntaxique. On y vé-
rifie en particulier ce qui relève du typage (voir type).
analyse syntaxique
La deuxième phase de la compilation où l’on recherche la structure des
« phrases » c’est-à-dire celle des instructions et déclarations.
archive
Voir bibliothèque.
argument formel et effectif d’une fonction
L’argument effectif d’une fonction est celui fourni à l’appel (l’invocation)
de la fonction; sa valeur sert à initialiser l’argument formel correspondant,
qui est un objet local à la fonction.
En cas de typage fort le type de l’argument formel et celui de l’argument
effectif doivent être identiques ou alors il doit exister une conversion im-
plicite du type de l’argument effectif en celui de l’argument formel.
En C, la correspondance entre arguments effectifs et formels s’effectue de
manière positionnelle (c’est-à-dire selon l’ordre des arguments définis dans
le prototype ou l’entête de la fonction).
assembleur
Forme symbolique du langage-machine. Les compilateurs C produisent
souvent de l’assembleur plutôt que du langage-machine directement.
1. En fait en C, l’affectation est une expression et non une instruction (voir 3.3.4).

V 2.1 – 7 février 2010 195 c Jean-Paul R IGAULT



196 GLOSSAIRE

On désigne aussi par assembleur le programme qui traduit ce langage


lui-même en binaire objet.
bibliothèque
Un ensemble de fichiers-objets regroupés en un fichier unique (library ou
archive en anglais) utilisable lors de l’édition de liens. Tout environne-
ment C comporte plusieurs bibliothèques dont le contenu est prédéfini:
la bibliothèque standard, la bibliothèque mathématique, etc... (voir le cha-
pitre 9).
binaire exécutable
Le résultat de l’édition de liens. Sous UNIX le fichier binaire exécuta-
ble porte par défaut le nom a.out. Il contient le code (les instructions)
du programme et la valeur initiale des données. Si l’édition de liens s’est
correctement passée, ce fichier est effectivement exécutable, c’est-à-dire
invoquable directement depuis le shell.
binaire objet
Voir fichier-objet.
compilation
Le processus de traduction d’un fichier-source en fichier-objet 2 . Elle
comporte quatre phases principales (en principe successives bien qu’une
certaine imbrication soit possible):
1. l’analyse lexicale,
2. l’analyse syntaxique,
3. l’analyse statique (analyse du typage),
4. la génération de code.
Optionnellement, il peut exister une cinquième phase d’optimisation.
En C, la compilation est précédée d’un prétraitement effectué par le
préprocesseur et est suivie d’une édition de liens. La figure 7.1 schématise
la chaîne de compilation d’un programme C constitué de 3 fichiers-sources
fic1.c, fic2.c et fic3.c. Le suffixe .c caractérise par convention les
fichiers contenant du texte C.
Chaque fichier-source est analysé séparément, d’abord par le préproces-
seur qui effectue des remplacements textuels (voir 6), puis par le compi-
lateur proprement dit qui produit un fichier-objet de même nom de base
que le source mais avec le suffixe .o.
Enfin l’éditeur de liens (ld sous UNIX) assemble les différents fichiers-
objets en un fichier binaire exécutable unique de nom conventionnel
par défaut a.out. C’est à l’éditeur de liens d’aller chercher dans les bi-
bliothèques standards ou utilisateurs les modules demandés par le pro-
gramme.
En fait, au moins sous UNIX, ces différentes phases sont enchainées par
2. En fait la compilation, dans l’acception la plus large, recouvre la traduction d’un langage
de programmation en un autre. La définition donnée ici (traduction d’un langage de haut niveau
en langage-machine ou assembleur) est cependant très répandue et la seule utile ici.

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
GLOSSAIRE 197

Figure 7.1 Chaîne de compilation C

une commande unique, dénommée en général cc 3 comme par exemple


sous le shell 4 :
% cc fic1.c fic2.c fic3.c

On peut aussi renommer le fichier a.out en fic:


% cc -o fic fic1.c fic2.c fic3.c

3. Lorsque les deux versions, C traditionnel et ANSI C, coexistent, cc est souvent la com-
mande de compilation en C traditionnel. Sur notre système, la commande de compilation
ANSI C est gcc.
4. Dans ces exemples d’utilisation de commande, nous faisons l’hypothèse que le shell utilisé
est celui de Steve Bourne (sh) ou lui est compatible (comme ksh, le shell dû à David Korn).
Ce que tape l’utilisateur est en police penchée alors que ce qu’affiche le système est en
police normale. Le caractère % est l’« invite » (le prompt) du shell.

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



198 GLOSSAIRE

compilation séparée
Lorsque le texte d’un même programme est reparti dans plusieurs fichi-
ers-sources différents, chacun de ces fichiers est traité séparément par
le compilateur. Ceci signifie que chaque fichier-source doit contenir toute
l’information nécessaire au compilateur, et en particulier toute celle qui
permet l’analyse de sémantique statique (typage).
constante
Un objet qui ne peut être modifié.
constante littérale
Un objet qui ne peut être modifié et dont la valeur peut être évaluée
lors de la compilation (on parle aussi de constante lexicale, ou encore
statique). Ainsi en C, 12, -4, "bonjour" sont des constantes littérales.
contrôle (flot de contrôle, instruction de contrôle)
Le flôt de contrôle est déterminé par l’ordre d’exécution des instructions.
En programmation procédurale Ce flot est en général séquentiel (les ins-
tructions sont exécutées dans l’ordre où elles sont écrites) mais cette sé-
quentialité peut être brisée par les instructions de contrôle qui sont le plus
souvent de trois types:
– sélection (if et switch en C),
– boucles (while, do ... while et for),
– la rupture de séquence (goto).
corps d’une fonction
Voir fonction.
corps d’un module
La partie du module contenant la définitions des données et des fonctions
du module. Certaines de ces données et de ces fonctions peuvent être
privées au module (encapsulation) et seront donc invisibles dans le reste
du programme. Les autres sont exportées dans les parties du programmes
qui voient les déclarations contenues dans la spécification du module.
déclaration d’un objet
La déclaration d’un objet indique simplement le type de cet objet au
compilateur. Il peut en général y avoir autant de déclarations que l’on
veut pour un même objet du moment qu’elles sont identiques (ou com-
patibles).
définition d’un objet
La définition d’un objet, outre qu’elle indique aussi son type, réserve et
éventuellement initialise la zone mémoire correspondant à l’objet. Il y a
une et une seule définition dans tout le programme pour chaque objet.
dynamique
Une notion quelconque est dynamique si elle ne peut être vérifiée, éva-
luée, utilisée... qu’à l’exécution. Sinon, elle peut l’être à la compilation et
est dite alors statique.
édition de liens
En cas de compilation séparée, l’éditeur de liens rassemble tous les
fichiers-objets en un fichier binaire exécutable unique. C’est donc lui qui
résoud les références externes. Certaines de ces objet externes peuvent
être définis dans des bibliothèques.

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
GLOSSAIRE 199

Sous UNIX, l’éditeur de liens s’appelle ld (le loader).


Il est à noter qu’en général aucune information sur le type des objets n’est
transmise à l’éditeur de liens.
encapsulation
Un mécanisme syntaxique qui permet de rendre des données ou des fonc-
tions privées à un module invisibles dans les autres modules.
L’encapsulation renforce la sécurité de programmation et évite la pollu-
tion de l’espace des noms globaux.
entête d’une fonction
Voir fonction.
expression statique
Une expression évaluable à la compilation (voir aussi constante statique).
externe
Voir référence externe.
fichier-objet
Le résultat de la compilation d’un fichier-source. C’est un fichier qui
contient le code généré (binaire objet) et la valeur initiale des données;
en cas de compilation séparée, il n’est pas en général directement exécu-
table, car il peut contenir des références externes non résolues, c’est-à-
dire des références à des objets définis dans d’autres fichiers-sources.
Le nom du fichier-objet est celui du fichier-source qui l’a engendré où l’on
remplace le suffixe (.c) par .o.
fichier-source
Un fichier contenant le texte du programme (ou d’une partie du pro-
gramme). Noter que compilateur analyse le fichier selon l’ordre lexical
(l’ordre de lecture).
Par convention expresse, les fichiers de source C ont un nom suffixé par
.c.
fonction
L’élément principal de structuration du flot de contrôle en programma-
tion procédurale. En C, la définition d’une fonction comporte deux par-
ties:
– l’entête qui en reprend la signature et précise le nom des arguments
formels.
– le corps qui contient les définitions d’objets locaux à la fonction et les
intructions à exécuter à chaque appel de la fonction.
génération de code
La dernière phase obligatoire de la compilation pour produire le binaire
objet. En fait, très souvent les compilateurs C produisent de l’assembleur
qu’il est nécessaire de traduire encore en langage-machine.
langage de haut niveau
Par opposition au langage-machine ou à l’assembleur, un langage de
haut niveau a une structure et une forme plus proche des applications
que de la machine.
C est un langage de haut niveau pour la programmation système, même
s’il comporte quelques caractéristiques de bas niveau (voir 1.2.2).

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



200 GLOSSAIRE

langage-machine
Le langage des instructions et des données interprétables directement par
le matériel. Il constitue le contenu du binaire exécutable.
library
Voir bibliothèque.
lvalue
Voir référence.
modularité
La possibilité de découper le source d’un programme en unités syntaxi-
quement indépendantes regroupant chacune des données et les fonctions
de manipulation directes de ces données.
La modularité désigne aussi la qualité d’un programme ainsi découpé.
La modularité en C fait l’objet du chapitre 8.
module
Une unité syntaxique de modularité. Un module comporte en général
deux parties:
– la spécification, qui indique les déclarations des objets et les définitions
des types que le module rend visible (qu’il exporte),
– le corps qui contient les définitions de ces objets.
Les parties du programme qui veulent utiliser un module doivent en voir
la spécification (elles l’importent).
En C, le corps d’un module est contenu dans un fichier-source .c, alors
que sa spécification est en général dans un fichier d’entête (de suffixe
conventionnel .h). L’importation est effectuée grâce à la directive d’inclu-
sion de fichier du préprocesseur.
nom (d’un objet)
Dans un langage de programmation, la plupart des objets sont désignés
par un nom représenté par un identificateur. Un identificateur constitue
une référence sur l’objet.
Cependant, certains objets sont anonymes: ils peuvent être temporaires
(créés automatiquement par le compilateur), ou alors alloués dynami-
quement et accessibles uniquement au travers de pointeurs.
objet
Un objet 5 est en fait une zône de mémoire, qu’il s’agisse de mémoire de
données ou de mémoire d’instruction.
En C, un objet peut être une constante, une variable ou une fonction;
chaque objet a un type qui doit être connu à la compilation avant toute
utilisation; un objet à aussi une valeur (le contenu de la zône mémoire).
objet (fichier-objet)
Voir fichier-objet.
optimisation de code
C’est la dernière phase, en général optionnelle, de la compilation.
5. Cette notion d’objet est plus large que la forme spécialisée et plus riche à laquelle on se
réfère dans des expressions comme « langage orienté-objets » ou « programmation à objets ».

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
GLOSSAIRE 201

pointeur
Un pointeur est un objet dont la valeur est l’adresse d’un autre objet. En
C, les pointeurs ont un type différent selon le type de l’objet pointé.
Les pointeurs jouent un rôle central en C, mais ils constituent aussi un
danger potentiel pour la sécurité de programmation.
préprocesseur
Avant la compilation, un fichier-source de C est analysé par le prépro-
cesseur (cpp) qui effectue un certain nombre de remplacement textuels
(c’est-à-dire lexicaux).
Le préprocesseur est étudié au chapitre 6. Profondément remodelé lors de
la normalisation, il constitue une des causes d’incompatibilité principales
entre ANSI C et le C traditionnel.
procédure
En C, il s’agit simplement d’une fonction qui n’a pas de valeur de retour
(on déclare ce type void).
programmation (procédurale, structurée, impérative)
C’est le type de programmation habituelle avec les langages comme FOR-
TRAN, Pascal, Ada, et bien sûr C. Le flot de contrôle y est séquentiel sauf
si l’on utilise des instructions de contrôle (sélection, boucles, goto...); les
procédures et fonctions permettent de structurer le flot de contrôle.
La programmation structurée est en fait un ensemble de règles ou de
conseils pour structurer le flot de contrôle. L’éradication du goto en est
son trait le plus connu.
prototype de fonction
Voir signature d’une fonction et type.
référence
Un référence est la désignation d’un objet. La forme la plus simple de réfé-
rence est donc le nom d’un objet (un identificateur). Mais C permet bien
d’autres formes qui seront étudiées au fur et à mesure de leur introduc-
tion.
Une référence ne doit pas être confondue avec la valeur de l’objet réfé-
rencé.
Dans le jargon de C, on designe une référence par le terme lvalue qui rap-
pelle qu’une référence est requise en partie gauche d’un affectation (lvalue
comme abréviation de left value); la valeur de l’objet est alors désignée,
pour la même raison, par le vocable rvalue (comme right value).
L’objet référencé peut être une variable ou une constante (dans le second
cas, la lvalue ne peut pas apparaitre en partie gauche d’une affectation!).
Bien sûr une référence rencontrée en partie droite d’un affectation est
« déréférencée » pour rendre la valeur de l’objet. Ainsi dans l’affectation
suivant entre deux entiers (type int)
int i, j;
...
i = j;

i est une référence (lvalue) alors que j est une valeur (rvalue).

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



202 GLOSSAIRE

référence externe
En cas de compilation séparée, une référence externe est une référence
dans un fichier, disons fic.c, à un objet possiblement défini dans un
autre fichier de compilation. Si l’objet est effectivement défini ailleurs,
alors le fichier-objet fic.o contiendra une référence externe non résolue.
référence (externe) non résolue
Voir référence externe et édition de liens.
sémantique d’exécution
La signification du programme, ce qu’il est censé faire, la fonction qu’il
calcule, la transformation qu’il effectue sur ses données d’entrée pour
produire les résultats en sortie, etc...
sémantique statique
Voir analyse statique.
signature d’une fonction
C’est le type d’une fonction qui est défini par
– le type de la valeur de retour (c’est-à-dire du résultat) de la fonction,
– le nombre et le type de chacun des arguments.
En ANSI C, la signature d’une fonction est déclarée grâce au prototype
(qui précise en outre le nom de la fonction).
spécification d’un module
La partie d’un module qui contient les déclarations des objets exportés
par le module (leur définitions sont dans le corps du module).
Si un autre module veut utiliser ces objets, il doit rendre visible (impor-
ter) la spécification. En C, ceci est réalisé par le directive #include du
préprocesseur.
source
Voir fichier-source.
statique
Voir dynamique.
typage fort
Voir type.
type
Un type définit un ensemble d’objets partageant un même ensemble
d’opérations. Par exemple, l’ensemble des entiers avec les opérations
usuelles (arithmétiques, logiques...).
On distingue en C les types de base (entiers et réels) et les types utilisateur
qui sont obtenus par des opérations de construction de type. Ces types
utilisateur sont des agrégats (tableaux, structures, unions).
En ANSI C, tout objet a un type qui doit être connu avant l’utilisation
de l’objet. Le compilateur vérifie (donc de manière statique) le bon usage
des types (typage fort).
valeur (d’un objet)
Voir objet.
variable
Un objet modifiable, le contraire d’une constante.

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
Index

– Symboles – <stdbool.h>, 58
=, 28, 63
, 71 -=, 64, 107
., 94, 97, 110 ==, 28, 58, 106
..., 144 >, 28, 58, 106
-, 32, 57, 64, 107 ->, 94, 97, 110
— unaire, 57 >=, 28, 58, 106
∼, 60 >>=, 64
#, 130, 134 [], 119
##, 134 __DATE__, 135
#define, 84, 131–135, 155 __FILE__, 135
#elif, 136 __LINE__, 135
#else, 136 __STDC__, 135
#endif, 136 __STDC_VERSION__, 135
#error, 138 __TIME__, 135
#if, 136 {}, 26
#ifdef, 136
#ifndef, 137
#define, 26, 130, 131, 157, 202 –A–
#line, 138
#pragma, 138 abort, 172
#undef, 135, 155 accolade, 26
%, 33, 57 Ada, 18
%=, 64 addition, 57
& — de pointeurs, 108
— unaire, 104 — d’un entier à un pointeur, 107
* , 32, 57, 119 adresse
— unaire, 105 — d’un objet, 31
*=, 64 opérateur —, 104
+, 32, 57, 107 affectation, 195
— unaire, 57 — composée, 63
++, 37, 64, 107 — de tableau, 85
+=, 64, 107 — simple, 63
,, 66, 68 conversion lors d’une —, 70
,=<=, 106 expression d’—, 28
/, 32, 57 agrégat, 50, 83, 195, 202
/=, 64 initialisation d’—, 85, 86, 91, 96
1 alignement, 93
complément à —, 60 allocation
2 — dynamique, 113, 158, 161
complément à —, 69 alternative, 32
;, 27, 71 analyse
<, 28, 58, 106 — lexicale, 195
<<=, 64 — statique, 195
<=, 28, 58 — syntaxique, 195
<assert.h>, 61 and, 176

V 2.1 – 7 février 2010 203 c Jean-Paul R IGAULT



204 INDEX

and_eq, 176 binaire


anjuta, 184 — exécutable, 196
anonyme — objet, 199
type —, 55, 91 bit à bit
ansi, 1, 17 et —, 60
-ansi, 21 opérateur —, 60
a.out, 196 ou —, 60
appel bitand, 176
— de fonction, 145 bitor, 176
— d’une fonction, 26 bits
aquamacs, 178 champ de —, 93
archive, 196 bloc, 26, 71, 145, 158
argc, 39 _Bool, 46, 58, 172
argument bool, 58, 172
— de fonction, 195 booléen, 58
— effectif, 142, 195 bord
— formel, 142, 195 effet de —, 28, 38, 63, 68
passage d’—, 142 boucle
pointeur en —, 147 instruction de —, 75
arguments Bourne, Stephen, 20, 197
— de main, 39 break, 74, 77
liste variable d’—, 144
nombre variable d’—, 31
ordre d’évaluation des —, 145 –C–
type des — d’une fonction, 142
argv, 39 C
arithmétique — traditionnel, 17
opérateur —, 32 calloc, 161, 172
ascii, 43 caractère
assembleur, 195 — multi-octet, 43
assert, 61, 135 constante —, 52
<assert.h>, 135, 171 caractères
associativité chaîne de —, 88
— des opérateurs, 66 chaîne de — littérale, 26, 48, 120,
atof, 172 143
atoi, 40, 172 case, 73
auto, 159 case-sensitivity, 43
automatique cast, 64, 108
objet —, 158 CC, 21, 115, 180
avant cc, 197
déclaration en —, 30 CDT, 183
CCFLAGS, 21, 180
CFLAGS, 115
–B– champ
— de bits, 93
b, 17 — d’une structure, 90
backquote, 43 sélection de — de structure, 94
base sélection de — d’union, 97
type de —, 202 chaîne
types de —, 50 — de caractères, 88
types scalaires de —, 50–56 — de caractères littérale, 26, 48, 120,
bash, 20 143
bcpl, 17 chaînes
bibliothèque, 196 concaténation des — littérales adja-
— standard, 19, 26 centes, 48, 129

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
INDEX 205

char, 51, 68 — explicite, 64


char*, 120 — lors d’une affectation, 70
code — usuelle, 70
génération de —, 199 spécification de —, 33
Code::Blocks, 183 copie
CodeLite, 183 — de tableau, 85
coloration corps
— syntaxique, 41 — d’un module, 164, 198
commentaire, 26, 31 — d’une fonction, 26, 145, 199
comparaison cpp, 201
— de pointeurs, 106 <ctype.h>, 72, 152, 173
compilation, 196 Cygwin, 20
— conditionnelle, 135–137
— séparée, 18, 156, 198
unité de —, 21, 156 –D–
compl, 176
complément ddd, 1, 180, 185
— à 1, 60 décalage
— à 2, 69 opérateur de —, 61
_Complex, 46 décimale
complex, 173 constante —, 48, 52
<complex.h>, 173 déclaration, 158
composée — de pointeur simple, 103
affectation —, 63 — d’externe, 160
concaténation — d’un objet, 198
— des chaînes littérales adjacentes, — en avant, 30
48, 129 déclaration d’—, 160
opérateur de —, 134 declarationdeclaration
conditionnelle forward —forward —, 30
— compilation, 135–137 décrémentation
expression —, 40, 65, 67 opérateur de —, 64
conjonction, 60 defined, 136
const, 53, 83, 85 définition, 53, 158
constante, 198 — de constante, 132
— caractère, 52 — de fonction, 26, 144
— décimale, 48, 52 — de macro, 131
— entière, 48, 52 — d’un objet, 198
— hexadécimale, 48, 52 — récursive de macro, 134
— lexicale, 198 différence
— littérale, 46–50, 198 — entre tableau et pointeur, 120
— octale, 48, 52 digraph, 44
— réelle, 48 dimension
— statique, 198 — d’un tableau, 83
définition de —, 132 tableau à — variable, 88
pointeur et —, 109 directive
pointeur sur une —, 148 — au préprocesseur, 26
continue, 78 disjonction, 60
contrôle division, 57
flot de —, 198 do ... while, 75
instruction de —, 72–79, 198 dot, 94
conversion, 68 double, 31, 53, 69, 70
— de pointeur, 108 doxygen, 178
— entre entier et réel, 69 durée
— entre entiers, 69 — de vie, 158
— entre réels, 69 dynamique

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



206 INDEX

allocation —, 113, 158, 161 évaluation


opérateur d’— séquentielle, 66, 68
ordre d’— des arguments, 145
–E– exécutable
binaire —, 196
ebcdic, 43 exécution
échappement sémantique d’—, 202
séquence d’—, 129 exit, 31, 79, 172
eclipse, 183, 185 EXIT_FAILURE, 172
ed, 178 EXIT_SUCCESS, 172
éditeur explicite
— de liens, 158 conversion —, 64
édition exponentiation, 57
— de liens, 196, 198 expression
effectif — conditionnelle, 40, 65, 67
argument —, 142, 195 — d’affectation, 28
effet — relationnelle, 28
— de bord, 28, 38, 63, 68 — statique, 65, 73, 199
élément valeur de vérité d’une —, 58
— lexical, 44 expressions, 56–71
ellipse, 144 externe
else, 72 objet —, 160
else if, 72 référence —, 202
emacs, 178, 180, 185
encapsulation, 199
entête –F–
— de fonction, 145
— d’une fonction, 26, 199 F, 53
fichier d’—, 131, 137, 165 f, 53
entier fabs, 32
addition d’un — à un pointeur, 107 false, 172
conversion entre — et réel, 69 fclose, 174
entière <fenv.h>, 172
— promotion, 68 ff, 45
constante —, 48, 52 Fibonacci, Leonardo Pisano dit, 38
entiers, 51 fichier
conversion entre —, 69 — d’entête, 131, 137, 165
enum, 55, 68, 155, 163 — -objet, 158
énuméré inclusion de —, 130, 131
type —, 54 fichier-objet, 196, 199
environnement fichier-source, 196, 199
variable d’—, 21 float, 53, 69, 70
EOF, 29 <float.h>, 53
équivalence flot
— entre tableau et pointeur, 120 — de contrôle, 198
erreur fonction, 141–153, 199
— standard, 32 — en ligne, 132, 146
<errno.h>, 171 — local, 145
espace, 45 — récursive, 38
— de nommage, 155 — retournant un pointeur, 146
et appel de —, 145
— bit à bit, 60 appel d’une —, 26
— logique, 38, 60 argument de —, 195
— séquentiel, 60 corps d’une —, 26, 145, 199
étiquette, 78, 163 définition de —, 26, 144

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
INDEX 207

entête de —, 145 -I, 130


entête d’une —, 26, 199 identificateur, 45, 200
invocation d’une —, 26 ieee, 17
nom de —, 149 if, 31, 72
pointeur sur —, 148 _Imaginary, 46
prototype de —, 201 imbrication
signature d’une —, 26, 202 — de fonctions, 145
type de retour d’une —, 141 impérative
type des arguments d’une —, 142 programmation —, 201
fonctions inclusion
imbrication de —, 145 — de fichier, 130, 131
fopen, 174 — unique, 137
for, 37, 75, 115 incrémentation
form feed, 34 — postfixe, 64
format, 33 — préfixe, 64
formel opérateur d’—, 64
argument —, 142, 195 indexation, 85
fort — de pointeur, 119
typage —, 202 indirection
forward opérateur d’—, 105, 119
— declaration, 30 initialisation, 53
fread, 174 — d’agrégat, 85, 86, 91, 96
free, 158, 172 — de pointeur, 104
Free Software Foundation, 1 — de tableau, 84
fseek, 174 — d’une union, 96
fuite inline, 46, 133, 146, 166
— de mémoire, 180 static —, 146
fwrite, 174 instruction, 26
— de boucle, 75
— de contrôle, 72–79, 198
–G– — de rupture de séquence, 77
— de sélection, 31, 72
gcc, 1, 21, 177, 183 — simple, 29, 71
gdb, 1, 178, 180, 183 instructions, 71–79
gedit, 178 int, 27, 51, 70
génération sous-types de —, 51
— de code, 199 int8_t, 172
générique int32_t, 172
pointeur —, 108 integral type, 51
getchar, 29, 174 interface
global — d’un module, 164
objet —, 159 <inttypes.h>, 172
gnu, 1 Invariant Code Set, 44
goto, 78, 155, 201 invite, 197
gprof, 1, 180 invocation
— d’une fonction, 26
isalpha, 72, 173
–H– isdigit, 72, 173
iso, 17
header-file, 131, 137, 165 iso 646-1983, 44
hexadécimale <iso646.h>, 176
constante —, 48, 52 iso-9899, 17
ht, 45 ispuct, 173
isspace, 72, 173
itération, 75
–I–

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



208 INDEX

–J– long, 51, 52, 70


long double, 53, 70
Java, 18 lvalue, 63, 142, 201

–K– –M–
kate, 178 machine
kde, 184 langage- —, 200
kdevelop, 184 MacOs X, 20
Kernighan, Brian W., 17 macro, 58, 61, 84, 131–135
Korn, David, 197 définition de —, 131
ksh, 20 définition récursive de —, 134
variadic —, 132
main, 26, 151, 172
–L– arguments de —, 39
majuscule
L, 52 — et minuscule, 43
l, 52 make, 21, 27, 115, 178, 179, 183, 185
LANG, 20 Makefile, 21
langage-machine, 200 malloc, 113, 125, 158, 161, 172, 180
ld, 196, 199 <math.h>, 173
lexical mélange
éément —, 44 — de types, 70
lexicale memcmp, 174
analyse —, 195 memcpy, 85, 174
constante —, 198 mémoire
library, 200 — partagée, 54
standard —, 19 fuite de —, 180
liens taille —, 65
éditeur de —, 158 Microsoft Windows, 20
édition de —, 196, 198 minuscule
ligne majuscule et —, 43
fonction en —, 132, 146 modifieur
ligne-suite, 129 — de type, 53
<limits.h>, 52 modulaire
Linux, 1 programmation —, 164
liste, 111 modularité, 18, 200
— variable d’arguments, 144 module, 158, 164, 200
littérale corps d’un —, 164, 198
chaîne de caractères —, 26, 48, 120, interface d’un —, 164
143 spécification d’un —, 202
constante —, 46–50, 198 modulo, 57
local mono-dimensionnel
fonction —, 145 tableau—, 83
objet —, 159 multi-dimensionnel
locale tableau—, 86
variable —, 71, 88, 92 multi-octet
<locale.h>, 174, 175 caractère —, 43
logique multiple
— et, 60 pointeur —, 105
— opérateur, 58 multiplication, 57
— ou, 60
et —, 38
ou —, 38 –N–

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
INDEX 209

,NDEBUG, 171 asociativité des —, 66


négation, 60 précédence des —, 28, 66
newline, 27 priorité des —, 66
Newton, Sir Isaac, 29 optimisation, 200
bit field, 93 or, 176
nl, 45 or_eq, 176
nom ordre
— de fonction, 149 — d’évaluation des arguments, 145
— de tableau, 120 ou
— de type, 64 — bit à bit, 60
— d’un objet, 200 — exclusif, 60
nombre — logique, 38, 60
— variable d’arguments, 31 — séquentiel, 60
nommage
espace de —, 155
not_eq, 176 –P–
NULL, 104–106, 112
nul, 34, 49 padding, 93
NULLNULL, 171 panorama
— des types, 50
partagée
–O– mémoire —, 54
Pascal, 18
objet, 200 passage
— automatique, 158 — d’argument, 142
— externe, 160 — par valeur, 142
— global, 159 pointeur, 18, 31, 103–122, 201
— local, 159 — en argument, 147
— privé, 160 — et constante, 109
— rémanent, 159 — générique, 108
— statique, 158 — multiple, 105
adresse d’un —, 31 — simple, 103
binaire —, 199 — sur fonction, 148
déclaration d’un —, 198 — sur une constante, 148
définition d’un —, 198 — sur void, 108
fichier- —, 158, 196, 199 addition d’un entier à un —, 107
nom d’un —, 200 conversion de —, 108
octale déclaration de — simple, 103
constante —, 48, 52 différence entre tableau et —, 120
odr, 158 équivalence entre tableau et —, 120
Onedefinition Rule, 158 fonction retournant un —, 146
opérateur indexation de —, 119
— adresse, 104 initialisation de —, 104
— arithmétique, 32 tableau de —, 118
— bit à bit, 60 pointeurs
— de concaténation, 134 addition de —, 108
— de décalage, 61 comparaison de —, 106
— de décrémentation, 64 soustraction de —, 107
— de sélection, 110 point-virgule, 71
— d’évaluation séquentielle, 66, 68 portabilité, 18
— d’incrémentation, 64 Posix, 171, 175
— d’indirection, 105, 119 postfixe
— logique, 58 incrémentation —, 64
— relationnel, 58 #pragma, 172
opérateurs, 56–71 précédence

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



210 INDEX

— des opérateurs, 28, 66 relationnel


préfixe — opérateur, 58
incrémentation —, 64 relationnelle
préprocesseur, 26, 129–138, 201 expression —, 28
directive au —, 26 rémanent
prétraitement, 129 objet —, 159
printf, 26, 32, 144, 174 résolue
priorité référence non —, 202
— des opérateurs, 66 reste
privé signe du —, 58
objet —, 160 restrict, 46
procédurale retour
programmation —, 18, 201 type de — d’une fonction, 141
procédure, 37, 141, 201 return, 32, 35, 79
programmation Ritchie, Dennis, 1, 17
— impérative, 201 rupture
— modulaire, 164 instruction de — de séquence, 77
— procédurale, 18, 201 rvalue, 201
— structurée, 18, 201
promotion
— entière, 68 –S–
prompt, 197
prototype, 26, 143, 144 scalaire
— de fonction, 201 type —, 50
putchar, 29, 174 scalaires
types — scalaires de base, 50–56
scanf, 31, 174
–Q– scope, 159
sélection
quotient — de champ de structure, 94
signe du —, 58 — de champ d’union, 97
instruction de —, 31, 72
opérateur de —, 110
–R– sémantique
— d’exécution, 202
rand, 172 — statique, 195
record, 18, 90, 97 séparée
récursif compilation —, 18, 156, 198
type —, 111 séquence
récursive — d’échappement, 129
définition — de macro, 134 instruction de rupture de —, 77
fonction —, 38 séquentiel
récursivité, 39 — et, 60
redirection — ou, 60
— du shell, 29 séquentielle
réel opérateur d’évaluation —, 66, 68
conversion entre entier et —, 69 sh, 20
réelle shell, 20, 31, 39
constante —, 48 redirection du —, 29
réels, 53 short, 51, 68
conversion entre —, 69 <signal.h>, 175
référence, 63, 85, 94, 97, 201 signature
— externe, 202 — d’une fonction, 26, 202
— non résolue, 202 signe
register, 159 — du quotient, 58

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010
INDEX 211

— du reste, 58 sélection de champ de —, 94


signed, 51 tableau de —, 95
simple structurée
affectation —, 63 programmation —, 18, 201
déclaration de pointeur —, 103 surcharge, 173
instruction —, 29, 71 switch, 73, 98
pointeur —, 103 syntaxique
size_t, 171 analyse —, 195
sizeof, 85, 89, 93, 171 coloration —, 41
sortie
— standard, 26
source –T–
fichier- —, 196, 199
soustraction, 57 tab, 34, 45
— de pointeurs, 107 tableau, 83–90
sous-types — à dimension variable, 88
— de int, 51 — de pointeur, 118
spécification — de structure, 95
— de conversion, 33 — mono-dimensionnel, 83
— d’un module, 202 — multi-dimensionnel, 86
srand, 172 (, 83
Stallman, Richard, 1 affectation de —, 85
standard copie de —, 85
— library, 19 différence entre — et pointeur, 120
bibliothèque —, 19, 26 dimension d’un —, 83
erreur —, 32 équivalence entre — et pointeur,
sortie —, 26 120
static, 146, 159–161, 166 initialisation de —, 84
— inline, 146 nom de —, 120
statique tag, 55, 90, 111, 155, 163
analyse —, 195 taille
constante —, 198 — mémoire, 65
expression —, 65, 73, 199 <tgmath.h>, 173
objet —, 158 Thomson, Ken, 17
sémantique —, 195 <time.h>, 175
-std=c99, 21 token, 129, 195
<stdarg.h>, 144 tolower, 152, 173
<stdbool.h>, 172 toupper, 173
<stddef.h>, 171 traditionnel
stderr, 32 C —, 17
stdin, 27 trigraph, 44, 129
<stdint.h>, 172 true, 172
<stdio.h>, 26, 174 toupper, 152
<stdlib.h>, 104, 172 typage
stdout, 26, 27 — fort, 202
strcat, 173 type, 202
strcmp, 173 — anonyme, 55, 91
strcpy, 173 — de base, 202
<string.h>, 173 — de retour d’une fonction, 141
stringification, 134 — des arguments d’une fonction,
strlen, 88 142
struct, 90 — enuméré, 54
structure, 90–95 — récursif, 111
— avec variante, 97 — scalaire, 50
champ d’une —, 90 — utilisateur, 83, 202

V 2.1 – 7 février 2010 c Jean-Paul R IGAULT



212 INDEX

integral —, 51 nombre — d’arguments, 31


modifieur de —, 53 tableau à dimension —, 88
nom de —, 64 variadic
typedef, 55, 155, 163 — macro, 132
types variant, 97
— de base, 50 variante
— scalaires de base, 50–56 structure avec —, 97
— utilisateur, 50 vérité
mélange de —, 70 valeur de —, 72
panorama des —, 50 valeur de — d’une expression, 58
vi, 178
vie
–U– durée de —, 158
vim, 178
U, 52 visibilité, 159
u, 52 Visual Studio, 20, 23
uint64_t, 172 void, 37, 50, 79, 141, 143, 201
unaire pointeur sur —, 108
- —, 57 void*, 108
& —, 104 volatile, 54, 85
* —, 105 vt, 45
+ —, 57
Unicode, 174
Unicode, 20, 43, 45 –W–
union, 96–99
initialisation d’une —, 96 -Wall, 21
sélection de champ de —, 97 warning, 21
unique wchar_t, 174
inclusion —, 137 <wchar.h>, 174
unsigned, 51 <wctype.h>, 174
unité while, 32, 75
— de compilation, 21, 156
Unix, 17
unsigned, 52, 69 –X–
unsigned int, 70
unsigned long, 70 X Window, 1
usuelle x3j11, 1, 17
conversion —, 70 XCode, 184
utf-8, 20 xemacs, 178
utilisateur xor, 176
type —, 83, 202 xor_eq, 176
types —, 50 xxgdb, 180, 185

–V– –Z–
valeur, 202 zsh, 20
— de vérité, 72
— de vérité d’une expression, 58
passage par —, 142
valgrind, 2
variable, 202
— d’environnement, 21
— locale, 71, 88, 92
liste — d’arguments, 144

c Jean-Paul R IGAULT
V 2.1 – 7 février 2010

Vous aimerez peut-être aussi