Vous êtes sur la page 1sur 248

INSTITUT DE TECHNOLOGIE

DUT Génie Electrique et Informatique Industrielle

PROGRAMMATION EN C/C++

Polycopié de cours

C.ALEXANDRE – C.PAUTOT
1. INTRODUCTION ....................................................................................................................................... 1
1.1 L’informatique ............................................................................................................ 1
1.2 Le système de traitement de l’information ................................................................. 1
1.3 Le codage de l’information......................................................................................... 2
1.4 L’ordinateur ................................................................................................................ 5
1.5 Le système d’exploitation........................................................................................... 6
1.6 Les applications logicielles ......................................................................................... 9
1.7 Le système de fichiers .............................................................................................. 10
1.8 Les répertoires (syntaxe Unix) ................................................................................. 10
1.9 Partitionnement et montage ...................................................................................... 11
1.10 Informations associées aux fichiers .......................................................................... 13
1.11 Protection des fichiers (Unix) ................................................................................... 14
1.12 Tableau comparatif des systèmes de fichiers ........................................................... 16
1.13 Les fichiers textes ..................................................................................................... 16
2. LANGAGES DE PROGRAMMATION .................................................................................................. 19
2.1 Définitions ................................................................................................................ 19
2.2 Méthodologie pour l’écriture d’un programme ........................................................ 20
2.3 Le langage C ............................................................................................................. 21
3. BASES DU LANGAGE C ......................................................................................................................... 23
3.1 Les variables ............................................................................................................. 23
3.2 L’instruction d’affectation ........................................................................................ 24
3.3 Les types entier ......................................................................................................... 27
3.4 Les types flottants (réels).......................................................................................... 29
3.5 Les conversions de type............................................................................................ 30
3.6 Les types char ........................................................................................................... 32
3.7 Communiquer avec le programme : les entrées-sorties standard ............................. 33
3.8 L’instruction printf ................................................................................................... 37
3.9 L’instruction scanf .................................................................................................... 40
3.10 Structure de choix : l’instruction if ........................................................................... 42
3.11 Structure de choix : les conditions en C ................................................................... 45
3.12 Structure de choix : l’instruction switch ................................................................... 49
3.13 Structure de répétition conditionnelle : l’instruction do... while .............................. 51
3.14 Structure de répétition conditionnelle : l’instruction while... ................................... 52
3.15 Structure de répétition inconditionnelle : l’instruction for... .................................... 53
3.16 Algorithmes élémentaires ......................................................................................... 56
4. LES FONCTIONS ..................................................................................................................................... 59
4.1 Introduction .............................................................................................................. 59
4.2 Premier exemple ....................................................................................................... 60
4.3 Fonction sans résultat ou sans paramètres ................................................................ 64
4.4 L’instruction return .............................................................................................. 66
4.5 Variables globales et locales..................................................................................... 67
4.6 Variable statique ....................................................................................................... 70
4.7 La récursivité ............................................................................................................ 71
4.8 Passage des paramètres par valeur. .......................................................................... 72
4.9 Les pointeurs ............................................................................................................ 74
4.10 Passage de pointeurs comme paramètres d’une fonction ......................................... 83
5. LES TABLEAUX ....................................................................................................................................... 85

i
5.1 Tableaux à une dimension........................................................................................ 85
5.2 Remarques importantes sur les tableaux .................................................................. 90
5.3 Les tableaux à deux dimensions............................................................................... 93
5.4 Passage d’un tableau comme paramètre d’une fonction .......................................... 97
5.5 Relations entre tableaux et pointeurs ..................................................................... 100
5.6 Allocation dynamique de la mémoire .................................................................... 104
6. LES CHAINES DE CARACTERES ...................................................................................................... 107
6.1 Déclaration ............................................................................................................. 107
6.2 Lire ou écrire des chaînes....................................................................................... 110
6.3 Connaître la longueur d’une chaîne ....................................................................... 117
6.4 Copier une chaîne dans une autre chaîne ............................................................... 118
6.5 Comparer deux chaînes .......................................................................................... 119
6.6 Concaténer deux chaînes ........................................................................................ 120
6.7 Rechercher un caractère dans une chaîne .............................................................. 121
6.8 Rechercher une chaîne dans une autre chaîne ........................................................ 121
6.9 Fonctions diverses .................................................................................................. 123
6.10 Le passage d’une chaîne comme paramètre d’une fonction. ................................. 124
6.11 Les tableaux de chaînes de caractères. ................................................................... 126
7. LES PARAMETRES DE LA FONCTION MAIN. ............................................................................... 129

8. LES STRUCTURES ................................................................................................................................ 133


8.1 Définition ............................................................................................................... 133
8.2 Transmission d’une structure en paramètre d’une fonction ................................... 137
8.3 La définition de types nouveaux ............................................................................ 140
9. LES FICHIERS ........................................................................................................................................ 143
9.1 Introduction ............................................................................................................ 143
9.2 Ouverture et fermeture d’un flux ........................................................................... 145
9.3 Buffers associés aux flux ....................................................................................... 149
9.4 Lecture et écriture dans un flux.............................................................................. 150
9.4.1 Les lectures et écritures par caractère ............................................................ 150
9.4.2 Les lectures et écritures par ligne ................................................................... 151
9.4.3 Les lectures et écritures formatées ................................................................. 153
9.4.4 Les lectures et écritures binaires .................................................................... 155
9.5 Positionnement dans un flux .................................................................................. 158
9.6 Utilisation d’un fichier de configuration ................................................................ 160
10. DIVERS ................................................................................................................................................ 163
10.1 Exécution de commandes....................................................................................... 163
10.2 Les opérateurs binaires........................................................................................... 164
10.3 Les énumérations ................................................................................................... 165
10.4 Les opérateurs d’incrémentation et de décrémentation.......................................... 166
10.5 L’opérateur virgule ................................................................................................ 168
10.6 L’opérateur conditionnel ? ..................................................................................... 168
10.7 Les macros avec paramètres .................................................................................. 169
10.8 Ce que vous ne verrez pas ...................................................................................... 172
10.9 Définition de macro à l’invocation du compilateur ............................................... 172
10.10 Compilation conditionnelle ................................................................................ 173
11. EDITION DE LIEN ............................................................................................................................ 179

ii
11.1 Les pointeurs de fonction ....................................................................................... 179
11.2 Notion de processus ................................................................................................ 182
11.3 Les zones mémoires d’un processus ....................................................................... 183
11.4 Projets multi-fichiers : édition de liens ................................................................... 186
12. LES LIBRAIRIES ............................................................................................................................... 195
12.1 Les bibliothèques statiques (archive) ..................................................................... 195
12.2 Les bibliothèques dynamiques (partagées) ............................................................. 196
12.3 Avantages et inconvénients des bibliothèques dynamiques ................................... 200
12.4 La bibliothèque standard du C ................................................................................ 202
12.4.1 Les entrées-sorties <stdio.h> .......................................................................... 202
12.4.2 Les fonctions mathématiques <math.h> ......................................................... 203
12.4.3 Les manipulations de caractères <ctype.h> .................................................... 204
12.4.4 Les manipulations de chaînes <string.h> ....................................................... 204
12.4.5 Manipulations de l’heure <time.h> ................................................................ 205
12.4.6 Diverses fonctions utilitaires <stdlib.h> ......................................................... 205
13. INTRODUCTION A LA PROGRAMMATION EN C++ ............................................................... 207
13.1 Généralités .............................................................................................................. 207
13.2 Intérêt de la conception objet.................................................................................. 207
13.3 Un exemple de programmation classique............................................................... 208
13.4 Les classes .............................................................................................................. 213
13.4.1 Définition ........................................................................................................ 213
13.4.2 Syntaxe ........................................................................................................... 213
13.4.3 Constructeur ................................................................................................... 214
13.4.4 Destructeur...................................................................................................... 214
13.4.5 Restriction d’accès .......................................................................................... 214
13.4.6 Les fonctions (méthodes) de la classe ............................................................ 215
13.4.7 Organisation en fichiers source et header ....................................................... 216
13.5 Création d’un objet ................................................................................................. 217
13.5.1 Au moyen d’une déclaration .......................................................................... 217
13.5.2 Avec l’opérateur new...................................................................................... 217
13.6 Manipulation des objets .......................................................................................... 219
13.6.1 Accès à une variable ....................................................................................... 219
13.6.2 Accès à une fonction....................................................................................... 219
13.7 Surcharge des fonctions et des opérateurs .............................................................. 221
13.8 Passage par référence.............................................................................................. 224
13.9 Héritage et composition .......................................................................................... 225
13.9.1 Introduction .................................................................................................... 225
13.9.2 La composition ............................................................................................... 226
13.9.3 L’héritage........................................................................................................ 226
13.9.3.1 Principe de l’héritage .............................................................................. 226
13.9.3.2 Restriction d’accès .................................................................................. 228
13.9.3.3 Substitution des membres hérités ........................................................... 229
13.9.3.4 Gestion des constructeurs ....................................................................... 230
13.9.3.5 Fonctions virtuelles ................................................................................ 232
13.10 Les flux d’entrée sortie ....................................................................................... 235
13.10.1 Généralités .................................................................................................. 235
13.10.2 Opérateur d'insertion et d’extraction de flux .............................................. 235
13.10.2.1 Extraction de flux >> ............................................................................. 235
13.10.2.2 Insertion de flux >> ................................................................................ 235

iii
13.10.3 Modification des formatages ...................................................................... 236
13.10.3.1 Liste des drapeaux .................................................................................. 236
13.10.3.2 Fonctions permettant de modifier les drapeaux ..................................... 237
13.10.3.3 Formatage de la sortie ............................................................................ 237
13.10.4 Manipulateurs non paramétriques .............................................................. 238
13.10.5 Entrée et sortie non formatées .................................................................... 239
13.10.5.1 La fonction get() ................................................................................ 239
13.10.5.2 La fonction getline() ...................................................................... 240
13.10.5.3 La fonction read() .............................................................................. 240
13.10.6 Fonctions de sortie non formatées ............................................................. 241
13.10.6.1 La fonction put ..................................................................................... 241
13.10.6.2 La fonction write ................................................................................ 241
13.10.7 Les fonctions de manipulations évoluées................................................... 241
13.11 Bibliographie ...................................................................................................... 242

iv
1. Introduction

1.1 L’informatique
C’est la manipulation de l’information à l’aide d’un ordinateur.

Ordinateur : machine électronique programmable destinée au traitement de l’information


numérique.

Information : texte, son, image, données binaires (produites par un système électronique,
utilisées par un système électronique), …

1.2 Le système de traitement de l’information

Applications Logicielles

Interface de programmation
Logiciels (software)
Système d’exploitation
Interface matériel/logiciel
Dispositif électronique
ordinateur
matériel (hardware)

Interface de programmation, l’Application Programming Interface (API) : exemple, l’API


Win32 pour Windows.

Interface matériel/logiciel : exemple, le Basic Input Output System (BIOS). Jusqu’à MS-
DOS, le BIOS sert d’interface entre l’ordinateur et le système d’exploitation. Cette méthode
n’est plus utilisée aujourd’hui, l’interface est intégrée au système d’exploitation (voir HAL
sous NT).

1
1.3 Le codage de l’information
Un bit (contraction de Binary digIT) est un chiffre pouvant prendre la valeur 0 ou 1 (base 2).

Dans un ordinateur, un bit est représenté par deux niveaux de tension électrique. Un nombre
binaire est une suite de bits (comme un nombre décimal est une suite de chiffres compris entre
0 et 9).

En décimal (base 10) :

Le nombre 259 = 2*102 + 5*101 + 9*100 = 2*100 + 5*10 + 9*1

En binaire (base 2) :

Le nombre 10101 = 1*24 + 0*23 + 1*22 + 0*21 + 1*20 = 1*16 + 1*4 + 1*1

Un octet (ou byte) est une suite de 8 bits :

L’octet 10000001 = 1*27 + 1*20 = 129

La valeur d’un octet est comprise entre 0 et 255. Pour raccourcir l’écriture, on utilise la
notation hexadécimale (base 16).

Un chiffre en base 16 peut prendre 16 valeurs allant de 0 à 15. Il peut être codé avec 4 bits.
Comme les chiffres s’arrêtent à 9 en décimal, on utilise les lettre a, b, c, d ,e et f pour
représenter les derniers états.

2
décimal binaire hexadécimal
0 0000 0
1 0001 1
2 0010 2
3 0011 3
4 0100 4
5 0101 5
6 0110 6
7 0111 7
8 1000 8
9 1001 9
10 1010 a
11 1011 b
12 1100 c
13 1101 d
14 1110 e
15 1111 f

L’octet 10000001 = 81 en hexa

Notation pour les bases :

(10000001)b = (81)h = (129)d

101011002 = ac16 = 17210

On utilise couramment les multiples suivants :

multiple valeur
Kilo 210 = 1024
Méga 220 = 1048576 = 1024 kilo
Giga 230 = 1073741824 = 1024 méga
Tera 240 = 1099511627776 = 1024 giga

3
Exemples :

1 kilobit = 1024 bits

128 mégaoctet = 128*1024*1024 octets

Attention : en informatique

1 kilo ≠ mille

1 méga ≠ 1 million

1 giga ≠ 1 milliard

Changement de base :
• Décimal vers binaire

47 2
1 23 2
1 11 2
1 5 2
1 2 2
0 1 2
1 0

4710 = 1011112

• Binaire vers décimal

1 x 25 + 0 x 24 + 1 x 23 + 1 x 22 + 1 x 21 + 1 x 20 = 4710

4
• Hexadécimal vers binaire

ab8516 = 1010 1011 1000 01012

• Binaire vers hexadécimal

1001 0000 1010 11112 = 90af16

Pour passer d’hexadécimal en décimal (et vice versa), vous pouvez passer par l’intermédiaire
du binaire ou faire le calcul directement.

Exercice 1.1 :

1) Quel est le nombre décimal le plus grand que l’on peut coder avec 4 bits, 8 bits, 16 bits, 32
bits.

2) Convertissez 11011012 en décimal.

3) Convertissez 1910 et 4510 et 6310 en binaire.

4) Convertissez 11001010010101112 en hexadécimal.

5) Convertissez 10A416 et CF8E16 et 974216 en binaire et en décimal.

1.4 L’ordinateur
Un ordinateur :
• Traite l’information grâce à un programme qu’il mémorise,
• Communique et archive des informations.

Il est constitué de trois éléments :

5
1. La mémoire centrale (vive) qui permet de stocker les programmes pendant le temps
nécessaire à leur exécution ainsi que les informations temporaires manipulées par ces
programmes. La mise hors tension de l’ordinateur efface le contenu de la mémoire vive.
2. L’unité centrale qui exécute les instructions contenues dans le programme qui se trouve en
mémoire vive.
3. Les périphériques qui échangent des informations avec l’unité centrale. On en trouve de
deux sortes :
• Les périphériques de communication : clavier, écran, souris, imprimante, modem, carte
son, carte réseau, …
• Les périphériques d’archivage : disque dur, disquette, CD-ROM, bande magnétique, ...
ils assurent le stockage permanent des données et des programmes.

Exemple : un PC.
Processeur Intel pentium IV, fréquence 3 GHz.
Mémoire vive 512 Mo.
Disque dur 200 Go.
Lecteur de DVD -ROM + graveur.
Lecture de disquette 3,5 pouces.
Ecran 17 pouces LCD.
Carte son.
Carte réseau.
Clavier, souris.

1.5 Le système d’exploitation


C’est un ensemble de programmes qui servent :
• à gérer les ressources de l’ordinateur et notamment à assurer leur partage harmonieux entre
les différents programmes.
• à présenter à l’utilisateur et aux programmes une interface plus facile à utiliser que la
machine physique. Cette interface est celle d’une machine virtuelle qui cache la complexité
du matériel. Elle sert :
d’interface entre l’ordinateur et les applications logicielles (API).
d’interface entre l’ordinateur et l’utilisateur (IHM).

6
L’utilisateur d’un ordinateur effectue généralement les tâches suivantes :
1. il développe un programme,
2. ou bien il utilise un programme pour générer ou manipuler de l’information.

Pour faciliter ces opérations, le système d’exploitation peut utiliser deux sortes d’interface
avec l’utilisateur :

1. L’interface graphique (mode graphique). Elle utilise la souris, les icônes et les menus
déroulants. Très conviviale elle ne permet pas l’automatisation des traitements.
Exemples : MAC OS, Windows, …

2. L’interpréteur de commandes textuel (mode ligne de commande ou console ou terminal).


L’utilisateur manipule ici les données à l’aide de commandes tapées au clavier. On
l’appelle un shell sous unix ou command.com sous MS-DOS. L’interpréteur de
commande est peu intuitif car l’utilisateur doit connaître les commandes pour pouvoir
l’utiliser mais il est en revanche plus puissant car programmable.

Le système d’exploitation comprend 4 parties essentielles :


1. La gestion des programmes.
2. Les entrées/sorties.
3. La gestion de la mémoire.
4. Le système de fichiers.

Caractéristiques :

⇒ La mémoire virtuelle. La taille des programmes et des données manipulées en mémoire


vive dépasse généralement la quantité de mémoire physiquement disponible dans
l’ordinateur. Pour augmenter la quantité de mémoire disponible, le système d’exploitation
va garder en mémoire vive les parties actives du programme et des données et stocker le
reste sur le disque dur. Un mécanisme de pagination permet de charger en mémoire
quand il le faut les parties de programme à exécuter. Le nombre de bits codant l’adresse
maximale de la mémoire utilisable par un programme caractérise le système
d’exploitation (16, 32 ou 64 bits). La MMU est chargé de traduire les adresses
virtuelles en adresses physiques.

7
⇒ La gestion des programmes. Le système monotâche ne peut exécuter qu’un seul
programme à la fois même si plusieurs programmes peuvent être présents à la fois en
mémoire (permutation). Un système multitâches préemptif fait tourner plusieurs
programmes en même temps grâce à un planificateur (scheduler) qui attribue à chacun
des programmes un petit laps de temps (slice time) pour s’exécuter. Tous les programmes
se trouvent dans une file d’attente et attendent leur tour. Les programmes les plus
prioritaires reviennent plus souvent dans la queue et donc s’exécutent plus rapidement.
Un système multitâches coopératif est un système monotâche (pas de scheduler) car ce
sont les programmes qui sont conçus pour s’arrêter de temps en temps pour passer à un
autre programme.

⇒ La gestion des utilisateurs. Un système multi-utilisateurs permet à plusieurs utilisateurs


d’accéder simultanément à l’ordinateur pour exécuter des taches différentes. Le système
doit distinguer les différents utilisateurs en les dotant d’un nom et d’un mot de passe ainsi
que d’un espace disque (et d’un espace mémoire) réservé. C’est le compte utilisateur. Le
système est forcément multitâches préemptif. Un système monotâche est forcément
mono-utilisateur.

Caractéristiques des principaux systèmes :

MS-DOS Mono-tache Mono-utilisateur 16 (20) bits

MAC OS 9 Mono-tache Mono-utilisateur 32 bits

Unix multi-tâches Multi-Utilisateurs 32 bits ou 64 bits

VMS multi-tâches Multi-Utilisateurs 32 bits

Windows 95 et 98 multi-tâches Mono-utilisateur 32 bits + 16 bits

Windows NT multi-tâches Mono-utilisateur 32 bits

Multi-Utilisateurs

Windows 2000 multi-tâches Multi-Utilisateurs 32 bits ou 64 bits

Windows XP

Vista

8
1.6 Les applications logicielles
Ce sont des programmes. Exemples :
• Bureautique : traitement de texte, tableur, gestion de base de données, …
• Gestion et comptabilité : facturation, paye, stocks, …
• Jeux vidéo,
• Navigation Internet,
• Prévisions météorologiques,
• Conception assistée par ordinateur (CAO),
• Gestion d’une chaîne de fabrication,
• Simulateur de vol,
• …

Un ordinateur est capable de mettre en mémoire un programme (résidant généralement sur le


disque dur), puis de l’exécuter.

Un programme est constitué d’instructions qui spécifient :


• Les opérations élémentaires que va exécuter l’ordinateur,
• La manière dont elles s’enchaînent.

données programme résultats

Les données d’entrée du programme peuvent être fournies manuellement ou lues sur un
disque dur (base de données) ou bien sur des capteurs.

Les résultats fournis par le programme peuvent être lus directement par l’utilisateur (sous
forme de textes ou de graphiques) ou bien stockés sur disque.

Si le temps de réaction du programme entre un changement sur une entrée (capteur) et le


résultat en sortie (actuateur) doit être garanti, alors on dit que le programme s’exécute en
temps réel.

9
1.7 Le système de fichiers
On ne peut pas maintenir toutes les informations utiles d’un ordinateur en mémoire vive. Il
faut les sauvegarder sur un support qui stocke l’information même lorsqu’il est hors tension
(un disque dur par exemple). Le système de fichiers est l’ensemble des mécanismes destiné à
manipuler de l’information sur ce support.

Un fichier est un objet qui peut contenir des programmes, des données ou tout autre type
d’information. Le système d’exploitation fournit des opérations spéciales, les appels système,
pour les créer, les détruire, les écrire ou les modifier.

Le système de fichiers est, avec le bureau, la partie la plus visible du système d’exploitation.
La plupart des programmes lisent ou écrivent au moins un fichier et les utilisateurs
manipulent beaucoup de fichiers. L’utilisateur attache une grande importance à l’interface du
système de fichiers, c’est-à-dire aux fichiers, à la manière de les nommer et de les protéger,
aux opérations permises sur les fichiers, …

1.8 Les répertoires (syntaxe Unix)


Le système de fichiers range les fichiers dans des répertoires (directories) ou dossiers. Un
répertoire contient un certain nombre d’entrées, une par fichier ou par répertoire. Etant donné
la très grande quantité de fichiers à gérer sur un ordinateur (plusieurs centaines de milliers), il
faut un système de classement performant. Une organisation hiérarchique constituée d’une
arborescence de répertoires permet d’avoir autant de répertoires qu’il est nécessaire afin de
regrouper les fichiers logiquement. Un répertoire peut contenir soit des fichiers, soit
d’autres répertoires.

10
Le premier répertoire dans la hiérarchie (ici /) s’appelle le répertoire racine (root).
L’utilisateur peut définir le répertoire dans lequel il veut travailler, le répertoire de travail
(working directory) ou répertoire courant (il y a une valeur par défaut spécifiée par le
système d’exploitation). Il existe deux méthodes pour spécifier l’emplacement (chemin
d’accès ou path) d’un fichier :

• Le chemin d’accès absolu. C’est le chemin spécifié à partir du répertoire racine.

Ex : /usr/local/bin/nom_de_fichier.

Le / (slash) représente soit le répertoire racine s’il est au début du chemin d’accès, soit un
séparateur qui indique un changement de niveau.

• le chemin d’accès relatif. C’est le chemin spécifié à partir du répertoire courant.

Exemple : le répertoire courant est /usr/local/bin. On veut accéder à un fichier se trouvant


dans /usr/bin

../../bin/ nom_de_fichier

.. désigne le répertoire père (répertoire juste au-dessus dans l’arborescence).


. désigne le répertoire dans lequel vous êtes.

Exercice 1.2 : le répertoire courant est /usr/local. Donnez le chemin d’accès absolu et relatif
des répertoires man, Yves et etc.

1.9 Partitionnement et montage


Il est possible de diviser un disque en plusieurs morceaux (des partitions) en effectuant un
partitionnement. Cela permet, par exemple, d’installer plusieurs systèmes d’exploitation (OS
= operating system) sur un disque.

Sur les OS Microsoft, on affecte aux partitions une lettre appelée lettre de lecteur. Chaque
partition sera appelée C, D, E, … Les lettres A et B sont généralement affectées aux lecteurs
amovibles.

11
Un chemin d’accès absolu sera nommé : C:\users\dut_info. Le répertoire racine est précédé
de la lettre du lecteur suivie de :. Le slash (/) d’Unix est remplacé par un backslash (\). Le
chemin d’accès relatif sous Windows est le même que sous Unix, mais avec un \ à la place
d’un /.

Sous Unix, il n’y a pas de lettre de lecteur pour identifier un lecteur physique (disque dur ou
disquette) ou bien une partition. Tout est monté sous le répertoire racine /.

Partition 0 /
montage
usr etc tmp home

lib bin include


dut tpb
Partition 1

12
L’opération de montage sert à indiquer au système à quel niveau de l’arborescence se placent
les différents disques et partitions. Dans l’exemple précédent, la partition utilisateurs
(partition 1) est rattachée (montée) au répertoire home de la partition racine (partition 0). Un
lecteur de DVD-ROM sera par exemple monté sous le répertoire mount (/mnt/cdrom).

Sous Unix, il faut ajouter à la notion de répertoire de travail (working directory) celle de
répertoire privé (home directory). Le répertoire privé est le répertoire dans lequel
l’utilisateur se retrouvera lors de sa connexion au système. C’est l’emplacement où il
enregistrera ses fichiers par défaut. Cette notion n’a pas de sens avec Windows 98 ni même
avec Windows NT (NT ne créé pas automatiquement un répertoire par utilisateur), mais existe
sous XP et Vista.

1.10 Informations associées aux fichiers


L’entrée du répertoire contient des informations associées au fichier telles que :

• Le nom du fichier. La FAT-16 (MS-DOS) ne permet que des noms de 8 caractères suivis
d’une extension de trois caractères (format 8+3).

Exemple : essai.txt

L’extension indique le type du fichier. Seuls les .bat, .exe et .com sont exécutables. La
FAT-32 et NTFS (XP, Vista) autorisent les noms longs.

Unix permet des noms longs de 255 caractères avec différentiation des majuscules et des
minuscules. Il n’y a pas d’extension, le . est un caractère comme un autre. Un attribut
décide de la possibilité pour un programme de s’exécuter ou non.

• La date et l’heure de la dernière modification.


• La taille du fichier en octets.
• Les attributs de protection (sous Unix).
• Les numéros de bloc contenant les données du fichier. Ces informations ne sont pas
visibles par l’utilisateur.

13
Exemple d’informations retournées par une commande dir sous Windows NT :

Le volume dans le lecteur D s'appelle MainNT


Le numéro de série du volume est EB2A-9B11

Répertoire de D:\users\dut_info

09/10/00 16:36 <DIR> .


09/10/00 16:36 <DIR> ..
09/10/00 16:35 <DIR> essai
09/10/00 08:53 21 fichier_dos.txt
09/10/00 08:53 20 fichier_unix.txt
09/10/00 16:36 531 result.txt

6 fichier(s) 41 octets

8 242 147 328 octets libres

Nous sommes sur le disque D dans le répertoire \users\dut_info. <DIR> indique la présence
d’un répertoire. Le . représente le répertoire courant. Le .. représente le répertoire père.

1.11 Protection des fichiers (Unix)


La protection des fichiers est inexistante sous Windows 98 (il n’y a en général qu’un
utilisateur sur le PC), correcte sous Windows XP et Vista (avec NTFS mais pas avec la FAT-
32 utilisée par défaut) et bonne sous Unix.

Sous Unix. Chaque utilisateur fait partie d’un groupe de travail. Les protections d’un fichier
concernent :

Le propriétaire du fichier Users (u)


Le groupe d’utilisateurs Group (g)
Les autres utilisateurs Others (o)

Les opérations concernées par les permissions rwx sont :

La lecture read (r)


L’écriture write (w)
L’exécution execute (x)

14
Ces permissions rwx ont une signification particulière lorsqu’il s’agit d’un répertoire :
• r : ce droit permet uniquement de lire les noms des fichiers du répertoire.
• w : cette permission autorise la création et la destruction de fichiers dans ce répertoire.
• x : cette permission indique que l’on pourra passer par ce répertoire (avec une commande
cd).

Exemple du contenu d’un répertoire sous Unix :

d : directory
user group taille Nom
l : lien symbolique

drwxr-xr-x 4 dut dut 4096 jui 12 15:30 Desktop/


-rw-r--r-- 1 dut dut 18 oct 9 09:26 essai.txt
drwx------ 2 dut dut 4096 oct 9 09:48 nsmail/
-rw-r--r-- 1 dut dut 0 oct 9 14:03 result.txt
drwx------ 2 dut dut 4096 oct 9 09:48 tmp/
-rwxr--r-- 1 dut dut 0 oct 9 13:53 toto*

Permissions (rwx) Date et heure de la dernière


user : group : others modification
(pas d’année si année en cours)

Exercice 1.3 : donnez tout les renseignements que vous pourrez sur essai.txt, toto et
tmp.

Tout fichier dont le nom commence par un . est dit caché. Cela signifie simplement deux
choses :
1. Le fichier ne sera pas vu lors d’une commande de listage de fichier classique de type ls.
2. Ce fichier ne sera pas détruit par une commande de destruction de type rm si vous utilisez
un joker comme *.

15
1.12 Tableau comparatif des systèmes de fichiers

Système de FAT 16 FAT 32 NTFS EXT2FS


fichiers

OS MS-DOS, Windows 98, XP, Windows NT, linux


Windows 95 Vista 2000, XP, Vista

Format des noms 8.3 (255 avec 8.3 (255 avec 255 255
VFAT) VFAT)

protection non non oui oui

Sensible à la casse non non Non [1] oui

Mécanismes de non non oui oui


correction

Taille max cluster 32 ko 4 ko 4 ko 4 ko

Taille max (216 – 10)*32 ko = (228 – 10)*8 ko = (264 * 4ko) 4 To


partition 2 Go 2 To
2 To pour un
disque « de base »

[1] NTFS affiche la différence entre majuscule et minuscule, mais vous ne pouvez avoir dans
le même répertoire deux fichiers toto.txt et TOTO.txt.

1.13 Les fichiers textes


Le codage d’un caractère dans un fichier (on parle alors de fichier texte) doit utiliser un code
pour faire correspondre un octet (en général) à un caractère. Le code le plus connu et le plus
utilisé est le code ASCII (American Standard Code for Information Interchange). Mais il
existe aussi le code EBCDIC (Extended Binary Coded Decimal Interchange Code), code
propriétaire IBM utilisé pour ses gros ordinateurs (mainframes).

Le code ASCII est un jeu normalisé de 128 caractères codés sur 7 bits, devenu un standard
quasi universel. Il comporte tous les caractères alphanumériques non accentués et est lisible
par pratiquement n'importe quelle machine. Ce sont les 8 premières lignes du tableau suivant.

16
Les 32 premiers codes sont utilisés comme caractères de contrôle pour représenter, par
exemple, une fin de ligne ou une tabulation.

17
Le code ASCII ne contient pas de caractères accentués et il a été complété par le code ISO-
8859-1 (ou Latin 1). Ce n’est hélas pas le seul. Les 128 premiers caractères correspondent au
code ASCII, les 128 suivants aux caractères accentués et caractères spéciaux (voir les 8
dernières lignes du tableau).

Unicode est un jeu de caractères codé sur 16 bits (contre 7 ou 8 bits pour les standards
anciens) qui permet le codage des caractères utilisés par toutes les langues du monde au sein
d'une table unique. 16 bits permettent de coder 65 536 (2 puissance 16) caractères différents
ce qui couvre largement les besoins en la matière. Unicode est supporté par tous les sytèmes
d’exploitations « modernes ». Les 256 premiers caractères d'Unicode correspondent au jeu
ISO Latin 1.

Un fichier texte comporte un caractère spécial EOL (End Of Line) pour signaler la fin d’une
ligne. Ce caractère est différent suivant que le fichier est créé sous Unix ou bien sous
Windows.

Fin de ligne Unix 0x0A (line feed)

Fin de ligne Windows 0x0D 0x0A (carriage return, line feed)

C’est une des principales sources d’incompatibilité aux transferts de fichiers entre les deux
mondes.

Exercice 1.4 :
1) Sous Unix, donnez le fichier texte correspondant aux octets suivants : 43 0A 4F 0A 55 0A
43 0A 4F 0A 55 0A.
2) Sous Windows, donnez les octets correspondant au fichier précédent.

18
2. Langages de programmation

2.1 Définitions
homme Langage évolué
compilateur

Langage assembleur
assembleur
ordinateur Langage machine

• Langage machine. L’ordinateur ne sait exécuter qu’un nombre limité d’opérations


élémentaires codées en binaire. C’est le langage machine. Les instructions sont
généralement codées avec un ou plusieurs octets.

• Langage assembleur (ou d’assemblage). Quand le programmeur veut écrire en langage


machine, au lieu d’écrire directement les instructions en binaire, il utilise un langage un
peu plus parlant quoique strictement équivalent, le langage assembleur. Celui-ci traduit les
codes binaires par des mnémoniques. Chaque microprocesseur a son propre langage
assembleur. Les langages machine et assembleur possèdent pratiquement les mêmes
instructions.

ADD A, B ≡ additionner (code 0101) les valeurs A (adresse mémoire 010010) et B


(adresse mémoire 010011) ≡ 0101010010010011.

• Langage évolué. C’est un langage général utilisable sur n’importe quel ordinateur. Il y en a
plusieurs : fortran, pascal, basic, C/C++, ADA, java, … Exemple :

Y = A*X + 2*B + C

à partir des variables A, B, C et X, cette instruction calcule l’expression mathématique et


range le résultat dans Y. Pour calculer le même genre d’instruction en assembleur, il
faudrait beaucoup d’instructions élémentaires (d’autant qu’il n’y généralement pas de
multiplication native dans un microprocesseur).

19
Dans tous les langages évolués, on trouvera les notions suivantes :
La variable : c’est un nom donné à un emplacement de la mémoire vive destiné à
contenir une information. La nature de cette information (entier, caractère, adresse, …)
est appelée son type.
L’affectation : elle permet de calculer la valeur d’une expression et de la ranger dans
une variable.
Le test conditionnel : il permet de faire un choix du genre « si le cours m’intéresse
alors j’écouterai le professeur, sinon je penserai à autre chose ».
La répétition : elle permet d’exécuter une action jusqu’à satisfaire une condition du
genre « tant que je ne serai pas suffisamment bon en C, je travaillerai le cours de
programmation ».

2.2 Méthodologie pour l’écriture d’un programme

problème

analyse

algorithme

programmation

programme en langage évolué

Compilation/assemblage tests

programme en langage machine

Edition de liens

programme exécutable

20
L’analyse du problème commence en général par une réécriture du problème sous une forme
textuelle précise ou bien mathématique. On passe ensuite à l’écriture de l’algorithme.

Définition : un algorithme est une suite finie de règles à appliquer dans un ordre déterminé à
un nombre fini de données pour arriver, en un nombre fini d'étapes, à un certain résultat, et
cela indépendamment des données. Un algorithme est donc une suite finie d'instructions qui
sert à réaliser un travail, un peu à la manière d’une recette de cuisine.

En règle générale, un algorithme doit tenir sur une page. Si tel n’est pas le cas, il faut le
diviser en plusieurs parties indépendantes à l’aide de fonctions de façon à ce que chaque
partie tienne sur une page. On peut l’écrire de deux manières :
• A l’aide d’un langage algorithmique (ou un langage de programmation comme le C),
• A l’aide d’un ordinogramme.

Dans ce cours, nous commencerons l’écriture des algorithmes avec un langage algorithmique
simplifié (voir sa définition au §3.16), puis nous passerons progressivement au langage C dès
que nous aurons vu sa syntaxe. Nous n’utiliserons les ordinogrammes qu’à titre d’exemple
dans les deux exercices suivants.

Exercice 2.1 : calcul de la somme des N premiers nombres entiers. On dispose d’une fonction
LireEntier() qui permet de lire la valeur de N au clavier.

Exercice 2.2 : calcul de la moyenne de N notes. On dispose de la fonction LireEntier() qui


permet de lire la valeur de N ainsi que la valeur d’une note au clavier.

2.3 Le langage C
Le langage C a été inventé vers 1972 pour réécrire Unix dans un langage évolué et le porter
sur d’autres machines que le PDP 7 pour laquelle il avait été écrit à l’origine en assembleur.

Le langage C a été normalisé ANSI en 1988 (le C pré-ANSI est appelé le C Kernighan et
Ritchie ou K&R ou encore compatible).

21
C’est un langage qui, par ses origines, est assez proche du matériel et donc bien adapté à
l’électronique. C’est un langage :

Evolué : il dispose des structures de contrôle d’un langage évolué ainsi que de la récursivité.

Impératif : il faut déclarer les variables (et leur type) et les fonctions avant de pouvoir les
utiliser.

Modulaire : le programme peut être découpé en modules indépendants qui seront compilés
séparément. C’est l’édition de liens qui regroupe les morceaux et crée l’exécutable.

Compilé : par opposition avec un langage interprété qui n’est pas traduit en langage machine.
Lors de l’exécution d’un programme interprété, un programme spécialisé, l’interpréteur, se
charge d’exécuter les instructions du langage sur une machine donné. Le langage compilé est
beaucoup plus rapide que le langage interprété.

Efficace : comme il est assez proche de l’ordinateur, le compilateur peut assez facilement
optimiser le code machine pour qu’il soit le plus rapide possible. Il est peu probable qu’un
programmeur humain écrive en assembleur un programme plus rapide qu’en C (sauf pour un
microprocesseur spécialisé de type DSP).

Très permissif : contrairement à Pascal ou à Fortran, le C permet d’écrire des absurdités que
le compilateur ne verra pas, notamment dans les opérations arithmétiques. Tout est permis, le
programmeur est supposé savoir ce qu’il fait ! ! !

Nous allons maintenant voir comment on définit en langage C :


les variables,
les affectations,
les entrées-sorties (clavier, écran),
les structures de choix,
les structures de répétition conditionnelle.

Avec ces éléments, vous serez à même d’écrire des programmes élémentaires.

22
3. Bases du langage C

3.1 Les variables


Une variable est un nom qui sert à repérer un emplacement en mémoire, dont on peut faire
évoluer la valeur au fil du déroulement du programme. Les noms de variables sont sensibles à
la casse. Les caractères qui les composent doivent être choisis parmi les 26 lettres majuscules
et minuscules de l’alphabet, les chiffres de 0 à 9 et l’underscore _. Le premier caractère du
nom ne doit pas être un chiffre. Il ne doit pas y avoir d’espace ni de caractères accentués dans
le nom. Le compilateur traite les noms de variable jusqu’à 32 caractères.

Noms corrects : A, A1, n_1, racine_carree


Noms incorrects : 1a, nombre 1, racine_carrée, nombre-1

Une variable peut contenir plusieurs types de données : nombre entier, nombre réel, caractère,

⇒ Il faut donc spécifier le type de la variable lors de sa déclaration.

A chaque type de variable correspond un nombre d’octets destinés à stocker un nombre limité
de valeurs différentes. Exemple :

Un entier non signé (>0) est stocké à l’aide de 4 octets et donc sa valeur est comprise entre 0
et 232-1 (soit 4 294 967 295).

Exemple de types :
1) int = nombre entier,
2) float = nombre réel,
3) char = caractère.

Exemples de déclaration :
int n, p;
float valeur, X1, X2;
char reponse;

23
Vous devez écrire vos instructions de déclaration avant les instructions d’exécution.

Le compilateur réserve un emplacement mémoire pour la variable, mais cet emplacement


n’est pas initialisé. Vous ne pouvez prévoir quelle valeur sera stockée dans la variable si vous
ne l’initialisez pas vous-même. Deux méthodes :

Au moment de la déclaration.
int n = 0, p = 100;

Dans le programme avant sa première utilisation.


int n;

n = 0;

3.2 L’instruction d’affectation


L’instruction d’affectation a pour rôle :
1) de calculer la valeur de l’expression figurant à droite du signe =,
2) de ranger le résultat dans la variable se trouvant à gauche du signe =.

Exemple :
int n, p;
n = 10;
p = 2*n – 3;

instructions n p commentaire

déclaration - - les variables ne sont pas initialisées

n = 10 ; 10 - affectation d’une constante

p = 2*n – 3 ; 10 17 calcul de l’expression puis affectation

C’est après l’exécution de l’instruction que la variable à gauche du signe = change de valeur.

Attention à ne pas confondre l’affectation avec l’égalité mathématique.

24
Exemple 1 : a = b;

Mathématiquement, cette expression signifie que a est égal à b pendant toute la durée du
problème.

En informatique, cette expression signifie que a prend la valeur de b au moment de


l’exécution de l’instruction.

⇒ en informatique, a = b; n’est pas équivalent à b = a; (alors que c’est la même


chose en mathématique)

Exemple 2 :

instructions a b commentaire

déclaration - - les variables ne sont pas initialisées

a=5; 5 - affectation d’une constante

b = a + 1; 5 6 l’action de cette instruction est purement instantanée (au


moment de son exécution)

a = 2; 2 6 le changement de la valeur de a n’affecte plus b (qui ne


passe pas à 3)

Exemple 3 : a = a + 1;

Mathématiquement, cette expression n’a pas de sens.

En informatique, cette expression signifie que la nouvelle valeur de a (après exécution de


l’instruction) est égale à l’ancienne valeur de a (avant exécution de l’instruction) + 1.
C’est une incrémentation.

25
Exemple 4 : a + 5 = 3;

Mathématiquement, cette expression a un sens. C’est une banale équation.

En informatique, cette expression est fausse. On ne peut attribuer une valeur qu’à une
variable et pas à une expression.

Exercice 3.1 : remplir les tableaux suivants.

instructions a b commentaire

déclaration

a=5;

b = a + 4;

a = a + 1;

b = a - 4;

instructions n1 n2 commentaire

déclaration

n1 = 5 ;

n2 = 7 ;

n1 = n2;

n2 = n1;

instructions n1 n2 commentaire

déclaration

n1 = 5 ;

n2 = 7 ;

n2 = n1;

n1 = n2;

26
Exercice 3.2 : soit trois variables entières. Compléter le programme suivant pour permuter
leurs valeurs, de telle sorte que a → b, b → c et c → a.

main()
{
int a = 1, b = 2, c = 3, tmp;

/* à compléter */

printf("a = %d, b = %d, c = %d\n ", a, b, c);


}

3.3 Les types entier


Les types entiers permettent de représenter une partie des nombres entiers naturels et relatifs :

type nombre de bits intervalle

unsigned short int 16 0 à (216-1 = 65535)

short int 16 (-215=-32768) à (215-1 = 32767)

unsigned long int 32 0 à (232-1= 4 294 967 295)

long int 32 (-231=-2 147 483 648) à (231-1 = 2 147 483 647)

int 32* (-231=-2 147 483 648) à (231-1 = 2 147 483 647)

* : le type int dépend du compilateur utilisé qui est généralement lié au système
d’exploitation (ou au compilateur). Sur un système 32 bits, il est généralement codé sur 32
bits. Il est préférable de spécifier short ou long sinon le comportement de vos programmes
changera avec la machine cible (absence de portabilité).

Les nombres signés utilisent le codage en complément à 2. Les constantes de type int
s’écrive comme en mathématique :

short int n, o, p;

n = 10;
o = +15;
p = -2542;

27
Les opérateurs mathématiques sont de deux types : les opérateurs unaires qui ne portent que
sur un terme et les opérateurs binaires qui portent sur deux termes.

symbole opération

+, - addition, soustraction

* multiplication

/ division entière

% reste de la division entière (modulo)

- opérateur unaire de négation (ex : -b;)

Lorsque plusieurs opérateurs apparaissent dans une même expression, le compilateur respecte
des règles de priorités qui sont celles de l’algèbre traditionnelle.

priorité symbole commentaires

max - (négation)

moy *, /, % en cas d’égalité, le calcul s’effectue de gauche à droite

min +, - en cas d’égalité, le calcul s’effectue de gauche à droite

En utilisant des parenthèses, vous pouvez outrepasser ces règles de priorité en forçant le
calcul préalable de l’expression qu’elles contiennent.

Exercice 3.3 : quelles sont les valeurs des expressions suivantes ?

main()
{
int n = 8, p = 13, q = 29, result;

result = n + p / q;
printf("resultat = %d\n ", result);

result = n + q / p;
printf("resultat = %d\n ", result);

28
result = (n + q) / p;
printf("resultat = %d\n ", result);

result = n + p % q;
printf("resultat = %d\n ", result);

result = n + q % p;
printf("resultat = %d\n ", result);

result = (n + q) % p;
printf("resultat = %d\n ", result);

result = n + p / n + p;
printf("resultat = %d\n ", result);

result = (n + p) / (n + p);
printf("resultat = %d\n ", result);
}

3.4 Les types flottants (réels)


Les types flottants permettent de représenter, de manière approchée, une partie des nombres
réels. La valeur d’un réel ne peut être ni trop grande, ni trop petite, ni trop précise. Le codage
en binaire est de la forme :

signe exposant (signé) mantisse (non signée)

type nombre de bits format valeur max (valeur précision max


min*)

float 32 1 + 8 + 23 2128 ≈ 10+38 2-23 ≈ 10-7

double 64 1 + 11 + 52 21024 ≈ 10+308 2-52 ≈ 10-15

long double 80 1 + 15 + 64 216384 ≈ 10+4932 2-64 ≈ 10-19


* : la valeur min se détermine à partir de la valeur max (ex : max = 10+38 ⇒ min = 10-38)

Les flottants s’écrivent en C sous la forme : mantisse + exposant (avec un point décimal et
pas une virgule) tels que :

+4.25E+4 ou encore -58.0e-25

29
C’est la notation scientifique traditionnelle comme sur une calculatrice. Le E signifie 10
puissance. Les constantes de type flottant s’écrivent sous la forme :
float n, o, p;

n = 12.43;
o = -0.38e-33;
p = -4.0;

On retrouve les mêmes opérateurs binaires (portant sur 2 termes) que pour les entiers (sauf le
%) ainsi que l’opérateur unaire de négation. Les règles de priorité restent les mêmes.

Attention : l’expression 5/2 sera calculée en entier (résultat = 2) même si le résultat est rangé
dans un flottant. Il faut utiliser 5./2. pour obtenir 2,5.

main()
{
float p;

p = 5/2;
printf("resultat = %f\n",p);

p = 5.0/2.0;
printf("resultat = %f\n",p);
}

3.5 Les conversions de type


En C, vous pouvez mélanger dans une expression des variables de types différents sans
effectuer aucune conversion contrairement au langage Pascal où la conversion est obligatoire
(le langage est dit fortement typé). La conversion en C est implicite (elle est effectuée
automatiquement par le compilateur) alors qu’elle est explicite en Pascal, en Fortran ou en
Ada.

Le compilateur C réalise automatiquement la conversion de type en respectant :


1) la règle de priorité des opérateurs,
2) la règle « du type le plus petit (en nombre d’octets) vers le type le plus grand » (c’est la
règle la moins dégradante pour les données). En général, tous les types plus petits qu’un
int sont systématiquement convertis en int avant toute autre conversion.

30
Exemple :
int n, p;
float x, y;

Dans l’expression y = n + x, n est d’abord converti en flottant puis l’addition est


effectuée en flottant et le résultat est rangé dans le flottant y.

Dans l’expression y = n*p + x, n*p est calculé en entier, le résultat est converti en
flottant puis additionné à x en flottant et le résultat est rangé dans y.

Exercice 3.4 : soient les instructions suivantes.


int n, p;
float x;
n = 10;
p = 7;
x = 2.5;

Donnez le type et la valeur des expressions suivantes :


x + n % p;
x + n / p;
(x + n) / p;
5. * n;
(n + 1) / n;
(n + 1.0) / n;

Il est à noter que la conversion d’int vers float est non dégradante (on ne perd rien dans la
conversion) alors que la conversion float → int est dégradante (on prend la partie entière
du réel que l’on met dans l’entier, la partie fractionnaire est perdue).

Il est possible de convertir explicitement une expression grâce à un cast en mettant


l’expression entre parenthèse et en la précédant de (type). Par exemple :

x + (float)n/p;

31
Exercice 3.5 : quels résultats donne le programme suivant ?

main()
{
int n=15, p=4;
float x;

x = n/p;
printf("resultat = %f\n",x);

x = (float)n/p;
printf("resultat = %f\n",x);

x = (float)(n/p);
printf("resultat = %f\n",x);
}

Une règle élémentaire de prudence consiste à ne jamais mélanger des types signés avec des
types non signés car les conversions n’ont généralement pas de sens. Si vous introduisez une
variable non signée dans une expression signée, utilisez un cast (exemple typique, un indice
de boucle).

main()
{
long int n=-15, p; /* signé */
unsigned long int x; /* non signé */
...

p = n + (long int)x;
...
}

3.6 Les types char


Les types char permettent de coder des caractères en utilisant le code ASCII. Mais ils peuvent
aussi être utilisés comme des petits entiers signés ou non signés ou bien directement en
binaire si le programme travaille sur des octets.

type nombre de bits intervalle

unsigned char 8 0 à (28-1 = 255)

char 8 (-27=-128) à (27-1 = 127)

32
Les constantes de type char s’écrivent sous la forme :

char n, o, p;

n = ‘s’; /* un caractère normal */


o = 100; /* un petit entier */
p = ‘\n’; /* le caractère spécial line feed */
p = 0x0A ; /* toujours un line feed */

Les caractères imprimables sont simplement écrits entre quotes et les caractères spéciaux
utilisent l’antislash (backslash).

Tout ce qui s’applique aux types entiers s’applique aux types char (opérateurs, priorités,
conversions). C’est une des grandes qualités du C (mais c’est aussi un défaut) de pouvoir
effectuer des opérations arithmétiques avec des variables de type char. Les possibilités de
confondre caractères et petits entiers sont nombreuses.

Exemple :
main()
{
char n;

n = 'A';
printf("Caractere = %c\n",n);
printf("Code ASCII en hexadecimal = %x\n",n);
printf("Code ASCII en decimal = %d\n",n);
}

Exercice 3.6 : soient trois variables c1, c2 et c3 de type char. Ecrivez un programme
permettant de permuter le contenu de ces variables.

3.7 Communiquer avec le programme : les entrées-sorties standard


La manière la plus simple de lire un caractère à la fois sur l’entrée standard (qui est
normalement le clavier) est d’utiliser la fonction getchar :

33
int getchar(void)

type de la variable d’entrée

type de la variable de sortie

Cette fonction n’accepte en entrée aucune variable (entrée de type void qui veut dire vide) et
retourne un entier (type int) dont la valeur est égale au code ASCII du caractère tapé au
clavier suivi d’un « retour chariot ».

Pour la sortie d’un caractère, on utilise la fonction putchar :

int putchar(int)

Cette fonction accepte un entier en entrée (celui retourné par getchar() par exemple) et
retourne un entier (type int). putchar(c) envoie le caractère c sur la sortie standard
(l’écran par défaut) et retourne le caractère écrit ou -1 en cas d’erreur.

Exemple 1 : ce programme accepte un caractère en entrée puis le restitue en sortie.


#include <stdio.h> /* déclaration de fonctions getchar et putchar */

main()
{
int c;

c=getchar();
putchar(c);

Exemple 2 : ce programme accepte un caractère en entrée puis le restitue en sortie et


recommence tant que vous ne tapez pas Ctrl-d pour sortir de la boucle (Ctrl-z sous Windows).

#include <stdio.h>

main()
{

34
int c;

c=getchar();

while (c != EOF)
{
putchar(c);
c=getchar();
}
}

Sous Unix, on peut rediriger la sortie standard (StdOut) d’un programme (l’écran par défaut)
vers un fichier grâce à la commande :

prog > sortie.txt

Mais il est aussi possible de remplacer le clavier comme entrée standard (StdIn) par un fichier
en utilisant la commande :

prog < entree.txt

De plus, il existe encore une autre sortie, la sortie des erreurs (StdErr, par exemple les
messages d’erreurs du compilateur), qui peut être redirigée vers un fichier grâce à :

prog 2> error.txt

ou bien vers le même endroit que la sortie standard en tapant :

prog 2>&1

C’est grâce aux entrées-sorties standards que le pipe fonctionne. Quand on tape :

prog1 | prog2

on redirige la sortie standard de prog1 vers l’entrée standard de prog2 (le pipe « branche »
StdOut1 sur StdIn2).

35
Reprenons l’exemple 2. Si vous créez avec un éditeur un petit fichier texte (entree.txt)
comportant des lettres majuscules et minuscules ainsi que des chiffres puis que vous tapez la
commande suivante :

exemple2 < entree.txt > sortie.txt

Vous obtenez un fichier sortie.txt identique à entree.txt. Ainsi, la fonction


getchar() peut accepter un flot de texte issu d’un fichier plutôt qu’un seul caractère au
clavier et la fonction putchar peut renvoyer un caractère dans un fichier.

Exemple 3 : le programme suivant accepte un caractère en entrée puis le restitue en sortie


mais converti en minuscule et recommence tant que vous ne tapez pas Ctrl-d pour sortir de la
boucle (en manuel au clavier) ou tant que le programme n’a pas atteint la fin du fichier EOF
(pour une entrée via un fichier). La fonction tolower est définie dans ctype.h. Elle
convertit les lettres majuscules en minuscules et retourne les autres caractères tels quels.

#include <stdio.h>
#include <ctype.h>

main()
{
int c, cm;

c=getchar();

while (c != EOF)
{
cm = tolower(c) ;
putchar(cm);
c=getchar();
}
}

Cet exemple fonctionne aussi bien au clavier qu’avec le fichier entree.txt.

Il existe en C des instructions plus sophistiquées pour afficher du texte à l’écran ou bien pour
lire du texte à partir du clavier : printf et scanf.

36
3.8 L’instruction printf
Nous avons déjà vu à plusieurs reprises des exemples simples de cette instruction :
int n=10, p=25;
printf("nombre : %d, valeur %d ", n, p);

On a entre double quotes ("") soit du texte, soit un ou plusieurs codes de format (%d) qui
seront remplacés par la valeur d’une ou de plusieurs variables lors de l’affichage. On trouve
ensuite les variables à afficher séparées par une virgule. Il doit y avoir autant de codes de
format que de variables à afficher. Il existe de nombreux codes de formats (%d correspond à
un entier) pour tous les types de variables. Ils commencent tous par un %. Tout ce qui n’est
pas un code de format mais qui se trouve entre doubles quotes est affiché tel quel.

On peut omettre les codes de format pour afficher seulement du texte :


printf("bonjour");

Il est possible, quoique non recommandé, de remplacer une variable par une expression :
printf("la somme de %d et de %d est %d", n, p, n+p);

Le code de format %c permet d’afficher un caractère :


int n=10;
char c=’c’;
printf("nombre : %d, caractère %c ", n, c);

Le code de format %e permet d’afficher un nombre flottant en notation exponentielle et le


code %f permet d’afficher un nombre flottant en notation décimale.
float x=1.23456e4;
printf("notation exp : %e, notation déc %f ", x, x);

Le formatage des données permet de contrôler le nombre de chiffres affichés à l’écran. Dans
ce texte, le caractère ^ sert à matérialiser un espace lors de l’affichage. En insérant un nombre
dans le code de format après le %, on précise le gabarit d’affichage, c’est-à-dire le nombre
minimal de caractères à utiliser pour afficher la valeur du nombre. Si le nombre peut
s’afficher avec moins de caractères, printf le fera précéder d’un nombre suffisant
d’espaces. Par contre, si le nombre nécessite plus de caractères pour s’afficher que vous en
avez spécifiés, printf utilisera le nombre de caractères nécessaire. Exemples :

37
printf("%3d", n);

valeur affichage

n = 20 ^20

n=3 ^^3

n = -5 ^-5

n = 2358 2358

n = -5200 -5200

printf("%10f", x);

valeur affichage

x = 1.2345 ^^1.234500

x = 12.345 ^12.345000

x = 1.2345E5 123450.00000

Par défaut, les flottants sont affichés avec 6 chiffres à droite du point décimal. Dans l’exemple
précédent, %10f signifie « en utilisant 10 caractères (y compris le .) avec au moins 6 chiffres
après le point ». Il est possible de modifier ce nombre est écrivant %A.Bf qui signifie « au
minimum A caractères dont au moins B après le point décimal ». Exemples :

printf("%10.3f ", x);

valeur affichage

x = 1.2345 ^^^^^1.234

x = 1.2345E3 ^^1234.500

x = 1.2345E7 12345000.000

38
printf("%12.4e", x);

valeur affichage

x = 1.2345 ^^1.2345e+00

x = 123.456789E8 ^^1.2345e+10

Chaque instruction printf affiche ses informations à la suite de celles qui ont déjà été
affichées par un printf précédent, ce qui revient à dire qu’il n’y a pas de passage à la ligne
entre deux printf. Exemple :
int n=10;
char c=’c’;
printf("nombre : %d, ", n);
printf("caractère %c", c);

Les deux printf précédent affichent exactement la même chose que le printf suivant :
printf("nombre : %d, caractère %c", n, c);

Le changement de ligne est réalisé avec le caractère non imprimable \n placé au milieu ou à
la fin d’un printf :

printf("nombre : %d\ncaractère %c", n, c);

Il est aussi possible d’afficher une tabulation avec \t comme dans :

printf("\n\tTerminé\n\n");

Exercice 3.7 : soient les déclarations :


int qte=50;
char cat=’B’;
char art=’S’;

Ecrivez le programme permettant d’afficher les variables de la manière suivante :


50 articles S, de la catégorie B

Quel sera le résultat si art est déclarée de la manière suivante :


char art=’\n’;

39
Exercice 3.8 : écrivez le programme qui calcule le prix TTC d’un nombre donné d’articles
d’un prix unitaire égal à 10.51 euros, compte tenu d’un taux de TVA de 19,6 %. Les résultats
seront affichés de la manière suivante :
nombre d'articles : 15
prix unitaire HT : 10.51
prix total TTC : 188.55

3.9 L’instruction scanf


La lecture au clavier d’un entier que l’on range dans la variable n s’écrit en langage C :
scanf("%d", &n);

Lorsque le programme exécute cette instruction, il attend que vous tapiez une valeur au
clavier suivie d’un retour chariot (↵). Le format (entre " ") est similaire à celui de l’instruction
printf, mais on voit que la variable est spécifiée par &n (et pas seulement n). Le & qui
précède n signifie « adresse de n ». Par contre, le gabarit, comme %3d, est interdit. Exemples
d’entrées entières :
12 ↵
0 ↵
-1 ↵
+1234 ↵

La lecture d’un nombre flottant s’effectue avec l’instruction suivante :


scanf("%f", &x);
scanf("%e", &x);

Les codes %e et %f jouent strictement le même rôle. Exemples d’entrées flottantes :


-12 ↵
0.5 ↵
-1. ↵
+1.2e-4 ↵
32e45 ↵

Attention : il ne faut pas placer de texte avant ou après le code de format ni même d’espace.
L’instruction suivante n’est pas légale :
scanf("Entrez une valeur : %f", &x);

40
Il faut écrire :
printf("Entrez une valeur : ");
scanf("%f", &x);

Si vous souhaitez lire deux valeurs numériques sur la même ligne, vous pouvez utiliser
l’instruction (sans aucun espace entre les ") :
scanf("%d%d", &n, &p);

Les deux valeurs doivent être saisies au clavier séparées par au moins un espace.
12^-15 ↵

L’entrée suivante est identique à la précédente car les codes de format %d ou %f sautent tous
les blancs qui précèdent une valeur.
^^^12^^^-15 ↵

Exercice 3.9 : modifiez le programme de l’exercice 3.8 afin de pouvoir saisir le nombre
d’articles et le prix unitaire.

Attention : lorsque l’utilisateur fournit trop peu de données, scanf continue d’attendre les
données suivantes. Exemple :
scanf("%d%d", &n, &p);

L’entrée 12^-15 ↵ donnera le même résultat que 12 ↵ suivi de -15 ↵. Supposons qu’à
cette même instruction, vous fournissiez :
12^-15^1254 ↵

Il y a maintenant trop de données. scanf va mettre bien 12 dans n, -15 dans p mais il va
garder 1254 en mémoire dans le buffer clavier pour la prochaine lecture avec scanf. En
cas de besoin, vous pouvez purger le buffer clavier à l’aide de l’instruction :

fflush(stdin);

Le code de format %c permet de lire un caractère. Ainsi, l’instruction suivante :

41
char c1 ;
...
scanf("%c", &c1);

lit un caractère au clavier et le range dans c1. Si vous souhaitez lire plusieurs caractères à la
suite, il suffit de les taper successivement et de les faire suivre d’un retour chariot. Exemple :
scanf("%c%c%c", &c1, &c2, &c3) ;

Avec l’entrée :
abc↵

a est rangé dans c1, b dans c2 et c dans c3. L’espace n’est en aucun cas un séparateur
comme pour les nombres. C’est un caractère comme un autre. Si vous tapez :
a^bc↵

a va dans c1, un espace va dans c2, b va dans c3 et c est disponible pour la lecture suivante.

En règle générale, évitez de lire plusieurs valeurs (numériques ou caractères) avec une
même instruction scanf.

Exercice 3.10 : modifiez le programme de l’exercice 3.7 afin de pouvoir saisir le nom de
l’article, sa catégorie ainsi que la quantité.

3.10 Structure de choix : l’instruction if


L’instruction if permet de réaliser des choix dans un programme comme le montre l’exemple
suivant :

main()
{
int n, p;

printf("Donnez deux nombres entiers : ");


scanf("%d%d", &n, &p);

42
if (n < p)
printf("croissant\n");
else
printf("non croissant\n");

printf("au revoir\n");
}

Sa compréhension est assez intuitive. Si n < p, alors on exécute l’instruction :


printf("croissant\n");

sinon on exécute l’instruction :


printf("non croissant\n");

Dans tous les cas, on exécute la dernière instruction du programme :


printf("au revoir\n");

Le compilateur se moque de la présentation des instructions. Ainsi, la forme :


if (n < p)
printf("croissant\n");
else
printf("non croissant\n");

est strictement équivalente à :


if (n < p) printf("croissant\n"); else printf("non croissant\n");

Mais il est fortement conseillé d’utiliser la première forme pour une meilleure lisibilité du
programme et donc une maintenance plus facile.

Dans l’exemple précédent, chacune des deux parties du choix se limite à une instruction
(printf). Il est possible d’en placer plusieurs à condition de les placer dans un bloc entre
{}. Voyons une nouvelle version de notre exemple :
main()
{
int n, p, maxi;

printf("Donnez deux nombres entiers : ");


scanf("%d%d", &n, &p);

43
if (n < p) {
maxi = p;
printf("croissant\n");
}
else {
maxi = n;
printf("non croissant\n");
}

printf("Le plus grand des deux nombres est : %d\n", maxi);


}

L’instruction if correspond maintenant à :


if (n < p) bloc_instructions_1
else bloc_instructions_2

ce qui équivaut dans notre exemple à : si n < p alors maxi prend la valeur de p et printf
sinon maxi prend la valeur de n et printf.

Les blocs d’instructions peuvent contenir n’importe quelles instructions du C y compris


d’autres instructions if. Un bloc peut ne contenir qu’une seule instruction :
if (n < p) {
printf("croissant\n");
}

ou bien une instruction vide (parfaitement légale en C) :


if (n < p) {
;
}

ou bien aucune instruction :


if (n < p) {
}

Il est possible d’avoir un bloc dans le if et une instruction dans le else :


if (n < p) {
maxi = p;
printf("croissant\n");
}
else
printf("non croissant\n");

ou le contraire.

44
if (n < p)
printf("croissant\n");
else {
maxi = n;
printf("non croissant\n");
}

La branche else de l’instruction if n’est pas obligatoire. Ainsi, l’enchaînement :


if (n < p)
printf("croissant\n");

printf("au revoir\n");

est tout à fait acceptable. Si n < p, le programme exécute le printf. Dans tous les cas, le
dernier printf est exécuté. Cette remarque est aussi valable avec un bloc d’instructions :
if (n < p) {
maxi = p;
printf("croissant\n");
}
printf("au revoir\n");

D’une manière générale, l’instruction if se présente de la manière suivante (La branche


else est optionnelle) :

if (condition)
instruction_1
[else
instruction_2]

où instruction_1 et instruction_2 sont :


• une instruction simple (terminée par un ;),
• une instruction structurée (comme un autre if),
• un bloc d’instructions entre {}.

Voyons maintenant la condition plus en détail.

3.11 Structure de choix : les conditions en C


Les opérateurs de comparaisons ont deux significations suivant qu’ils s’appliquent à deux
types numériques (float ou int) ou à deux types caractères (char) :

45
opérateur signification « numérique » signification « caractère »
== égal identique
< inférieur de code ASCII inférieur
> supérieur de code supérieur
<= inférieur ou égal de code inférieur ou égal
>= supérieur ou égal de code supérieur ou égal
!= non égal différent

Les comparaisons sur les caractères utilisent le code ASCII (sur un octet) comme un petit
entier. Dans ce code, les lettres sont rangées dans leur ordre naturel, à savoir a...z, A...Z, 0...9
de façon à ce que les conditions suivantes soient toujours vraies :

‘a’ < ‘b’, ‘A’ < ‘B’, ‘0’ <’1’

Les opérateurs de comparaison sont moins prioritaires que les opérateurs arithmétiques. La
condition fausse est égale à 0 et la condition vraie est différente de 0. Voici quelques
exemples de comparaison :

int n, p;
char c1, c2;

n != p vrai si la valeur de n est différente de celle de p


n + 3 == p vrai si la valeur de n+3 est égale à celle de p
n + 3 < 2 * p vrai si la valeur de n+3 est inférieure à celle de 2*p
n * p + 2 * n < 5 vrai si la valeur de n*p+2*n est inférieure à 5
c1 <= c2 vrai si le caractère dans c1 est avant (ou au même
endroit que) le caractère dans c2 dans la table
ASCII
c1 == ‘e’ vrai si le caractère dans c1 est le caractère e
c1 < ‘g’ vrai si le caractère dans c1 est avant le caractère g
dans la table ASCII
n = 0 toujours faux
n = -5 toujours vrai

46
Attention aux tests d’égalité avec des nombres flottants. Il ne faut pas oublier que l’égalité
entre deux réels est plus difficile à réaliser qu’entre deux entiers. Deux flottants égaux sont
deux nombres dont tous les bits sont identiques (y compris 15 chiffres après la virgule).

Exercice 3.11 : écrivez un programme qui lit deux nombres flottants puis qui demande à
l’utilisateur s’il souhaite les afficher dans l’ordre croissant ou décroissant. Dans les deux cas,
la réponse de l’utilisateur sera fournie sous forme d’un caractère seul : la lettre O sera
interprétée comme une réponse positive à la question tandis que tout autre caractère sera
interprété comme une réponse négative.

Il est possible de combiner un nombre quelconque de conditions simples à l’aide des


opérateurs logiques :

opérateur signification

&& et

|| ou

! non

Quelques exemples :

(a<b)&&(c<d) vrai si les deux conditions a<b et c<d sont


vraies

(a<b)||(c<d) vrai si l’une au moins des deux conditions


a<b et c<d est vraie

!(a<b) vrai si la condition a<b est fausse

((a<b)&&(c<d))||(i==5) vrai si les deux conditions a<b et c<d sont


vraies ou si i=5

Les opérateurs && et || sont les moins prioritaires de ceux que nous avons vus. Dans les deux
premières expressions, les parenthèses ne sont pas indispensables (mais conseillées pour la
lisibilité). Dans la troisième elles sont obligatoires. Dans la quatrième expression, certaines
parenthèses pourraient être supprimées.

47
Exercice 3.12 : modifiez le programme de l’exercice 3.11 de façon à ce que les lettres O et o
soient prises comme une réponse positive.

La structure suivante permet la réalisation de choix multiples :


if (condition1) {
bloc1
}
else if (condition2) {
bloc2
}
else if (condition3) {
bloc3
}

Il faut comprendre :
• si la condition1 est vraie alors le programme exécute le bloc1,
• sinon si la condition2 est vraie alors le programme exécute le bloc2,
• sinon si la condition3 est vraie alors le programme exécute le bloc3.

L’exemple suivant est très utilisé pour tester plusieurs réponses de l’utilisateur à une question :
if (reponse == 1) {
/* faire ceci */
}
else if (reponse == 2) {
/* faire cela */
}
else if (reponse == 3) {
/* ou encore cela */
}

D’une manière plus générale, il est possible d’imbriquer les instructions if :


if (condition1) {
if (condition2) {
bloc1 /* si condition1 et condition2 vraies */
}
else {
bloc2 /* si condition1 vraie et condition2 fausse */
}
}
else {
bloc3 /* si condition1 fausse */
}

48
Un strict respect de la présentation du programme (indentation, passage à la ligne, ...) est
indispensable afin de préserver sa lisibilité.

Exercice 3.13 : écrivez un programme qui, à partir d’un montant lu en donnée, détermine un
montant net obtenu par l’application d’une remise de :
• 10 % si montant est compris entre 1000 et 5000 € (bornes inclues),
• 20 % si montant est supérieur à 5000 €.

3.12 Structure de choix : l’instruction switch


Nous avons vu précédemment qu’il était possible d’utiliser des instructions if pour réaliser
un choix multiple (on parle d’un aiguillage). Il existe en C une instruction spécialisée pour
cela, le switch. Le programme suivant en donne un exemple.

main()
{
int n;

printf("Donnez un nombre entier : ");


scanf("%d", &n);

switch (n) {
case 0 : printf("nul\n");
break ;
case 1 : printf("un\n");
break ;
case 2 : printf("deux\n");
break ;
}

printf("au revoir\n");
}

Ce programme évalue la valeur de l’expression entière se trouvant entre parenthèses après le


switch (ici, n). Ensuite, il recherche la branche case qui correspond à cette valeur et
exécute la liste d’instructions se trouvant dans cette branche ainsi que toutes les autres
branches case se trouvant en dessous. Dans notre exemple, l’instruction break est
obligatoire afin de sortir du switch après traitement de la branche correspondant à la valeur
de n.

49
Exercice 3.14 : dans le programme précédent, supprimez le break de la branche case 0.
On entre la valeur 0. Que se passe-t-il ?

Il est possible de définir une branche par défaut pour traiter le cas où la valeur de n serait
différente des valeurs testées dans les instructions case. C’est l’étiquette default.
Exemple :

main()
{
int n;

printf("Donnez un nombre entier positif : ");


scanf("%d", &n);

switch (n) {
case 0 : printf("nul\n");
break ;
case 1 : printf("un\n");
break ;
case 2 : printf("deux\n");
break ;
default : printf("trop grand\n");
}

printf("au revoir\n");
}

Si n est différent de 0, 1 ou 2, c’est la branche default qui est utilisée. Les instructions
après un case ne sont pas obligatoires pas plus que le break. Vous pouvez écrire :
switch (n) {
case 0 : printf("nul\n");
break ;
case 1 :
case 2 : printf("deux\n");
default : printf("grand\n");
}

La syntaxe générale du switch est :


switch (expression) {
case constante_1 : [suite_instructions_1]
case constante_2 : [suite_instructions_2]
case constante_3 : [suite_instructions_3]
[default : suite_instructions_4]
}

50
Où ce qui est entre crochets est facultatif et :
• expression est une expression entière quelconque (int ou char),
• constante est une expression constante d’un type entier quelconque (int ou char),
• suite_instructions est une suite d’instructions quelconques.

Ainsi le switch peut être utilisé pour traiter des caractères :


switch (n) {
case ‘a’ : ...
case ‘X’ : ...
}

3.13 Structure de répétition conditionnelle : l’instruction do... while


Les structures de répétition (on parle aussi de boucles) permettent d’exécuter à plusieurs
reprises une suite d’instructions. Dans une structure de répétition conditionnelle, la poursuite
de la répétition dépend d’une condition. L’instruction do ... while() permet d’effectuer
une répétition dans un programme comme le montre l’exemple suivant :

main()
{
int n;

do {
printf("Donnez un nombre entier : ");
scanf("%d", &n);
printf("voici son carré : %d\n", n*n);
}
while (n != 0);

printf("Fin du programme\n ");


}

Elle signifie « exécuter le bloc d’instructions entre {} tant que n est différent de 0 ». La
condition entre parenthèse après le while est donc une condition de poursuite et non une
condition d’arrêt. D’une façon générale, l’instruction se présente de la manière suivante :
do
instruction
while (condition);

Attention au ; à la fin du while.

51
Comme pour le if, l’instruction peut être une instruction simple, une instruction structurée
ou bien un bloc d’instructions. Cette instruction est toujours exécutée au moins une fois
(puisque le test de la condition se trouve après l’instruction). Il est possible de faire une
boucle infinie en écrivant :
do
instruction
while (1);

En effet, en C, la condition vraie vaut ≠ 0 et la condition fausse vaut 0. C’est la raison pour
laquelle on définit souvent un type booléen (qui n’existe pas en C) comme un char qui peut
prendre la valeur 0 ou 1 ainsi que des valeurs TRUE et FALSE comme suit :
#define BOOL char /* les lignes commençant par # sont */
#define TRUE 1 /* destinées au preprocesseur et pas au */
#define FALSE 0 /* compilateur */

main()
{
int n;
BOOL toto = TRUE ;

do {
printf("Donnez un nombre entier : ");
scanf("%d", &n);
printf("voici son carré : %d\n", n*n);

}
while (toto == TRUE);

printf("Fin du programme\n ");


}

Dans ce programme, le préprocesseur va remplacer chaque occurrence de BOOL par char, de


TRUE par 1 et de FALSE par 0. Le programme obtenu après remplacement sera ensuite
compilé.

3.14 Structure de répétition conditionnelle : l’instruction while...


La décision de poursuite de la répétition dans l’instruction do... while se trouve à la fin
de la boucle d’exécution des instructions. En C, il existe une autre structure de répétition
conditionnelle où ce test a lieu au début de la boucle, l’instruction while. Le programme de
la page précédente a été modifié pour utiliser un while au lieu d’un do... while.

52
main()
{
int n;

printf("Donnez un nombre entier : ");


scanf("%d", &n); /* ou bien n = 1 par exemple */
printf("voici son carré : %d\n", n*n);

while (n != 0) {
printf("Donnez un nombre entier : ");
scanf("%d", &n);
printf("voici son carré : %d\n", n*n);

printf("Fin du programme\n ");


}

Comme le test a lieu en premier, il faut commencer par saisir une valeur de n avant de rentrer
dans le while (sinon, n n’est pas initialisé) ou bien initialiser n avec une valeur différente de
0 pour forcer le programme à rentrer dans le while.

Conclusion : les variables intervenant dans la condition doivent être initialisées avant le
while, ce qui n’est pas forcement nécessaire pour un do... while (cela peut être fait
dans le premier tour de la boucle). D’une façon générale, l’instruction se présente de la
manière suivante :
while (condition)
instruction

Attention, il n’y a plus de ;.

D’une manière générale, on démontre que tout programme peut s’écrire avec une structure de
choix et une structure de répétition conditionnelle. Les instructions while et do ...
while sont donc redondantes, mais commodes à utiliser.

3.15 Structure de répétition inconditionnelle : l’instruction for...


Dans une structure de répétition inconditionnelle, la répétition est effectuée un nombre fixe de
fois, ce nombre dépendant d’un compteur. Pour imposer un nombre de tours fixe dans un
while, il faut définir un compteur de boucle et tester une condition sur sa valeur pour sortir
du while. Comme par exemple dans le programme suivant :

53
main()
{
int i;

i = 0;
while (i < 5) {
printf("Voici un nombre entier : %d\n", i);
printf("Voici son carré : %d\n", i*i);
i = i + 1;
}
printf("Fin du programme\n ");
}

Le programme va tourner 5 fois dans la boucle pour i = 0, 1, 2, 3 et 4. Pour cela, trois


éléments doivent être définis :
1) la valeur de départ (i = 0),
2) la valeur d’arrivée (i=5),
3) et l’incrément (ici, 1).

Il est possible de paramétrer le nombre de tours dans la boucle en écrivant :


main()
{
int i, stop;

printf("Donnez le nombre de tours : ");


scanf("%d", &stop);

i = 0;

while (i < stop) {


printf("Voici un nombre entier : %d\n", i);
printf("Voici son carré : %d\n", i*i);
i = i + 1;
}
printf("Fin du programme\n ");
}

Le nombre de tours dans la boucle n’est plus connu à l’écriture du programme, mais
seulement au moment de son exécution.

L’instruction for permet de simplifier l’écriture de la boucle inconditionnelle avec compteur.

54
main()
{
int i;

for(i = 0; i < 5; i = i + 1) {
printf("Voici un nombre entier : %d\n", i);
printf("Voici son carré : %d\n", i*i);
}
printf("Fin du programme\n ");
}

Il est à noter que l’instruction d’incrémentation i = i + 1; s’écrit aussi i++;. D’une


façon générale, l’instruction se présente de la manière suivante :
for (avant; condition; fin_de_tour)
instruction

où :
• avant est une instruction simple qui sera exécutée avant le premier tour de boucle. C’est
généralement l’initialisation du compteur.
• condition est la condition de poursuite de la boucle examinée avant chaque tour de
boucle (y compris le premier).
• fin_de_tour est une instruction simple qui sera exécutée à la fin de chaque tour de
boucle. C’est généralement l’incrémentation du compteur.
• instruction est une instruction au sens large. Il peut s’agir d’un autre for (les for
imbriqués sont tout à fait utilisables).

Cette instruction est strictement équivalente à :


avant;
while (condition) {
instruction
fin_de_tour;
}

Il est possible d’utiliser un for pour faire autre chose qu’une simple boucle avec compteur.
Ceci est fortement déconseillé pour préserver la lisibilité et donc faciliter la maintenance du
programme. Il est aussi fortement conseillé de ne pas modifier la valeur du compteur de
boucle dans la boucle sous peine d’obtenir un comportement hasardeux durant l’exécution.
L’instruction break (vu avec le switch) est utilisable dans une boucle. Elle permet de

55
quitter la boucle en cours d’exécution. Vous n’avez le droit de l’utiliser que si vous pouvez
démontrer que le programme n’est pas faisable autrement (c’est-à-dire jamais).

Exercice 3.15 : modifiez le programme de la page 54 (boucle paramétrée) pour utiliser une
boucle for à la place du while. Insérez l’instruction i = i - 1; à la fin de la boucle.
Que va-t-il se passer ?

Exercice 3.16 : écrivez un programme qui affiche un nombre n d’entiers consécutifs à partir
d’une valeur p, n et p étant lues en données.

3.16 Algorithmes élémentaires


Vous disposez pour écrire les algorithmes suivants :
• de variables de type entier, caractère et réel,
• des fonctions LireEntier(), LireCar(), LireRéel(),
• de la fonction affiche "texte à afficher" variable,
• du test conditionnel si condition vraie, sinon,
• des deux boucles tant que condition vraie et faire tant que
condition vraie.

Programme 1 (comptage) : écrivez un programme qui lit une ligne de texte, c’est-à-dire une
suite de caractères terminée par un retour chariot ↵ (fin de ligne \n) et qui affiche :
1) le nombre de caractères contenu dans la ligne,
2) le nombre de e contenu dans la ligne,
3) le pourcentage de e par rapport au nombre total de caractères.

Programme 2 (comptage) : écrivez un programme qui lit 10 notes entières et qui affiche le
pourcentage de notes supérieures à 10.

Programme 3 (accumulation) : écrivez un programme qui lit 10 notes entières et qui


affiche leur somme.

Programme 4 (accumulation) : écrivez un programme qui affiche la somme des N premiers


nombres entiers positifs, N étant fourni par l’utilisateur.

56
Programme 5 (accumulation): écrivez un programme qui lit des notes entières jusqu’à ce que
l’utilisateur tape 0 pour signaler qu’il n’a plus de valeurs à fournir et qui affiche alors leur
moyenne.

Programme 6 (accumulation) : écrivez un programme qui lit des notes entières positives et
négatives jusqu’à ce que l’utilisateur tape 0 pour signaler qu’il n’a plus de valeurs à fournir
puis qui affiche la moyenne des notes positives et la moyenne des notes négatives.

Programme 7 (recherche de maximum) : écrivez un programme qui lit des notes entières
positives et négatives jusqu’à ce que l’utilisateur tape 0 pour signaler qu’il n’a plus de valeurs
à fournir puis qui affiche la plus grande note positive et la plus petite note négative.

Programme 8 (imbrication de répétitions) : écrivez un programme qui calcule les moyennes


de 5 élèves. Pour chaque élève, le programme lit des notes entières jusqu’à ce que l’utilisateur
tape -1 pour signaler qu’il n’a plus de valeurs à fournir, puis affiche la moyenne de ces notes.
On identifiera les élèves avec un numéro : élève n°1, élève n°2, ...

Programme 9 (imbrication de répétitions) : écrivez un programme qui affiche les tables de


multiplication de 1 à 9. Chaque table se présentera comme suit :

TABLE DES 4
4 x 1 = 4
4 x 2 = 8
...
4 x 10 = 40

Programme 10 (imbrication de répétitions) : écrivez un programme qui affiche une diagonale


d’astérisques dont le nombre est fourni par l’utilisateur.
Nombre d’astérisques : 5
*
*
*
*
*

57
Programme 11 (itération, la suite de Fibonacci) : un couple de lapins donne naissance à un
autre couple de lapin tous les mois. Le couple nouveau-né devient fertile au bout d’un mois et,
pour simplifier, on suppose que les lapins ne meurent jamais et qu’il y a assez de nourriture
pour tous. Sur une île déserte, il y a au départ un seul couple de lapin. Voici l’évolution du
nombre de lapins au cours des 4 premiers mois :

N évolution du nombre de couples FN

0 C1 1

1 C1→C2m 2

2 C1→C3m C2 3

3 C1→C4m C2→C21m C3 5

4 C1→C5m C2→C22m C3→C31m C4 C21 8

Un couple majeur CXX met au monde un couple mineur CXXm tous les mois. Le couple
mineur devient majeur au bout d’un mois puis commence à se reproduire au bout d’un mois.

On remarquera que le nombre de couples durant le mois N (FN) est égal au nombre de couples
du mois N-1 (FN-1) plus le nombre de couples au mois N-2 (FN-2).

FN = FN-1 + FN-2

Ecrivez le ou les programmes permettant de répondre aux questions suivantes :


1) Combien y aura-t-il de lapins sur l’île au bout de N mois ?
2) Combien faudra-t-il de mois pour atteindre une population de L lapins ? Par exemple, L =
100000.

58
4. Les fonctions

4.1 Introduction
Un algorithme n’est facilement compréhensible que s’il tient sur une page. Au-delà, il faut le
diviser en plusieurs sous algorithmes plus petits. D’autre part, il arrive souvent dans un gros
programme que l’on effectue plusieurs fois un traitement identique et il serait dommage
d’avoir à écrire à chaque fois les instructions correspondant au traitement. On utilise alors ce
que l’on nomme un sous-programme en langage de programmation. Il s’agit d’un ensemble
d’instructions auquel on attribue un nom. Le sous-programme peut ensuite être utilisé en tout
point du programme en l’appelant par son nom. Il est possible de paramétrer un sous-
programme en lui passant des variables en entrée comme en sortie.

En programmation, on fait généralement la distinction entre procédure et fonction selon le


nombre de variables de sortie qu’il en possible de récupérer. En langage C, il n’existe que la
fonction (par analogie avec la fonction mathématique) qui permet de passer un ou plusieurs
paramètres (ou aucun) en entrée comme en sortie. La fonction permet d’enfermer certains
traitements dans une boite noire. Il n’y a plus alors à se soucier de la manière dont le
traitement s’effectue dans la fonction mais seulement de la manière dont on utilise cette
fonction (et notamment des paramètres d’entrée-sortie). Le langage C fait un usage intensif
des fonctions. En fait, à cause de son histoire (l’écriture du système UNIX), le langage C fait
une distinction très nette entre :

1) le langage. C’est ce qui est purement algorithmique (déclarations, instructions, ...) avec des
mots réservés que vous ne devez en aucun cas utiliser comme nom de variable ou de fonction
(les mots clés).

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

59
2) la bibliothèque. Tout ce qui est interaction avec le système d’exploitation (entrées sorties,
gestion de la mémoire, ...) est réalisé par appel de fonctions se trouvant dans une bibliothèque
dite bibliothèque standard (ou librairie standard). Par exemple, les fonctions printf,
scanf, getchar, putchar font partie de la bibliothèque standard.

En résumé, on crée une fonction dans les deux cas suivants :


1. Quand il y a une tache répétitive dans le programme,
2. pour clarifier le code source. On écrit souvent en C des fonctions assez courtes (pas trop
quand même, quelques lignes au moins) même si elles ne sont utilisées qu’une fois avec
pour but uniquement d’améliorer la lisibilité du programme.

4.2 Premier exemple


Le langage C ne dispose pas de l’opérateur mathématique de puissance comme par exemple
xy. Nous allons définir une fonction puissance(m,n) qui calcule mn (cette fonction ne
marchera qu’avec des entiers ce qui la rend peu pratique, mais il existe dans la bibliothèque
standard une fonction pow(x,y) qui calcule xy quels que soient x et y).

1. /* prototype de la fonction */
2. int puissance(int m, int n);

3. void main()
4. {
5. int x = 2, n = 5, res;

6. /* appel n°1 de la fonction */


7. res = puissance(2, 5);
8. printf("%d\n", res);

9. /* appel n°2 de la fonction */


10. res = x * puissance(x, 5.2) / n;
11. printf("%d\n", res);

12. /* appel n°3 de la fonction */


13. printf("%d\n", puissance(5, 2) / n);
14. }

15. /* définition de la fonction */


16. int puissance(int x, int y)
17. {
18. int i, puis;

19. puis = 1;

60
20. for (i = 0; i < y; i++)
21. puis = puis * x;

22. return puis;


23. }

Il est nécessaire de distinguer :

1. L’appel de la fonction (ligne 7 ou ligne 10 ou ligne 13). Chaque appel à la fonction


puissance passe deux arguments (un nombre directement ou le contenu d’une variable
ou le résultat d’une expression).

res = puissance(2, n);


res = puissance(2*x, n+1);

Cette fonction retourne un entier (le résultat d’un calcul par exemple, ou un code d’erreur)
qui peut être utilisé dans une expression.

res = x * puissance(x, 5.2) / n;

2. La définition de la fonction (lignes 16 à 23). La structure de la fonction est similaire à


celle du programme principal main (c’est normal, c’est aussi une fonction). Elle est
constituée d’une entête (ligne 16) puis du corps de la fonction entre {} qui contient les
instructions à exécuter. L’entête est de la forme :

int puissance(int x, int y)

type du type et nom du


résultat 1er paramètre
nom de la type et nom du
fonction 2ème paramètre

Les paramètres (appelés paramètres formels ou muets) de la fonction sont des variables
dont le nom et le type sont définis dans l’entête de la fonction. Ils servent à récupérer les

61
arguments (ou paramètres effectifs) passés à la fonction. La valeur calculée par
puissance est retournée à main par l’instruction à la ligne 22 :

return puis;

Les fonctions ne retournent pas obligatoirement une valeur. Les variables définies dans une
fonction (y compris les paramètres) n’existent que dans cette fonction et ne peuvent ni être
vues ni donc utilisées à l’extérieur de la fonction. Ce sont des variables locales qui sont
créées lorsque l’on rentre dans la fonction et détruites lorsque l’on en sort. On dit que la
portée des variables locales est limitée à la fonction où elles sont définies. Cette remarque
est valable aussi pour la fonction main. Les variables créées dans la fonction main sont
locales à la fonction main et ne peuvent être vues dans la fonction puissance.

3. Le prototype de la fonction (ligne 2). Cette déclaration est normalement obligatoire en C


ANSI. Le prototype permet au compilateur de faire une vérification des types lors de
l’appel de la fonction. Il s’agit d’une sécurité afin de vérifier que vous utilisez
correctement la fonction.

int puissance(int m, int n);

Seuls les types des paramètres sont nécessaires et pas les noms. La ligne suivante est
équivalente à la précédente.

int puissance(int, int);

Il est toutefois préférable d’utiliser un nom de variable judicieusement choisi afin


d’améliorer la lisibilité (et la documentation) du programme. Il n’est pas nécessaire que les
noms de variable utilisés dans le prototype soient les mêmes que dans la définition de la
fonction.

En l’absence du prototype (qui n’est hélas pas obligatoire pour la compilation, même en
ANSI C), le compilateur considère en général que la fonction retourne un entier mais il ne
peut pas contrôler le type des paramètres ni leur nombre. Voyons deux exemples :

62
• Avec le prototype, le compilateur émettra un avertissement (warning) indiquant une
possible perte de précision si vous appelez la fonction avec des flottants (il y a alors un
cast implicite).

res = puissance(2.5, 5.5);

• Avec le prototype, le compilateur émettra un message d’erreur si vous oubliez un


paramètre.

res = puissance(2);

Sans prototype, le programme est compilé sans erreur (Visual C++ par exemple).

Remarques :

1. Si la fonction puissance est définie avant la fonction main dans le même fichier, le
prototype n’est plus obligatoire. Le compilateur utilisera directement l’entête de la fonction
pour contrôler les paramètres.

2. En général, le programme principal et les fonctions se trouvent dans des fichiers différents
et sont compilés séparément. Les prototypes des fonctions se trouvent eux aussi dans un
fichier séparé appelé fichier d’entête (header) ayant une extension en « .h ». Par exemple,
les fonctions de la bibliothèque standard concernant les entrées sorties standards ont leurs
prototypes définis dans le fichier stdio.h. Pour inclure ces définitions dans votre
programme, il suffit d’ajouter au début du fichier :
#include <stdio.h>

main()
...

63
Exercice 4.1 : quels sont les résultats fournis par ce programme ?

int arrondi(float nombre);

void main()
{
float nb_flt_1 = 1.3, nb_flt_2 = 2.8 ;
int nb_arr;

nb_arr = arrondi(nb_flt_1);
printf("%d\n", nb_arr);

nb_arr = arrondi(nb_flt_2);
printf("%d\n", nb_arr);

printf("%d\n", arrondi(nb_flt_1 + nb_flt_2));


printf("%d\n", arrondi(nb_flt_1) + arrondi(nb_flt_2));
}

int arrondi(float nb_flt)


{
float tmp;
int nb_arr;

tmp = nb_flt + 0.5;


nb_arr = (int)tmp;
return nb_arr;
}

Exercice 4.2 : écrivez la définition d’une fonction qui calcule la valeur de l’expression
y=a.x2+b.x+c. a, b, c et x seront les paramètres flottants de la fonction qui retournera la valeur
flottante de y. Donnez son prototype et écrivez un exemple d’appel à cette fonction.

4.3 Fonction sans résultat ou sans paramètres


Une fonction peut ne pas retourner de résultat. Par exemple :

void meteo(int nb_fois)


{
int i;

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


printf("il fait beau\n");
}

64
Le résultat retourné est de type void qui veut dire vide en français, ce qui signifie qu’il n’y a
pas de valeur retournée. L’instruction return n’existe pas dans la définition de la fonction.
Il n’est plus possible d’utiliser l’appel dans une expression comme :
y = meteo(5);

mais il faut l’appeler avec une instruction comme :


meteo(5);

Exercice 4.3 : écrivez la définition d’une fonction triangle affichant un triangle


d’astérisques avec le nombre de lignes nl comme paramètre :
*
**
***
****

Puis écrivez le programme principal utilisant triangle de façon à afficher :


*
*
**
*
**
***
*
**
***
****

Une fonction peut aussi ne posséder aucun paramètre. Sa déclaration est alors :
int toto(void)
{
...
}

L’appel d’une telle fonction doit comporter des parenthèses vides comme dans :
n = toto();

Exercice 4.4 : écrivez la définition d’une fonction bonjour affichant le message bonjour
quand on l’appelle. Ecrivez le prototype et l’appel correspondant.

65
4.4 L’instruction return
L’instruction return peut mentionner une variable ou bien une expression. Les exemples
suivants sont valables :
return x;
return x*y;
return(x*y);
return 0;

Si le type retourné est différent du type du résultat, alors le compilateur effectuera un cast
implicite comme dans :
int toto(...)
{
float x;
...
return x;
}

Le rôle de l’instruction return est double. Il indique la valeur qui sera fourni en résultat et
il met fin à l’exécution des instructions de la fonction. L’instruction return ne se trouve
pas obligatoirement à la fin de la fonction. Il est tout à fait possible d’utiliser plusieurs
return dans une même fonction :

int max(int a, int b)


{
if (a > b)
return a;
else
return b ;
}

Si a est supérieur à b, alors la fonction max retourne a et le programme sort de la fonction.


Sinon, la fonction retourne b et le programme sort de max.

Lorsqu’une fonction ne retourne aucun résultat, soit on ne met pas de return dans la
fonction, soit on met un return sans le faire suivre d’une expression.

return;

66
A l’appel de la fonction, il n’est pas obligatoire d’utiliser le résultat retourné. Si vous ne
souhaitez pas l’utiliser, il suffit d’écrire :

printf("il fait beau\n");

au lieu de (printf retourne le nombre de caractères affichés, ou une valeur négative en cas
d’erreur d’affichage) :

status = printf("il fait beau\n");

4.5 Variables globales et locales


Il est possible de définir en langage C des variables qui, contrairement aux variables locales,
sont accessibles à toutes les fonctions du programme : les variables globales. Il suffit pour
cela de les déclarer en dehors de toute fonction comme dans l’exemple suivant :

int nb_fois; /* variable globale */

main()
{
void meteo(void); /* prototype local */

nb_fois = 2;
meteo();

nb_fois = 3;
meteo();
}

void meteo(void)
{
int i; /* variable locale */

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


printf("il fait beau\n");
}

La variable nb_fois est globale et est accessible à la fois par main et par meteo. Le
programme principal affecte à nb_fois des valeurs qui sont utilisées dans la fonction
meteo. En revanche, la variable i dans meteo reste locale et n’est pas accessible depuis
main.

67
De la même manière, le prototype, suivant l’endroit où il est déclaré, peut être local ou bien
global. Dans l’exemple précédent, le prototype n’est connu que dans la fonction main. Pour
qu’il soit vu par toutes les fonctions du programme, il faut déclarer le prototype en dehors de
toute fonction comme dans l’exemple suivant. Cette méthode est nécessaire quand on veut,
par exemple, appeler une fonction dans une autre fonction.
int nb_fois; /* variable globale */
void meteo(void); /* prototype global */

main()
{
nb_fois = 2;
meteo();

nb_fois = 3;
meteo();
}

Dans l’exemple précédent, rien n’empêche meteo de modifier la valeur de la variable


globale nb_fois. On appelle effet de bord (side effect = effet secondaire) la modification
indirecte de la valeur d’une variable globale dans une fonction. Rien dans l’appel ou la
définition de la fonction ne vous permet de savoir qu’elle modifie une variable globale
puisque cette variable n’est pas passée comme paramètre. En n’utilisant que des variables
locales, vous pouvez savoir quelles variables sont modifiées par une fonction en regardant sa
définition ou son prototype. De plus, la fonction devient totalement indépendante car elle ne
dépend pas d’une variable globale déclarée ailleurs dans le programme. La fonction peut donc
être réutilisée dans n’importe quel programme. Ce sont les raisons pour lesquelles vous ne
devez pas utiliser de variables globales dans vos programmes sauf si vous pouvez
démontrer qu’elles sont indispensables.

D’une manière générale, on appelle « effet de bord » de l’évaluation d’une expression tout
changement apporté à la valeur d’une variable. Une affectation a évidemment un effet de bord
sur la variable à gauche du signe = comme dans :
y = 3*x;

Mais un appel de fonction peut aussi en avoir si on utilise des variables globales. Les
opérations d’incrémentation ou de décrémentation ont également un effet de bord :
y = 3*x++;

68
Après cette instruction, y et x ont changé de valeur. Le comportement de l’instruction
suivante est particulièrement imprévisible :
y = b*x + 3*x++;

L’ordre d’évaluation des deux termes est non précisé et x peut être incrémenté avant ou après
sa multiplication par b. Il ne faut jamais utiliser un opérateur d’incrémentation (ou de
décrémentation) sur une variable apparaissant plus d’une fois dans une expression. Il y a un
autre exemple classique d’effet de bord causé par la confusion traditionnelle entre affectation
= et test conditionnel ==. La condition est toujours fausse et y passe à 0 par effet de bord :
if (y = 0) au lieu de if (y == 0)

Lorsqu’une variable locale est créée, son emplacement en mémoire est réservé à chaque fois
que l’on rentre dans la fonction où elle est déclarée. Si vous l’initialisez au moment de sa
déclaration, elle prendra sa valeur d’initialisation à chaque appel de la fonction.

Lorsqu’une variable globale est créée, son emplacement en mémoire est réservé à la
compilation du programme. Si vous l’initialisez au moment de sa déclaration, elle prendra sa
valeur d’initialisation une seule fois au début de l’exécution du programme. Une variable
globale non initialisée est toujours mise à zéro par le compilateur.

Une variable globale peut être cachée dans une fonction par une variable locale de même
nom. C’est alors la variable locale qui est utilisée, la variable globale n’étant plus accessible
dans cette fonction.

int n, p; /* variables globales */

main()
{
int n; /* cette variable locale cache la variable globale
n tandis que p est toujours accessible */
}

void fct(float x, float p)


{
/* ici n est une variable globale alors que p est une
variable locale */
}

69
4.6 Variable statique
La variable locale perdant sa valeur à chaque fois que l’on sort de la fonction (puisqu’elle est
détruite), il parait obligatoire d’utiliser une variable globale si l’on veut garder la trace d’un
état interne de cette fonction. L’utilisation de variables globales étant fortement déconseillée,
il existe la possibilité en langage C de créer des variables locales statiques, c’est-à-dire des
variables locales qui sont créées lors de la première entrée dans la fonction, mais qui ne sont
détruites que lorsque l’on sort du programme. Mais elles sont tout de même locales et ne sont
accessibles qu’à l’intérieur de la fonction où elles sont déclarées. Prenons l’exemple d’une
fonction qui compte le nombre de fois où elle est appelée.
void meteo(int nb_fois);

main()
{
int nb_fois;

nb_fois = 1;
meteo(nb_fois);
nb_fois = 2;
meteo(nb_fois);
nb_fois = 3;
meteo(nb_fois);
}

void meteo(int nb_fois)


{
static int compteur = 0;
int i;

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


printf("il fait beau\n");

compteur++ ;
printf("appel n°%d\n", compteur) ;
}

Le résultat est alors :


il fait beau
appel n°1
il fait beau
il fait beau
appel n°2
il fait beau
il fait beau
il fait beau
appel n°3

70
Exercice 4.5 : écrivez la définition d’une fonction trois_etats qui affiche :
1. « c’est la première fois » lors du premier appel,
2. « c’est la deuxième fois » lors du deuxième appel,
3. « régime de croisière » lors des appels suivants.

4.7 La récursivité
On dit qu’une fonction est récursive quand elle s’appelle elle-même. Grâce au principe des
variables locales dans les fonctions (qui sont créées à l’appel de la fonction et détruites à sa
sortie), toutes les fonctions en C peuvent être récursives. Cette propriété est fondamentale
d’un point de vue algorithmique même si son utilisation réelle est en général assez rare. Elle
permet toutefois de simplifier certains algorithmes. Prenons l’exemple d'une fonction qui
affiche un nombre décimal positif à l’aide de la fonction putchar.

void affich_dec(int n);

main()
{
affich_dec(479);
}
void affich_dec(int n)
{
static int compteur = 0;

compteur++ ;
printf("appel numero %d\n", compteur);

if ((n / 10) != 0)
affich_dec(n / 10);

putchar(n % 10 + '0');
/* code ASCII de 0 + un chiffre entier
= code ASCII du chiffre */
}

Le résultat affiché est le suivant :


appel numéro 1
appel numéro 2
appel numéro 3
479

Lors du premier appel affich_dec(479), cette fonction appelle affich_dec(47) qui


appelle affich_dec(4). Comme 4 est inférieur à 10, la fonction affiche 4 puis rend la

71
main au niveau supérieur qui affiche 47%10=7 puis rend la main au premier niveau qui
affiche 479%10=9 et termine l’exécution. A chaque appel de fonction, un nouveau jeu de
variables locales (différentes de celles du niveau précédent) est créé. Seule la variable
compteur déclarée en statique en identique pour tous les niveaux d’appels.

Exercice 4.6 : écrivez la définition d’une fonction factor qui calcule la factorielle d’un
nombre n ! = 1.2.3.4 ... (n-1).n. Expliquez clairement le fonctionnement de la fonction avec
l’appel factor(5).

4.8 Passage des paramètres par valeur.


Jusqu’à présent, nous avons considéré que les paramètres de la fonction étaient des entrées et
le résultat la seule sortie. C’est rarement le cas dans un programme réel. Voyons l’exemple
suivant :

void echange(int a, int b);

main()
{
int n = 1, p = 2 ;

printf("avant appel : n = %d, p = %d\n", n, p);


echange(n, p);
printf("après appel : n = %d, p = %d\n", n, p);
}

void echange(int a, int b)


{
int tmp;

printf("début échange : n = %d, p = %d\n", a, b);

tmp = b;
b = a;
a = tmp;
printf("fin échange : n = %d, p = %d\n", a, b);
}

Le résultat est le suivant :


avant appel : n = 1, p = 2
début échange : n = 1, p = 2
fin échange : n = 2, p = 1
après appel : n = 1, p = 2

72
L’échange des valeurs des deux variables n et p a bien eu lieu dans la fonction echange,
mais une fois revenu dans le programme principal, il ne reste plus trace de cette opération.

En effet, lors de l’appel de la fonction, il y a eu transmission de la valeur des expressions n et


p. Ces valeurs ont été recopiées localement dans les variables a et b de la fonction echange.
C’est sur ces copies de n et p que l’échange des valeurs a eu lieu, de sorte que les variables n
et p dans la fonction main n’ont pas été modifiées.

Les paramètres d’une fonction sont toujours transmis par valeur en C.

Evidemment, ce mode de travail pose de gros problèmes puisque :


1. la fonction est le seul sous programme existant en C,
2. elle ne peut retourner qu’un seul résultat.

Dans le cas général, une fonction doit pouvoir fournir plusieurs résultats. Pour cela, il n’y a
que deux solutions :

1. utiliser des variables globales (ce qui est fortement déconseillé sauf cas exceptionnel).

2. transmettre en paramètres la valeur de l’adresse d’une variable. La fonction travaillera sur


la copie de cette adresse, mais aura de ce fait accès à la case mémoire contenant la valeur
de la variable qu’elle pourra donc modifier. C’est ce qui est fait dans la fonction scanf :

scanf("%d", &a);

Cette méthode va nous amener à parler des pointeurs.

En C, on passe généralement les adresses des variables comme paramètre afin de pouvoir les
modifier (les paramètres sont donc des entrées-sorties). Le résultat de la fonction sert
généralement à vérifier son fonctionnement. En général, une valeur 0 indique que tout s’est
bien passé et une valeur non nulle indique une erreur.

73
4.9 Les pointeurs
Il est possible, en langage C, de définir une variable destinée à contenir une adresse. On
appelle cette variable un pointeur. Il existe plusieurs type de pointeur suivant que l’adresse
pointe sur une variable entière, flottante, caractère, ... Pour un système d’exploitation 32 bits,
un pointeur contient toujours une adresse codée sur 32 bits, quel que soit le type de la variable
pointée. Voici un exemple de déclaration d’un pointeur sur un entier :

int *ptri;

ptri est une variable qui peut contenir une adresse codée sur 32 bits. Cette variable est pour
l’instant non initialisée et sa valeur est inconnue. Nous pouvons affecter une valeur à ce
pointeur en utilisant l’opérateur &. Par exemple en déclarant une variable entière initialisée ou
non :

int n = 20;

puis en exécutant :

ptri = &n;

Cette dernière instruction copie l’adresse de n dans ptri. On dit que ptri pointe sur n.
Représentons l’évolution des variables avant et après l’exécution de l’instruction :

avant après
variable contenu variable contenu
n 20 n 20

ptri ? ptri addresse_n

Lorsqu’il n’est pas utilisé dans une déclaration, mais dans une instruction, l’opérateur * joue
un rôle symétrique de l’opérateur &. En effet, quand il précède un pointeur, il désigne
l’information pointée. Supposons que l’on a déclaré :

int p;

74
On peut transférer la valeur pointée par ptri dans p en exécutant :

p = *ptri;

ce qui revient à copier 20 dans p. On a aussi le droit d’écrire :

printf("%d", *ptri);

ou bien

*ptri = 50;

Récapitulons :
• &variable : c’est l’adresse de la variable.
• xxxx *ptr (ou xxxx est le type d’une variable comme par exemple int, char,
float, void) : c’est la déclaration d’un pointeur.
• *ptr : c’est la valeur pointée par le pointeur ptr. On l’utilise exactement comme une
variable de même type que celui défini lors de la déclaration du pointeur.

Récapitulons l’évolution des variables sur un exemple simple :


int *ptri;
int n = 20;
int p;

variable contenu
n 20

p ?

ptri ?

75
ptri = &n;

variable contenu
n 20

p ?

ptri addr_n

p = *ptri;

variable contenu
n 20

p 20

ptri addr_n

*ptri = 50;

variable contenu
n 50

p 20

ptri addr_n

76
ptri = &p;

variable contenu
n 50

p 20

ptri addr_p

Exercice 4.7 : A l’aide d’un schéma, expliquez l’évolution des différentes variables au cours
de l’exécution du programme.
int *ptri1, *ptri2;
int n = 1, p = 2, q = 3;

ptri1 = &n;
ptri2 = &p;
*ptri1 = *ptri2 + 3;
ptri1 = ptri2;
*ptri1 = *ptri2 + 5;

Remarques importantes :
• Il n’est normalement pas permis d’affecter la valeur d’un pointeur d’un certain type à un
pointeur d’un type différent comme par exemple dans le programme suivant :
main()
{
int *ptri;
char *ptrc;

ptrc = ptri;
ptri = ptrc;
}

Copier un pointeur de type entier dans un pointeur de type caractère ou bien l’inverse
provoque un avertissement au moment de la compilation ce qui n’empêche d’ailleurs pas la
copie au moment de l’exécution du programme. Il est préférable toutefois de faire un
cast comme pour des variables normales.

77
main()
{
int *ptri;
char *ptrc;

ptrc = (char *)ptri;


ptri = (int *)ptrc;
}

De la même manière, la copie de l’adresse d’une variable d’un type dans un pointeur d’un
autre type est normalement interdite comme dans le programme :

main()
{
int n;
char *ptrc;

ptrc = &n;
}

Encore une fois, seul un avertissement est généré durant la compilation ce qui n’empêche
toujours pas la copie au moment de l’exécution du programme. Un cast est toutefois
souhaitable.

main()
{
int n;
char *ptrc;

ptrc = (char *)&n;


}

• La priorité de l’opérateur unaire * est plus élevée que celle des opérateurs arithmétiques de
sorte que, par exemple, l’expression :

2 * *ptri

est interprétée comme :

2 * (*ptri)

c’est-à-dire comme le produit par 2 de la valeur pointée par ptri. Les espaces n’ont
aucune importance et les expressions suivantes sont strictement équivalentes :

78
2 * *ptri
2**ptri
2 **ptri
2** ptri

Généralement, on n’utilise pas les parenthèses pour clarifier l’expression mais plutôt les
espaces. L’expression finalement retenue est la plus simple à comprendre et la plus lisible.

y = 2 * *ptri;

• Lors de la déclaration de plusieurs pointeurs, il ne faut pas écrire :

float * ptrf1, ptrf2;

En effet, on a déclaré ici un pointeur sur flottant ptrf1 et une variable flottante ptrf2.
L’* ne s’applique qu’à la première variable rencontrée. Si l’on souhaite que ptrf2 soit
aussi un pointeur, il faut écrire :

float * ptrf1, * ptrf2;

En supprimant les espaces, on supprime aussi les ambiguïtés. Il est clair alors que l’* ne
s’applique qu’à la variable suivante.

float *ptrf1, *ptrf2;

• Il existe un type de pointeur générique, c’est à dire capable de pointer vers n’importe quel
type d’objet. C’est le type void.

void *ptr;

Nous aurons l’occasion de le revoir dans le cours sur les variables dynamiques.

• La déclaration d’un pointeur ne réserve pas d’emplacement pour une information


pointée.

79
La déclaration d’une variable pointeur ptri réserve 32 bits en mémoire pour stocker une
adresse. Cette adresse n’est pas initialisée au moment de la déclaration et le pointeur ne
pointe donc sur rien.

int *ptri;

Si, à la suite de cette déclaration, on essaye d’exécuter

*ptri = 100;

on force le programme à placer la valeur 100 à une adresse non définie, ce qui bien
entendu est une grave erreur (on parle d’erreur de débordement). Prenons un exemple
simple de programme et voyons ce qu’il se passe avec Visual C++ sous NT et avec GCC
sous Linux.

main()
{
int *ptri;

*ptri = 100;

printf("%d\n", *ptri);
}

o Sous NT, le compilateur ne détecte aucune erreur. A l’exécution dans le debugger, le


programme produit l’erreur suivante et s’arrête :

A l’éxécution directement sous Windows NT (en dehors de Visual C++), le programme


produit l’erreur suivante et s’arrête :

80
Dans les deux cas, le programme signale que vous essayez d’écrire dans un
emplacement mémoire auquel vous ne devriez pas avoir accès. Signalons que cet
emplacement mémoire peut contenir une autre variable ou même une instruction du
programme lui-même. Bien sur, il est aussi possible que cet emplacement soit libre et
qu’il ne contienne rien du tout. L’écriture dans une zone mémoire non réservée est la
principale cause d’erreur et de plantage aléatoires dans les programmes.

o Sous Linux, le programme fonctionne normalement, c’est à dire qu’il affiche 100. Il n’y
a aucun message d’erreur ni de warning à la compilation. En fait, la case mémoire
utilisée étant libre, le système d’exploitation autorise l’exécution du programme. C’est
là un comportement tout à fait désastreux car cela renforce le caractère aléatoire de
l’erreur selon les portions du programme qui seront exécutées. Si l’écriture de la donnée
écrase une instruction du programme, le programme s’arrête et génère normalement un
fichier core. Ce fichier contient l’image du programme au moment du plantage et il est
possible, si le programme a été compilé avec les options de debug, de restituer sous
debugger l’état exact du programme au moment du crash, à savoir la ligne où le
programme s’est arrêté ainsi que le contenu des variables (le tout bien entendu en
langage C). Sans la compilation en mode debug, la même chose est obtenue mais en
langage assembleur ce qui bien entendu ne facilite pas la découverte du problème.

Il existe une variante de cette erreur, c’est la lecture d’une zone mémoire non allouée.
Prenons l’exemple suivant :
main()
{
int *ptri, i;

i = *ptri;

printf("%d\n", i);
}

81
Ce programme copie dans la variable i la valeur pointée par ptri qui n’a pas été
initialisé. Bien sur, ptri contient forcément une adresse même si nous ne la connaissons
pas. Ce cas est moins grave puisque le programme n’écrase aucune donnée. On va
simplement obtenir dans i une valeur indéterminée qui sera ensuite utilisée dans le
programme. Il n’y a ni message d’erreur, ni avertissement à la compilation sous NT ou
sous Linux. A l’exécution, le programme affiche n’importe quoi sous les deux systèmes
d’exploitation. La lecture dans une zone mémoire non réservée est aussi une cause
d’erreur et de plantage aléatoires dans les programmes.

Exercice 4.8 : écrivez le code C correspondant aux actions suivantes.


1. Déclarer un entier i et un pointeur ptri vers un entier.
2. Initialiser l’entier à la valeur 10 et faire pointer ptri vers i.
3. imprimer la valeur de i.
4. Modifier la valeur de i à 5 en utilisant le pointeur.
5. imprimer la valeur de i en utilisant le pointeur.

Exercice 4.9 : expliquez ligne par ligne l’action du programme suivant.

#include <stdio.h>

main()
{
int i, j, *ptri;

*ptri = 1;
ptri=&i;
*ptri=19;
i=19;
ptri=&j;
*ptri=45;
scanf("%d", &i);
scanf("%d", ptri);
printf("%d\n", i);
printf("%d\n", *ptri);
}

82
4.10 Passage de pointeurs comme paramètres d’une fonction
Nous avons vu précédemment qu’il n’était pas possible en C de modifier la valeur des
arguments d’une fonction. Prenons un exemple.

void inverse(float y);

main()
{
float x =3.14;

printf("adresse de x = %x\n", &x);

printf("avant appel, x = %f\n", x);


inverse(x);
printf("après appel, x = %f\n", x);
}

void inverse(float y)
{
printf("adresse de y = %x\n", &y);

printf("début fonction, y = %f\n", y);


y = 1/y;
printf("fin fonction, y = %f\n", y);
}

Le résultat du programme est :


adresse de x = 12ff7c
avant appel, x = 3.140000
adresse de y = 12ff78
début fonction, y = 3.140000
fin fonction, y = 0.318471
après appel, x = 3.140000

La fonction inverse travaille sur une copie des arguments et les modifications qu’elle peut
y apporter restent locales et ne « remontent » pas dans la fonction main. A l’aide des
pointeurs, nous pouvons résoudre ce problème. Modifions l’exemple précédent.

void inverse(float *ptrx);

main()
{

83
float x =3.14;

printf("adresse de x = %x\n", &x);

printf("avant appel, x = %f\n", x);


inverse(&x);
printf("après appel, x = %f\n", x);
}

void inverse(float *ptrx)


{
printf("adresse dans ptrx = %x\n", ptrx);

printf("début fonction, *ptrx = %f\n", *ptrx);


*ptrx = 1/(*ptrx);
printf("fin fonction, *ptrx = %f\n", *ptrx);
}

Le résultat du programme est :


adresse de x = 12ff7c
avant appel, x = 3.140000
adresse dans ptrx = 12ff7c
début fonction, *ptrx = 3.140000
fin fonction, *ptrx = 0.318471
après appel, x = 0.318471

L’argument passé à la fonction est maintenant l’adresse de la variable x. Il y a toujours


transmission par valeur et la fonction va travailler avec une copie de l’adresse de x. La
fonction ne peut donc pas modifier l’adresse de x, mais par contre, elle peut maintenant
modifier la valeur pointée par l’adresse de x. Il suffit pour cela de déclarer un pointeur comme
paramètre et de changer la valeur pointée.

Exercice 4.10 : reprenez le programme utilisant la fonction echange du §4.8 (p72) et


modifiez-la à l’aide des pointeurs pour qu’elle réalise effectivement l’échange des deux
valeurs contenues dans les variables a et b.

84
5. Les tableaux

5.1 Tableaux à une dimension


Les variables que nous avons utilisées jusqu’à présent ne pouvaient contenir qu’une seule
valeur à un moment donné. On les appelle des variables scalaires. Il existe en programmation
de nombreuses structures de données plus ou moins élaborées pouvant contenir plusieurs
valeurs. La plus simple et la plus répandue d’entre elles est le tableau. L’élément
mathématique équivalent au tableau en programmation est la matrice. Comme elle, un tableau
peut avoir un nombre quelconque de dimensions. Nous allons tout d’abord voir l’équivalent
du vecteur mathématique, le tableau à une dimension. Un exemple de déclaration est le
suivant :

float tab[5];

Comme pour une variable :


• un tableau a un nom (ici, c’est tab).
• il contient des valeurs d’un certain type (ici, des flottants).
• De plus, il contient un certain nombre de valeurs spécifié entre [] (ici, 5 valeurs).

Lorsqu’il rencontre cette déclaration, le compilateur doit réserver 5 emplacements contigus de


4 octets chacun dans la mémoire du programme. Le tableau ainsi créé peut être vu sous la
forme suivante :

tab[0]
tab[1]
tab[2]
tab[3]
tab[4]

ou bien

tab[0] tab[1] tab[2] tab[3] tab[4]

85
Attention, en langage C, le premier élément (la première case) d’un tableau est l’élément
0 et non l’élément 1.

Il est possible d’initialiser le tableau au moment de la déclaration de la manière suivante :


int x[5] = {1, 2, 3, 4, 5};
char car[3] = {’a’, ’b’, ’c’};

ce qui donne :

1 2 3 4 5

x[0] x[1] x[2] x[3] x[4]

a b c

car[0] car[1] car[2]

Il est aussi possible de ne mentionner que les premières valeurs :


int x[5] = {1, 2, 3};

ce qui donne :

1 2 3

x[0] x[1] x[2] x[3] x[4]

ou bien d’omettre la taille du tableau :


int x[] = {1, 2};

x prend alors automatiquement une taille égale au nombre de valeurs contenues entre les
accolades.

1 2

x[0] x[1]

86
On peut repérer chaque élément d’un tableau par le nom de ce tableau suivi, entre [] d’une
valeur entière que l’on nomme un indice. Ainsi, tab[0] désigne le premier élément du
tableau tab et tab[i] désigne le ième élément du tableau tab, i devant obligatoirement
être une variable entière comprise entre 0 et 4. D’une manière générale, en langage C, un
indice peut être n’importe quelle expression arithmétique entière. Ainsi, les notations
suivantes sont légales (i et j sont des entiers) :

tab[i+j], tab[2*j + i], tab[2+3]

Une fois déclaré, on peut manipuler les éléments de ce tableau. Un élément d’un tableau
s’emploie exactement comme une variable de même type. Comme tous les tableaux que nous
allons manipuler maintenant ne contiendront que des éléments scalaires, un élément d’un
tableau pourra :

1. faire l’objet d’une affectation. Exemples :


int x[4];

x[0] = 1;
x[1] = 2;
x[2] = 3;
x[3] = 4;

1 2 3 4

x[0] x[1] x[2] x[3]

ou bien
char voy[6];

voy[0] = ’a’;
voy[1] = ’e’;
voy[2] = ’i’;
voy[3] = ’o’;
voy[4] = ’u’;
voy[5] = ’y’;

a e i o u y

voy[0] voy[1] voy[2] voy[3] voy[4] voy[5]

87
Si l’on souhaite placer la même valeur dans tous les éléments, on utilise une boucle avec
compteur :
int x[4], i;

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


x[i] = 0;

Il est à noter qu’il est impossible de copier un tableau dans un autre tableau de la manière
suivante :
int x[4], y[4];

x = y;

il faut obligatoirement copier élément par élément.


int x[4], y[4];

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


x[i] = y[i];

2. figurer dans une expression arithmétique. Comme pour toute variable, les notations
suivantes sont possibles :

y=tab[2]+1, tab[0]=2*x+3, tab[i]= 3*tab[j+1]+2

3. être utilisé dans une instruction de lecture (comme un scanf) ou dans une instruction
d’écriture (comme un printf). Exemples :
int x[4];

scanf("%d%d", &x[0], &x[1]);

&x[i] étant l’adresse du ième élément du tableau x. Bien entendu, il est possible
d’écrire :

for (i = 0; i < 4; i++) {


printf("donnez la valeur numéro %d : ", i);
scanf("%d", &x[i]);
}

88
Comme nous l’avons déjà vu, il est impossible de manipuler globalement un tableau au
moment de l’affectation. Cela s’applique aussi pendant la lecture :
scanf("%d", &x);

Cette instruction ne signifie pas que le programme va lire sur l’entrée standard tous les
éléments du tableau x. Cependant, le compilateur laisse passer cette instruction. C’est
parce que, en C, &x est l’adresse de la première case de x, c’est à dire que &x est
équivalent à &x[0]. Nous verrons ultérieurement que x utilisé seul est un pointeur qui
pointe sur l’adresse de la première case du tableau x.

&x ≡ &x[0] ≡ x

Les trois instructions suivantes sont donc strictement équivalentes :


scanf("%d", &x);
scanf("%d", &x[0]);
scanf("%d", x);

L’écriture sur la sortie standard ne pose pas de problèmes particuliers comme le montre le
petit programme suivant :
main()
{
int tab[6], i;

tab[0] = 0;
for (i = 1; i < 5; i++)
tab[i] = 1;
tab[5] = 2;

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


printf("%d\n", tab[i]);
}

Exercice 5.1 : écrivez un programme qui lit 5 valeurs avant d’en afficher les carrés. Le
dialogue avec l’utilisateur se présentera comme dans l’exemple suivant :
donnez 5 nombres entiers :
1 3 5 7 9

89
NOMBRE CARRE
1 1
3 9
5 25
7 49
9 81

5.2 Remarques importantes sur les tableaux


Il est important de noter que :
• La taille d’un tableau en mémoire dépend à la fois de son type et de sa dimension. En
langage C, cette taille doit être connue au moment de la compilation. Le programme
suivant ne devrait donc pas compiler (hélas si avec Dev-C++ et CodeBlocks à cause de
GCC qui est une vraie passoire) :
main()
{
int dim;

scanf("%d", &dim);

int tab[dim]; /* interdit */

...
}

En effet, il est impossible de disposer de tableaux dont le nombre d’éléments serait


déterminé pendant l’exécution du programme et bien entendu que ce nombre varie durant
son exécution. Les tableaux en C sont dit statiques. Il existe une technique appelée
« gestion dynamique de la mémoire » qui permet de résoudre ce problème. Nous la verrons
ultérieurement.

Il est toutefois possible (et même souhaitable) d’utiliser une constante pour définir le
nombre d’éléments dans le tableau afin de pouvoir modifier facilement sa taille.
#define DIM 100

main()
{
float tab[DIM], tab1[DIM+1], tab2[2*DIM];
int i;

90
for (i = 0; i < DIM; i++)
tab[i] = ...

for (i = 0; i < DIM+1; i++)


tab1[i] = ...

for (i = 0; i < 2*DIM; i++)


tab2[i] = ...
}

Le préprocesseur va remplacer, avant la compilation, la macro DIM par 100 (comme la


commande de remplacement d’un traitement de texte). Le programme sera ensuite compilé
normalement.Par convention, les macros sont écrits en majuscules afin de les différencier
facilement des variables qui elles ne sont jamais entièrement écrites en majuscules.

• Il est interdit de lire ou d’écrire dans un élément du tableau qui se trouve en dehors du
tableau, c’est à dire que l’indice utilisé soit négatif ou supérieur à la valeur maximale
possible (on appelle cela un « débordement d’indice » ou une « erreur de
débordement »). Exemple :
int x[10];

x[15] = 0;

Il faut savoir que le compilateur fait correspondre à la notation x[15] un calcul de


l’adresse de l’élément 15, c’est à dire que x[15] est strictement équivalent à *(x +
15). La déclaration de x a réservé 40 octets pour stocker les éléments du tableau. x[15] se
trouve 60 octets au-dessus de la première case de x, c’est à dire au-delà de la zone
réservée. Ecrire :
x[15] = 0;

revient à essayer d’écrire dans une zone mémoire non réservée à l’aide d’un pointeur
(problème déjà vu au §4.9). Vous risquez donc d’écraser une variable existante. En cas de
lecture :
y = x[15];

La valeur contenue par y est indéterminée.

91
Exercice 5.2 : donnez les résultats du programme suivant.
#define DIM 6

main()
{
int tab[DIM], i;

tab[0] = 1;
for (i = 1; i < DIM; i++)
tab[i] = tab[i-1] + 2;

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


printf("%d\n", tab[i]);
}

Exercice5.3 : donnez les résultats du programme suivant.


#define DIM 8

main()
{
int suite[DIM], i;

suite[0] = 1;
suite[1] = 1;
for (i = 2; i < DIM; i++)
suite[i] = suite[i-1] + suite[i-2];

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


printf("%d\n", suite[i]);
}

Exercice5.4 : écrivez la portion de programme calculant la somme de tous les éléments d’un
tableau.

Exercice 5.5 : écrivez la portion de programme déterminant le plus grand élément d’un
tableau.

Exercice 5.6 : écrivez la portion de programme déterminant le plus grand élément d’un
tableau ainsi que sa position.

92
Exercice5.7 : soit le programme incomplet suivant.
#define NEL 20

main()
{
int x[NEL], i, j, temp;

printf("donnez 20 valeurs entières \n");


for (i = 0; i < NEL; i++)
scanf("%d", &x[i]);

...

printf("voici vos valeurs triées par ordre croissant\n");


for (i = 0; i < NEL; i++)
printf("%d", x[i]);
}

Complétez-le afin de trier le tableau x par ordre croissant.

Exercice 5.8 : même chose mais en ordre décroissant.

5.3 Les tableaux à deux dimensions


Nous avons jusqu’à maintenant utilisé des tableaux à une dimension (une ligne ou bien une
colonne) similaires aux vecteurs en mathématiques. Dans la vie courante, le mot tableau est
plutôt utilisé pour un ensemble de valeurs rangées en lignes et en colonnes (comme dans un
tableur par exemple). Un exemple de déclaration d’un tableau à deux dimensions est le
suivant :

float tab2[4][2];

Le compilateur va réserver 8 (2 fois 4) emplacements mémoire de 4 octets soit 32 octets au


total. Par convention, nous considérerons que le premier indice entre crochets correspond aux
lignes et que le second indice entre crochets correspond aux colonnes, ce qui donne la
représentation suivante :

93
colonne 0 colonne 1

ligne 0 tab2[0][0] tab2[0][1]

ligne 1 tab2[1][0] tab2[1][1]

ligne 2 tab2[2][0] tab2[2][1]

ligne 3 tab2[3][0] tab2[3][1]

Evidemment, il est tout à fait possible de voir ce tableau sous la forme colonnes x lignes :

colonne 0 colonne 1 colonne 2 colonne 3

ligne 0 tab2[0][0] tab2[1][0] tab2[2][0] tab2[3][0]

ligne 1 tab2[0][1] tab2[1][1] tab2[2][1] tab2[3][1]

En général, on utilise la convention lignes x colonnes. Il est possible d’initialiser le tableau au


moment de sa déclaration :

float tab2[4][2] = {{0, 1},


{2, 3},
{4, 5},
{6, 7}};

ce qui donne la représentation suivante :

colonne 0 colonne 1

ligne 0 0 1

ligne 1 2 3

ligne 2 4 5

ligne 3 6 7

Cette notation peut être remplacée par la suivante (qui est cependant moins claire) :

94
float tab2[4][2]={{0, 1},{2, 3},{4, 5},{6, 7}};

Mais aussi par :

float tab2[4][2] = {0, 1, 2, 3, 4, 5, 6, 7};

Le compilateur remplit alors le tableau ligne par ligne. Cette notation est encore moins claire
que la précédente. La première déclaration est nettement préférable.

Il est impossible d’omettre les dimensions du tableau au moment de la déclaration. La


déclaration suivante est illégale :

float tab2[][] = {{0, 1},{2, 3},{4, 5},{6, 7}};

Par contre, l’initialisation incomplète est toujours possible :

float tab2[4][2]={{0},{2, 3},{4},{6, 7}};

ce qui donne la représentation suivante :

colonne 0 colonne 1

ligne 0 0

ligne 1 2 3

ligne 2 4

ligne 3 6 7

L’affectation d’une valeur à un élément d’un tableau à deux dimensions se fait soit élément
par élément :

tab2[0][0] = 0;
tab2[0][1] = 1;
tab2[1][0] = 2;

95
tab2[1][0] = 3;
...

Soit à l’aide de deux boucles for imbriquées :


float tab2[5][3] ;
int i, j;

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


for (j = 0; j < 3; j++)
tab2[i][j] = 2*i + j;

Exercice 5.9 : représentez le tableau précédent avec ses valeurs.

La lecture sur l’entrée standard de tous les éléments d’un tableau s’effectue de la manière
suivante :
int tab2[2][5], i, j;

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


for (j = 0; j < 5; j++)
scanf("%d", &tab2[i][j]);

Exercice 5.10 : les données suivantes sont fournies au programme précédent. Représentez le
tableau avec ses valeurs.
10 20 30 40 50 60 70 80 90 100

Exercice 5.11 : même question en permutant les deux boucles for.

Exercice 5.12 : écrivez la portion de programme calculant la somme de tous les éléments d’un
tableau à deux dimensions.

Exercice 5.13 : écrivez la portion de programme déterminant le plus grand élément d’un
tableau à deux dimensions ainsi que sa position.

96
Les tableaux à deux dimensions servent souvent à représenter des images quoique l’utilisation
des pointeurs soit plus commode pour cela. Il est possible d’utiliser des tableaux ayant plus de
deux dimensions. Pour ce faire, il suffit de généraliser ce que nous avons vu pour les tableaux
à deux dimensions. Ainsi, les instructions suivantes sont légales :

float tab3[5][3][4];

tab3[0][1][2] = 1.5;

L’utilisation de tableaux à plus de deux dimensions est assez rare.

5.4 Passage d’un tableau comme paramètre d’une fonction


En utilisant les pointeurs, le passage d’un tableau comme paramètre d’une fonction est assez
facile à résoudre. Le programme suivant vous montre un exemple :
void raz(int tab[10]);

main()
{
int tableau[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, i;

printf("tableau avant raz :");


for (i = 0; i < 10; i++)
printf(" %d", tableau[i]);
printf("\n");

raz(tableau);

printf("tableau après raz :");


for (i = 0; i < 10; i++)
printf(" %d", tableau[i]);
printf("\n");
}

void raz(int tab[10]) {


int i;

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


tab[i] = 0;
}

Le résultat est le suivant :


tableau avant raz : 0 1 2 3 4 5 6 7 8 9
tableau après raz : 0 0 0 0 0 0 0 0 0 0

97
Nous avons déjà vu que le nom du tableau est identique à son adresse, c’est-à-dire à l’adresse
de sa première valeur. L’appel à raz(tableau); provoque la transmission à la fonction
raz de l’adresse du tableau. Dans la fonction raz, à chaque appel, cette adresse est recopiée
dans la variable tab. tab pointe donc maintenant sur tableau et tab[i] est la même
valeur que tableau[i]. Le passage du paramètre s’effectue donc toujours par valeur.

La déclaration dans l’entête de la fonction n’a pas pour rôle de réserver de l’espace mémoire
puisque tab pointe sur tableau qui a déjà son emplacement réservé dans le programme
principal. La dimension du tableau est donc inutile et la déclaration suivante est possible :

void raz(int tab[])


{
int i;

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


tab[i] = 0;
}

On peut aussi utiliser un simple pointeur (void raz(int *toto)) pour récupérer
l’adresse du tableau. La fonction raz doit connaître la taille du tableau pour pouvoir modifier
ses valeurs. Il est possible de passer cette taille en paramètre.

void raz(int tab[], int taille);

main()
{
int t1[10], t2[5];

raz(t1, 10);
raz(t2, 5);

...

void raz(int tab[], int taille)


{
int i;

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


tab[i] = 0;
}

98
Rien n’empêche d’appeler :

raz(t1, 20);

Il se produit alors un débordement d’indice avec écrasement des 40 octets se trouvant au-
dessus de t1 dans la mémoire (sans doute t2).

Il est possible de ne traiter qu’une partie d’un tableau en utilisant :

raz(t1, 5); /* les 5 premiers éléments */


raz(&t1[4], 3); /* les éléments 5, 6 et 7 */

Exercice 5.14 : écrivez une fonction retournant la valeur maximale d’un tableau à une
dimension. Ecrivez un petit programme principal appelant cette fonction.

Exercice 5.15 : même question pour une fonction fournissant la valeur maximale et la
position.

Dans le cas d’un tableau à deux (ou plus) dimensions, le mécanisme reste le même.
void raz(int tab2[2][3]);

main()
{
int tab2x3[2][3];

raz(tab2x3);
...
}

void raz(int tab2[2][3])


{
int i, j;

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


for (j = 0; j < 3; j++)
tab2[i][j] = 0;
}

99
Par contre, il faut obligatoirement spécifier les dimensions de tab2. Le programme suivant ne
se compile pas.

void raz(int tab2[][]); ♦ERREUR

main()
{
int tab2x3[2][3];

raz(tab2x3);
...
}

void raz(int tab2[][]) ♦ERREUR


{
int i, j;

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


for (j = 0; j < 3; j++)
tab2[i][j] = 0;
}

Le compilateur doit savoir, au moment de la déclaration, l’organisation du tableau (nombre de


lignes et de colonnes) pour pouvoir les ranger en mémoire dans l’ordre ligne x colonne.

5.5 Relations entre tableaux et pointeurs


L’opérateur + permet de réaliser la somme de deux valeurs arithmétiques, mais il permet aussi
de réaliser la somme d’un pointeur et d’un entier. Il n’y a rien là d’anormal puisqu’un
pointeur n’est, en général, qu’un entier sur 32 bits. Quand on additionne un entier à un
pointeur de type T, on lui ajoute la valeur de l’entier que multiplie le nombre d’octets
nécessaire pour coder une variable de type T (char = 1, short = 2, int = 4, float = 4,
double = 8). Prenons un exemple :

int *t, *u;


int tab[5];

t = &tab[0];
u = t + 2;

La représentation en mémoire est la suivante :

100
t u un entier = 4 octets

&tab[0] &tab[1] &tab[2] &tab[3]

adresses croissantes

u est égal à t plus 2*4 octets, le type entier étant codé avec 4 octets. Comme t pointe sur le
premier élément de tab, alors u pointe sur le troisième élément de tab. On voit que u = t
+ 2; aurait pu être écrit u = &tab[2];.

Il en va de même pour l’opérateur soustraction. Si t pointe sur l’élément i d’un tableau, alors
t-j pointe sur l’élément i-j de ce tableau.

Exercice 5.16 : représentez la mémoire correspondant au programme suivant. Sur quoi


pointent p, q, r, et s.
#define N 10

main() {
int *p, *q, *r, *s;
int tab[N];

p = &tab[0];
q = p + (N-1);

r = &tab[N-1];
s = r - (N-1);
}

On peut appliquer les opérateurs d’incrémentation ++ et de décrémentation -- à des


pointeurs. On peut les utiliser, par exemple, pour parcourir les différents éléments d’un
tableau.

101
Exercice 5.17 : quel résultat fournit le programme suivant.
#define N 10

main()
{
int *p;
int tab[N] = {1, 2, 3, 4, 5, 6, 7, 8, 9, -1};

for (p = &tab[0]; *p != -1; p++)


printf("%d ", *p);
}

Il existe des relations très étroites en langage C entre les tableaux et les pointeurs. En fait, tout
identificateur de type « tableau de X » est converti automatiquement en une valeur constante
dont :
1. le type est « pointeur vers X »
2. la valeur est l’adresse du premier élément du tableau.

Quand on déclare int tab[10], le compilateur comprend que tab est un tableau de 10
entiers et réserve 40 octets en mémoire. Lorsque l’on utilise l’identificateur tab dans le
programme, il est converti en type int *, de valeur d’adresse &tab[0]. Cette conversion
automatique de l’identificateur du tableau empêche de désigner un tableau comme un tout.
C’est pour cette raison que l’on ne peut copier tous les éléments d’un tableau dans un autre
tableau comme dans le programme suivant :
int tab1[10];
int tab2[10];

tab2 = tab1; ♦ERREUR

On essaye de copier &tab1[0] dans &tab2[0], ce qui est impossible car tab2 est une
constante puisque c’est l’adresse du début de la zone de mémoire réservée pour le tableau. Il
faut copier les valeurs élément par élément avec une boucle.

Pour les mêmes raisons, il est impossible d’affecter une autre adresse à l’identificateur d’un
tableau.
int *t;
int tab[10];

tab = t; ♦ERREUR

102
Par contre, il est tout à fait possible de définir un pointeur qui pointe aussi sur la première
valeur du tableau :
int *t;
int tab[10];

t = tab;

En effet, cela revient à écrire t = &tab[0], ce qui est tout à fait légal.

En langage C, l’opérateur d’indexation est défini de telle manière que, après déclarations :
int i;
int tab[N];

tab[i] est rigoureusement équivalent à *(tab + i).

Vérifions cela. Nous avons vu que tab a pour valeur l’adresse du premier élément du
tableau. D’autre part, d’après ce que nous avons vu sur l’addition entre un pointeur et un
entier, nous savons que tab + i est l’adresse de l’élément de rang i du tableau. En
appliquant l’opérateur d’indirection *, nous voyons que *(tab + i) est bien identique à
tab[i]. Cela a trois conséquences plus ou moins importantes :

1. L’opérateur d’indexation noté [] est inutile et n’a été offert au programmeur que pour la
lisibilité des programmes et pour ne pas changer les habitudes des programmeurs.

2. Puisque l’opérateur d’indexation s’applique à des valeurs de type pointeurs, on va pouvoir


l’appliquer à n’importe quelle valeur de type pointeur et pas seulement aux constantes
repérant les tableaux. Après les déclarations :
int *t;
int tab[10];

on peut écrire :
t = &tab[4];

et utiliser l’opérateur d’indexation sur t, t[0] étant tab[4], t[1] étant tab[5], etc.
t peut donc être utilisé comme un sous tableau de tab.

103
3. L’opérateur d’indexation est commutatif ! En effet, tab[i] étant équivalent à *(tab +
i), et l’addition étant commutative, tab[i] est équivalent à *(i + tab) et donc à
i[tab]. L’élément de rang i d’un tableau tab peut donc être indifféremment appelé
tab[i] ou i[tab]. Il est évident que pour des raisons de lisibilité, il vaut mieux utiliser
la première notation.

5.6 Allocation dynamique de la mémoire


Jusqu’à présent, nous n’avons jamais directement réservé de la mémoire puis affecté l’adresse
de début de zone à un pointeur. Nous sommes toujours passés par l’intermédiaire d’un tableau
en deux étapes :
1. déclaration d’un tableau et donc réservation de la mémoire,
2. copie dans le pointeur de l’adresse du ième élément du tableau.

Cette méthode peut avoir son utilité, mais elle a aussi des inconvénients :

1. Le tableau est créé une fois pour toutes lors de la compilation. Il continue d’occuper de la
mémoire inutilement même si le programme n’en a plus besoin.

2. Le tableau est statique. Sa taille est déterminée à la compilation et n’est donc pas
paramétrable en fonction du déroulement du programme. De plus, elle ne peut pas être
modifiée.

En C, deux fonctions permettent de réserver de la mémoire et retournent l’adresse de début de


zone : malloc et calloc. La fonction free permet de libérer la mémoire allouée. La
fonction malloc admet un paramètre qui est le nombre d’octets à réserver et retourne un
pointeur de type void. Voici un exemple d’utilisation :

#include <stdlib.h>

main()
{
int *ptr, taille, i;

scanf("%d", &taille);

ptr = (int *)malloc(taille*4);

104
for (i = 0; i < taille; i++)
scanf("%d", &ptr[i]);

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


printf("%d ", ptr[i]);

free(ptr);
}

Remarques :
• Bien que malloc retourne un pointeur de type void, il est préférable de faire un cast
pour l’affecter à ptr.

• La fonction sizeof permet de connaitre la taille d’un type de variable. Par exemple,
sizeof(double) retourne 8. Il aurait donc été possible d’écrire :

ptr = (int *)malloc(taille* sizeof(int));

L’appel de la fonction malloc réserve 4*taille octets pour stocker des valeurs dans la
mémoire de l’odinateur, l’adresse du début de la zone de stockage étant copié dans ptr.
La variable taille étant saisie par l’utilisateur avec un scanf, le tableau pointé par ptr
est devenu dynamique.

• Vous noterez l’utilisation de ptr[i] dans le programme. On aurait pu écrire *(ptr+i)


à la place.

• free(ptr) libère l’espace mémoire réservé pointé par ptr. En cas de besoin, on peut
maintenant allouer à nouveau la mémoire (avec un malloc) pour créer un nouveau
tableau dont on copiera l’adresse dans ptr.

La fonction calloc admet deux paramètres : le nombre d’éléments désirés et la taille en octets
d’un élément. Elle retourne un pointeur de type void. Dans le programme précédent, vous
auriez pu utiliser :

ptr = (int *)calloc(taille, sizeof(int));

105
à la place du malloc pour réserver l’emplacement mémoire. Contrairement à malloc,
calloc initialise à 0 les octets alloués ce qui prend beaucoup plus de temps.

En langage C, l’allocation dynamique de la mémoire est utilisée systématiquement dès que la


taille d’un tableau devient élevée ou bien dès que la taille du tableau est inconnue à la
compilation. C’est le cas par exemple dans un programme qui lit une image dans un fichier
puis qui l’affiche à l’écran.

La fuite mémoire (memory leak ou memory leakage) est une erreur classique de
programmation dans un programme qui utilise l’allocation dynamique de la mémoire. La
mémoire qui est allouée n’est pas libérée complètement au fur et à mesure du fonctionnement
du programme, ce qui conduit à une consommation de mémoire de plus en plus importante,
voir à des erreurs de débordement. Il existe des outils logiciels permettant de détecter les
fuites mémoires car ce type d’erreur est difficile à détecter lorsque la complexité du
programme augmente.

106
6. Les chaînes de caractères

6.1 Déclaration
Nous avons déjà utilisé des chaînes de caractères constantes avec les instructions printf et
scanf. Une chaîne de caractères (« string » en anglais) est une suite de caractères entourés
de double quotes ", par exemple : "ceci est une chaîne". Des séquences
d’échappement sont possibles afin de représenter des caractères spéciaux tels que :

séquence d’échappement signification


\n saut à la ligne
\t tabulation
\0 caractère nul
\" "
\\ \

On peut les désigner par la notation \xnb où nb est le code en hexadécimal du caractère. Par
exemple, \n est équivalent à \x0A. On peut aussi utiliser le code octal directement avec \nb
où nb est le code en octal du caractère. Par exemple, \0 est le caractère nul. Toutes ces
séquences d’échappement peuvent être utilisées dans les chaînes de caractères comme dans :

"ligne 1\nligne 2\nligne 3"

Le caractère \ suivi d’un retour à la ligne est ignoré. Cela permet d’écrire une longue chaîne
sur plusieurs lignes dans vos programmes en C comme dans l’exemple :

"longue ligne 1\
longue ligne 2\
longue ligne 3"

Si deux chaînes sont adjacentes dans le source, le compilateur les fusionne. On appelle cela
une concaténation :

printf("cou" "cou"); ≡ printf("coucou");

107
La variable de type chaîne de caractères n’existe pas en langage C. Elle est remplacée par le
tableau de caractères, la chaîne étant toujours terminée par le caractère nul (\0). Ce caractère
est en général inséré automatiquement par le compilateur. Voici un exemple de chaîne et sa
représentation en mémoire :

"coucou" c o u c o u \0

0x63 0x6F 0x75 0x63 0x6F 0x75 0x00

Les chaînes suivantes sont possibles :

" " ^ \0

0x20 0x00

"" \0

0x00

"\"" " \0

0x22 0x00

"\0" \0 \0

0x00 0x00

"c:\\tmp" c : \ t m p \0

0x63 0x3A 0x5C 0x74 0x6D 0x70 0x00

On peut évidemment initialiser un tableau de caractères de manière traditionnelle avec une


liste de constantes :

char toto[4] = {'c','o','u','\0'};

mais c’est assez lourd à l’usage. Heureusement, le C propose une facilité en utilisant
directement une chaîne littérale :

char toto[4] = "cou";

108
Le compilateur ajoutant automatiquement le caractère nul en fin de chaîne, il faut que le
tableau ait au moins un élément de plus que le nombre de caractères de la chaîne littérale.
Mais rien n’empêche que la taille déclarée pour le tableau soit supérieure à la taille de la
chaîne (c’est le caractère nul qui signale la fin de la chaîne au programme).

char toto[100] = "coucou";

Seuls les 7 premiers caractères de la variable toto sont utilisés. Il est également possible de
ne pas spécifier la taille du tableau. Dans ce cas, le compilateur va automatiquement réserver
la bonne taille pour le tableau, y compris le caractère nul.

char toto[] = "coucou";

Vous pouvez aussi utiliser un pointeur :

char *toto = "coucou";

Il est très important de bien comprendre la différence entre tableau et pointeur. Prenons
l’exemple suivant :
char toto[100];
toto = "coucou"; illégal

A l’exécution du programme, toto est converti en un pointeur sur char constant qui
contient l’adresse du premier élément du tableau. L’instruction toto = "coucou";
essaye d’affecter à toto l’adresse du début de la chaîne, ce qui est impossible (puisqu’il est
constant). Par contre, l’exemple suivant est tout à fait légal :
char *toto;
toto = "coucou";

car le pointeur sur char toto reçoit l’adresse du début de la chaîne "coucou" que le
compilateur a placé quelque part en mémoire lors de sa création. C’est exactement le même
résultat que lorsque l’on déclare : char *toto = "coucou";

toto

c o u c o u \0

109
Il n’est pas possible de manipuler des chaînes de caractères sans passer par des fonctions
spécifiques de la librairie standard. En effet, des instructions telles que :

char *s1 = "coucou";


char *s2 = "c’est moi";
char *s3;

s3 = s1 + s2;
s1 = s2;

se contentent d’additionner les pointeurs s1 et s2 et de mettre le résultat dans s3 (ce qui ne sert
à rien et en aucun cas à concaténer les deux chaînes) puis de faire pointer s1 sur le premier
caractère de la chaîne "c’est moi" (ce qui entraîne la perte définitive de la chaîne
"coucou" et ne copie pas la première chaîne dans la deuxième). Toutes ces opérations
portent sur les pointeurs de début de chaînes mais en aucun cas sur les chaînes elles-mêmes.
De la même manière, le programme :

char *s1 = "coucou";


char *s2 = "c’est moi";

if(s1 == s2)
printf("les deux chaînes sont identiques");

ne teste pas que les chaînes sont identiques, mais vérifie l’égalité entre les pointeurs. La
condition sera donc toujours fausse.

6.2 Lire ou écrire des chaînes


Les prototypes des fonctions permettant de lire ou bien d’écrire des chaînes se trouvent dans
le fichier d’entête stdio.h. Si vous souhaitez les utiliser, la ligne suivante doit être inclue
au début de votre fichier source :

#include <stdio.h>

Les fonctions printf et scanf opèrent à partir des entrées-sorties standard stdin (le
clavier par défaut) et stdout (l’écran par défaut). Le programme suivant :

110
#include <stdio.h>

main()
{
char s1[10];

scanf("%s", s1);
}

va lire une suite de caractères au clavier pour les ranger dans le tableau s1 en commençant à
partir de s1[0] et en ajoutant automatiquement un caractère nul en fin de chaîne. Le code de
format %s fonctionne comme les codes de formats numériques (et non comme le code de
format %c), c’est-à-dire qu’il commence par sauter les délimiteurs éventuels (espace ou fin de
ligne) et qu’il s’interrompt lorsqu’il rencontre un de ces délimiteurs. scanf ne peut donc
pas lire de chaîne commençant par un espace ou contenant un espace.

Il est tout à fait possible que scanf provoque un débordement de tableau. Il suffit pour cela
de taper au clavier plus de 9 caractères. Les caractères excédentaires seront placés après la fin
du tableau et risque donc d’écraser d’autres données. Il faut donc réserver généreusement
l’emplacement nécessaire au stockage de la chaîne (char s1[255];).

Notez bien que scanf ne marche qu’avec un tableau (ou un pointeur initialisé par malloc)
et pas avec un pointeur non-initialisé (il faut que l’espace mémoire soit réservé pour accueillir
la chaîne).
#include <stdio.h>

main()
{
char *s1;

scanf("%s", s1); débordement


}

ATTENTION : la chaîne littérale est constante et ne peut pas être modifiée :


#include <stdio.h>

main()
{
char *s1, *s2 = "toto", s3[128] = "titi";
s1 = "tata";

111
s1[0] = '1'; chaine constante
s2[0] = '1'; chaine constante
s3[0] = '1'; ♥ chaine modifiable
}

Pour être sûr que le buffer du clavier est vide de tout caractère avant de faire un scanf, la
fonction fflush peut être utilisée :
#include <stdio.h>

main()
{
char s1[100], s2[100];

scanf("%s", s1);
fflush(stdin); /* vide le buffer clavier */
scanf("%s", s2);
}

Avec le programme précédent, si vous tapez :


un deux ↵
trois ↵

Vous obtiendrez un dans s1 et trois dans s2. Sans fflush, vous auriez obtenu un dans s1
et deux dans s2 (sans attente sur le deuxième scanf).

La fonction printf accepte elle aussi le code de format %s et l’utilise comme les autres
codes de format.
#include <stdio.h>

main()
{
char s1[100];

scanf("%s", s1);
printf("s1 = %s", s1);
}

Le résultat obtenu est :


essai ↵
s1 = essai

112
Exercice 6.1 : quels résultats fournira le programme suivant ?
#include <stdio.h>
main()
{
char *s1;

s1 = "bonjour";
printf("%s\n", s1);

s1 = "maitre !";
printf("%s\n", s1);
}

Exercice 6.2 : expliquez le fonctionnement du programme suivant. Quels résultats fournira-t-


il ?
#include <stdio.h>
#include <stdlib.h>

#define NB_CHAR 100

main()
{
char *s1;

s1 = malloc(NB_CHAR * sizeof(char));
scanf("%s", s1);
printf("%s\n", s1);
}

Exercice 6.3 : expliquez le fonctionnement du programme suivant. Quels résultats fournira-t-


il ?
#include <stdio.h>

main()
{
char *s1 = "bonjour";
int i;

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


putchar(s1[i]);

printf("\n");

i = 0;
while(s1[i] != '\0')
putchar(s1[i++]);

113
printf("\n");

i = 0;
while(s1[i])
putchar(s1[i++]);
}

Exercice 6.4 : modifiez le programme de l’exercice 6.3 en n’utilisant que les pointeurs (sans
utiliser le formalisme « tableaux »). Deux versions sont possibles.

Il est possible d’utiliser une variable chaîne à la place de la chaîne littérale du


printf comme dans cet exemple :
#include <stdio.h>

main()
{
char s1[100] = "essai";
char s2[100] = "s1 = %s";

printf(s2, s1);
}

Le résultat obtenu est :


s1 = essai

Attention, il faut noter que printf ne peut pas connaître la longueur de la chaîne. Il ne connaît
que le pointeur de début de chaîne s1 (qui est égal à &s1[0]) et affiche tous les caractères dans
la chaîne jusqu’à ce qu’il rencontre un caractère nul (\0). Si vous supprimez le caractère nul à
la fin de s1, printf affichera tout ce qu’il trouve en mémoire jusqu’à ce qu’il tombe sur un \0.

Exercice 6.5 : faites un tableau représentant les 8 premiers emplacements en mémoire de s1 et


montrer son évolution lors de l’exécution du programme suivant.
#include <stdio.h>

main()
{
char s1[100] = "coucou";

s1[6] = 1;
printf("s1 = %s", s1);
}

114
Exercice 6.6 : modifiez le programme suivant pour qu’il n’imprime que les 4 derniers
caractères de s1.
#include <stdio.h>

main()
{
char *s1 = "bonjour";

printf("%s\n", s1);
}

Les fonctions sprintf et sscanf opèrent à partir d’une chaîne de caractères. La fonction
sscanf réalise la même chose que scanf mais à partir d’une chaîne et pas à partir du
clavier (entrée standard = stdin). Exemple :

#include <stdio.h>

main()
{
char s1[100] = "1 2";
int x, y;

sscanf(s1, "%d%d", &x, &y);


printf("x = %d, y = %d", x, y);
}

Le résultat obtenu est :


x = 1, y = 2

La fonction sprintf se comporte de la même manière que printf sauf qu’au lieu d’écrire
sur l’écran (sortie standard = stdout), elle écrit dans une chaîne de caractères. Exemple :

#include <stdio.h>

main()
{
char s1[100] = "1 2";
int x = 1, y = 2;

sprintf(s1, "x = %d, y = %d\n", x, y);


printf(s1);
}

115
Le résultat obtenu est :
x = 1, y = 2

Exercice 6.7 : écrivez un programme qui lit deux nombres entiers fournis obligatoirement sur
une même ligne. Le programme ne devra pas « planter » en cas de réponse incorrecte (avec
des caractères invalides par exemple) comme le ferait scanf("%d%d", …); mais
simplement afficher un message et demander une autre saisie. Le comportement du
programme devra être le même si le nombre de valeurs correctement saisies est insuffisant.
Par contre, il devra ignorer les valeurs excédentaires. On utilisera gets et sscanf.

Exercice 6.8 : complétez les ???? dans le programme suivant afin d’obtenir le
comportement suivant :
1) l’utilisateur tape un nom de fichier sans extension (nom_de_base),
2) le programme crée 10 fichiers appelés nom_de_base.1, nom_de_base.2, ...,
nom_de_base.10.

#include <stdio.h>

main()
{
char nom_de_base[100];
char nom_de_fichier[100];
FILE *fp; /* pointeur sur fichier */
int i;

scanf(????);

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


sprintf(????);

/* création du fichier */
fp = fopen(nom_de_fichier, "w");

fclose(fp); /* fermeture du fichier */


}
}

Les fonctions gets et puts opèrent à partir des entrées-sorties standards stdin (le clavier
par défaut) et stdout (l’écran par défaut). Elles manipulent exclusivement des chaînes de
caractères. Dans le programme suivant :

116
#include <stdio.h>

main()
{
char s1[100];

gets(s1);
puts(s1);
}

Le résultat obtenu est :


^^un^^deux^^trois ↵
^^un^^deux^^trois

L’instruction gets(s1) va lire une suite de caractères et la ranger dans s1, terminée par un
caractère nul. Il faut noter qu’à la différence de scanf :
• aucun délimiteur n’est sauté avant la lecture,
• les espaces sont lus comme les autres caractères,
• la lecture ne s’arrête qu’à la rencontre d’un caractère fin de ligne qui n’est pas copié
dans la chaine. Aucun contrôle du nombre de caractères lus n’étant réalisé, le
débordement de s1 est tout à fait possible si la chaine saisie est trop longue. C’est une
faille de sécurité importante du langage C.

La fonction puts(s1) affiche les caractères trouvés à partir de &s1[0] jusqu’à atteindre le
caractère nul, puis réalise un changement de ligne (c’est là la seule différence notable avec
printf).

Exercice 6.9 : écrivez un programme ayant le comportement suivant en utilisant les fonctions
gets, puts, scanf et printf.
Où habitez-vous ? paris ↵
Donnez votre nom et prénom : dupont jean ↵
Bonjour M. dupont jean qui habitez paris

6.3 Connaître la longueur d’une chaîne


La fonction strlen (STRing LENtgh) permet de connaître la longueur d’une chaîne de
caractères (entre l’adresse de début de chaîne et le caractère nul). Voici son prototype :

117
int strlen(const char *chaine);

qui se trouve dans le fichier string.h. Elle accepte en entrée une chaîne de caractères
qu’elle ne peut pas modifier (c’est le rôle du qualificatif de type const) et retourne la
longueur de la chaîne, c’est-à-dire le nombre de caractères contenus excepté le caractère nul.
#include <stdio.h>
#include <string.h>

main()
{
char s1[100] = "essai";
int l;

l = strlen(s1);
printf("s1 est de longueur %d\n", l);
}

Le résultat obtenu est :


s1 est de longueur 5

6.4 Copier une chaîne dans une autre chaîne


La fonction strcpy (STRing CoPY) permet de copier une chaîne dans une autre chaîne.
Voici son prototype :
char * strcpy(char *destination, const char *source);

qui se trouve dans le fichier string.h. Elle copie la chaîne source (qu’elle ne peut pas
modifier) dans la chaine destination, y compris le caractère nul. Elle retourne l’adresse de la
chaîne de destination.

#include <stdio.h>
#include <string.h>

main()
{
char s1[100], s2[100], *s3;

s3 = strcpy(s1, "bonjour"); /* s3 → s1 */
s3 = strcpy(s2, "bonjour"); /* s3 → s2 */

printf("s1 = %s\n", s1);


printf("s2 = %s\n", s2);
}

118
Le résultat obtenu est :
s1 = bonjour
s2 = bonjour

Il existe une fonction strncpy analogue à strcpy qui permet de ne copier qu’un nombre
fini de caractères. Par exemple,

Si le nombre de caractères copiés est inférieur à la longueur


strncpy(s1, s2, 10); de S2, le caractère \0 n’est pas inséré à la fin de s1.

ne copiera que les 10 premiers caractères de s2 dans s1.

6.5 Comparer deux chaînes


La fonction strcmp (STRing CoMPare) permet de comparer deux chaînes caractère par
caractère. Voici son prototype :
int strcmp(const char *chaine1, const char *chaine2);

qui se trouve dans le fichier d’entête string.h. Elle compare les deux chaînes (qu’elle ne
peut pas modifier) depuis le premier caractère et jusqu’à ce que les caractères soient différents
ou qu’une chaîne se termine. Elle retourne un entier :
• < 0 si chaine1 est inférieure à chaine2 (au sens des codes ASCII),
• = 0 si les deux chaînes sont identiques,
• > 0 si chaine1 est supérieure à chaine2.

#include <stdio.h>
#include <string.h>

main()
{
char s1[] = "abcdef", s2[] = "abcdEF";
int cmp;

cmp = strcmp(s1, s2);


if (cmp == 0)
printf("s1 est égale à s2\n");
else if (cmp < 0)
printf("s1 est inférieure à s2\n");
else
printf("s1 est supérieure à s2\n");
}

119
Le résultat obtenu est :
s1 est supérieure à s2

car les minuscules ont un code ASCII plus grand que les majuscules. Il existe une fonction
strncmp analogue à strcmp qui permet de ne comparer qu’un nombre fini de caractères.
Par exemple :

strncmp (s1, s2, 10);

ne comparera s1 et s2 que pour les 10 premiers caractères.

6.6 Concaténer deux chaînes


La fonction strcat (STRing conCATenation) permet de concaténer deux chaînes, c’est à
dire de fabriquer une chaîne en mettant bout à bout deux autres chaînes. Voici son prototype :
char * strcat(char *destination, const char *source);

qui se trouve dans le fichier string.h. Elle ajoute la chaîne source (qu’elle ne peut pas
modifier) à la fin de la chaîne destination. Elle retourne l’adresse de la chaîne de destination.

#include <stdio.h>
#include <string.h>
main()
{
char s1[100] = "bonjour";
char s2[] = " maitre !", *s3;

s3 = strcat(s1, s2);

printf("s3 = %s\n", s3);


}

Le résultat obtenu est :


s3 = bonjour maitre !

Vous noterez que s1 n’est pas déclarée de la manière suivante : char s1[] =
"bonjour"; car la concaténation provoquerait alors un débordement par manque de place.
Il existe une fonction strncat analogue à strcat qui permet de ne concaténer que les n
premiers caractères de la chaîne source. Par exemple :

120
strncat(s1, s2, 10);

n’ajoutera à la fin de s1 que les 10 premiers caractères de s2. Contrairement à strncpy, le


caractère \0 est copié quelque soit le nombre de caractères concaténés.

6.7 Rechercher un caractère dans une chaîne


La fonction strchr (STRing CHaRacter) permet de chercher un caractère dans une chaîne.
Voici son prototype :

char * strchr(const char *chaine, int c);

qui se trouve dans le fichier string.h. Elle cherche dans la chaîne chaine (qu’elle ne
peut pas modifier) la première occurrence du caractère c et retourne un pointeur sur ce
caractère ou bien un pointeur NULL si le caractère n’existe pas.
#include <stdio.h>
#include <string.h>

main()
{
char s1[] = "bonjour";
char *p, c='j';

p = strchr(s1, c);
if (p != NULL)
printf("le caractère %c est en position %d\n", c, p -
s1);
else
printf("le caractère %c ne se trouve pas dans la
chaine \"%s\"\n", c, s1);
}

Le résultat obtenu est :


le caractère j est en position 3

6.8 Rechercher une chaîne dans une autre chaîne


La fonction strstr (STRing STRing) permet de chercher une chaîne dans une autre chaîne.
Voici son prototype :

char * strstr (const char *s1, const char *s2);

121
qui se trouve dans le fichier string.h. Elle cherche dans la chaîne s1 (qu’elle ne peut pas
modifier) la première occurrence de la chaîne s2 (qu’elle ne peut pas modifier) et retourne un
pointeur sur le caractère de s1 où commence s2 ou bien un pointeur NULL si s2 n’existe
pas dans s1.
#include <stdio.h>
#include <string.h>

main()
{
char s1[] = "bonjour maitre !";
char *p, s2[] = "jour";

p = strstr(s1, s2);

if (p != NULL) {
printf("la chaine \"%s\" existe dans la chaine \"%s\"", s2, s1);
printf(" en position %d.\n", p - s1);
printf("le reste de la chaine est \"%s\"\n", p);
}
}

Le résultat obtenu est :


la chaine "jour" existe dans la chaine "bonjour maitre !" en
position 3.
le reste de la chaine est "jour maitre !"

Exercice 6.10 : écrivez un programme qui lit deux mots et qui les affiche par ordre
alphabétique.

Exercice 6.11 : écrivez un programme qui lit un mot et qui vérifie qu’il se termine par « er ».

Exercice 6.12 : écrivez un programme qui lit une ligne de texte puis qui supprime dans cette
ligne toutes les lettres e. Le programme modifiera le texte directement dans l’emplacement
mémoire de la ligne saisie. On pourra utiliser la fonction strchr.

122
Exercice 6.13 : dans le projet mastermind, la proposition du joueur était stockée dans un entier
(voir programme suivant). Proposez une solution avec une chaîne de caractères permettant les
contrôles suivants :
1) vérification du nombre de chiffres de la proposition,
2) vérification que chaque caractère est bien un chiffre.

int entree(int essai[], int dim)


{
unsigned int x, limite;
int i;

printf("\nEntrez votre combinaison de %d chiffres : ", dim);


scanf("%d", &x);

limite = (int)pow(10, dim) - 1;


if (x > limite) {
printf("\nnombre de chiffres trop grand, recommencez\n");
return (-1);
}

for (i = 0; i < dim; i++) {


essai[i] = x %10;
x = x/10;
}
return(0);
}

6.9 Fonctions diverses


Voici quatres fonctions permettant de tester des caractères :

fonction description
isalpha teste si le caractère est une lettre
isdigit teste si le caractère est un chiffre
islower teste si le caractère est une lettre minuscule
isupper teste si le caractère est une lettre majuscule

Ces fonctions acceptent un caractère en entrée (vous pouvez utiliser un char ou un int),
retournent 0 si le test est faux ou ≠ 0 si le test est vrai. Leur prototype se trouve dans
l’entête <ctype.h>. Il est le suivant :

int isXXXXX(int carac);

123
Voici deux fonctions permettant de changer la casse d’un caractère :

fonction description
tolower convertit une lettre en minuscule
toupper convertit une lettre en majuscule

Ces fonctions acceptent un caractère en entrée, et retournent le caractère converti si c’est


possible. Leur prototype se trouve dans l’entête <ctype.h>. Il est le suivant :

int toXXXXX(int carac);

Voici deux fonctions permettant de convertir une chaine en un nombre :

fonction description
atof conversion d’une chaîne en nombre flottant
atoi conversion d’une chaîne en nombre entier

Elles acceptent l’adresse d’une chaîne en entrée et retournent la valeur entière ou flottante si
c’est possible ou 0 sinon. Leur prototype se trouve dans l’entête <stdlib.h>. Il est le
suivant :

int atoi(const char *str);


double atof(const char *str);

Il existe bien d’autres fonctions de manipulation de chaînes de caractères dans la librairie


standard (voir §12.4). Elles sont toutefois d’un usage moins courant.

6.10 Le passage d’une chaîne comme paramètre d’une fonction.


Une chaîne se traite exactement comme un tableau. Il faut passer en paramètre l’adresse du
début du tableau.
#include <stdio.h>

void imprime(char *s1); /* prototype */

main()
{

124
char ligne[] = "coucou";

imprime(ligne); /* appel de la fonction */


}

void imprime(char *s1) /* entête de la fonction */


{
int i;

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


printf("%s\n", s1);
}

L’appel de la fonction peut être indifféremment :


imprime(ligne);
imprime(&ligne);
imprime(&ligne[0]);

On peut aussi passer directement une chaîne littérale (c’est ce qui est fait dans printf) :
imprime("essai");

Le compilateur réserve l’espace mémoire nécessaire au stockage de la chaîne "essai" et


passe à la fonction imprime l’adresse de début de chaîne.

Dans l’entête de la fonction, il est inutile de spécifier la taille de la chaîne car seule l’adresse
de début de tableau ou de chaîne va être copiée dans s1. s1 pointe alors soit sur le début d’un
tableau, soit sur le début d’une chaîne littérale. On peut donc utiliser soit :
void imprime(char *s1)

soit :
void imprime(char s1[])

Il est possible d’interdire à la fonction de modifier la chaîne en déclarant le pointeur constant.


#include <stdio.h>

void imprime(const char *s1); /* prototype */

main()
{
char ligne[] = "coucou";

125
imprime(ligne); /* appel de la fonction */
}

void imprime(const char *s1) /* entête de la fonction */


{
int i;

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


printf("%s\n", s1);
}

Si dans la fonction imprime vous essayez de modifier s1 :

s1[0] = 'a';

alors le compilateur refuse de compiler votre programme avec un message d’erreur. Il s’agit
d’une sécurité pour le développeur.

6.11 Les tableaux de chaînes de caractères.


Les tableaux de chaînes de caractères sont des tableaux de tableaux de caractères (des
tableaux à deux dimensions). L’exemple suivant montre la déclaration et l’initialisation d’un
tel tableau :

char tab[5][10] = {"zéro", "un", "deux", "trois", "quatre"};

Le stockage en mémoire peut être représenté de la manière suivante :

z é r o \0
u n \0
d e u x \0
t r o i s \0
q u a t r e \0

Pour entrer une chaîne dans ce tableau à la quatrième ligne, il faut utiliser l’instruction :

scanf("%s", &tab[3][0]);

126
scanf va lire les caractères sur stdin et les ranger dans les emplacements consécutifs à
partir de l’élément tab[3][0]. Si le nombre de caractères est plus grand que 10, il va
passer à la ligne suivante. De la même manière,

strcpy(&tab[1][0], "ceci est un essai");

va se traduire en mémoire par :

z é r o \0
c e c i ^ e s t ^ u
n ^ e s s a i \0
t r o i s \0
q u a t r e \0

Tout ceci n’est guère pratique. De plus, une grande quantité de mémoire peut être perdue si
les chaînes sont de longueur différente. Il y a une autre manière bien plus efficace pour
résoudre ce problème, c’est d’utiliser un tableau de pointeur sur char :

char *tab[5] = {"zéro", "un", "deux", "trois", "quatre"};

Dans le tableau tab, on trouve maintenant 5 pointeurs 32 bits initialisés chacun avec
l’adresse de début de chaque chaîne.

tab[0] z é r o \0

tab[1] u n \0

tab[2] d e u x \0

tab[3] t r o i s \0

tab[4] q u a t r e \0

127
La réservation mémoire est maintenant minimale. Chaque chaîne peut être traitée
indépendamment avec des instructions comme :
scanf("%s", tab[3]);
gets(tab[0]);
printf("%s", tab[1]);

Bien entendu, les problèmes de débordement demeurent comme pour les opérateurs sur des
chaînes ordinaires. Attention, si vous déclarez seulement char *tab[5];, aucune
réservation mémoire n’est effectuée pour stocker des chaînes. Vous n’avez créé qu’un tableau
de 5 pointeurs 32 bits qui ne pointent sur rien.

Exercice 6.14 : écrivez un programme qui demande à l’utilisateur de lui fournir un nombre
entier compris entre 1 et 7 et qui affiche le nom du jour de la semaine correspondant (le lundi
vaut 1).

Exercice 6.15 : écrivez un programme qui lit un verbe du premier groupe et qui en affiche la
conjugaison au présent sous la forme :

Entrez un verbe du premier groupe : chanter ↵

je chante

tu chantes
il chante

nous chantons
vous chantez

ils chantent

Le programme vérifiera que le verbe se termine bien par er et on supposera qu’il s’agit d’un
verbe régulier.

128
7. Les paramètres de la fonction main.
Jusqu’ici, nous avons toujours écrit des programmes utilisant les fonctions printf, scanf,
gets, puts, … pour dialoguer avec l’utilisateur. Nous avons utilisé un mode de dialogue
interactif, non-graphique. Ce n’est pas le seul mode possible. En C, il existe trois manières
classiques (l’utilisation de la base de registre « à la Microsoft » n’est pas un exemple de
portabilité ni de transparence) de passer des arguments à un programme au début ou en cours
d’exécution :
1) L’interface graphique (mode interactif, graphique),
2) Le fichier de configuration (mode non-interactif, non-graphique),
3) Le passage direct de paramètres à la fonction main depuis la ligne de commande (mode
non-interactif, non-graphique).

Il faut avoir conscience du fait que la majorité des programmes s’exécutent sans dialogue
interactif avec l’utilisateur. On les configure au moment du lancement, puis on récupère les
résultats par exemple dans un fichier. Le mode interactif graphique (comme avec Windows)
est l’exception en informatique industrielle plutôt que la règle. Nous aurons l’occasion de
revenir sur le mode 2 (fichier de configuration) à la fin du §9. Voyons maintenant le mode 3.

La fonction main peut recevoir des paramètres de son appelant. Ce sera le plus souvent
l’utilisateur qui les fournira au lancement du programme sur la ligne de commande, mais cela
peut aussi être une autre application qui lancera le programme. Voici un exemple de
déclaration :

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

Les trois paramètres que peut recevoir la fonction main sont :


• un entier indiquant le nombre de paramètres reçus. Par convention, on l’appelle argc
pour « argument count ».
• un tableau de chaînes de caractères qui contiennent les paramètres. On le nomme argv
pour « argument vector ». Le dernier élément de ce tableau sera toujours le pointeur
NULL.

129
• un tableau de chaînes de caractères qui contiennent les variables d’environnement du
système d’exploitation. On le nomme env pour « environnement ». Le dernier élément de
ce tableau sera toujours le pointeur NULL.

La fonction main peut aussi retourner un paramètre, généralement un entier qui contient un
code de retour. Si le programme s’appelle par exemple essai et que l’on tape :

essai toto 1 "un 2" ↵

On obtiendra argc = 4 car le nom du programme est compris dans les arguments, et le
tableau argv sera le suivant :

argv[0] e s s a i \0

argv[1] t o t o \0

argv[2] 1 \0

argv[3] u n ^ 2 \0

argv[4] NULL

De la même manière, env contiendra les variables d’environnement. Voici un exemple de


programme affichant les arguments de la fonction main :
#include <stdio.h>

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


{
int i;

printf("la fonction main a reçu %d arguments\n", argc);

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


printf("argument[%d] : %s \n", i, argv[i]);

printf("\nLes variables d'environnement sont : \n");

for (i = 0; env[i]!= NULL; i++)


printf("variable d'environnement[%d] : %s \n", i, env[i]);
}

130
Après avoir tapé la commande :

essai un deux "3 3 3" 4 ↵

Le résultat suivant a été obtenu sur un compte dut1g1 sous linux :


la fonction main a reçu 5 arguments
argument[0] : essai
argument[1] : un
argument[2] : deux
argument[3] : 3 3 3
argument[4] : 4

Les variables d'environnement sont :


variable d'environnement[0] : PWD=/home/dut1g1
variable d'environnement[1] : XAUTHORITY=/home/dut1g1/.Xauthority
variable d'environnement[2] : LC_MESSAGES=fr_FR
variable d'environnement[3] : HOSTNAME=ray12.cnam.fr
variable d'environnement[4] : LESSKEY=/etc/.less
variable d'environnement[5] : LESSOPEN=|/usr/bin/lesspipe.sh %s
variable d'environnement[6] : LANGUAGE=fr_FR:fr
variable d'environnement[7] : HISTIGNORE=[ ]*:&:bg:fg
variable d'environnement[8] : PS1=[\u@\h \W]\$
variable d'environnement[9] : KDEDIR=/usr
variable d'environnement[10] : LESS=-MM
variable d'environnement[11] : BROWSER=/usr/bin/netscape
variable d'environnement[12] : USER=dut1g1
variable d'environnement[13] :
LS_COLORS=no=00:fi=00:di=01;34:ln=01;36:pi=40;33:so=01;35:bd=40;33;0
1:cd=40;33;01:or=01;05;37;41:mi=01;05;37;41:ex=01;32:*.cmd=01;32:*.e
xe=01;32:*.com=01;32:*.btm=01;32:*.bat=01;32:*.tar=01;31:*.tgz=01;31
:*.tbz2=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lzh=01;31:*.lha=
01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.gz=01;31:*.bz2=01;31:*.bz=01
;31:*.tz=01;31:*.rpm=01;31:*.jpg=01;35:*.jpeg=01;35:*.gif=01;35:*.bm
p=01;35:*.xbm=01;35:*.xpm=01;35:*.png=01;35:*.tif=01;35:*.tiff=01;35
:
variable d'environnement[14] : LC_TIME=fr_FR
variable d'environnement[15] : MACHTYPE=i586-mandrake-linux-gnu
variable d'environnement[16] : MAIL=/var/spool/mail/dut1g1
variable d'environnement[17] : INPUTRC=/etc/inputrc
variable d'environnement[18] : BASH_ENV=/home/dut1g1/.bashrc
variable d'environnement[19] : LANG=fr
variable d'environnement[20] : LC_NUMERIC=fr_FR
variable d'environnement[21] : COLORTERM=
variable d'environnement[22] : DISPLAY=:0
variable d'environnement[23] : LOGNAME=dut1g1
variable d'environnement[24] : SHLVL=1
variable d'environnement[25] : LC_CTYPE=fr_FR
variable d'environnement[26] : SHELL=/bin/bash
variable d'environnement[27] : USERNAME=
variable d'environnement[28] : HOSTTYPE=i586
variable d'environnement[29] : OSTYPE=linux-gnu
variable d'environnement[30] : HISTSIZE=1000
variable d'environnement[31] : HOME=/home/dut1g1

131
variable d'environnement[32] : TERM=xterm
variable d'environnement[33] :
PATH=/usr/local/bin:/bin:/usr/bin::/usr/X11R6/bin:/usr/games:/home/d
ut1g1/bin:/usr/X11R6/bin:/usr/games:/usr/X11R6/bin:/usr/games
variable d'environnement[34] : SECURE_LEVEL=3
variable d'environnement[35] : RPM_INSTALL_LANG=fr_FR:fr
variable d'environnement[36] : LC_MONETARY=fr_FR
variable d'environnement[37] : LC_COLLATE=fr_FR
variable d'environnement[38] : _=./essai
variable d'environnement[39] : OLDPWD=/home/dut1g1/hanoi

Exercice 7.1 : écrivez un programme qui lit sur la ligne de commande deux noms de fichier et
qui affiche un court message explicatif si le nombre d’arguments est incorrect.

essai nom_de_fichier1 nom_de_fichier2

Exercice 7.2 : écrivez un programme qui réalise les opérations suivantes.


• lecture d’un nom de fichier sur la ligne de commande,
• vérification de la présence ou non de l’option -x,
• récupération du nombre se trouvant après l’option -o,
• affichage d’un court message explicatif en cas d’erreur de saisie ou si l’option -h a été
détectée.

Le programme doit s’utiliser de la manière suivante :

essai [-x] [-h] [-o n] nom_de_fichier

Les paramètres entre crochets sont optionnels mais ils doivent être saisis dans l’ordre. Vous
disposerez pour cela des fonctions :
• void exit(int status). Cette fonction force la sortie du programme et renvoie la
valeur status au programme appelant.
• int atoi(const char *s). Cette fonction convertit s en un entier. Elle retourne
l’entier correspondant ou bien 0 si la conversion est impossible. Il existe aussi une
fonction double atof(const char *s) que nous n’utiliserons pas dans ce
programme.

132
8. Les structures

8.1 Définition
Les types de variables simples que nous avons vus jusqu’ici ne permettent pas de représenter
les données complexes que l’on est souvent amené à traiter en informatique. Il faudrait
pouvoir regrouper plusieurs types simples en agrégats afin de définir un type de données plus
compliqué. Imaginons par exemple que nous souhaitions créer un type de données comportant
le nom et le prénom d’une personne, son adresse, son âge, son numéro de sécurité sociale, ...
En langage C, il existe la structure qui permet de désigner sous un seul nom un ensemble de
valeurs de types différents. Il y a plusieurs méthodes pour déclarer une structure. La méthode
préférée consiste en une déclaration d’un modèle de structure comme par exemple :

struct personne {
char nom[100];
char prenom[100];
char adresse[100];
int age;
int noss;
};

On appelle personne l’étiquette de la structure. On doit ensuite utiliser cette étiquette pour
déclarer des variables comme :

struct personne p1, p2;

p1 et p2 sont des variables de type personne destinées à contenir chacune 3 chaînes de


caractères et deux entiers. Pour abréger, on dira de la variable p1 (ou p2) dont le type est un
modèle de structure qu’elle est une structure de type personne. Les autres méthodes telles
que la déclaration de variables de type structure sans utiliser d’étiquette :

struct {
char nom[100];
char prenom[100];
char adresse[100];
int age;
int noss;
} p1, p2;

133
Ou bien encore la déclaration d’étiquette et des variables en même temps :
struct personne {
char nom[100];
char prenom[100];
char adresse[100];
int age;
int noss;
} p1, p2;

sont fortement déconseillées. En effet, il est préférable de séparer la définition du modèle de


structure de son utilisation.

L’accès à chaque élément de la structure (nommé « champ » ou « membre ») se fait par son
nom au sein de cette structure. Chaque champ se manipule comme n’importe quelle variable
du type correspondant. La désignation d’un champ se note en faisant suivre le nom de la
variable structure d’un point (.), puis du nom du champ défini dans le modèle. Par exemple :

p1.age = 20;

affecte la valeur 20 au champ age de la structure p1. Le type de p1.age est le type du
champ age, c’est-à-dire un entier. D’une manière générale, le type de
nom_structure.champ est le type du champ. Par exemple :

scanf("%s", p2.nom);

lit les caractères au clavier et les range à l’adresse contenue dans le champ nom de p2, qui est
l’adresse de la première case du tableau de char nom[100]. Il est possible d’initialiser les
champs d’une structure au moment de sa déclaration comme avec un tableau :

struct personne p2 = {"dupond", "jean", "5 rue de la paix


75008 PARIS", 55, 111111};

A la différence des tableaux, il est possible d’affecter à une structure le contenu d’une autre
structure de manière globale à condition qu’elles soient toutes deux définies à partir du même
modèle. Par exemple, on peut écrire :

p1 = p2;

134
Par contre, aucune opération globale de comparaison n’est permise. L’opération d’affectation
suivante est impossible, il faut remplir chaque champ séparément.

p2 = {"dupond", "jean", "5 rue de la paix 75008 PARIS", 55};

Nous avons déjà vu que les déclarations de variables ou de fonctions peuvent être globales
(accessibles à toutes les fonctions) ou locales (accessible à une seule fonction) suivant
qu’elles sont faites dans une fonction ou bien en dehors de toute fonction. Cette règle
s’applique aussi bien aux structures qu’aux modèles de structure. Voyons un exemple :
#include <stdio.h>

struct modele_global {
int numero;
int quantite;
float prix;
};

struct modele_global var_global;

int main() {
struct modele_local {
int numero;
int quantite;
float prix;
};

struct modele_global var_local1;


struct modele_local var_local2;

/* ... */
}

int fct()
{
struct modele_global var_local3;

/* ... */
}

Dans cet exemple, on voit qu’une structure peut être locale ou globale et que son modèle peut
être aussi local ou global. Le modèle global est indispensable pour que l’on puisse passer une
structure comme paramètre d’une fonction. Nous utiliserons des structures locales (comme
pour les variables) avec un modèle global.

135
En langage C, il est possible de réaliser des tableaux de structures, c’est-à-dire des tableaux
dont les éléments sont d’un type structure, comme dans l’exemple suivant :
struct point {
char nom[100];
int x;
int y;
};

struct point courbe[100];

le tableau courbe est rempli d’éléments de type point. On accède aux différents champs
d’un élément avec :
courbe[5].x
courbe[10].nom[2]

Par ailleurs,
courbe[3]

représente la structure de type point correspondant au quatrième élément du tableau


courbe. Enfin, courbe est un identificateur de tableau qui équivaut à un pointeur
contenant l’adresse de son premier élément. Il est possible d’initialiser (partiellement) le
tableau lors de sa déclaration :
struct point courbe[100]= {{"A", 0, 0}, {"B", 1}, {"E", 4,
4}};

Exercice 8.1 : écrivez un programme qui :


1) lit au clavier des informations et les range dans un tableau de structures du type point
défini comme suit :
struct point {
int num ;
float x;
float y;
};

Le nombre d’éléments du tableau sera fixé par une instruction #define.

2) affiche à l’écran l’ensemble des informations précédentes.

136
Une structure peut contenir d’autres structures. Dans l’exemple qui suit, une structure date
est utilisée dans une autre structure qui stocke des renseignements concernant un salarié.

#include <stdio.h>

struct date {
int jour;
int mois;
int annee;
};

struct salarie {
char nom[100];
char prenom[100];
int age;
struct date date_embauche;
struct date date_poste;
};

int main()
{
struct salarie truc, machin;
/* ... */
}

On accèdera à un champ de la manière suivante :

truc.date_embauche.annee = 1990;

Comme les structures date_embauche et date_poste ont le même modèle, l’instruction


suivante est possible :

truc.date_embauche = machin.date_poste;

8.2 Transmission d’une structure en paramètre d’une fonction


La transmission du paramètre se fait toujours par valeur. Les valeurs de la structure sont
recopiées localement lors de l’appel et les modifications effectuées dans la fonction n’ont
aucune incidence sur la structure de la fonction appelante.

137
#include <stdio.h>

struct str {
int a;
int b;
};

void echange(struct str z);

main()
{
struct str x;
x.a = 1;
x.b = 2;
printf("avant appel : a = %d, b = %d\n", x.a, x.b);
echange(x);
printf("après appel : a = %d, b = %d\n", x.a, x.b);
}

void echange(struct str z)


{
int tmp;

printf("début échange : a = %d, b = %d\n", z.a, z.b);


tmp = z.b;
z.b = z.a;
z.a = tmp;
printf("fin échange : a = %d, b = %d\n", z.a, z.b);
}

Le résultat est le suivant :


avant appel : a = 1, b = 2
début échange : a = 1, b = 2
fin échange : a = 2, b = 1
après appel : a = 1, b = 2

Comme prévu, l’échange n’a pas eu lieu. Vous noterez que le modèle de structure str doit
être global pour pouvoir être utilisé dans echange.

Comme nous l’avons déjà vu pour les variables simples ou les tableaux, c’est l’adresse de la
structure qu’il faut passer à la fonction.
#include <stdio.h>

struct str {
int a;
int b;
};

138
void echange(struct str *z);

main()
{
struct str x;

x.a = 1;
x.b = 2;
printf("avant appel : a = %d, b = %d\n", x.a, x.b);
echange(&x);
printf("après appel : a = %d, b = %d\n", x.a, x.b);
}

void echange(struct str *z)


{
int tmp;

printf("début échange : a = %d, b = %d\n", (*z).a,


(*z).b);
tmp = (*z).b;
(*z).b = (*z).a;
(*z).a = tmp;
printf("fin échange : a = %d, b = %d\n", (*z).a, (*z).b);
}

Le résultat est alors :


avant appel : a = 1, b = 2
début échange : a = 1, b = 2
fin échange : a = 2, b = 1
après appel : a = 2, b = 1

L’échange a bien eu lieu. Dans la fonction echange, c’est maintenant un pointeur sur la
structure qui est utilisé pour recueillir l’adresse du paramètre. L’accès aux champs de la
structure se fait à l’aide de (*z).a et (*z).b. Les parenthèses sont importantes car
l’opérateur * est moins prioritaire que l’opérateur (.). *z est la structure pointée par z. Pour
éviter cette notation un peu lourde, le langage C propose une facilité que vous devez utiliser,
l’opérateur ->. L’accès au champ se fait alors avec : z->a et z->b. La fonction echange
peut être réécrite de la manière suivante :

void echange(struct str *z)


{
int tmp;
printf("début échange : a = %d, b = %d\n", z->a, z->b);
tmp = z->b;
z->b = z->a;

139
z->a = tmp;
printf("fin échange : a = %d, b = %d\n", z->a, z->b);
}

Exercice 8.2 : reprenez le programme de l’exercice 8.1 mais en utilisant une fonction pour la
lecture de toutes les informations contenues dans le tableau et une fonction pour l’affichage.

8.3 La définition de types nouveaux


Dans la norme ANSI C, il est possible de définir des types qui n’existent pas à partir de types
simples. Il suffit pour cela de faire suivre le mot clé typedef d’une construction ayant
exactement la même syntaxe qu’une déclaration de variable, puis de définir le nom du
nouveau type. Par exemple :

nom du
mot clé déclaration nouveau type

typedef unsigned long int * PULONG

Le nouveau type peut ensuite être utilisé dans une déclaration de variable :

PULONG x;

x est de type PULONG, c’est-à-dire un pointeur sur entier long non signé. En réalité,
l’opérateur typedef ne créé pas un nouveau type de donnée. Il ne fait qu’attribuer un
nouveau nom à un type existant. Cette méthode de déclaration permet de simplifier le code et
de résoudre les problèmes de portabilité. Pour cela, il suffit de changer les typedef pour les
types qui dépendent d’un système d’exploitation donné. La déclaration du typedef peut être
locale ou globale. Voici un autre exemple :

typedef int tab[10];


tab y;

tab est maintenant le type tableau de 10 entiers. La déclaration typedef peut aussi être
utilisée pour simplifier l’utilisation des structures comme par exemple dans :

140
typedef struct {
int x;
int y;
} point;

point *z ;

point est maintenant le type structure à deux champs x et y. z pointe sur une structure de ce
type. On peut aussi utiliser cette méthode tout en spécifiant le nom de la structure :

typedef struct nom_de_la_structure {


int a;
int b;
} NOUVEAU_TYPE, *PTR_SUR_NOUVEAU_TYPE;

Ces nouveaux types peuvent maintenant être utilisés :


NOUVEAU_TYPE x;
PTR_SUR_NOUVEAU_TYPE ptrx;

Le programme suivant résume toutes ces possibilités :


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

typedef unsigned long int * PULONG;


typedef int tab[10];

typedef struct nom_de_la_structure {


int a;
int b;
} NOUVEAU_TYPE, *PTR_SUR_NOUVEAU_TYPE;

typedef struct point {


int x;
int y;
} POINT, *PPOINT;

main()
{
PULONG x;
tab y;
NOUVEAU_TYPE w;
PPOINT z;

141
x = malloc(sizeof(unsigned long int));
*x = 10;
z = malloc(sizeof(POINT));
z->x = 5;
z->y = -5;

/* .. */
}

ATTENTION : après exécution de la ligne « PPOINT z; », le compilateur n’a réservé que


32 bits pour stocker l’adresse de la structure dans z. Cette adresse est non initialisée, comme
après toute déclaration de pointeurs. Il faut ensuite obligatoirement exécuter un malloc pour
réserver la mémoire nécessaire aux différents éléments de la structure puis attribuer l’adresse
de début de zone au pointeur. Si vous ne le faites pas, il y aura une erreur de débordement
quand vous essayerez d’écrire dans un des champs de la structure.

Exercice 8.3 : soit le modèle de structure suivant.


typedef struct _POINT {
char c;
int x, y;
} POINT, *PPOINT;

Ecrire une fonction qui reçoit en argument l’adresse d’une structure de type _POINT et qui
retourne une structure de même type correspondant à un point de même nom et de
coordonnées opposées. La fonction mettra à 0 le point reçu. Ecrire un petit programme d’essai
de cette fonction.

142
9. Les fichiers

9.1 Introduction
Un fichier est un objet physique qui sert à créer des entrées-sorties entre les programmes qui
s’exécutent et le monde extérieur. Dans le monde Unix (et donc dans Linux), toutes les
entrées-sorties sont vues comme des fichiers (écran, clavier, terminaux, disques, ...). On peut
accéder à ces fichiers :

• soit grâce à des fonctions de bas niveaux (les appels systèmes) qui gèrent des descripteurs
de fichiers. Les descripteurs de fichiers sont des valeurs de type int que le système
d’exploitation associe à un fichier à la demande d’un programme.

• soit grâce à des fonctions de plus haut niveau (dont le degré d’abstraction est plus élevé
car elles masquent les appels systèmes) qui manipulent des flux. C’est la méthode que
nous allons voir maintenant.

Les flux (stream ou flot de données) sont des abstractions ajoutant automatiquement aux
descripteurs de fichiers des mémoires tampons d’entrée-sortie, des verrous ainsi que d’autres
informations de contrôle. Les flux sont de type « opaque » FILE, c’est à dire que le modèle de
structure de type FILE est défini dans <stdio.h>, mais que l’on ne s’intéresse pas au
contenu de la structure. Il ne faut pas chercher à accéder aux champs internes de la structure
FILE ni à utiliser des objets de type FILE, mais uniquement à utiliser des pointeurs sur ces
objets. Les fonctions de haut niveau de la bibliothèque standard utilisent le pointeur sur le flux
et gèrent automatiquement les allocations et les libérations mémoires nécessaires.

Lorsque l’on désire accéder à un fichier par l’intermédiaire d’un flux, on invoque la fonction
fopen(). Cette fonction prend en argument le nom du fichier désiré ainsi qu’une chaîne de
caractères indiquant le mode d’accès au fichier. Elle renvoie un pointeur sur un flux de type
FILE. La fonction fclose() met fin à l’association entre le fichier et le flux et donc ferme
le fichier. On utilise ces fonctions ainsi :

143
1) #include <stdio.h>

2) main()
3) {
4) FILE *fp;

5) fp = fopen("essai.txt", "w");

6) fclose(fp);
7) }

Ligne 4 : création d’un pointeur sur un flux de type FILE. Le pointeur est bien sur non
initialisé.
Ligne 5 : fopen() ouvre le fichier essai.txt (dans le répertoire courant) en mode
écriture et lui associe un flux dont l’adresse est copiée dans le pointeur fp.
Ligne 6 : le flux dont l’adresse est dans fp est détruit et le fichier correspondant est
fermé.

Tout programme s’exécutant sous Linux (et sous Windows) dispose de trois flux ouverts
automatiquement lors de son démarrage :
• stdin : flux d’entrée standard. Ce flux est ouvert en lecture seule. Il s’agit par défaut du
clavier. Le programme peut y prendre ses données.
• stdout : flux de sortie standard. Le programme affiche ses résultats dans ce flux qui est
ouvert en écriture seule. Par défaut, il s’agit de l’écran.
• stderr : flux d’erreur standard. Ce flux, ouvert en écriture seule, sert à afficher des
informations concernant le fonctionnement du programme ou ses éventuels problèmes.
Par défaut, ces informations sont aussi affichées sur l’écran.

Au niveau de l’interpréteur de commandes (le shell), il est possible de rediriger les flux
d’entrée et de sortie d’un programme grâce aux opérateurs >, < et |. On peut par exemple :
• rediriger la sortie standard d’un programme vers un fichier grâce à l’opérateur >;
mon_programme > sortie.txt
• rediriger l’entrée standard d’un programme depuis un fichier grâce à l’opérateur < ;
mon_programme < entree.txt
• rediriger la sortie standard d’un programme vers l’entrée standard d’un autre programme
en utilisant l’opérateur | (le pipe) ; programme1 | programme2

144
9.2 Ouverture et fermeture d’un flux
Comme nous l’avons déjà vu, la fonction fopen crée un lien entre un flux et un fichier. Son
prototype est le suivant :

FILE * fopen(char *nom_de_fichier, char *mode) ;

Le nom du fichier est une chaîne de caractères qui peut utiliser un nom relatif
(../dir/toto.txt) ou bien un nom absolu (/users/dut/titi.txt). Si aucun
chemin n’est spécifié (essai.txt) alors le fichier se trouve dans le même répertoire que le
programme exécutable.

Le mode indiqué en deuxième argument permet de préciser le type d’accès au fichier. Six
modes sont possibles :

mode type d’accès

r lecture seule

w écriture seule

a écriture seule en fin de fichier

r+ lecture et écriture

w+ lecture et écriture

a+ écriture en fin de fichier et lecture

Sur certains systèmes non standard comme MS-DOS (et tous les Windows), on peut
rencontrer des lettres supplémentaires comme « b » pour indiquer que le flux ne contient que
des données binaires ou « t » pour indiquer qu’il ne contient que du texte. On rencontre par
exemple des modes comme wb : écriture seule en binaire ou bien rt : lecture seule en mode
texte. On doit distinguer le mode texte du mode binaire pour la raison suivante. La fin de la
ligne dans un fichier texte sous Windows est constituée d’un code 0x0D (retour chariot) et
d’un code 0x0A (ligne suivante) alors qu’en langage C, la fin de ligne est seulement
constituée d’un code 0x0A.

145
Il faut donc obligatoirement que la fin de ligne « langage C » soit convertie en fin de ligne
Windows à l’écriture dans le fichier (transformation du code 0x0A en deux codes 0x0D,
0x0A) et que la fin de ligne Windows soit convertie en fin de ligne « langage C » à la lecture
du fichier (transformation des deux codes 0x0D, 0x0A en un seul code 0x0A). Cette
transformation est effectuée automatiquement si le fichier a été ouvert en mode texte. Si le
fichier est ouvert en mode binaire, aucune transformation n’est effectuée. En effet, si vous
souhaitez lire une image à partir d’un fichier, il vaut mieux que les octets valant 0x0A dans ce
fichier ne soient pas transformés en 2 octets 0x0D, 0x0A (ce qui changerait la taille de
l’image).

Cette distinction binaire/texte n’a aucun sens sous Unix et est ignorée car la fin de ligne dans
un fichier texte est constituée d’un code 0x0A comme en langage C. Il n’y a donc aucune
transformation à effectuer. Voici deux exemples écrivant la chaine "coucou" dans deux
fichiers ouverts respectivement en mode texte et en mode binaire sous Windows :

En mode texte :
#include <stdio.h>

main()
{
char str[] = "coucou";
FILE *fp;

fp = fopen("essai.txt", "wt");

fputs(str, fp);
fputs("\n", fp);
fputs(str, fp);
fputs("\n", fp);

fclose(fp);
}

Le contenu du fichier vu avec un éditeur hexadécimal est le suivant :

146
En mode binaire :
#include <stdio.h>

main()
{
char str[] = "coucou";
FILE *fp;

fp = fopen("essai.txt", "wb");

fputs(str, fp);
fputs("\n", fp);
fputs(str, fp);
fputs("\n", fp);

fclose(fp);
}

Le contenu du fichier vu avec un éditeur hexadécimal est le suivant. C’est ce que l’on verrait
sous Unix.

Voyons maintenant plus en détails l’influence des modes sur la position de la première lecture
ou écriture dans le fichier, ainsi que ce qui se passe si le fichier existe déjà ou si le fichier
n’existe pas.

type d’accès position à l’ouverture fichier existant fichier non


existant
r lecture seule début de fichier ok erreur
w écriture seule début de fichier initialisation création
a écriture seule fin de fichier ok création
r+ lecture et écriture début de fichier ok erreur
w+ lecture et écriture début de fichier initialisation création
a+ lecture et écriture écriture à la fin lecture ok création
au début

147
Une erreur classique du débutant consiste à ouvrir un fichier existant en mode « w »
ou « w+ ». La fonction fopen()écrase alors l’ancien fichier avec un nouveau
fichier vide de même nom. Les données contenues dans l’ancien fichier sont perdues.

La fonction fopen() retourne l’adresse du flux ouvert. Cette adresse doit être copiée dans
un pointeur de type FILE qui sera passé en paramètre à toutes les fonctions permettant
d’accéder au fichier ouvert. Si l’ouverture est impossible, fopen() renvoie le pointeur
NULL. Il est fortement conseillé de tester la valeur retournée par fopen() et de traiter
dans le programme le cas de la mauvaise ouverture du fichier, sinon il se produira une erreur
de débordement dans la suite du programme lors de l’exploitation du flux.

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

main()
{
FILE *fp;
char *FileName = "essai.txt";

if ((fp = fopen(FileName, "r")) == NULL) {


printf("ouverture du fichier %s impossible\n",
FileName);
exit(-1); /* arrêt du programme */
}

/* lectures/ecritures sur le fichier */

fclose(fp);
}

Quand on a terminé les opérations d’entrées-sorties sur le fichier, on ferme le flux ainsi que le
fichier à l’aide de la fonction fclose dont voici le prototype.

int fclose(FILE *stream);

Cette fonction accepte en entrée un pointeur de type FILE qui pointe sur un flux (stream) et
retourne la valeur 0 si le fichier a été fermé ou bien EOF en cas d’erreur de fermeture. La
sortie du programme force la fermeture de tous les fichiers ouverts. Il est toutefois préférable
de sortir proprement du programme en fermant explicitement tous les flux ouverts.

148
9.3 Buffers associés aux flux
Un buffer (ou mémoire tampon ou mémoire cache) est une petite quantité de mémoire vive de
l’ordinateur qui est réservée par le système d’exploitation pour stocker temporairement des
données. Il existe, lors de l’écriture dans un flux, trois niveaux de buffers susceptibles de
différer l’écriture. Tout d’abord, le flux lui-même est l’association d’un buffer et d’un
descripteur de fichier. Des fonctions existent dans la librairie standard (setvbuf, setbuf)
qui permettent de paramétrer le comportement de ces buffers. La fonction fflush permet de
forcer le vidage du buffer :
int fflush(FILE *flux);

Elle provoque :
1) Le vidage du buffer dans le fichier pour un flux en sortie,
2) La mise à 0 du buffer pour un flux en entrée. On n’utilise généralement que
fflush(stdin) pour vider le buffer clavier. La mise à 0 d’un buffer en entrée n’a
généralement pas de sens pour un fichier.

fflush retourne 0 si l’opération s’est bien déroulée ou EOF dans le cas contraire. L’appel
de fflush(NULL); vide les buffers de tous les flux en attente d’écriture. La fin de
programme vide automatiquement tous les buffers associés aux flux ouverts. La fonction
fclose(FILE *flux) provoque automatiquement le vidage du buffer associé au flux.

Toutefois, le vidage du buffer ne provoque pas obligatoirement l’écriture des données dans le
fichier physique. En effet, le système d’exploitation gère son propre niveau de mémoire cache
pour limiter les accès aux disques. En général, tous les mécanismes de mémoire tampon sont
destinés à différer les écritures sur périphérique lent afin qu’elles aient lieu à un moment où
l’ordinateur est inoccupé, ceci afin d’améliorer les performances globales du système. L’appel
système :
int sync(void);

force l’écriture sur le contrôleur du disque. Mais même avec cette fonction, l’écriture n’est
pas certaine car il existe des mémoires tampons sur le contrôleur matériel du disque dur ainsi
que sur le disque dur lui-même.

149
utilisateur programme

librairie flux
standard C buffer
fflush

système fichier
d’exploitation mémoire cache
sync

périphérique contrôleur
mémoire cache

support mémoire
physique cache
(disque dur) intégré

C’est la raison pour laquelle il ne faut pas éteindre son ordinateur brutalement en appuyant sur
le bouton marche/arrêt car la coupure de l’alimentation vous ferait perdre le contenu des
divers buffers et mémoires tampons qui ne sont pas encore écrits sur le disque. En suivant la
procédure d’extinction du système d’exploitation, vous avez la garantie que toutes les
écritures sont effectivement réalisées et que les caches sont vidés.

9.4 Lecture et écriture dans un flux


La majorité des fonctions permettant de lire ou d’écrire dans un fichier nous est déjà connue.
On retrouve trois méthodes d’entrées-sorties connues.

9.4.1 Les lectures et écritures par caractère


La fonction :

int fgetc(FILE *stream);

lit un caractère dans un flux et retourne son code ASCII dans un entier. La fonction :

int fputc(int c, FILE *stream);

150
écrit le caractère c (de type entier) dans un flux et retourne la valeur du caractère écrit si
l’écriture s’est bien passée ou EOF en cas d’erreur. Le programme suivant copie le fichier
entree.txt dans le fichier sortie.txt caractère par caractère.
#include <stdio.h>
#include <stdlib.h>

main()
{
FILE *fi, *fo;
int c;

fi = fopen("entree.txt", "r");
fo = fopen("sortie.txt", "w");

while((c = fgetc(fi)) != EOF)


fputc(c, fo);

fclose(fi);
fclose(fo);
}

Exercice 9.1 : écrivez un programme qui calcule la taille d’un fichier en le lisant caractère par
caractère.

9.4.2 Les lectures et écritures par ligne


La fonction :

char * fgets(char *chaine, int n, FILE *stream);

accepte en paramètres une chaîne de caractères, un entier qui contient le nombre de caractères
à lire et un pointeur sur FILE. Elle retourne le pointeur sur le début de chaîne en cas de
lecture sans erreur ou bien le pointeur NULL en cas de fin de fichier ou d’erreur. Le
fonctionnement de fgets est un peu plus compliqué que celui de gets. gets lit sur
stdin tous les caractères disponibles jusqu’au caractère fin de ligne ↵. Si le nombre de
caractères lus est supérieur à la taille du tableau où ils doivent être stockés, il y a
débordement. Avec fgets, on peut spécifier le nombre de caractères à lire, ce qui empêche
le débordement du tableau. Son comportement est le suivant. fgets lit les caractères du
fichier et les range dans le tableau pointé par chaine jusqu’à ce qu’une des trois conditions
suivantes soit vraie :

151
1) rencontre d’un caractère fin de ligne ↵ (qui est mis dans le tableau).
2) rencontre de la fin du fichier.
3) il ne reste plus qu’un seul caractère libre dans le tableau.

fgets complète alors le tableau avec un caractère null (\0). Par exemple, la lecture de la
première ligne du fichier essai.txt suivant :

Avec le programme :
#include <stdio.h>

main()
{
char s1[100];
FILE *fp;

fp=fopen("essai.txt", "r");
fgets(s1, 99, fp);
fclose(fp);
}

va donner dans s1 :

c o u c o u \n \0
0x63 0x6F 0x75 0x63 0x6F 0x75 0x0A 0x00

La fonction fputs :
int fputs(const char * chaine, FILE *stream);

accepte en paramètres un pointeur vers une chaîne se terminant par un caractère null et un
pointeur vers FILE. Elle retourne une valeur positive ou nulle si l’écriture se déroule
normalement et EOF en cas d’erreur. fputs écrit dans le fichier le contenu d’un tableau
contenant une chaîne dont la fin est indiquée par un caractère null. Ce tableau peut contenir un
caractère fin de ligne. fputs peut donc servir à écrire indifféremment une ligne de texte ou

152
une chaîne quelconque. Le programme suivant copie le fichier entree.txt dans le fichier
sortie.txt ligne par ligne si la taille de la chaîne est supérieure à la taille de la ligne.
Sinon, la copie se fait portion de ligne par portion de ligne.
#include <stdio.h>

main()
{
FILE *fi, *fo;
char ligne[20];

fi = fopen("entree.txt", "r");
fo = fopen("sortie.txt", "w");

while(fgets(ligne, 10, fi) != NULL)


fputs(ligne, fo);

fclose(fi);
fclose(fo);
}

Exercice 9.2 : écrivez un programme qui lit une phrase au clavier, qui l’écrit dans un fichier,
puis qui relit le fichier et affiche le contenu à l’écran.

9.4.3 Les lectures et écritures formatées


Les fonctions :
int fscanf( FILE *stream, const char *format [, argument ]... );
int fprintf( FILE *stream, const char *format [, argument ]...);

s’utilisent de la même manière que les fonctions printf et scanf sauf qu’elles lisent ou
écrivent sur un flux au lieu de le faire sur les entrées-sorties standards. Par exemple :
fprintf(fp, "bonjour maitre\n");

écrit la chaîne de caractères "bonjour maitre" dans le fichier associé au flux fp et


provoque un changement de ligne. Le programme suivant lit dans le fichier entree.txt
deux valeurs flottantes et les écrit dans les variables x et y.
#include <stdio.h>

main()
{
FILE *fp;
float x, y;

153
fp = fopen("entree.txt", "r");
fscanf(fp, "%f%f", &x, &y);
fclose(fp);
}

La gestion des espaces avec la fonction fscanf est aussi pénible qu’avec la fonction
scanf, notamment en ce qui concerne les chaînes de caractères. Sauf cas particulier, on
utilisera plutôt la fonction fgets (associée éventuellement avec sscanf).

Exercice 9.3 : écrivez un programme qui lit des double au clavier et qui les écrit dans un
fichier au fur et à mesure de la saisie, puis qui relit le fichier et affiche les nombres à l’écran.
On interrompra la saisie en saisissant un 0. On testera l’ouverture du fichier.

Il faut noter qu’il est possible de remplacer, pour chacune de ces trois méthodes, le flux
associé à un fichier par un des trois flux ouverts automatiquement au démarrage. Cela veut
dire par exemple que les deux lignes suivantes sont équivalentes :
printf("bonjour maitre\n");
fprintf(stdout, "bonjour maitre\n");

Le traitement d’une mauvaise ouverture d’un fichier peut donc se faire comme suit :
#include <stdio.h>
#include <stdlib.h>

main()
{
FILE *fp;
char *FileName = "essai.txt";

if ((fp = fopen(FileName, "r")) == NULL) {


fprintf(stderr, "ouverture du fichier %s
impossible\n", FileName);
exit(-1);
}
/* ...*/

fclose(fp);
}

Le message d’erreur est maintenant envoyé vers stderr au lieu de stdout comme avec
printf.

154
9.4.4 Les lectures et écritures binaires
En langage C, il existe deux manières de lire et d’écrire des valeurs dans des fichiers :
• le mode texte avec les fonctions que nous venons de voir, fgetc, fputc, fgets,
fputs, fscanf, fprintf.
• le mode binaire avec les fonctions fread et fwrite.

Dans le mode texte, on écrit (ou on lit) dans le fichier les codes ASCII des chiffres ou des
lettres qui composent la valeur. Dans le mode binaire, on écrit (ou on lit) dans le fichier les
octets qui codent la valeur. Prenons par exemple un entier. En mode texte, on écrira la valeur
de l’entier avec un fprintf :
#include <stdio.h>

main()
{
FILE *fp;
int x = 123456;

fp = fopen("entier_texte.txt", "wt");
fprintf(fp, "%d", x);
fclose(fp);
}

Avec un éditeur hexadécimal, on trouvera dans la colonne du milieu la valeur des octets dans
le fichier (ce sont bien des codes ASCII) et dans la colonne de droite les caractères
correspondants :

En mode binaire, on écrira la valeur de l’entier avec un fwrite :


#include <stdio.h>

main()
{
FILE *fp;
int x = 123456;

fp = fopen("entier_binaire.txt", "wb");
fwrite(&x, sizeof(int), 1, fp); fclose(fp);
}

155
Avec le même éditeur hexadécimal, on trouvera dans le fichier les 4 octets (poid faible en
premier) qui codent le nombre entier (123456 est bien égal à 0001E240 en hexadécimal).
Comme ce ne sont plus des codes ASCII, il y a n’importe quoi dans la colonne de droite :

On obtiendra le même genre de contenu avec un nombre flottant : des codes ASCII en mode
texte et des octets (4 pour un float ou 8 pour un double) en mode binaire. En général, le fichier
binaire est plus petit que le fichier texte. Donc, si vous souhaitez sauvegarder une grande
quantité de résultats numériques tout en conservant un fichier de taille raisonnable, il vaut
mieux utiliser le mode binaire. En contrepartie, le fichier ne sera plus lisible avec un éditeur
de texte.

Remarque : en ce qui concerne les variables de type char, la distinction entre mode texte ou
mode binaire n’a guère de sens. Dans les deux cas, on va écrire dans le fichier la valeur de
l’octet. Si vous écrivez dans un fichier des octets qui correspondent à des codes ASCII, alors
ce sera du texte que vous pourrez éditer. Si vous écrivez dans un fichier des octets qui
correspondent par exemple aux points (pixels) d’une image alors un éditeur de texte ne pourra
pas le lire correctement. Faites juste attention au "wb" et "rb" sous Windows.

Voyons maintenant plus en détail les fonctions dîtes d’entrées-sorties binaires, fread et
fwrite. Elles permettent de lire ou d’écrire le contenu d’un bloc mémoire sans se soucier de
son interprétation.

int fwrite(const void * ptr, int taille_elem, int nb_elem, FILE *flux);

La fonction fwrite accepte comme paramètres :


1) un pointeur sur un tableau de n’importe quel type,
2) la taille du type de donnée à écrire,
3) le nombre d’éléments à écrire,
4) un pointeur sur FILE.

156
Elle retourne le nombre d’éléments effectivement écrits. Dans l’exemple suivant, le
programme écrit un tableau de 256 entiers 32 bits dans le fichier sortie.bin.
#include <stdio.h>
#include <stdlib.h>

main()
{
FILE *fp;
int *ptr, i;

ptr = malloc(256*sizeof(int));
for (i = 0; i < 256; i++) ptr[i] = i;

fp = fopen("sortie.bin", "wb");
i = fwrite(ptr, sizeof(int), 256, fp);
fclose(fp);
}

Attention, sous Windows, il faudrait utiliser le mode « wb » car les fichiers sont en mode
texte par défaut. Voyons maintenant la fonction de lecture binaire :

int fread(void * ptr, int taille_elem, int nb_elem, FILE *flux);

Comme pour fwrite, la fonction fread accepte comme paramètres :


1) un pointeur sur un tableau de n’importe quel type,
2) la taille du type de donnée à lire,
3) le nombre d’éléments à lire,
4) un pointeur sur FILE.

Elle retourne le nombre d’éléments effectivement lus. Dans l’exemple suivant, le programme
lit 256 entiers 32 bits dans le fichier sortie.bin et les range dans un tableau.
#include <stdio.h>
#include <stdlib.h>

main()
{
FILE *fp;
int *ptr, i;

ptr = malloc(256 * sizeof(int));


fp = fopen("sortie.bin", "rb");
i = fread(ptr, sizeof(int), 256, fp);
fclose(fp);
}

157
9.5 Positionnement dans un flux
Il est rare que dans un programme un peu complexe, on ait uniquement besoin de lire les
données d’un fichier séquentiellement, c’est à dire les unes à la suite des autres, du début à la
fin, sans jamais revenir en arrière ou sans sauter des portions de fichier. Il est donc naturel que
la librairie standard mette à notre disposition des fonctions permettant de se déplacer
librement dans un flux (fichier) avant de lire son contenu ou d’y écrire des données.

Le système d’exploitation gère un compteur de position sur chaque flux qui indique la
différence en octets entre le début du flux et la position courante. Ce compteur est mis à jour
après chaque opération de lecture ou d’écriture. La fonction :

long ftell(FILE *flux);

retourne la valeur de la position courante dans le flux, c’est à dire la valeur du compteur de
position. Ce compteur vaut 0 au début du flux. Si la position est fausse, ftell retourne -1.
La fonction :

int fseek(FILE *flux, long position, int depart);

permet de se déplacer dans le flux. La position (deuxième argument, positif ou négatif) est
indiquée en octets depuis le point de départ (troisième argument). Celui-ci peut prendre les
valeurs suivantes :
• SEEK_SET : le point de départ est le début du fichier (0).
• SEEK_CUR : le point de départ est la position courante (1).
• SEEK_END : le point de départ est la fin du fichier (2).

Ces trois constantes symboliques (macros) sont définies dans <stdio.h>. La fonction
fseek retourne 0 si elle réussit et -1 en cas d’échec. Par exemple, à l’appel suivant, le
compteur d’octets se place à la fin du fichier (sur l’octet qui suit le dernier octet du fichier de
façon à être prêt pour une écriture) :

fseek(fp, 0, SEEK_END);

158
Par exemple, à l’appel suivant, le compteur recule de trois octets par rapport à la position
courante :

fseek(fp, -3, SEEK_CUR);

La fonction rewind :

void rewind(FILE *flux);

permet de ramener le compteur de position au début du flux. Elle est équivalente à

fseek(fp, 0, SEEK_SET);

Il n’y a aucun rapport entre le pointeur de flux fp et le compteur de position dans le


fichier. Lire une valeur dans le fichier ne modifie en rien l’adresse contenue dans fp.
L’adresse de cette structure est déterminée lors de l’ouverture du fichier et ne doit
jamais être modifiée. Ecrire fp++; n’a jamais fait avancer d’un octet dans le fichier.

Voici par exemple un programme qui indique diverses positions sur un fichier (qui contient
18 octets quelconques) et qui détermine la taille de ce fichier.
#include <stdio.h>

main()
{
FILE *fp;
int position;

fp = fopen("entree.txt", "r");
position = ftell(fp);
printf("Au début, position = %d\n", position);

fseek(fp, 0, SEEK_END);
position = ftell(fp);
printf("A la fin, position = %d\n", position);
printf("taille du fichier = %d\n", position);

rewind(fp);
position = ftell(fp);
printf("Après rewind, position = %d", position);
fclose(fp);
}

159
Le résultat est le suivant :
Au début, position = 0
A la fin, position = 18
taille du fichier = 18
Après rewind, position = 0

entree.txt

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

↑ ↑
début fin

Lorsque fseek ou rewind sont invoquées, le contenu éventuel du buffer de sortie associé
au flux est écrit dans le fichier avant le déplacement. Il existe des systèmes d’exploitation
ayant une restriction à l’utilisation d’un flux en lecture et en écriture. Sur ces systèmes, une
lecture ne peut suivre une opération d’écriture que si on a appelé fflush, fseek ou
rewind entre les deux opérations. De même, avant une écriture qui suit une lecture, il faut
obligatoirement appeler fseek ou rewind. Ces limitations n’existent ni sous Linux, ni sous
Windows.

Exercice 9.4 : reprenez le programme de l’exercice 9.3 mais affichez les nombres stockés
dans le fichier en partant de la fin.

9.6 Utilisation d’un fichier de configuration


Le passage direct de paramètres à la fonction main depuis la ligne de commande devient peu
pratique lorsque le nombre de paramètres est trop important. On utilise alors un fichier de
configuration dont on passe le nom en ligne de commande. L’utilisation du programme
devient par exemple :

essai [-h] nom_du_fichier_de_configuration

160
Par convention, on appelle le fichier de configuration essai.cfg. Il s’agit d’un fichier texte
que l’on écrit avec un éditeur ASCII standard (et surtout pas un traitement de texte). Il
contient généralement :
• Une première ligne de commentaires que l’on utilise uniquement pour identifier le
programme associé.
• Plusieurs lignes contenant les paramètres à récupérer.

Voici un exemple de fichier essai.cfg :


fichier de configuration du programme essai
toto.bin /* nom du fichier à traiter */
50 /* paramètre 1 */
100 /* paramètre 2 */
0 /* paramètre 3 */

Exercice 9.5 :
1. On pourrait bien sur utiliser la fonction fscanf pour essayer de récupérer les valeurs
directement. Pourquoi ne le fait-on pas ?
2. Quels sont les avantages de l’utilisation d’un fgets suivi d’un sscanf ?
3. Ecrivez le programme permettant de récupérer les paramètres contenus dans le fichier de
configuration essai.cfg.
4. Cette manière de procéder a-t-elle des limitations ?

Il est à noter que l’on peut associer l’utilisation d’un fichier de configuration avec celle d’une
interface graphique par exemple pour sauver une configuration. Il suffit de créer un fichier de
configuration par défaut lors de la première utilisation du programme dans le répertoire racine
de l’utilisateur, puis de sauver les changements de configuration du programme dans ce
fichier avant d’en sortir. Traditionnellement, sous Unix, le nom des fichiers de configuration
commence par un point, car ce type de fichier n’est pas listé par défaut avec la commande ls
(on appelle cela un fichier invisible car il n’est listé que par la commande ls -la). Par
exemple, l’éditeur Linux nedit crée un fichier de configuration appelé .neditdb que vous
pouvez éditer.

L’autre solution (propriétaire) consiste à utiliser la base de registre de Windows pour sauver
la configuration propre à chaque utilisateur. Il n’y a guère d’autre choix car Windows (avant

161
2000) ne crée pas de compte utilisateur par défaut. Si vous utilisez un fichier de configuration,
il s’appliquera automatiquement à tous les utilisateurs (ce qui n’est pas forcément gênant).

162
10. Divers

10.1 Exécution de commandes


La fonction system permet d’exécuter une commande à partir d’un programme. Voici son
prototype :

int system(const char *str);

Cette fonction exécute la commande contenue dans la chaîne str, puis reprend l’exécution
du programme en cours. Par exemple :

system("date");

Cette instruction exécute le programme date qui affiche la date et l’heure sur la sortie
standard (l’écran par défaut). Elle retourne la valeur retournée par le programme exécuté. En
général, une valeur 0 indique que tout s’est bien passé. Il est important de noter le phénomène
suivant : si vous lancez l’exécution d’un programme qui fonctionne en mode interactif comme
un éditeur ou bien un programme qui dure très longtemps, le programme en cours (celui qui a
lancé la fonction system) est arrêté tant que le programme lancé n’a pas terminé. Par
exemple, sous Linux, si vous exécutez

system("nedit toto.txt");

dans votre programme, celui-ci sera suspendu tant que l’éditeur nedit n’aura pas été fermé.
Evidemment, si vous lancez la commande en tache de fond :

system("nedit toto.txt &");

le programme appelant reprend la main immédiatement.

Exercice 10.1 : écrivez un programme qui fasse la copie dans le répertoire courant d’un
fichier dont le nom est contenu dans le tableau nom1 dans un fichier dont le nom est contenu
dans le tableau nom2.

163
#include <stdlib.h>
void main()
{
char nom1[] = "toto.txt";
char nom2[] = "titi.txt";

...
system( ??? ) ;
}

10.2 Les opérateurs binaires


Le C fournit six opérateurs qui réalisent des manipulations au niveau des bits. On ne peut les
appliquer qu’aux variables entières, c’est-à-dire de type char, short, long et int, signés
ou non.

opérateur opération effectuée


& ET bit à bit
| OU bit à bit
^ OU exclusif bit à bit
~ inversion bit à bit
<< décalage à gauche
>> décalage à droite

Ne confondez pas les opérateurs bit à bit & et | avec les opérateurs logiques && et || dont le
résultat est booléen.

Exercice 10.2 : Soit la portion de programme suivante. Donnez les valeurs successives de y.
Dans quel cas utilise-t-on chacun de ces 4 opérateurs ?

unsigned int x = 255, y;

y = x & 0xff;
y = y & 0x33;
y = x | 0xcc;
y = (y ^ y) | 1;
y = y ^ 0x81;
y = ~y;
y = ~0;

164
Les opérateurs << et >> décalent leur opérande de gauche du nombre de bits indiqué par leur
opérande de droite, qui doit être un entier positif. Ainsi, x << 2 décale la valeur de x de 2
bits vers la gauche, en remplissant les 2 bits de droite par des 0, ce qui revient à une
multiplication par 4. Ceci est vrai que x soit signé ou non.

Si l’on décale une quantité non signée vers la droite (x >> 2), les bits de gauche sont
toujours mis à 0. Si cette quantité est signée, alors les bits de gauche peuvent soit se remplir
avec les bits de signe (décalage arithmétique avec extension de signe) soit se remplir avec des
0 (décalage logique). Cela dépend de la machine. Sur PC (sous Windows comme sous Linux),
il y a extension de signe. Sauf exception, il vaut mieux éviter d’utiliser les opérateurs
binaires sur des entiers signés.

Exercice 10.3 : écrivez une fonction qui change la position des octets dans un entier 32 bits
(4321 → 1234) : unsigned change_indian(unsigned x)

10.3 Les énumérations


Il est toujours possible de définir des constantes avec la directive #define comme dans :
#define LUNDI 0
#define MARDI 1
#define MERCREDI 2
#define JEUDI 3
#define VENDREDI 4
#define SAMEDI 5
#define DIMANCHE 6

mais l’affectation d’une valeur à chaque constante devient pénible quand le nombre de
constantes est grand. Le langage C offre une autre méthode pour créer des constantes
multiples, l’énumération :

enum {LUNDI, MARDI, MERCREDI, JEUDI, VENDREDI, SAMEDI,


DIMANCHE};

L’avantage de cette méthode est que, par défaut, les identificateurs LUNDI, MARDI, ...
sont des constantes de type int dont les valeurs sont 0, 1, 2, 3, 4, 5, 6. La génération des
valeurs se fait de manière automatique à partir de 0. Si on désire donner des valeurs
particulières aux constantes, c’est possible en écrivant :

165
enum {FRANCE = 10, ESPAGNE = 20, ITALIE = 30};

Il n’est d’ailleurs pas nécessaire de donner une valeur à chaque constante :

enum {FRANCE = 10, ESPAGNE, SUISSE, ITALIE = 30};

donnera la valeur 11 à ESPAGNE et 12 à SUISSE. Les caractères en C étant des entiers, il est
aussi possible d’écrire :

enum {OUI = ‘O’, NON = ‘N’};

N’oubliez pas qu’il est d’usage de mettre en majuscule les noms des constantes dans un
programme. Cela améliore grandement sa lisibilité. Les énumérations sont identiques aux
structures d’un point de vue syntaxique. Après le mot clé enum, il peut y avoir une étiquette
qui permettra plus loin dans le programme de déclarer des variables de type énumération
comme par exemple :

enum reponse {OUI = ‘O’, NON = ‘N’};


enum reponse rep;

rep = OUI;

Il est aussi possible de créer un nouveau type en utilisant l’instruction typedef :

typedef enum {OUI = ‘O’, NON = ‘N’} REPONSE;


REPONSE rep;

rep = OUI;

10.4 Les opérateurs d’incrémentation et de décrémentation


Nous avons déjà vu les opérateurs d’incrémentation et de décrémentation du langage C dont
les équivalences sont les suivantes :

i = i + 1; i++; ++i;
i = i - 1; i--; --i;

166
Nous sommes en présence de deux expressions qui non seulement possèdent une valeur, mais
qui réalisent aussi une action. Utilisées seules, les expressions i++; et ++i; sont
équivalentes. Utilisées dans une autre expression, il faut savoir que dans i++; on évalue
d’abord et on incrémente après alors que dans ++i; on incrémente d’abord et on évalue
après.

i--; évaluation puis décrémentation


--i; décrémentation puis évaluation

Ainsi, si i vaut 5, l’expression :


n = i++ - 5;

affectera à i la valeur 6 et à n la valeur 0, alors que dans les mêmes conditions initiales,
l’expression :
n = ++i - 5;

affectera à i la valeur 6 et à n la valeur 1. Cela signifie aussi que dans l’expression :


tab[i++] = getchar();

on met le caractère lu au clavier dans tab[i], puis on incrémente i alors que dans
tab[++i] = getchar();

on incrémente d’abord i avant de mettre le caractère lu au clavier dans tab[i].

Exercice 10.4 : remplissez le tableau suivant avec les valeurs de x, y et z.

x y z
x = y = 1;
z = x++ - 1;
z += - x++ + ++y;
z = x / ++x;

167
10.5 L’opérateur virgule
Deux expressions séparées par une virgule :

expr1 , expr2

s’évaluent de gauche à droite (expr1 puis expr2), et le résultat de l’expression prend le


type et la valeur de l’opérande de droite. On peut écrire par exemple :

i = (j = 2, 1);

ce qui est une manière particulièrement affreuse d’écrire :

i = 1;
j = 2;

Par contre, une utilisation courante de l’opérateur virgule se trouve dans les expressions d’une
boucle for. Si on désire utiliser deux indices dans une seule boucle, il est agréable et lisible
d’écrire :

for (i = 0, j = 1; i < LIMITE; i++, j = j + 3) {


...
}

10.6 L’opérateur conditionnel ?


Les instructions :
if(a > b)
z = a;
else
z = b;

affectent à z le maximum de a et de b. L’expression conditionnelle qui utilise


l’opérateur ? : est une autre manière, plus compacte, d’arriver au même résultat :

z = (a > b) ? a : b;

168
Dans l’expression conditionnelle :

expr1 ? expr2 : expr3

on commence par évaluer l’expression expr1. Si elle n’est pas nulle (c’est-à-dire si elle est
vraie) alors la valeur de l’expression conditionnelle est la valeur de expr2. Sinon la valeur
de l’expression conditionnelle est la valeur de expr3.

Exercice 10.5 : écrivez l’expression conditionnelle calculant la valeur absolue d’un nombre.

Exercice 10.6 : écrivez l’expression conditionnelle qui détermine si un nombre est pair ou
non.

10.7 Les macros avec paramètres


Nous avons déjà vu l’utilisation de constantes avec la directive #define comme dans :

#define NB_COLONNES 100

En réalité, pour le préprocesseur, il s’agit de macros sans paramètres au même titre que

#define forever for( ; ; )

qui réalise une boucle infinie. Il est possible de créer des macros (en majuscules par
convention) avec paramètres de la manière suivante :

#define NOM_MACRO(liste_de_paramètres_formels) définition_macro

La liste de paramètres formels est une liste d’identificateurs séparés par des virgules. Par
exemple, la macro MAX détermine le maximum de a et de b :

#define MAX(a,b) (((a) > (b)) ? (a) : (b))

Tout appel de cette macro dans le programme devra se faire sous la forme :

z = MAX(x,y);

169
Avant la compilation du programme, le préprocesseur va remplacer l’ensemble formé du nom
de la macro suivi de la liste de paramètres entre parenthèses (ici MAX(x,y)) par
définition_macro dans laquelle chaque paramètre formel est remplacé par le paramètre
effectif correspondant. Cette opération de remplacement s’appelle l’expansion de la macro.
Dans notre exemple, on trouvera dans le code source après expansion (attention au ;) :

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

ce qui correspond bien à la recherche de maximum vu précédemment. L’utilité principale des


macros avec paramètres est de bénéficier de la clarté d’expression de la fonction sans souffrir
de sa lenteur : le code est inséré directement dans le source, il n’y a donc aucune perte de
temps dû à l’appel et au retour de la fonction. On n’utilise des macros avec paramètres que
pour réaliser des traitements courts (quelques lignes au maximum). Attention, la distinction
entre macro avec et sans paramètre se fait sur le caractère qui suit le nom de la macro. Si c’est
un espace, il n’y a pas de paramètre, si c’est une parenthèse, il y a des paramètres. L’écriture
suivante est une erreur classique :

#define MAX (a,b) (((a) > (b)) ? (a) : (b))

L’espace après MAX indique qu’il s’agit d’une macro sans paramètre. L’expansion de

z = MAX(x,y);

risque de vous réserver quelques surprises. L’écriture de macros recèle de nombreux pièges,
certains évitables et d’autres non. Supposons que l’on écrive :

#define CARRE(a) a * a

ce qui semple parfaitement correct. L’expansion de cette macro va donner :

z = CARRE(x); z = x * x;

ce qui parait toujours correct. Si on teste l’expansion avec une expression au lieu d’une
variable :

170
z = CARRE(x + y); z = x + y * x + y;

On voit que la macro ne donne pas le bon résultat qui devrait être (x + y) * (x + y).
Pour éviter ce genre de problème, il faut respecter les deux règles suivantes dans la définition
d’une macro :
• mettre entre parenthèses les occurrences des paramètres formels,
• mettre entre parenthèses le corps de la macro.

La définition correcte de CARRE est donc :

#define CARRE(a) ((a) * (a))

et l’expansion avec une expression est maintenant correcte.

z = CARRE(x + y); z = ((x + y) * (x + y));

Par contre, le problème suivant ne peut être résolu :

z = CARRE(x++); z = x++ * x++;

L’opérateur d’incrémentation est appliqué deux fois. Il vaut mieux éviter d’utiliser des effets
de bord sur les paramètres effectifs d’une macro, car les conséquences sont généralement
imprévisibles. N’oubliez pas que l’option -E du compilateur (GCC ou Visual C++) vous
permet d’obtenir le source après preprocessing sur la sortie standard et donc de tester
l’expansion des macros. C’est la méthode normale de mise au point.

Exercice 10.7 : écrivez la définition de la macro ABS(x) calculant la valeur absolue d’un
nombre.

Exercice 10.8 : écrivez la définition de la macro IsEvenNumber(x) qui détermine si un


nombre est pair ou non.

171
Exercice 10.9 : écrivez la définition de la macro IsOddNumber(x) qui détermine si un
nombre est impair ou non.

Exercice 10.10 : écrivez la définition de la macro SIGN(x) qui détermine si le signe d’un
nombre est positif ou non.

Exercice 10.11 : écrivez la définition de la macro RANGE(val,min_val,max_val) qui


fonctionne de la manière suivante (opérateur de saturation) :
• si val est inférieure à min_val, val prend la valeur de min_val.
• si val est supérieure à max_val, val prend la valeur de max_val.
• si val est comprise entre min_val et max_val, val est inchangée.

Exercice 10.12 : écrivez la définition de la macro MIN(x,y) qui détermine le minimum de


deux nombres.

Exercice 10.13 : écrivez la définition de la macro ROUND(x) qui arrondit un nombre flottant
à l’entier le plus proche.

Exercice 10.14 : écrivez la définition de la macro TEST_BIT_5(x) qui teste la présence du


6ème bit dans un entier.

10.8 Ce que vous ne verrez pas


Les unions.
Les champs de bits.
Les fonctions avec un nombre variable de paramètres.

10.9 Définition de macro à l’invocation du compilateur


La plupart des compilateurs permettent de définir des macros sans paramètres. Il est alors
possible d’écrire un programme utilisant une macro qui n’est définie nulle part dans le source.
La définition de cette macro se fera à l’invocation du compilateur de la manière suivante :

cc -c -DNB_LIGNES=24 essai.c -o essai

172
La macro NB_LIGNES prend la valeur 24 pendant la compilation de essai.c. Cette méthode
peut être très commode, notamment si elle est associée à la compilation conditionnelle.

10.10 Compilation conditionnelle


Les mécanismes de compilation conditionnelle ont pour but de compiler ou d’ignorer
certaines parties du programme, en fonction d’un test effectué à la compilation. La commande
du préprocesseur permettant de réaliser la compilation conditionnelle est la commande #if
qui peut prendre plusieurs formes :

• Le #if simple. Quand le préprocesseur rencontre :


#if condition
ensemble_lignes_de_code
#endif
il évalue la condition. Si la condition est vraie, les lignes de code sont compilées sinon,
elles sont ignorées.

• Le #if avec #else. Le else permet de compiler deux blocs de lignes différents suivant
l’état de la condition :
#if condition
ensemble_lignes_de_code_1
#else
ensemble_lignes_de_code_2
#endif

• Le #if avec #elif. Pour réaliser plusieurs branches, on utilise le #elif qui est
équivalent à un else if.
#if condition1
ensemble_lignes_de_code_1
#elif condition2
ensemble_lignes_de_code_2
#elif condition3
ensemble_lignes_de_code_3
#else
ensemble_lignes_de_code_4
#endif

173
Dans les 3 cas précédents, il est possible de remplacer les #if par des #ifdef ou
#ifndef :

#ifdef nom_de_macro
ou bien
#ifndef nom_de_macro

Dans ce cas, le test porte sur la définition ou non d’une macro (et non sur sa valeur). #ifdef
nom_de_macro est équivalent à « si la macro est définie » et #ifndef nom_de_macro est
équivalent à « si la macro n’est pas définie ». Une autre possibilité existe, l’utilisation de
l’opérateur defined associé à un #if. Elle est similaire au #ifdef, mais elle permet en
plus de tester plusieurs macros en même temps.

#if defined(macro1) || defined(macro2)

Un cas classique d’utilisation de la compilation conditionnelle est d’éviter plusieurs


inclusions d’un même fichier d’entête .h. Cela arrive notamment dans le cas d’un projet
complexe comprenant plusieurs fichiers sources. Pour être certain d’inclure un fichier une
seule fois, il suffit d’utiliser le mécanisme suivant :

/* fichier exemple.h */

#ifndef EXEMPLE_H
#define EXEMPLE_H

/* corps du fichier exemple.h */


...

#endif

Dans cet exemple, le fichier exemple.h ne peut être inclus qu’une fois dans un source. En
effet, la macro EXEMPLE_H n’est définie que dans ce fichier. Lors du premier passage du
préprocesseur, la variable étant indéfinie, celui-ci analyse l’intégralité du fichier et définit la
macro. Si ce fichier est à nouveau inclus dans le source par erreur, le préprocesseur ne le lira
pas car la macro EXEMPLE_H existe déjà. Il faut noter que l’on aurait aussi pu utiliser un
defined :

174
/* fichier exemple.h */

#if !defined(EXAMPLE_H)
#define EXEMPLE_H

/* corps du fichier exemple.h */


...

#endif

La commande #error permet d’afficher un message d’erreur lors de la compilation. Cette


commande a pour intérêt de vérifier des conditions permettant au programme de s’exécuter
sur ce système d’exploitation. Voici un exemple où on vérifie que la taille des entiers est
suffisante :
#include <limits.h>
#if INT_MAX < 1000000
#error " entiers trop petits sur cette machine"
#endif

Une des applications traditionnelle de la compilation conditionnelle est l’adaptation d’un


programme à un environnement comme WINDOWS ou LINUX. Par exemple, dans le projet
image, la commande système permettant la copie d’un fichier était différente entre les deux
systèmes d’exploitation. La macro WIN32 étant systématiquement définie sous Visual C++, il
suffit donc d’écrire les lignes suivantes pour régler définitivement le problème :

#ifdef WIN32
system("copy toto tutu");
#else
system("cp toto tutu");
#endif

De la même manière, il est parfois commode d’avoir une version d’un programme en mode
Debug avec beaucoup de printf pour surveiller l’évolution des variables et une version
normale sans ces printf. Voici par exemple, le projet mastermind :

main()
{
int ref[NBCHIFFRES], essai[NBCHIFFRES];
int NbPos, NbChif, NbCoup, i, status;

175
NbCoup = 0;
tirage(ref, NBCHIFFRES);

#ifdef DEBUG
for (i = 0 ; i < NBCHIFFRES ; i++)
printf("ref[%d]=%d", i, ref[i]);
#endif

do {
do {
status = entree(essai, NBCHIFFRES);
}
...

Sous Linux, si vous compilez le programme avec :

cc -DDEBUG master.c -o master

vous allez visualiser le tirage alors que si vous compilez normalement :

cc master.c -o master

le tirage n’apparaît plus. Il est à noter qu’avec Visual C++, une variable _DEBUG est définie
automatiquement par le compilateur quand vous êtes en mode Debug mais qu’elle ne l’est
plus quand vous êtes en mode Release (version finale).

Exercice 10.15 : le fichier suivant est un exemple réel de fichier d’entête, limits.h. Il indique
à Visual C++ les limitations numériques des variables qui dépendent du système d’exploitation.
Expliquez son fonctionnement. Faites attention à bien repérer les paires #if ... #endif.

/***
*limits.h - implementation dependent values
*
* Copyright(c)1985-1997, Microsoft Corporation. All rights reserved.
*
* Purpose:
* Contains defines for a number of implementation dependent values
* which are commonly used in C programs. [ANSI]
****/

#if _MSC_VER > 1000


#pragma once
#endif

176
#ifndef _INC_LIMITS
#define _INC_LIMITS

#if !defined(_WIN32) && !defined(_MAC)


#error ERROR: Only Mac or Win32 targets supported!
#endif

#define CHAR_BIT 8 /* number of bits in a char */


#define SCHAR_MIN (-128) /* minimum signed char value */
#define SCHAR_MAX 127 /* maximum signed char value */
#define UCHAR_MAX 0xff /* maximum unsigned char value */

#ifndef _CHAR_UNSIGNED
#define CHAR_MIN SCHAR_MIN /* mimimum char value */
#define CHAR_MAX SCHAR_MAX /* maximum char value */
#else
#define CHAR_MIN 0
#define CHAR_MAX UCHAR_MAX
#endif /* _CHAR_UNSIGNED */

#define MB_LEN_MAX 2 /* max. # bytes in multibyte char */


#define SHRT_MIN (-32768) /* minimum (signed) short value */
#define SHRT_MAX 32767 /* maximum (signed) short value */
#define USHRT_MAX 0xffff /* maximum unsigned short value */
#define INT_MIN (-2147483647 - 1) /* minimum (signed) int value */
#define INT_MAX 2147483647 /* maximum (signed) int value */
#define UINT_MAX 0xffffffff /* maximum unsigned int value */
#define LONG_MIN (-2147483647L - 1) /* minimum (signed) long value */
#define LONG_MAX 2147483647L /* maximum (signed) long value */
#define ULONG_MAX 0xffffffffUL /* maximum unsigned long value */

#if _INTEGRAL_MAX_BITS >= 8


#define _I8_MIN (-127i8 - 1) /* minimum signed 8 bit value */
#define _I8_MAX 127i8 /* maximum signed 8 bit value */
#define _UI8_MAX 0xffui8 /* maximum unsigned 8 bit value */
#endif

#if _INTEGRAL_MAX_BITS >= 16


#define _I16_MIN (-32767i16 - 1) /* minimum signed 16 bit value */
#define _I16_MAX 32767i16 /* maximum signed 16 bit value */
#define _UI16_MAX 0xffffui16 /* maximum unsigned 16 bit value */
#endif
#if _INTEGRAL_MAX_BITS >= 32
#define _I32_MIN (-2147483647i32-1) /* minimum signed 32 bit value */
#define _I32_MAX 2147483647i32 /* maximum signed 32 bit value */
#define _UI32_MAX 0xffffffffui32 /* maximum unsigned 32 bit value */
#endif

#if _INTEGRAL_MAX_BITS >= 64


/* minimum signed 64 bit value */
#define _I64_MIN (-9223372036854775807i64 - 1)
/* maximum signed 64 bit value */
#define _I64_MAX 9223372036854775807i64
/* maximum unsigned 64 bit value */
#define _UI64_MAX 0xffffffffffffffffui64
#endif

177
#if _INTEGRAL_MAX_BITS >= 128
/* minimum signed 128 bit value */
#define _I128_MIN (-170141183460469231731687303715884105727i128 - 1)
/* maximum signed 128 bit value */
#define _I128_MAX 170141183460469231731687303715884105727i128
/* maximum unsigned 128 bit value */
#define _UI128_MAX 0xffffffffffffffffffffffffffffffffui128
#endif

#ifdef _POSIX_

#define _POSIX_ARG_MAX 4096


#define _POSIX_CHILD_MAX 6
#define _POSIX_LINK_MAX 8
#define _POSIX_MAX_CANON 255
#define _POSIX_MAX_INPUT 255
#define _POSIX_NAME_MAX 14
#define _POSIX_NGROUPS_MAX 0
#define _POSIX_OPEN_MAX 16
#define _POSIX_PATH_MAX 255
#define _POSIX_PIPE_BUF 512
#define _POSIX_SSIZE_MAX 32767
#define _POSIX_STREAM_MAX 8
#define _POSIX_TZNAME_MAX 3

#define ARG_MAX 14500 /* 16k heap, minus overhead */


#define LINK_MAX 1024
#define MAX_CANON _POSIX_MAX_CANON
#define MAX_INPUT _POSIX_MAX_INPUT
#define NAME_MAX 255
#define NGROUPS_MAX 16
#define OPEN_MAX 32
#define PATH_MAX 512
#define PIPE_BUF _POSIX_PIPE_BUF
#define SSIZE_MAX _POSIX_SSIZE_MAX
#define STREAM_MAX 20
#define TZNAME_MAX 10

#endif /* POSIX */

#endif /* _INC_LIMITS */

178
11. Edition de lien

11.1 Les pointeurs de fonction


Peu courante dans les langages de programmation, la possibilité de définir des variables
pointant sur des fonctions en très riche en potentialités. Cela permet de considérer une
fonction comme une variable qui pourra être affectée ou transmise comme paramètre à une
autre fonction. Ceci est possible car, en langage C, le nom d’une fonction est équivalent à
l’adresse du début de son programme en langage machine dans la mémoire (de la même
manière que le nom d’un tableau est équivalent à l’adresse de sa première case). Bien
entendu, la valeur de cette adresse ne peut pas être modifiée. Il est toutefois possible de
définir une variable de type pointeur de fonction et d’affecter l’adresse d’une fonction à cette
variable. Prenons un exemple, l’affectation de l’adresse de la fonction putchar. On rappelle
le prototype de cette fonction qui affiche un caractère à l’écran :

int putchar(int c)

La méthode la plus simple pour créer un pointeur sur la fonction putchar est :
#include <stdio.h>

void main()
{
int status ;
char c = 'A';
int (* toto)(int c);

toto = putchar;
status = putchar(c) ;
status = toto(c);
}

toto est un pointeur de fonction recevant un entier en entrée et retournant un entier.

int (* toto)(int c);

Le nombre et le type des variables reçues et retournées doivent être les mêmes que pour la
fonction sur laquelle toto doit pointer. La ligne :

toto = putchar;

179
affecte à toto l’adresse de la fonction putchar. Les deux lignes suivantes sont alors
strictement équivalentes et le programme affiche deux A consécutifs.

status = putchar(c);
status = toto(c);

La méthode préférée pour créer un pointeur de fonction (et notamment utilisée pour l’appel
des librairies dynamiques sous Windows) utilise un typedef :
#include <stdio.h>

typedef int (* PTC_TYPE)(int c);

void main()
{
int status ;
char c = 'A';
PTC_TYPE putchar_ptr;

putchar_ptr = putchar;
status = putchar(c) ;
status = putchar_ptr(c);
}

On voit là que la directive typedef est bien plus efficace qu’un simple #define
puisqu’elle permet des substitutions de texte plus puissantes que celles du préprocesseur. En
effet,

typedef int (* PTC_TYPE)(int c);

crée le type équivalent à « pointeur de fonction ayant un paramètre entier et retournant un


entier ». On peut ensuite s’en servir pour déclarer un pointeur de fonction :

PTC_TYPE putchar_ptr;

qui, après affectation de l’adresse de la fonction putchar :

putchar_ptr = putchar;

peut être utilisé à sa place.

180
status = putchar(c) ;
status = putchar_ptr(c);

Il est à noter que la directive typedef ne définit pas un nouveau type, mais qu’elle crée un
nouveau nom désignant un type existant.

Une des applications possible des pointeurs de fonction est le passage d’une fonction comme
paramètres d’une autre fonction. Exemple :
#include <math.h>
#include <stdio.h>

typedef double (* MAFCT)(double);


void affiche(double x, MAFCT trigo);

void main(void)
{
double pi = 3.1415926535;
double x;

x = pi / 2;
affiche(x, sin);
affiche(x, sinh);
affiche(x, cos);
affiche(x, cosh);
}

void affiche(double x, MAFCT trigo)


{
double y;

y = trigo(x);
printf("result(%f) = %f\n", x, y);
}

On passe à la fonction affiche les fonctions sin, sinh, cos, cosh qui sont des
fonctions de la librairie standard qui acceptent un double en paramètre et retournent un double
et qui calculent respectivement (en radian) le sinus, le sinus hyperbolique, le cosinus et le
cosinus hyperbolique. Les résultats suivants sont obtenus :

result(1.570796) = 1.000000
result(1.570796) = 2.301299
result(1.570796) = 0.000000
result(1.570796) = 2.509178

181
Exercice 11.1 : on souhaite concevoir une fonction qui accepte en entrée une fonction
trigonométrique sous forme de chaîne de caractères (suite à une saisie au clavier par exemple)
et qui retourne un pointeur sur la fonction de la bibliothèque mathématique correspondante ou
bien NULL si elle n’existe pas. Vous utiliserez pour cela une structure qui contiendra une
chaîne de caractères pour stocker le nom de la fonction et un pointeur sur cette fonction.

Exercice 11.2 : expliquez à quoi correspondent les déclarations suivantes.

void fonction1(void);
int fonction2(double);
int *fonction3(char *);
int (*PtrFct1)(char);
int (*PtrFct2)(char *);
int *(*PtrFct3)(char *);
int *tab[10];
int (*tab)[10];
int (* TabFct[10])(char);

11.2 Notion de processus


Avec un système d’exploitation moderne, toute tâche en cours d’exécution est représentée par
un processus. On peut imaginer un processus comme un programme en cours d’exécution,
mais cette représentation est très imparfaite car un programme peut lancer plusieurs
processus. Un programme exécutable n'est qu'une suite d'octets totalement inerte stockée sur
disque. Il ne deviendra processus que lorsqu'il sera chargé en mémoire et pris en charge par le
système d'exploitation. Le scheduler, mécanisme de régulation des tâches du noyau du
système d’exploitation (en général multi-tâches) lui attribuera un état (en attente d’exécution,
en cours d’exécution, inactif, ...) selon l’activité générale de l’ordinateur. En effet, seul un
processus peut s’exécuter à la fois sur un ordinateur mono processeur. Le processus héritera
des droits de l’utilisateur qui l’aura lancé. Il héritera par exemple de droits :
• sur certains fichiers,
• sur une certaine quantité de mémoire physique de l’ordinateur pour pouvoir s’exécuter,
• de priorité vis à vis des autres processus actifs du système.

182
Il faut noter qu’en cas de lancement en tâche de fond (commande at sous Unix ou sous
Windows XP ; cette commande n’existe pas sous Windows 98), le processus est rattaché
directement au noyau et pas à la fenêtre de commande de l’utilisateur qui a servie à le lancer.
Cela permet à un utilisateur de lancer un programme puis de se déconnecter de son compte
afin de laisser l’ordinateur disponible pour d’autres utilisateurs. Le programme continue alors
à s’exécuter, ce qui est très pratique pour des traitements qui durent longtemps (plusieurs
jours, voir plusieurs semaines).

11.3 Les zones mémoires d’un processus


Grâce au mécanisme de la mémoire virtuelle, un processus dispose d’un espace linéaire
d’adressage de 4 Go sur un système d’exploitation 32 bits. Sous Linux, cet espace est séparée
en deux zones, le segment noyau (1 Go) et le segment utilisateur (3 Go).

4 Go
segment
kernel noyau du système d’exploitation noyau

env environnement 3 Go
stack variables locales (pile)

bibliothèques partagées

segment
heap variables allouées utilisateur
dynamiquement (tas)
bss variables globales non initialisées
variables statiques
data variables globales initialisées

text code exécutable


0

Le segment utilisateur est lui-même divisé en plusieurs sous-ensembles :


• Le segment text contient le code exécutable du processus.
• Le segment data contient les variables statiques ainsi que les variables globales
initialisées au chargement du processus.

183
• Le segment bss (block started by symbol) contient les variables globales non-initialisées.
• Le segment heap contient les variables allouées dynamiquement (via malloc). On
appelle cette zone le tas.
• Le segment stack contient les variables locales. On appelle cette zone la pile.
• Le segment env contient des données concernant l’environnement.

Il existe des relations étroites entre la durée de vie d’une variable, sa portée (variable locale ou
globale) et les différentes classes (segments) de mémoire. Du point de vue de la durée de vie,
il existe trois types de variables :

• Les variables statiques qui sont allouées au début de l’exécution du programme et ne sont
libérées qu’à la fin de son exécution. Une variable globale (c’est-à-dire déclarée en dehors
de toute fonction) est toujours statique (par définition puisqu’elle est créée au début de
l’exécution et libérée à la fin). Une variable locale peut être statique à condition de faire
précéder sa déclaration du mot clé static.

• les variables dynamiques qui sont allouées et libérées explicitement par le programmeur
grâce à des fonctions telles que malloc et free. Elles peuvent être locales ou bien
globales, cela dépend de l’endroit où est déclaré le pointeur.

• Les variables automatiques qui sont allouées à l’entrée de la fonction où elles ont été
déclarées et libérées lors de la sortie de cette fonction. Par définition, il s’agit de variables
locales (y compris les arguments de la fonction). Pour créer une variable automatique, il
suffit de faire précéder sa déclaration du mot clé auto. En pratique, personne ne le fait
puisque c’est le mode par défaut quand vous déclarez une variable locale.

Selon sa durée de vie, la variable se retrouvera :


1. dans la zone contenant les variables statiques (et globales) ; data et bss.
2. dans la zone contenant les variables dynamiques ; le tas.
3. dans la zone contenant les variables automatiques (locales) ; la pile.

Le programme Linux suivant illustre l’utilisation des différents emplacements de la mémoire.


Vous noterez que la limite basse de la pile diminue à mesure que l’on crée des variables.

184
#include <stdio.h>

int glob_init1[8] = {0, 0, 0, 0, 0, 0, 0, 0};


int glob_init2[8] = {0, 0, 0, 0, 0, 0, 0, 0};
int glob_non_init1[1024];
int glob_non_init2[1024];

main()
{
int auto_var1[1024];
int auto_var2[1024];
int *ptr1, *ptr2;
static int stat_var1[1024];
static int stat_var2[1024];

ptr1 = (int *)malloc(1024);


ptr2 = (int *)malloc(1024);

printf("auto_var1 = %8x, %10u\n", auto_var1, auto_var1);


printf("auto_var2 = %8x, %10u\n", auto_var2, auto_var2);
printf("ptr2 = %8x, %10u\n", ptr2, ptr2);
printf("ptr1 = %8x, %10u\n", ptr1, ptr1);
printf("glob_non_init2 = %8x, %10u\n", glob_non_init2, glob_non_init2);
printf("glob_non_init1 = %8x, %10u\n", glob_non_init1, glob_non_init1);
printf("stat_var2 = %8x, %10u\n", stat_var2, stat_var2);
printf("stat_var1 = %8x, %10u\n", stat_var1, stat_var1);
printf("glob_init2 = %8x, %10u\n", glob_init2, glob_init2);
printf("glob_init1 = %8x, %10u\n", glob_init1, glob_init1);
printf("adresse main = %8x, %10u\n", main, main);
printf("adresse printf = %8x, %10u\n", printf, printf);
}

Les adresses suivantes sont obtenues pour les différentes fonctions et variables :

auto_var1 = bfffe9ec, 3221219820


stack
auto_var2 = bfffd9ec, 3221215724
ptr2 = 804dcd0, 134536400 heap
ptr1 = 804d8c8, 134535368
glob_non_init2 = 804b8c0, 134527168 bss
glob_non_init1 = 804c8c0, 134531264
stat_var2 = 804a8c0, 134523072
stat_var1 = 80498c0, 134518976 data
glob_init2 = 80497a0, 134518688
glob_init1 = 8049780, 134518656
adresse main = 8048424, 134513700 text
adresse printf = 8048344, 134513476

Un problème rencontré fréquemment en programmation est la saturation de la pile (stack


overflow). Elle se produit quand vous essayez de créer un tableau local de grande taille sans
utiliser l’allocation dynamique de la mémoire. En effet, la valeur par défaut de la taille de la

185
pile est assez faible : 1 Mo avec Visual C++ et 8 Mo avec gcc. Cela veut dire par exemple que
le programme suivant plante avec Visual C++ sous Windows.
#include <stdio.h>

void main()
{
int tab[500000]; /* demande 2 Mo */

printf("coucou\n");
}

Pour corriger ce problème, il faut modifier la taille de la pile avec par exemple sous Linux, la
commande « ulimit -s 32768 » pour passer la taille de la pile à 32 Mo (sous Visual
C++, c’est une option du projet).

11.4 Projets multi-fichiers : édition de liens


Dès que l’on développe un programme d’une taille un peu importante (ce qui est
généralement le cas dans un projet réel), il devient nécessaire d’utiliser plusieurs fichiers
source. Il y a au moins deux raisons pour cela :
1. Il est plus facile de chercher des informations et de modifier des fonctions dans plusieurs
fichiers source de taille raisonnable (quelques centaines de lignes au maximum) que dans
un seul gros fichier.
2. En cas de modification, il est plus rapide de ne compiler qu’un seul petit fichier (celui où a
eu lieu la modification) qu’un gros fichier qui contient tout le programme.

Chaque fichier est compilé séparément, puis les fichiers objets obtenus sont réunis pour
former un exécutable à l’aide de l’éditeur de liens (linker).

DUTmain.c DUTmain.o

DUTfct1.c compilateur DUTfct1.o linker dut.exe

DUTfct2.c DUTfct2.o

Pour réaliser ce genre de projet, il faut pouvoir dire au compilateur que certaines variables ou
fonctions sont définies dans un autre fichier source. Pour cela, deux méthodes existent :

186
l’utilisation du mot clé extern ou bien l’utilisation d’un fichier d’entête (.h) général qui
contiendra toutes les définitions globales. C’est la deuxième méthode que nous utiliserons.
Notre projet multi-fichiers sera donc constitué :
• d’un fichier source qui ne contiendra que la fonction main.
• d’un fichier source par famille de fonctions.
• d’un fichier d’entête (ou plusieurs si nécessaire) qui contiendra toutes les déclarations
globales (structures, prototypes de fonction, macros, ...). Il sera inclus au début de tous les
fichiers sources.

DUTstd.h

DUTmain.c DUTmain.o

DUTfct1.c compilateur DUTfct1.o linker dut.exe

DUTfct2.c DUTfct2.o

Les variables globales seront déclarées dans un des fichiers source et déclarées comme
extern dans le fichier d’entête.

Penchons-nous maintenant un peu sur le contenu des fichiers objets et exécutables ainsi que
sur le travail effectué par l’éditeur de lien. Il existe deux grands formats de fichiers objets (et
exécutables) en informatique : le format COFF « Common Object File Format » et le format
ELF « Executable and Linking Format ». Le format COFF est le format historique d’Unix
(avec le format a.out). Il a été repris par Microsoft et est toujours utilisé sous Windows
sous une forme évoluée (format PE « Portable Executable »). Le format ELF a remplacé le
format COFF sous Unix et est devenu le format standard sous Linux.

Un fichier objet (extension .o sous Linux, .obj sous Windows) contient les instructions en
langage machine correspondant au source et des données destinées à l’éditeur de lien afin que
celui-ci puisse créer un exécutable. Chaque fonction ou chaque variable définie dans un
fichier objet est référencée par un nom de symbole. Ces symboles peuvent avoir une
définition locale (utilisation à l’intérieur du fichier seulement) ou globale (utilisation à

187
l’extérieur du fichier). Toute référence à un symbole qui n’est pas défini dans le fichier objet
(un appel à une fonction définie dans un autre fichier par exemple) est appelée une référence
externe. Pour garder la trace des différents symboles internes ou externes, un fichier objet
contient une table des symboles. L’éditeur de lien utilise cette table pour relier les références
externes avec les définitions globales des symboles. Voici de manière très schématique la
structure d’un fichier objet ou exécutable :

entête fichier
section text
section data
section bss
table des
symboles

On retrouve la section text qui contient le code exécutable de la fonction, la section data
qui contient les variables statiques (et les variables globales initialisées) et enfin la section
bss qui contient les variables globales non-initialisées. Le but de l’édition de lien va être de
fusionner les sections de chaque fichier objet afin de créer un fichier exécutable.

entête fichier
section text
section data
section bss entête fichier
table des
symboles section text
essai1.o section data
section bss
table des
entête fichier symboles
section text essai
section data
section bss
table des
symboles
essai2.o

188
Pour cela, il va falloir relier les références externes avec les définitions globales contenues
dans d’autres fichiers objets (c’est la résolution de symbole) et changer les adresses du code
et des données (c’est le relogement du code ou code relocation en anglais). Prenons un
exemple simple. Le programme essai comprend un fichier source ESSmain.c :

#include <stdio.h>

extern void affiche(char *);

main()
{
static char str[] = "coucou\n";

affiche(str);
}

qui appelle une fonction affiche contenue dans un autre fichier source ESSfct.c :

void affiche(char *s)


{
printf(s);
}

Sous Linux, la compilation via la commande cc -c *.c donne deux objets ESSmain.o et
ESSfct.o. Grâce au programme objdump, nous pouvons visualiser le contenu de certaines
parties d’un objet, à savoir les entêtes de section (option h), la table des symboles (option t) et
le code machine désassemblé (option d). La commande « objdump -dth ESSmain.o »
donne le résultat suivant :

ESSmain.o: file format elf32-i386

Sections:
Idx Name Size VMA LMA File off Algn
0 .text 0000001a 00000000 00000000 00000034 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000008 00000000 00000000 00000050 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 00000000 00000000 00000058 2**2
ALLOC
3 .note 00000014 00000000 00000000 00000058 2**0
CONTENTS, READONLY
4 .comment 00000029 00000000 00000000 0000006c 2**0
CONTENTS, READONLY

189
SYMBOL TABLE:
00000000 l df *ABS* 00000000 ESSmain.c
00000000 l d .text 00000000
00000000 l d .data 00000000
00000000 l d .bss 00000000
00000000 l .text 00000000 gcc2_compiled.
00000000 l O .data 00000008 str.3
00000000 l d .note 00000000
00000000 l d .comment 00000000
00000000 g F .text 0000001a main
00000000 *UND* 00000000 affiche

Disassembly of section .text:

00000000 <main>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 83 ec 08 sub $0x8,%esp
6: 83 c4 f4 add $0xfffffff4,%esp
9: 68 00 00 00 00 push $0x0
e: e8 fc ff ff ff call f <main+0xf>
13: 83 c4 10 add $0x10,%esp
16: 89 ec mov %ebp,%esp
18: 5d pop %ebp
19: c3 ret

Le fichier objet est au format ELF 32 bits Intel 386, sa section .text comporte 26 octets
(taille du code machine) et sa section .data en compte 8 (taille de la chaîne str avec le \0).
On voit dans la table des symboles que str est définie (dans .data), que main est définie
(dans .text) mais qu'affiche ne l’est pas. Par ailleurs, les adresses 32 bits dans les
entêtes de section et dans la table des symboles sont toutes à 0. Dans le code assembleur de la
fonction main, on note que l’appel à la fonction affiche (c’est le call) est non résolu
(l’adresse est fausse) et que le push qui précède ce call est à 0. Ce push correspond à la mise
sur la pile du paramètre passé à la fonction, c’est-à-dire str dans notre exemple. En général,
les paramètres d’une fonction sont toujours passés par la pile. Dans notre exemple, str ne se
trouve bien entendu pas à l’adresse 0, mais on ne connaît pas encore son adresse. La
commande « objdump -dth ESSfct.o » donne le résultat suivant :

ESSfct.o: file format elf32-i386

Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000019 00000000 00000000 00000034 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE

190
1 .data 00000000 00000000 00000000 00000050 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 00000000 00000000 00000050 2**2
ALLOC
3 .note 00000014 00000000 00000000 00000050 2**0
CONTENTS, READONLY
4 .comment 00000029 00000000 00000000 00000064 2**0
CONTENTS, READONLY

SYMBOL TABLE:
00000000 l df *ABS* 00000000 ESSfct.c
00000000 l d .text 00000000
00000000 l d .data 00000000
00000000 l d .bss 00000000
00000000 l .text 00000000 gcc2_compiled.
00000000 l d .note 00000000
00000000 l d .comment 00000000
00000000 g F .text 00000019 affiche
00000000 *UND* 00000000 printf

Disassembly of section .text:

00000000 <affiche>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 83 ec 08 sub $0x8,%esp
6: 83 c4 f4 add $0xfffffff4,%esp
9: 8b 45 08 mov 0x8(%ebp),%eax
c: 50 push %eax
d: e8 fc ff ff ff call e <affiche+0xe>
12: 83 c4 10 add $0x10,%esp
15: 89 ec mov %ebp,%esp
17: 5d pop %ebp
18: c3 ret

On retrouve le même format d’objet, la taille du code est de 25 octets mais la section .data
est vide. La fonction affiche est définie dans le fichier objet, mais pas la fonction
printf. Dans le code assembleur de la fonction, on note l’appel à printf ainsi que le passage
de paramètre qui le précède.

L’éditeur de lien doit combiner les deux objets avec une fonction d’initialisation (_start) et
les routines de la librairie standard du C pour produire l’exécutable essai (cc *.c -o
essai). La commande « objdump -dth essai» donne le résultat suivant :

191
essai: file format elf32-i386

Sections:
Idx Name Size VMA LMA File off Algn
0 .interp 00000013 080480f4 080480f4 000000f4 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
...
11 .text 00000134 08048320 08048320 00000320 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
...
14 .data 00000014 08049478 08049478 00000478 2**2
CONTENTS, ALLOC, LOAD, DATA
...
20 .bss 00000018 08049560 08049560 00000560 2**2
ALLOC
...

SYMBOL TABLE:
080480f4 l d .interp 00000000
...
08048400 l .text 00000000 gcc2_compiled.
08049484 l O .data 00000008 str.3
080483e4 g F .text 00000019 affiche
080494c0 g O .dynamic 00000000 _DYNAMIC
08048454 g O *ABS* 00000000 _etext
08048298 g F .init 00000000 _init
08048320 g .text 00000000 _start
08049560 g O *ABS* 00000000 __bss_start
08048400 g F .text 0000001a main
08049478 w .data 00000000 data_start
...

Disassembly of section .text:

08048320 <_start>:
8048320: 31 ed xor %ebp,%ebp
8048322: 5e pop %esi
8048323: 89 e1 mov %esp,%ecx
8048325: 83 e4 f8 and $0xfffffff8,%esp
8048328: 50 push %eax
8048329: 54 push %esp
804832a: 52 push %edx
804832b: 68 54 84 04 08 push $0x8048454
8048330: 68 98 82 04 08 push $0x8048298
8048335: 51 push %ecx
8048336: 56 push %esi
8048337: 68 00 84 04 08 push $0x8048400
804833c: e8 bb ff ff ff call 80482fc <_init+0x64>
8048341: f4 hlt
...

192
080483e4 <affiche>:
80483e4: 55 push %ebp
80483e5: 89 e5 mov %esp,%ebp
80483e7: 83 ec 08 sub $0x8,%esp
80483ea: 83 c4 f4 add $0xfffffff4,%esp
80483ed: 8b 45 08 mov 0x8(%ebp),%eax
80483f0: 50 push %eax
80483f1: e8 16 ff ff ff call 804830c <_init+0x74>
80483f6: 83 c4 10 add $0x10,%esp
80483f9: 89 ec mov %ebp,%esp
80483fb: 5d pop %ebp
80483fc: c3 ret

08048400 <main>:
8048400: 55 push %ebp
8048401: 89 e5 mov %esp,%ebp
8048403: 83 ec 08 sub $0x8,%esp
8048406: 83 c4 f4 add $0xfffffff4,%esp
8048409: 68 84 94 04 08 push $0x8049484
804840e: e8 d1 ff ff ff call 80483e4 <affiche>
8048413: 83 c4 10 add $0x10,%esp
8048416: 89 ec mov %ebp,%esp
8048418: 5d pop %ebp
8048419: c3 ret
...

L’éditeur de liens a donc fusionné les sections .text, .data et .bss des différents objets.
Par exemple, dans la section .text, les codes machines des différentes fonctions ont été
mises les uns à la suite des autres, ce qui implique que toutes les adresses ont été modifiées :
le code a été relogé. L’adresse de la fonction affiche est maintenant connue et l’appel dans
la fonction main se fait à cette adresse. D’autre part, l’adresse de str est correctement
passée en paramètre. Un lien a donc été créé entre les références externes et les définitions
globales des symboles.

Pour exécuter ce programme, le système d’exploitation doit maintenant charger chacune des
trois sections du fichier exécutable dans la mémoire virtuelle du processus à l’adresse fixe
contenue dans le fichier puis lancer le programme au début de la fonction _start. Cette
opération appelée « program loading » est réalisée par le loader (sous Linux, il s’agit de la
fonction système exec()). La fonction d’initialisation _start sert, entre autres, à
récupérer les paramètres se trouvant sur la ligne de commande et à les passer à la fonction
main.

193
Il est à noter que le fichier exécutable contient un certain nombre d’informations qui ne sont
pas nécessaires à son bon fonctionnement (comme la table des symboles par exemple) mais
qui servent seulement pour la mise au point. Ces informations peuvent être supprimées grâce
au programme strip (strip -s essai) afin de réduire la taille du fichier.

194
12. Les librairies

12.1 Les bibliothèques statiques (archive)


Une évolution naturelle de la programmation multi-fichiers a été de regrouper toutes les
fonctions d’un programme (sauf la fonction main) dans une archive que l’on a appelé une
bibliothèque (library en anglais). Il s’agit un fait d’une autre forme de fichier objet (un fichier
qui contient plusieurs fichiers objets) dont le format est connu de l’éditeur de lien mais qui ne
contient aucune information supplémentaire. Sous Linux, l’extension d’une bibliothèque de
type archive est .a pour archive (extension .lib sous Windows). La création de la
bibliothèque se déroule de la manière suivante :
1. création des fichiers objets à l’aide de la commande « cc -c *.c ».
2. création de l’archive à l’aide du programme ar, « ar -rv libDUT.a DUTfct1.o
DUTfct2.o ».

DUTstd.h

DUTfct1.c DUTfct1.o
compilateur ar libDUT.a
DUTfct2.c DUTfct2.o

Pour utiliser la bibliothèque ainsi créée, il suffit de dire à l’éditeur de lien de l’utiliser.

cc DUTmain.o libDUT.a -o dut.exe

Vous noterez que la commande cc inclut la phase d’édition de lien (en réalité, le linker
s’appelle ld). En ne désignant au compilateur que des objets, il procède automatiquement à
l’édition de lien.

195
libDUT.a
compilateur dut.exe
DUTmain.o (linker)

Du point de vue de l’édition de lien, c’est-à-dire de la résolution des symboles et du


relogement du code, l’utilisation d’une bibliothèque archive est strictement équivalente à la
solution sans bibliothèque. Il n’y aura pas une ligne de code générée en plus. Il est important
de noter que le code machine des fonction utilisées sera incorporé dans le fichier exécutable
mais pas le code machine des fonctions que l’on n’utilise pas. On dit que cette bibliothèque
est statique.

L’utilisation de la bibliothèque standard du C est automatique lorsque vous compilez un


programme. C’est la libc qui contient le code des fonctions printf, malloc, fopen, ...
Par contre, sous Linux, l’utilisation de la bibliothèque mathématique libm doit être spécifiée.
Il existe pour cela une convention. Comme tous les noms de bibliothèque commencent par
lib, il suffit de passer l’option -lfin pour utiliser la bibliothèque libfin. Par exemple, la
commande suivante indique l’utilisation de la libm.

cc essai.c -o essai -lm

La commande ar -t libDUT.a permet de connaître les noms des fichiers objets inclus
dans l’archive mais pas de connaître le nom des fonctions qui se trouvent dans les objets. Pour
cela, il faut utiliser objdump associé au programme grep qui recherche une chaîne de
caractères dans un fichier texte.

objdump -t /usr/lib/libc.a|grep fgets

12.2 Les bibliothèques dynamiques (partagées)


Un inconvénient important des bibliothèques statiques est que le code machine des fonctions
est incorporé dans chaque fichier exécutable qui les utilise. Par exemple, cela veut dire que le
code de la fonction printf de la libc va être copié autant de fois qu’il y aura de
programmes qui l’utilise dans chaque espace d’adressage virtuel. Cela conduit à un gaspillage
important de ressources mémoire.

196
pile pile pile

bibliothèques partagées bibliothèques partagées bibliothèques partagées

tas tas tas

bss bss bss


data data data

printf printf printf


text prog1 text prog2 text prog3

Le partage du code permet à plusieurs processus de partager des portions de code machine
communes. Le code commun est géré directement par le système d’exploitation et se trouve
en dehors de l’espace d’adressage virtuel des processus. Par exemple, dans le cas de la libc,
chaque appel à la fonction printf dans prog1, prog2 et prog3 va pointer sur une adresse
dans la zone des bibliothèques partagées, adresse qui pointe en réalité sur la bibliothèque
partagée.

pile

bibliothèque
bibliothèques partagée libc.so
partagées
printf

tas

bss
data
text prog1

pile pile

bibliothèques bibliothèques
partagées partagées

tas tas

bss bss
data data
text prog2 text prog3

197
L’utilisation de bibliothèques partagées a des conséquences importantes :
1. Toutes les bibliothèques partagées ne peuvent être chargées en mémoire en permanence.
Le chargement se fait de manière dynamique lors du premier appel à la bibliothèque (d’où
son nom).
2. La résolution des symboles et donc le calcul des adresses ne peuvent plus être effectués au
moment de l’édition de lien puisque l’on ne connaît pas l’adresse exacte de la fonction. Il
faut donc modifier le source du programme (par rapport à l’utilisation traditionnelle d’une
librairie statique) et utiliser une procédure spéciale pour récupérer l’adresse de la fonction
et la copier dans un pointeur de fonction. Il existe cependant un mode simplifié appelé
édition de lien implicite qui évite cette modification du code source.
3. Selon les bibliothèques déjà chargées en mémoire, l’adresse de chargement d’une
bibliothèque varie avec le temps et n’est jamais la même. Cela veut dire que le code
machine des fonctions doit obligatoirement être indépendant de la position en mémoire.
On appelle ce type de code du PIC (Position Independent Code).

Reprenons maintenant l’exemple du programme essai. Il était composé du fichier ESSfct.c qui
contenait la fonction affiche() et du fichier ESSmain.c qui contenait la fonction main()
qui appelait affiche(). Nous allons créer une bibliothèque dynamique libESS.so qui
contiendra affiche() puis nous l’appellerons depuis main(). Le contenu de ESSfct.c n’a
pas changé :

void affiche(char *s)


{
printf(s);
}

Il faut compiler ce fichier de façon à obtenir un objet relogeable :

cc -c ESSfct.c -fPIC

Le contenu de cet objet ESSfct.o est identique à celui du §11.4. Nous pouvons maintenant
créer la bibliothèque partagée libESS.so (.so pour shared object) :

cc -shared -o libESS.so ESSfct.o

198
Seule l’option -shared change par rapport à la création d’un exécutable « normal » (et
l’absence de la fonction main() bien sur). Il existe deux méthodes permettant d’utiliser une
librairie dynamique :

• L’édition de lien implicite (ou Shared library sous Unix). C’est la méthode la plus simple.
La librairie est chargée automatiquement au lancement de l’exécutable. C’est le mode par
défaut sous Linux quand vous développez un programme. Il n’y a rien de spécial à écrire
dans le programme qui utilise la librairie. Sous Windows, il est nécessaire d’utiliser une
libraire d’importation (voir projet).

• L’édition de lien explicite (ou Dynamically Loaded Library sous Unix). C’est une
méthode plus compliquée. La librairie est chargée dynamiquement pendant l’exécution du
programme, à la demande du programmeur. Il faut modifier ESSmain.c de la façon
suivante :

#include <stdio.h>
#include <dlfcn.h>

main()
{
static char str[] = "coucou\n";
void *handle;
void (*affiche_dyn)(char *);

handle = dlopen("/root/format/libESS.so", RTLD_LAZY);


affiche_dyn = dlsym(handle, "affiche");

affiche_dyn(str);
dlclose(handle);
}

La fonction dlopen() permet de charger la bibliothèque en mémoire si nécessaire et


retourne son adresse dans le pointeur handle (en anglais, un handle est une variable
qui identifie un objet. Ici, il s’agit seulement du nom du pointeur). La fonction dlsym()
retourne l’adresse de la fonction affiche() de la bibliothèque dans le pointeur de
fonction affiche_dyn. Celui-ci peut alors être utilisé à la place de la fonction
affiche() puisqu’il pointe dessus. dlclose() décharge la bibliothèque de la
mémoire s’il n’y a pas d’autre programme qui l’utilise. En fait, il y a un compteur qui
s’incrémente à chaque fois qu’un programme charge la librairie avec dlopen() et qui se

199
décrémente quand un programme décharge la libraire avec dlclose(). La compilation
du source (libdl contient les fonctions dlopen, dlsym et dlclose) se fait avec :

cc -rdynamic ESSmain.c -o essai -ldl

Avec le programme objdump, on peut voir que le code de la fonction affiche() ne se


trouve plus dans l’exécutable essai. Il n’y a pas de symbole non résolu pendant l’édition de
liens puisqu’il n’y a pas d’appel direct à affiche(). Il n’y a plus qu’un pointeur de
fonction affiche_dyn qui récupère l’adresse de affiche() en cours d’exécution du
programme.

12.3 Avantages et inconvénients des bibliothèques dynamiques


Les bibliothèques dynamiques ont la réputation d’être plus « modernes » que les
bibliothèques statiques. Sous Windows notamment, l’utilisation des DLL (Dynamic Link
Library) est systématique et la plupart des développeurs ignore même qu’il existe une autre
méthode. Il est cependant important de bien comprendre les avantages et les inconvénients
des bibliothèques dynamiques sur les points suivants :

1. taille de l’exécutable.
• Avantage : La taille d’un exécutable utilisant des bibliothèques dynamiques est
beaucoup plus petite que celle d’un exécutable lié statiquement. Sous Linux, Vous
pouvez essayez de compiler un programme avec l’option -static pour forcer
l’utilisation des bibliothèques standards en statique (un programme avec un simple
printf suffit). Le résultat est assez spectaculaire (une dizaine de ko contre 1Mo
environ). Il y a donc un gain en espace de stockage.
• Inconvénient. Aucun.

2. temps de chargement de l’exécutable.


• Avantage / Inconvénient. L’exécutable statique étant plus gros, son chargement en
mémoire doit prendre plus de temps que le chargement d’un exécutable dynamique.
Mais dans ce dernier cas, il faut aussi compter le temps de chargement des bibliothèques
dynamiques si elles ne sont pas déjà chargées en mémoire. Il n’est pas évident de
déterminer laquelle des deux méthodes est la plus rapide.

200
3. Partage du code.
• Avantage : plusieurs programmes peuvent utiliser la même bibliothèque chargée en
mémoire. Le code n’est pas dupliqué dans chaque segment text, ce qui réduit
l’utilisation de la mémoire de l’ordinateur.
• Inconvénient : toutes les fonctions de la bibliothèque sont chargées en mémoire alors
qu’en statique, seul le code des fonctions utilisées est copié dans l’exécutable. Si toutes
les fonctions d’une bibliothèque dynamique sont utilisées par plusieurs programmes, il
y a un gain. Si un seul programme n’utilise qu’une partie de la bibliothèque dynamique,
il peut y avoir perte.

4. Evolution du programme sans recompilation.


• Avantage : quand les fonctions de la bibliothèque dynamique évoluent, les programmes
qui les utilisent évoluent aussi sans qu’il soit nécessaire de refaire une édition de lien.
Avec une bibliothèque statique, l’édition de lien est obligatoire pour copier le nouveau
code machine dans l’exécutable. Pour faire évoluer une application chez un client, un
fournisseur de programme a juste à livrer de nouvelles versions de ses bibliothèques
dynamiques. Le client les installe et son programme obtient automatiquement de
nouvelles fonctionnalités ou bien des corrections de défauts.
• Inconvénient : le programme n’est plus autonome, il a besoin des bibliothèques
dynamiques pour fonctionner. S’il en manque une, le système d’exploitation génère une
erreur. Si plusieurs programmes livrés par des fournisseurs différents utilisent la même
bibliothèque dynamique, toute évolution de cette bibliothèque aura des conséquences
sur ces programmes. Il y a un gros problème (notamment sous Windows) de version de
DLL et d’incompatibilité entre les versions de programmes et les versions de DLL.
Chaque nouveau programme installe sa version de DLL qui provoque des
incompatibilités avec des programmes déjà installés sur l’ordinateur. En statique, les
exécutables sont autonomes et ne dépendent d’aucune bibliothèque externe.

5. Accès depuis d’autres langages.


• Avantage / Inconvénient. Une bibliothèque statique ou dynamique développée en
langage C peut être utilisée avec des programmes écrits dans d’autres langages. Il faut
juste respecter la convention d’appel (telle que C, Pascal ou bien standard) qui définit la

201
méthode de passage de paramètres. Un programme en C peut aussi utiliser une
bibliothèque développée dans un autre langage.

12.4 La bibliothèque standard du C


La bibliothèque standard contient toutes les fonctions normalisées en ANSI C. Nous allons
voir les plus importantes dans ce document. Commençons par les fonctions d’entrées-sorties.

12.4.1 Les entrées-sorties <stdio.h>


Opérations sur les fichiers :
fonction description
remove destruction de fichier
rename modification de nom de fichier
tmpfile création d’un fichier temporaire
tmpnam génération de nom pour un fichier temporaire

Accès aux fichiers :


fclose fermeture de fichier (flux)
fflush vidage des buffers dans un fichier
fopen ouverture d’un fichier
freopen ouverture d’un fichier

Entrées-sorties formatées :
printf écriture formatée sur la sortie standard
scanf lecture formatée sur l’entrée standard
fprintf écriture formatée dans un fichier
fscanf lecture formatée depuis un fichier
sprintf écriture formatée dans une chaîne de caractères
sscanf lecture formatée depuis une chaîne de caractères

Entrées-sorties caractères :
fgetc lecture d’un caractère dans un fichier
fputc écriture d’un caractère dans un fichier
getc lecture d’un caractère sur l’entrée standard ( ne pas utiliser, c’est une
macro. Il y a des effets de bord )
getchar lecture d’un caractère sur l’entrée standard
putc écriture d’un caractère sur la sortie standard ( ne pas utiliser, c’est une
macro. Il y a des effets de bord)

202
putchar écriture d’un caractère sur la sortie standard
ungetc annule la lecture d’un caractère dans un flux.
fgets lecture d’une chaîne de caractères dans un fichier
fputs écriture d’une chaîne de caractères dans un fichier
gets lecture d’une chaîne de caractères sur l’entrée standard
puts écriture d’une chaîne de caractères sur la sortie standard

Entrées-sorties binaires :
fread lecture d’un groupe d’octets dans un fichier
fwrite écriture d’un groupe d’octets dans un fichier

Positionnement dans un fichier :


ftell indique la position courante dans un fichier
fseek permet de se positionner dans un fichier
rewind permet de se positionner au début d’un fichier
fgetpos indique la position courante dans un fichier
fsetpos permet de se positionner dans un fichier

12.4.2 Les fonctions mathématiques <math.h>


Fonctions trigonométriques et hyperboliques :
fonction description
acos arc cosinus
asin arc sinus
atan arc tangente
atan2 arc tangente
cos cosinus
cosh cosinus hyperbolique
sin sinus
sinh sinus hyperbolique
tan tangente
tanh tangente hyperbolique

Fonctions exponentielles et logarithmiques :


exp exponentielle
frexp étant donné x, trouve n et p tels que x = n * 2p
ldexp multiplie un nombre par une puissance entière de 2
log logarithme népérien

203
log10 logarithme en base 10
modf calcule la partie entière et la partie décimale d’un flottant

Fonctions diverses :
ceil arrondi à l’entier le plus proche par valeur supérieure
floor arrondi à l’entier le plus proche par valeur inférieure
fabs valeur absolue
fmod reste de la division
pow puissance xy
sqrt racine carrée

12.4.3 Les manipulations de caractères <ctype.h>


fonction description
isalnum teste si le caractère est une lettre ou un chiffre
isalpha teste si le caractère est une lettre
isctnrl teste si le caractère est caractère de contrôle
isdigit teste si le caractère est un chiffre
isgraph teste si le caractère est imprimable
islower teste si le caractère est une lettre minuscule
isprint teste si le caractère est imprimable
ispunct teste si le caractère est une ponctuation
isspace teste si le caractère est un espace
isupper teste si le caractère est une lettre majuscule
isxdigit teste si le caractère est un chiffre hexadécimal
tolower convertit une lettre en minuscule
toupper convertit une lettre en majuscule

12.4.4 Les manipulations de chaînes <string.h>


fonction description
memcpy copie de n caractères d’une chaîne vers une autre
memmove déplacement de n caractères d’une chaîne vers une autre
strcpy copie d’une chaîne dans une autre
strncpy copie de n caractères d’une chaîne dans une autre
strcat concaténation de deux chaînes
strncat concaténation de n caractères d’une chaîne avec une autre
memcmp comparaison de n caractères entre deux chaînes

204
strcmp comparaison de deux chaînes
strncmp comparaison de n caractères entre deux chaînes
strcoll comparaison de deux chaînes
strxfrm transformation de chaîne
memchr recherche d’un caractère dans une chaîne
strchr recherche d’un caractère dans une chaîne
strcspn recherche d’une chaîne dans une autre chaîne
strpbrk recherche d’une liste de caractères dans une chaîne
strrchr recherche d’un caractère dans une chaîne (par la fin)
strspn recherche d’une chaîne dans une autre chaîne
strstr recherche d’une chaîne dans une autre chaîne
strtok décomposition d’une chaîne en sous chaîne
memset remplissage d’une chaîne avec un caractère
strlen calcul de la longueur d’une chaîne

12.4.5 Manipulations de l’heure <time.h>


fonction description
clock calcul du temps d’exécution du programme
difftime calcul de la différence entre deux heures
mktime conversion d’heure
time retourne l’heure courante
asctime conversion d’heure
ctime conversion d’heure
gmtime conversion d’heure
localtime conversion d’heure
strftime formatage de l’heure

12.4.6 Diverses fonctions utilitaires <stdlib.h>


Conversions de nombres :
fonction description
atof conversion d’une chaîne en nombre flottant
atoi conversion d’une chaîne en nombre entier
atol conversion d’une chaîne en nombre entier long
strtod conversion d’une chaîne en nombre flottant
strtol conversion d’une chaîne en nombre entier long
strtoul conversion d’une chaîne en nombre entier long non signé

205
Génération de nombres pseudo-aléatoires :
rand génération d’un nombre pseudo aléatoire
srand initialisation du générateur pseudo aléatoire

Gestion de la mémoire :
malloc allocation d’une zone mémoire
calloc allocation d’une zone mémoire mise à 0
free libération d’une zone mémoire allouée
realloc changement de taille d’une zone mémoire allouée

Communication avec l’environnement :


abort arrêt anormal d’un programme
system exécution d’une commande système
getenv obtention d’une variable d’environnement
atexit définition d’une fonction exécutée en cas de sortie normale du programme

Recherche et tri :
bsearch recherche d’un élément dans un tableau
qsort tri d’un tableau par ordre croissant

Arithmétique sur entier :


div calcul du quotient et du reste d’une division entière
abs calcul de la valeur absolue d’un entier
labs calcul de la valeur absolue d’un entier long

206
13. Introduction à la programmation en C++

13.1 Généralités
Le langage C++ est une évolution du langage C et apporte en particulier la notion de
Programmation Orienté Objet (POO). Le premier langage orienté POO a été conçu en 1967
par une équipe de chercheurs norvégiens. Il s’agissait de Simula. Ce langage avait été conçu
en vue de simulation de systèmes physiques et en particulier pour la recherche nucléaire. Tout
comme le C, le C++ a été conçu au Bell Labs. Bjarne Stroustrup, son concepteur, souhaitait
ajouter au C les notions de classes introduites par Simula. La première version stable voit le
jour en 1983, avant de trouver plus récemment sa forme définitive au travers d’une norme.

13.2 Intérêt de la conception objet


L’intérêt d’une conception orientée objet est mis en évidence par l’examen des différentes
contraintes imposées à un logiciel tout au long du cycle de vie, qui va de la conception à la
maintenance avec pour objectif de produire un logiciel de qualité.

Les critères à prendre en compte pour apprécier la qualité d’un logiciel sont :
• la validité du logiciel : le logiciel effectue les tâches pour lesquelles il a été conçu.
• l’extensibilité : le logiciel peut intégrer de nouvelles fonctionnalités demandées par
l’utilisateur.
• la ré-utilisabilité : le logiciel doit pouvoir être réutilisable complètement ou en partie.
• la robustesse : C’est la capacité du logiciel à fonctionner dans des situations non prévues, à
détecter ces situations.

Il faut également noter que la modularité est un facteur essentiel permettant de parvenir à
produire un logiciel de qualité et à faciliter la réutilisation de tout ou partie du code
développé. Le développement d’un programme informatique peut se résumer par la relation
suivante :

Algorithmes + Structure de données = programme.

Le développeur peut opter pour une architecture de son application, soit basée sur les données
soit sur les traitements.

207
Dans les méthodes de programmation organisées autour des traitements, l’ajout de nouvelles
fonctionnalités notablement différentes de ce qui était envisagé initialement pourra s’avérer
difficile. Par contre, les données sont beaucoup plus stables dans le temps et il peut apparaître
plus sain de baser sa conception autour de la représentation des données.

Les considérations liées à l’évolutivité, la ré-utilisabilité du logiciel, à sa modularité


conduisent vers des programmations orientées objet. B.Meyer [5] donne la définition suivante
de la conception orientée objet :

« La conception par objet est la méthode qui conduit à des architectures logicielles fondées
sur les objets que tout système ou tout sous-système manipule »

« Ne commencez pas par demander ce que fait le système, demandez A QUOI il le fait »

13.3 Un exemple de programmation classique


Nous allons illustrer l’intérêt de la programmation orientée objet en C++ par un exemple que
nous commencerons par traiter en C. Nous considérons dans cet exemple un rectangle. Pour
un néophyte en programmation, il apparaît assez évident de considérer que le rectangle est un
objet, possédant certaines caractéristiques (couleur, taille,…) et sur lequel on peut agir. On
peut par exemple le déplacer, changer sa couleur, le faire tourner. Si maintenant nous confions
cet objet à un programmeur qui ne pratique pas la programmation objet, nous allons obtenir
un ensemble de variables et de fonctions, qui vont rendre fastidieuses toutes les opérations
que le néophyte considère pourtant comme élémentaires.

Dans un premier temps, nous allons définir les caractéristiques de notre rectangle et voir ce
qu’un programmeur en C peut en faire. On suppose qu’un rectangle est représenté par un
certain nombre de pixels de couleur, chaque couleur étant codée en RGB, avec 1 octet pour
chacune des composantes RGB. Pour définir un rectangle, il faut donc définir :
• Sa taille : largeur, hauteur
• Sa position : par exemple, les coordonnées du point en bas à gauche
• Un tableau bidimensionnel contenant tous les pixels.

208
Au cours de la « manipulation informatique » de ce rectangle, on peut être amené à faire un
certain nombre de modifications, plus ou moins compliquées. On se bornera ici à considérer 2
actions basiques :
• Changement de position du rectangle,
• Changement de couleur du rectangle.

En programmant de façon élémentaire, on aboutit à un programme organisé de la façon


suivante :
• Déclaration des variables définissant taille, position, couleur, pointeur sur un tableau bi-
dimensionnel,
• Initialisation de la taille, de la position, de la couleur,
• Allocation des pointeurs du tableau bidimensionnel,
• Initialisation des pixels avec la couleur définie,
• Changement de position du triangle,
• Changement de couleur des pixels du rectangle.

Le code obtenu peut par exemple être celui-ci :

#include <malloc.h>

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


{
unsigned int XPosition , YPosition ; // Position du triangle
unsigned int XTaille , YTaille ; // Taille du triangle
unsigned char RComp ; // Codage de la composante R de la couleur
unsigned char GComp ; // Codage de la composante G de la couleur
unsigned char BComp ; // Codage de la composante B de la couleur
unsigned char ** PixelR ; // Tableau bidimensionnel pour les pixels R
unsigned char ** PixelG ; // Tableau bidimensionnel pour les pixels G
unsigned char ** PixelB ; // Tableau bidimensionnel pour les pixels B

unsigned int CbLigne ; // Compteur sur les lignes du rectangle


unsigned int CbColonne ; // Compteur sur les colonnes du rectangle

// Initialisation de la taille du rectangle


XTaille= 200 ;
YTaille= 50;
//Initialisation de la position du rectangle
XPosition=0;
YPosition=150;

// Allocation des pointeurs pour PixelR


PixelR = (unsigned char **)malloc(YTaille*sizeof(char *));
for(CbLigne=0 ; CbLigne<YTaille ; CbLigne ++) PixelR[CbLigne]=(unsigned
char *)malloc(XTaille*sizeof(unsigned char *));

209
// Allocation des pointeurs pour PixelG
PixelG = (unsigned char **)malloc(YTaille*sizeof(char *));
for(CbLigne=0 ; CbLigne<YTaille ; CbLigne ++) PixelG[CbLigne]=(unsigned
char *)malloc(XTaille*sizeof(unsigned char *));

// Allocation des pointeurs pour PixelB


PixelB = (unsigned char **)malloc(YTaille*sizeof(char *));
for(CbLigne=0 ; CbLigne<YTaille ; CbLigne ++) PixelB[CbLigne]=(unsigned
char *)malloc(XTaille*sizeof(unsigned char *));

// Définition de la couleur
RComp=128 ;
GComp=0;
BComp=36;

// On initialise les pixels du rectangle


for (CbColonne=0 ; CbColonne < XTaille ; CbColonne++){
for(CbLigne=0 ; CbLigne< YTaille ; CbLigne++){
PixelR[CbLigne][CbColonne]=RComp;
PixelG[CbLigne][CbColonne]=GComp;
PixelB[CbLigne][CbColonne]=BComp;
} // fin du for sur CbLigne
}// fin du for sur CbColonne

// Si on veut créer un deuxième rectangle ???

printf("Fin du programme\n");
return 0;
}

La façon dont est écrit ce programme n’est toutefois pas satisfaisante. En effet, si on veut
créer un deuxième rectangle, comment doit-on procéder ? Cela est toujours possible, mais en
créant à nouveau l’ensemble des variables définissant la taille, la position, la couleur, les
pointeurs…

Sans aborder les notions de programmation objet au sens stricte du langage C++, ce
programme aurait pu être écrit plus aisément en utilisant la notion de structure et le code
correspondant à des actions effectuées sur le rectangle aurait pu être placé dans des fonctions.
Nous définissons alors les deux structures suivantes :

typedef struct {
unsigned char R ;
unsigned char G ;
unsigned char B ;
} PIXEL ;

210
typedef struct {
PIXEL Color ; // définit la couleur du rectangle
unsigned int XPosition ;
unsigned int YPosition ;
unsigned int XTaille ;
unsigned int YTaille ;
PIXEL ** Image ;
} RECTANGLE ;

Par ailleurs on définit les fonctions qui prennent en charge l’ensemble des actions que nous
voulons effectuer sur le rectangle.

#include "stdafx.h"
#include <malloc.h>
#include "rectangle.h"

// Cette fonction change la couleur de tous les pixels du rectangle


void ColorRectangle(RECTANGLE *PtRect, PIXEL NewColor){
unsigned int CbLigne;// Compteur sur les lignes
unsigned int CbColonne;// Compteur sur les colonnes

// On change tous les pixels de couleur


for (CbColonne=0 ; CbColonne < PtRect->XTaille ; CbColonne++){
for(CbLigne=0 ; CbLigne< PtRect->YTaille ; CbLigne++){
PtRect->Image[CbLigne][CbColonne].R=NewColor.R;
PtRect->Image[CbLigne][CbColonne].G=NewColor.G;
PtRect->Image[CbLigne][CbColonne].B=NewColor.B;

} // fin du for sur CbLigne


}// fin du for sur CbColonne

// On met à jour la couleur


PtRect->Color.R=NewColor.R;
PtRect->Color.G=NewColor.G;
PtRect->Color.B=NewColor.B;

}// fin de la fonction ColorRectangle

// Cette fonction initialise les paramètres du rectangle *PtRect


void InitRectangle(RECTANGLE * PtRect,
unsigned int XTaille, // Taille en X
unsigned int YTaille, // Taille en Y
unsigned int XPosition,// Position en X
unsigned int YPosition,// Position en Y
PIXEL Couleur)// Couleur du rectangle
{

unsigned int CbLigne;

PtRect->XTaille=XTaille;
PtRect->YTaille=YTaille;
PtRect->XPosition=XPosition;
PtRect->YPosition=YPosition;

// Initialisation des pointeurs


PtRect->Image=(PIXEL **)malloc(YTaille*sizeof(PIXEL *));

211
for(CbLigne=0 ; CbLigne<YTaille ; CbLigne ++)
PtRect->Image[CbLigne]=(PIXEL *)malloc(XTaille*sizeof(PIXEL *));

// Initialisation de la couleur
ColorRectangle( PtRect,Couleur);

} // fin de la fonction InitRectangle

// Cette fonction modifie la position du rectangle


void ChangePosition( RECTANGLE * PtRect,unsigned int X, unsigned int Y){
PtRect->XPosition=X;
PtRect->YPosition=Y;
} // fin de la fonction ChangePosition

Le programme principal devient alors beaucoup plus compact et lisible.

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


{

RECTANGLE Rect1;
RECTANGLE Rect2;

PIXEL Couleur ;
PIXEL NewCouleur ;

// Définition de la couleur
Couleur.R=128 ;
Couleur.G=0;
Couleur.B=36;

// Définition de NewCouleur
NewCouleur.R=36 ;
NewCouleur.G=100;
NewCouleur.B=36;

// Initialisation du rectangle
// XTaille=200 /YTaille=50 /XPosition=0 /YPosition=150
InitRectangle(&Rect1,200,50,0,150,Couleur);

// Changement de position du rectangle


//Ancien code //XPosition=XPosition+20; //YPosition=YPosition+10;
ChangePosition(&Rect1,Rect1.XPosition+20,Rect1.YPosition+10);

//Changement de couleur des pixels


ColorRectangle(&Rect1,NewCouleur);

// initialisation du deuxième rectangle


InitRectangle(&Rect2,200,50,0,150,NewCouleur);

printf("Fin du programme\n");
return 0;
}

212
Bien qu’ayant conservé une écriture classique en C, nous avons commencé à concevoir notre
programme selon une méthode orientée objet, en considérant d’une part les données
regroupées dans la structure RECTANGLE et d’autre part les fonctions (appelées méthodes
en POO) s’appliquant aux éléments contenus dans la structure RECTANGLE. Le langage
C++ va nous permettre de regrouper les définitions des données et des fonctions au sein d’une
classe. Avant de poursuivre l’amélioration de notre programme, nous allons définir ce qu’est
une classe et les différents éléments qui la constituent.

13.4 Les classes

13.4.1 Définition
On appelle classe la structure d’un objet, c’est à dire la déclaration de l’ensemble des entités
qui composeront un objet. Un objet est donc « issu » d’une classe, c’est le produit qui sort
d’un moule. En réalité, on dit qu’un objet est une instanciation d’une classe, c’est la raison
pour laquelle on pourra parler indifféremment d’objet ou d’instance (éventuellement
d’occurrence). Une classe est composée de deux parties :
• Les attributs (parfois appelés données membres) : il s’agit des données représentant l’état
de l’objet
• Les méthodes (parfois appelées fonctions membres) : il s’agit des opérations applicables
aux objets.

Si on définit une classe « Peugeot 207 », alors on pourra instancier (créer) à partir de cette
classe plusieurs objets :
• Voiture 1 => peugeot 207 bleue immatriculée 007QG75.
• Voiture 2 => peugeot 207 orange immatriculée 747CPP25.

13.4.2 Syntaxe
D’un point de vue syntaxe, on définit une classe de la façon suivante :
class MaClasse
{
public:
MaClasse(); // Constructeur
~MaClasse();// Destructeur
void Fonction1() ;// Fonction
typeA var3 ; // variable membre

protected :
void Fonction2(type param1,type param2) ; // Fonction
void Fonction3() ;// Fonction
typeB var1 ;// variable membre

213
private:
typeC var2 ;// variable membre
unsigned int var4 ; //variable membre
};

La déclaration d’une classe commence toujours avec le mot-clé class suivi du nom de la
classe (ici MaClasse) et se termine par un point-virgule. Les variables var1, var2, var3
sont des variables membres. On trouve un certain nombre de fonctions dans cette classe :
Fonction1(), Fonction2(type param1,type param2) , Fonction3(). Ce
sont des fonctions membres de la classe. Il y a aussi 2 fonctions particulières, MaClasse()
et ~MaClasse(), qui sont respectivement le constructeur et le destructeur attachés à cette
classe.

13.4.3 Constructeur
Un constructeur est une fonction membre qui est appelée automatiquement à la déclaration de
l’objet. Une fonction constructeur doit avoir le même nom que la classe et ne pas avoir de
type de retour. Nous verrons plus loin qu’il est possible de définir plusieurs constructeurs
pour une même classe. C’est typiquement dans cette fonction constructeur que l’on pourra
faire l’initialisation des différentes variables membres.

Rappelons qu’il faut bien faire la distinction entre la notion de classe et d’objet. La classe
n’est que la définition d’un type d’objet mais n’est pas un objet en elle-même. Il faut ensuite
créer des objets à partir de cette classe.

13.4.4 Destructeur
Il existe une fonction dont le nom est toujours le nom de la classe précédé de ~. Dans
l’exemple ci-dessus, il s’agit de ~MaClasse(). Cette fonction est le destructeur et de façon
similaire au constructeur, cette fonction est appelée lors de la disparition de l’objet. Le
destructeur doit toujours être unique.

13.4.5 Restriction d’accès


La définition de la classe comporte 3 mots clé (public :, protected : et
private :) qui divisent en trois catégories les fonctions et variables membres. Ces 3
catégories définissent les restrictions d’accès aux fonctions et variables.

214
Il est à noter que si une catégorie ne comporte ni variable, ni fonction, il n’est pas
indispensable de faire figurer le mot-clé correspondant. Ainsi dans l’exemple précédent, si
var2 n’existait pas, nous aurions pu supprimer le mot-clé private: .

Private :
On ne peut accéder aux données et fonctions private que de l’intérieur de l’objet. Cela
implique qu’une fonction private ne peut être appelée qu’à partir des fonctions
membres de la classe et qu’une donnée membre private ne peut être lue ou modifiée
que par des fonctions membres de la classe.

Notamment pour ce qui est des variables, on dispose ici d’un mécanisme permettant de
sécuriser la modification en imposant l’utilisation d’une fonction membre (qui elle n’est
forcément pas private). Cette fonction membre pourra par exemple effectuer une
détection d’erreur par rapport à la modification demandée sur la variable et le cas échéant,
ne pas modifier la variable.

Public :
Pour les données et variables membres public, il n’y a pas de restriction et on peut y
accéder à l’extérieur comme à l’intérieur de l’objet.

Protected :
Il s’agit d’un cas intermédiaire entre private et public. Les fonctions et variables
membres de type protected sont accessibles par toute fonction membre de l’objet ou
d’une des classes dérivées de l’objet. La notion de classe dérivée n’ayant pas encore été
introduite, on se reportera au chapitre traitant des notions d’héritage dans lequel on revient
sur ces notions.

13.4.6 Les fonctions (méthodes) de la classe


Nous avons défini dans le paragraphe précédent une classe avec ses données membres et ses
fonctions membres, mais nous n’avons pas écrit le code de la fonction. Nous allons définir
dans ce paragraphe les quelques règles qui définissent l’écriture du code des fonctions.

Pour la fonction nommée « Fonction1 », nous écrirons :

215
MaClasse ::Fonction1(){
// Code source de la fonction

} ;

:: est l’opérateur binaire de résolution de portée de classe.

Les données membres de la classe, quelle que soit leur catégorie ( private, protected
ou public) sont directement accessibles dans toutes les fonctions membres de la classe.
Ainsi pour modifier var2, qui est une donnée membre private, il suffit, par exemple,
d’écrire dans la fonction :
var2=3 ;

En fait, chaque fonction membre dispose implicitement du pointeur sur l’objet auquel on
applique la fonction. C’est le pointeur implicite sur l’objet dont le nom est « this ». Ainsi
lorsque l’on écrit :
var2=3 ;

Cela est équivalent à écrire :


this->var2=3 ;

13.4.7 Organisation en fichiers source et header


Classiquement, lorsque l’on définit une classe, on répartit le code de la façon suivante :
• Dans le fichier entête MaClasse.h, on place la définition de la classe, comme celle
présentée au paragraphe 13.4.2.
• Dans un fichier source MaClasse.cpp, on écrit le code des différentes fonctions
membres de la classe.

On aura alors le fichier MaClasse.cpp qui sera de la forme :


#include « MaClasse.h »

MaClasse ::MaClasse(){
// Code source du constructeur

} ;

MaClasse : :~MaClasse(){
// Code source du destructeur

} ;

216
// Code des différentes fonctions

MaClasse : :Fonction1(){
// Code source de la fonction

} ;

MaClasse : :Fonction2(type param1,type param2){


// Code source de la fonction

} ;

MaClasse : :Fonction3(){
// Code source de la fonction

} ;

Une classe étant définie, on peut à partir de celle-ci créer et manipuler des objets.

13.5 Création d’un objet


Il existe essentiellement 2 méthodes pour créer un objet, soit au moyen d’une déclaration, soit
en déclarant un pointeur et en allouant la mémoire avec l’opérateur new.

13.5.1 Au moyen d’une déclaration


MaClasse Objet;

ou bien
MaClasse Objet();

Ces deux formes sont équivalentes. Nous sommes dans le cas d’un constructeur auquel il
n’est pas besoin de passer des paramètres.

13.5.2 Avec l’opérateur new


En définissant un pointeur sur un objet de type MaClasse et en allouant ce pointeur au moyen
de l’opérateur new.
MaClasse * PtObjet;
PtObjet = new MaClasse;

ou bien sous une forme équivalente.


MaClasse * PtObjet;
PtObjet = new MaClasse();

217
L’opérateur new qui est introduit ici permet de faire des allocations de mémoire. Dans
l’exemple ci-dessus, nous avons alloué la mémoire pour un seul objet de type MaClasse. On
aurait pu allouer plusieurs objets en utilisant la syntaxe suivante :
PtObjet = new MaClasse[10] ();

Dans ce cas, on alloue un pointeur sur 10 objets de classe MaClasse.

Remarque
Il est à noter que nous sommes dans le cas particulier où le constructeur ne prend aucun
paramètre d’entrée. Si nous avions un constructeur qui prend des paramètres en entrée, il
faudrait alors noter que les syntaxes suivantes sont fausses :

MaClasse Objet; car équivalente à MaClasse Objet();

PtObjet=new Ma Classe; car équivalente à PtObjet=new MaClasse();

Si on prend l’exemple d’un constructeur ainsi défini :


MaClasse ::MaClasse(unsigned int a)

alors les syntaxes correctes pour créer un objet sont :


int b=5 ;
MaClasse Objet(b);

Ou bien :
int b=5 ;
MaClasse *PtObjet ;
PtObjet= new MaClasse(b) ;

Il est également à noter que l’allocation de mémoire pour plusieurs objets n’est possible que si
la classe dispose d’un constructeur sans argument d’entrées. L’écriture :

PtObjet = new MaClasse [10] (b);

n’est pas admise pour le cas d’objets multiples.

218
Opérateur delete
Lorsqu’un objet a été alloué avec l’opérateur new, comme par exemple avec la déclaration
suivante :

MaClasse *PtObjet ;
PtObjet= new MaClasse(b) ;

alors on détruit l’objet et on libère la mémoire avec l’opérateur delete :


delete PtObjet ;

L’opérateur delete ne doit être utilisé que dans le cas où l’objet a été créé avec
l’opérateur new.

13.6 Manipulation des objets


La notion de classe étant une extension de la notion de structure, on retrouve des syntaxes
similaires.

13.6.1 Accès à une variable


Tout comme pour une structure nous aurons la syntaxe suivante :
NomObjet.NomVariable

Ou
PointeurSurObjet->NomVariable

13.6.2 Accès à une fonction


Le principe est strictement identique à celui utilisé pour une variable :
NomObjet.NomFonction(arguments)

Ou
PointeurSurObjet->NomFonction(arguments)

On reprend ici la définition de la classe faite au paragraphe 13.4.2. Nous aurons les lignes
suivantes dans notre programme :

219
typeA alpha ; // Déclaration d’une variable de type typeA
MaClasse Objet() ; // Déclaration d’un premier objet
MaClasse * PtObjet() ;// Déclaration d’un pointeur sur un deuxième objet
PtObjet = new MaClasse() ; // Allocation du pointeur du deuxième objet

Objet.var3=alpha ;// On affecte alpha à var3 de Objet


Objet.Fonction1() ; // Appel de Fonction1 sur le premier objet.

PtObjet->var3=2*alpha ; // On affecte 2.alpha à var1 du deuxième objet


PtObjet->Fonction1() ; // Appel de Fonction1 sur le deuxième objet

Exercice 13.1 : Dans les éléments de code source suivants, trouvez les erreurs, expliquez ce
qui est faux et proposez une solution pour corriger chacune des erreurs.

La définition de la classe est la suivante dans le fichier MaClasseExo.cpp :


class MaClasseExo
{
public:
MaClasseExo(); // Fonction toujours présente, c’est le Constructeur
~MaClasseExo();//Fonction toujours présente, c’est le Destructeur
void Fonction1() ;// Fonction
unsigned int var3 ; // variable membre
protected :
void Fonction2(int ParamInt,char ParamChar) ; // Fonction
void Fonction3() ;// Fonction
unsigned char var1 ;// variable membre
private:
unsigned int var2 ;// variable membre
unsigned char var4 ; //variable membre
};

Le fichier main.cpp contient ceci :


int main()
{
MaClasseExo Objet ; // Déclaration d’un objet
MaClasseExo * PtObjet ; // Déclaration d’un pointeur sur un objet

// Initialisation des variables


Objet.var1=0 ;
Objet.var2=0 ;
Objet.var3=0 ;
Objet.var4=0 ;
PtObjet.var1=0 ;
PtObjet.var2=0 ;
PtObjet.var3=0 ;
PtObjet.var4=0 ;

Objet.var1=(unsigned char)Objet.var3 ;

PtObjet.var2=Objet.var2 ;

// On applique la Fonction1 et la Fonction2 sur les 2 objets


PtObjet.Fonction1() ;
PtObjet.Fonction3() ;
Objet.Fonction1() ;

220
Objet.Fonction3 ;
}

Dans le fichier MaClasseExo.cpp, pour la Fonction1, on trouve ceci :


void IncrementeVar1_2 () {
var1++ ;
var2++ ;
} ;// Fin de la fonction IncrementeVar1_2 ;

void MaClasseExo::Fonction1(){
IncrementeVar1_2() ;
} ; // fin de la fonction Fonction1

Exercice 13.2 : reprendre ce qui a été fait pour la création d’un rectangle à l’aide de structure
et donnez la structure de la classe et des fonctions. Ré-écrire le code source du programme
principal.

13.7 Surcharge des fonctions et des opérateurs


Contrairement au C où pour une fonction Fonction1(), il y a un seul et unique prototype
et une seule et unique définition de la fonction, le C++ autorise pour une même fonction
plusieurs déterminations. Nous pourrons par exemple modifier la déclaration de la classe
MaClasse en ajoutant un deuxième prototype de la fonction Fonction1() :

class MaClasse
{
public:
MaClasse(); // Constructeur
~MaClasse();// Destructeur
void Fonction1() ;// Fonction
void Fonction1 ( unsigned int a) ;
typeA var3 ; // variable membre

protected :
void Fonction2(type param1,type param2) ; // Fonction
void Fonction3() ;// Fonction
typeB var1 ;// variable membre
private:
typeC var2 ;// variable membre
unsigned int var4 ; //variable membre
};

Il nous faudra alors définir la deuxième définition de la fonction Fonction1(unsigned int


alpha) :

MaClasse ::Fonction1(unsigned int a )


{
// code de la fonction
} ;

221
Nous pourrons alors utiliser l’une et/ou l’autre de ces fonctions :

CMaClasse MonObjet() ;
unsigned int alpha ;

MonObjet.Fonction1() ;
MonObjet.Fonction1(alpha) ;

Les deux écritures sont valides et correspondent à deux fonctions différentes, même si leur
nom est identique. La distinction sera faite par le compilateur en examinant les arguments qui
sont passés à la fonction. Si aucun paramètre n’est passé à la fonction, il s’agit de la première
fonction Fonction1(). Si on passe un unsigned int, alors il s’agit forcément de la
deuxième définition de la fonction que nous venons d’introduire dans ce paragraphe.

De la même façon qu’il est possible de redéfinir une fonction, on peut faire de même avec les
opérateurs. Par exemple, les opérateurs « + » ou « = » qui s’appliquent d’ordinaire à des
nombres entiers ou flottants peuvent s’appliquer à des nombres complexes.

Considérons tout d’abord une classe constituée par des nombres complexes dont les parties
réelle et imaginaire sont du type float. La définition de la classe sera la suivante :

class CComplex
{
public:
CComplex();
CComplex(float x, float y);
float I;
float R;
virtual ~CComplex();

};

On notera ici la présence de deux constructeurs. On veut ajouter à cette classe, une fonction
membre, « Egal », telle que la valeur des arguments réels et imaginaires de l’objet deviennent
égaux à ceux de l’objet complexe que l’on passe comme argument à cette fonction.

Nous redéfinirons alors la classe en ajoutant la fonction Egal(CComplex u). La classe


complexe devient alors :

222
class CComplex
{
public:
CComplex();
CComplex(float x, float y);
Egal(CComplex u) ;
float I;
float R;
virtual ~CComplex();

};

Et la définition de la fonction :
CComplex : :Egal(CComplex u){
R=u.R ;
I=u.I ;
return *this ;
} ;

Nous pourrons écrire :

CComplex d(3,4) ;
CComplex a,b,c;
c.Egal(d) ; // c=d ;
a.Egal(b.Egal(c)) ; // a=b=c ;

Si au lieu de manipuler des nombres complexes qui sont des instances de la classe
CComplex, nous traitions des nombres de type float par exemple, alors on aurait pu écrire
directement :
c=d;
a=b=c;

Si l’on souhaite pouvoir disposer de cette forme d’écriture en utilisant l’opérateur « = »


comme dans le cas de n’importe quel nombre alors, il faut ajouter une fonction membre à la
classe CComplex. Cette fonction membre sera définie par :
CComplex operator=(CComplex u);

Et son implémentation sera la suivante :


CComplex CComplex : :operator=(CComplex u) {
R=u.R ;
I=u.I ;
return *this
};

223
Exercice 13.3 : Ajouter deux fonctions membres à la classe CComplex qui permettent
d’effectuer le produit. La première de ces fonctions sera une fonction classique nommée
Plus. La deuxième de ces fonctions sera une surcharge de l’opérateur +.

13.8 Passage par référence


Le C++ offre par rapport au C une souplesse d’écriture pour le passage par pointeur.
Reprenons tout d’abord un exemple en C. Supposons que dans le programme principal, on ait
une variable de type unsigned int a que l’on souhaite incrémenter dans une fonction
que l’on appellera Incremente. Nous aurons alors :
void Incremente (int *);
unsigned int a;
Incremente (&a);

La seule solution pour que la fonction Incremente puisse modifier la variable a du


programme principal est de passer le pointeur sur a à Incremente. L’implémentation de la
fonction sera alors la suivante :
void incremente ( int * PtAlpha) {
*PtAlpha ++ ;
}

Le C++ permet l’écriture suivante selon le principe du passage par référence :


void Incremente (int &);
unsigned int a;
incremente (a);

L’implémentation de la fonction sera alors la suivante :


void incremente ( int & Alpha) {
Alpha ++ ;
}

Bien que l’appel à la fonction fasse apparaître a, ce n’est en fait pas a qui est passé à la
fonction mais le pointeur sur a et la variable Alpha de la fonction Incremente n’est pas
une copie locale de la valeur de a, mais correspond à la même variable que a.

224
Si dans le programme principal, nous examinons l’adresse de a, (&a) et que nous faisons la
même opération dans la fonction Incrémente avec Alpha (&Alpha), nous constaterons
que les deux adresses sont strictement identiques, et que l’on manipule en réalité la même
variable sous deux noms différents. Alpha est un alias de a.

Exercice 13.4 : soit le code suivant écrit en C++. Représentez graphiquement le signal obtenu
dans le tableau Signal[]. Trouvez l’erreur dans ce programme et proposez au moins 2
méthodes pour la corriger.
#include <math.h>

void ampli(double gain,double Sample);


void ecrete(double limite,double &Tension);

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


{
double Signal[200];
double Sample;
double pi=3.14159265359;
unsigned int CbSample;

for (CbSample=0; CbSample<200; CbSample++){


Sample=sin(2*pi*CbSample/200);
ampli(2, Sample);
ecrete(1,Sample);
Signal[CbSample]=Sample;
}// fin du for

printf("fin du programme sample\n");


return 0;
}

void ampli( double gain, double Sample){


Sample=Sample*gain;
}// fin de la fonction ampli

void ecrete(double limite, double &Tension){


if(Tension>limite) Tension=limite;
}// fin de la fonction ecrete

13.9 Héritage et composition

13.9.1 Introduction
Les intérêts de la programmation objet du code en C++ sont notamment la modularité et la
possibilité de ré-utiliser du code déjà développé. Si l’on fait une analogie avec la construction
d’un module électronique, le concepteur n’a pas à redéfinir l’ensemble des fonctions à partir
du niveau le plus bas (le transistor par exemple) mais utilise des composants existants. Les
possibilités de composition et d’héritage de classe facilitent en C++ la ré-utilisation des

225
classes déjà existantes pour en construire de nouvelles adaptées au besoin de l’application à
développer.

13.9.2 La composition
La composition (appelée aussi agrégation) de classes permet l’utilisation de la définition
d’une ou de plusieurs classes dans la définition d’une nouvelle classe. Lorsqu’une donnée
membre d’une classe est un objet d’une autre classe, on dit que la nouvelle classe est une
classe composée des autres objets. C’est cette possibilité que nous avons utilisé dans la
correction de l’exercice 13.2, en incluant un objet de classe CPixel dans la définition de la
classe CRectangle.

13.9.3 L’héritage
L’héritage est quelquefois appelé « Spécialisation » ou « Dérivation ». Elle nous conduit à
une organisation hiérarchique en classe et en sous-classe. Un camion et une voiture sont tout
deux des véhicules. On peut donc concevoir que la classe des véhicules est composée de deux
sous-classes, les camions et les voitures. On crée alors une classe de base véhicule qui
contient les caractéristiques et les opérations communes à tous les véhicules et on introduit
ces caractéristiques dans les classes camion et voiture par héritage de la classe véhicule. Cela
évite de réécrire les caractéristiques et opérations communes à tous les véhicules pour chaque
nouveau type de véhicules.

13.9.3.1 Principe de l’héritage


Si nous reprenons l’exemple du rectangle, et que de façon plus générale nous voulions traiter
un ensemble de formes différentes dont le rectangle, nous pouvons établir la hiérarchie
suivante de classe :

226
Ceci nous permet par exemple de développer une classe Forme, puis ensuite une classe
Deux_Dimensions et Trois_Dimensions qui ont les mêmes données et fonctions
membres que la classe Forme et pour chacune de ces deux classes d’ajouter des données et
des fonctions membres spécifiques.

Par exemple, pour toute forme, on peut définir comme données membres la position et la
couleur. Pour un objet à deux dimensions, on peut définir sa surface, et pour un objet à trois
dimensions son volume. Si l’on construit la classe Deux_Dimensions à partir de la classe
Forme par héritage, alors tout objet instancié à partir de la classe Deux_Dimensions aura
pour données membres :
• la position (par héritage de Forme)
• la couleur (par héritage de Forme)
• la surface.

Pour la classe Trois_Dimensions qui sera construite à partir de la classe Forme, nous aurons,
de façon similaire comme données membres :
• la position (par héritage de Forme)
• la couleur (par héritage de Forme)
• le volume.

Supposons que l’on veuille créer une nouvelle classe Y à partir d’une classe X existante. La
définition de la classe sera alors la suivante :
class Y : public X{

// Définition des membres de la classe Y

};

On appelle X la classe de base (ou encore superclasse) et Y la classe dérivée (ou sous-classe).
Ici nous avons choisi de faire un héritage de type public, ce qui signifie que toutes les
membres (données ou fonctions) de la classe de base qui sont publiques seront également
publiques pour les objets instanciés à partir de la classe dérivée Y. Ceci appliqué à l’exemple
précédent nous donne :

227
Classe Forme {
// Définition des membres de la classe
public :
double XPosition ; // coordonnées de la position du centre de gravité de l’objet
double YPosition ;
double Zposition ;
unsigned char CouleurR ; // composantes RGB de la couleur de l’objet
unsigned char CouleurG ;
unsigned char CouleurB ;

}

Remarque : Pour simplifier la présentation de l’exemple, on a fait apparaître séparément les


trois composantes RGB pour la couleur. L’utilisation d’une structure ou d’une classe couleur
est en fait préférable. Il en est de même pour la définition des coordonnées du centre de
gravité de l’objet, où il serait préférable d’utiliser une classe définissant des points.

Class Deux_Dimensions : public Forme{


// Définition des membres de la classe
public :
double Surface ; // surface de la forme à 2 dimensions

}

Si on déclare alors un objet Objet2D, on accédera aux données membres de la façon suivante :

Deux_Dimensions Objet2D(); // Création de l’objet


// Initialisation des coordonnées du centre de gravité
Objet2D.Xposition=2 ;
Objet2D.Yposition=4;
Objet2D.Zposition=11;
Objet2D.Surface=54.3;

13.9.3.2 Restriction d’accès


Les restrictions d’accès pour les membres locaux de la sous-classe sont directement
déterminées par le choix public :, protected : ou private. Pour les membres
de la classe de base, nous aurons le tableau suivant :

228
Type d’héritage Niveau d’accès classe de base Niveau d’accès équivalent
classe dérivée
public public public
protected protected
private inaccessible
protected public protected
protected protected
private inaccessible
private public private
protected private
private inaccessible

13.9.3.3 Substitution des membres hérités


La démarche de construction de classes dérivées à partir de classes de base permet de
reprendre des éléments déjà développés pour les étendre, les adapter à son besoin. Il est
toutefois possible que certaines données ou fonctions membres hérités de la classe de base ne
conviennent pas. Il est alors possible de les modifier dans la sous-classe et elles se substituent
alors à la définition initiale de la classe de base. Exemple :
class X{
public :
void f() { a=-100 ;}
int a ;
} ;

classY :public X{
public :
void f() {a=500 ;}
unsigned int a ;
} ;

Si l’on crée un objet instance de la classe dérivée Y :


Y ObjetY ();

Alors ObjetY.a accède à unsigned int a, et ObjetY.f() exécute la fonction


définie dans la classe dérivée. On aura ObjetY.a qui vaudra 500.

229
Si l’on veut toutefois accéder à la fonction f() de la classe de base, alors on utilisera la
syntaxe suivante :
ObjetY.X::f();

C’est la fonction définie dans la classe de base qui s’exécutera et qui modifiera int a. On
accède à la variable membre définie dans la classe X par la syntaxe :
ObjetY.X::a;
ObjetY.X::a vaudra alors –100 ;

13.9.3.4 Gestion des constructeurs


Lorsque l’on fabrique une classe par dérivation d’une classe de base, cela pose le problème
des constructeurs. En effet, parmi les membres de la nouvelle classe obtenue, nous aurons :
• les membres hérités de la classe de base et s’il existe, un constructeur membre de la classe
de base qui s’applique en particulier à l’initialisation des données membres,
• les membres de la sous-classe ( membres locaux) et éventuellement un constructeur.

Le mécanisme mis en place pour l’héritage permet de conserver l’action du constructeur de la


classe de base et de compléter cette action en écrivant un constructeur spécifique pour la
classe dérivée. Considérons l’exemple suivant. Soit la classe ClasseBase, ainsi définie :

class ClasseBase{
ClasseBase(int a) ;
~ClasseBase() ;
int X;

} ;

ClasseBase::ClasseBase(int a){
x=a ;
}

Soit la sous-classe SousClasse


class SousClasse :ClasseBase{
SousClasse(int a, float b);
~SousClasse() ;
float Y;

};

230
Pour que le constructeur de la classe de base s’exécute correctement, il faut lui passer un
argument de type int. Pour cela, on écrira le nouveau constructeur de la façon suivante :

int a=2;
float b=3.5;
SousClasse ::SousClasse(int a, float b) :ClasseBase(a) {
// Code propre au constructeur de SousClasse
Y=b;

} ;

L’appel au constructeur de la classe SousClasse se fera de la façon suivante :


SousClasse Objet( a, b)

et le constructeur de la classe de base sera appelé en lui passant l’argument int a avant que le
constructeur de la sous-classe ne s’exécute. Détaillons les différentes étapes de la construction
de l’objet dans ce cas. Que se passe t-il à la construction de l’objet ?

1. Programme principal :
int a =2 ;
float b =3.5
SousClasse Objet (a, b) ;

2. Entraîne l’appel du constructeur de la classe dérivée


SousClasse ::SousClasse(int a, float b) :ClasseBase(a) {
// Code propre au constructeur de SousClasse
Y=b;

} ;
3. Entraîne l’appel au constructeur de la classe de base
ClasseBase:: ClasseBase(a){
X=a;
}
( X membre de la classe de base est alors initialisé)

231
4. On retourne au constructeur de la classe dérivée
SousClasse ::SousClasse(int a, float b) :ClasseBase(a) {
// Code propre au constructeur de SousClasse
Y=b;

} ;

5. On exécute le code de ce constructeur et on initialise la variable membre Y de la


classe dérivée.

6. On retourne au programme principal. L’objet est créé et toutes les variables


membres sont initialisées.

13.9.3.5 Fonctions virtuelles


Dans l’exemple précédent, nous avons défini 2 fois la fonction f(), à la fois dans la classe de
base et dans la classe dérivée. Déclarons 2 pointeurs sur des objets de type X et de type Y :
Y ObjetY ;
X * PtX ;
Y * PtY ;

PtX = & ObjetY ;


PtY = & ObjetY ;

PtX->f() appelle la fonction f() définie dans la classe de base X, et PtY->f() appelle la
fonction f() définie dans la classe dérivée. C’est le type du pointeur qui détermine la
fonction qui va être appelée et non pas la classe de l’objet sur lequel porte le pointeur.

Si l’on modifie la déclaration de la classe de base, en ajoutant le mot clé virtual dans la
déclaration de f(), nous aurons ceci :
class X{
public :
virtual void f() { a=-100 ;}
int a ;
} ;

232
Dans ce cas, nous modifions le comportement des appels de fonction. Les lignes de code
PtX->f(); et PtY->f(); appellent toutes les deux la fonction f() de la classe dérivée.
Pour déterminer la fonction qui doit être appelée, c’est la classe de l’objet sur lequel on pointe
qui est pris en compte et non plus le type du pointeur.

On a maintenant un lien dynamique :


• dans le paragraphe précédent, comme c’était la nature du pointeur qui déterminait le choix
de l’une ou l’autre des fonctions, le choix pouvait être établi a priori.
• dans ce cas, il doit être fait au moment de l’exécution, dynamiquement.

Quelques Règles pour un bon héritage


La syntaxe :
X classe de base
Y classe dérivée
class Y : public X{
// Définition des membres de la classe Y

} ;

La substitution :
La fonction ou variable définie dans la classe dérivée qui a le même nom que celle de la
classe de base l’emporte. Si on veut avoir accès à la fonction f() de la classe de base plutôt
qu’à celle de la classe dérivée, la syntaxe est alors : ObjetY.X::f();

Restriction d’accès : héritage public


Toutes les variables membres de la classe de base sont accessibles comme si elles avaient été
définies dans la classe dérivée avec les mêmes règles. Si héritage non public, voir 13.9.2.2 sur
cours

233
Constructeur :
Si le constructeur de la classe de base ne nécessite pas de paramètre, on écrit un constructeur
pour la classe dérivée sans ajout particulier. Si le constructeur de la classe de base nécessite
des paramètres, on ajoute ces paramètres au constructeur de la classe dérivée puis on génère
un appel au constructeur de la classe de base avec la syntaxe indiquée ci-dessous.

ClasseBase::ClasseBase(int a) ;
Paramètre rajouté car nécessaire
pour le constructeur de la classe de
base
SousClasse ::SousClasse(int a, float b) :ClasseBase(a) {

….
} ; Paramètre propre au constructeur Appel du constructeur de la classe
SousClasse de base en passant a en argument

Exercice 13.5 : on reprend la hiérarchie des classes définie au paragraphe 13.9.2.1. On


suppose que :
• pour tous les objets on a une fonction calculant l’aire de l’objet
• que pour les objets en deux dimensions, on calcule le périmètre
• que pour les objets en trois dimensions on calcule le volume.

On implémentera en particulier les classes correspondant au cercle et au cône.

234
13.10 Les flux d’entrée sortie

13.10.1 Généralités
Le langage C++ définit des classes permettant de gérer les flux d’entrée et de sortie des
données. Un flux d’entrée est un objet de classe istream et un flux de sortie un objet de
classe ostream. Pour gérer les entrées et sorties standard, il existe deux objets qui sont des
instances des classes istream et ostream. Ces deux objets sont cin ( flux entrant) et
cout (flux sortant). Pour manipuler les flux d’entrées sorties, deux opérateurs ont été
surchargées : << et >>. Pour un flux de sortie, on utilisera << et pour un flux d'entrée >>.

13.10.2 Opérateur d'insertion et d’extraction de flux


13.10.2.1 Extraction de flux >>
La classe istream définit l’opérateur >> d’insertion de flux qui s’applique donc à l’objet
cin. Cet opérateur permet l'entrée formatée car il reconnaît le type des objets et effectue le
formatage en conséquence. On peut ainsi écrire directement :
int n,p ;
cin >> n ;

Si on saisit alors 16, on obtiendra n = 16. Comme l’opérateur >> renvoie une référence à
l’objet, on peut enchaîner de la façon suivante :
cin >>n >>p ;

Si on saisit, 16 puis 76, on obtient alors n=16, p=76.

13.10.2.2 Insertion de flux >>


De façon parfaitement similaire, il existe un opérateur d'insertion de flux, qui s'applique au
flux de données sortant c'est à dire à cout. Nous pouvons donc écrire :
int n=16;
int p=45;
cout << n <<endl;

qui produira :
16

235
endl rajoute un saut de ligne et vide le tampon. En réalité cette écriture permet l'appel à la
fonction endl() définie dans la classe ostream.

De la même façon, comme le formatage se fait en fonction du type des objets transmis, et que
l'on peut chaîner les opérateurs <<, nous pourrons écrire :

cout << "La valeur de n est égale à " << n << "et celle de p à " << p << endl;

ce qui nous donnera :

La valeur de n est égale à 16 et celle de p à 45

13.10.3 Modification des formatages


13.10.3.1 Liste des drapeaux
Il y a possibilité de modifier le formatage en sortie des données en jouant sur un certain
nombre de drapeaux :

Drapeau Effets
ios::skipws saute les blancs de tête dans une entrée formatée. (par défaut)
ios::left sortie justifiée à gauche, bourrage à droite pour obtenir la bonne
largeur
ios::right sortie justifiée à droite, bourrage à gauche. (par défaut)
ios::internal sortie numérique justifiée à droite, signe ou base justifiée à gauche et
bourrage au milieu
ios::dec entrée et sortie d'entiers en base 10. ( défaut pour la sortie)
ios::oct entrée et sortie d'entiers en base 8
ios:hex entrée et sortie d'entiers en base 16
ios::showbase sortie d'entiers avec préfixe de base ; ex 027 (oct), 0x2c1(hex).
(défaut)
ios::showpoint sortie de nombres réels avec point décimal et zéros de traîne
ios::uppercase utilisation des majuscules pour sorties nombres hexadécimaux et réels
en scientifique

236
ios::showpos entiers positifs précédés par '+'
ios::scientific sortie des nombres réels en notation scientifique ; ex 1.23456e-09
ios::fixed sortie des nombres réels avec n chiffre à droite du point décimal où n
est la précision (_prec)
ios::unitbuf vidage du flux de sortie après chaque insertion
ios::stdio vidage de stdio et stderr après chaque insertion

13.10.3.2 Fonctions permettant de modifier les drapeaux


Pour appliquer ces drapeaux sur le flux considéré, il faut faire appel à la fonction flags().
Nous aurons donc :
cout.flags( ios::uppercase | ios::showpos|ios::fixed);

si nous voulons appliquer les trois drapeaux uppercase, showpos et fixed. La


fonction flags() remet à zéro tous les autres drapeaux. Si nous voulons uniquement
positionner certains drapeaux sans modifier ceux qui sont déjà positionnés, alors nous
utiliserons la fonction setf() :
cout.setf( ios::uppercase | ios::showpos|ios::fixed);

Pour annuler des drapeaux, nous avons la fonction unsetf(). On pourra par exemple
écrire :
cout.unsetf(ios::uppercase | ios::fixed );

13.10.3.3 Formatage de la sortie


Dans la classe de base ios sont définies trois grandeurs qui sont :
• la précision : le nombre de chiffres affichés pour les double et les float,
• le caractère de remplissage : c'est le caractère utilisé pour combler les vides du champ de
sortie,
• la taille du champ de sortie : le nombre de caractères du champ de sortie.

L'interrogation de ces valeurs se fait respectivement au travers des fonctions :


cout.precision();
cout.fill() ;
cout.width();

237
et la modification :
cout.precision(int);
cout.fill(char) ;
cout.width(int);

A noter que le paramètre de remplissage et le paramètre définissant la taille du champ de


sortie reviennent à leur valeur par défaut, après que l'on ait effectué une sortie sur le flux. Les
valeurs par défaut sont pour la précision 6, pour la largeur de champ 0, et pour le caractère de
remplissage, l'espace.

13.10.4 Manipulateurs non paramétriques


Ces manipulateurs s'utilisent avec les opérateurs << et >> selon qu'on agisse sur un flux de
sortie ou d'entrée. Ils permettent une simplification de l'écriture et sont équivalents à l'appel
d'une fonction membre ou au positionnement d'un drapeau. Nous avons en particulier les
manipulateurs suivants :

Manipulateur Application Action Equivalent à

cin >>dec ou Entrée / Sortie Active le bit de cin.setf(ios::dec)


ou
cout << dec conversion décimale
cout.setf(ios::dec)
cin >>hex ou Entrée / Sortie Active le bit de cin.setf(ios::dec)
ou
cout <<hex conversion
cout.setf(ios::dec)
hexadécimale
cin >>oct ou Entrée / Sortie Active le bit de cin.setf(ios::oct)
ou
cout <<oct conversion octale
cout.setf(ios::oct)
cout<<endl Sortie insère un saut de cout.endl()
ligne et vide le
tampon
cout<<ends Sortie insère un caractère cout.ends()
de fin de chaîne

238
Exercice 13.6 : on considère le code suivant :
int n ,p;
cin >> n >> p;

// Interrogation des paramètres de configuration de cout


cout << "precision :" << cout.precision()<<endl;
cout << "width " << cout.width()<<endl;
cout << "bourrage :" << cout.fill()<<endl;

// première sortie
cout << 2200. <<"\n"<<54<<endl;

// modification des drapeaux


cout.flags(ios::scientific|ios::showpos|ios::hex|ios::showbase) ;

// deuxième sortie
cout << 2200. <<"\n"<<54<<endl;

// Modification des paramètres de sortie


cout.fill('#');
cout.width(20);
cout.precision(3);

// Troisième sortie
cout << 2200. <<"\n"<<54<<endl;

// Quatrième sortie
cout << 2200. <<"\n"<<54<<endl;

Qu'obtient on ?

13.10.5 Entrée et sortie non formatées


Il est également possible de lire et d'écrire sur les flux cin et cout au moyen de fonctions
qui ne prennent pas en charge le formatage des données.

13.10.5.1 La fonction get()


Pour le flux cin, on peut utiliser la fonction get dont le prototype est le suivant : int
get();. Cette fonction retourne le caractère suivant dans le flux d'entrée. Une utilisation
typique de cette fonction est présentée dans le code ci dessous :
main {
char c;

while ((c=cin.get())!=EOF)
cout << c ;
cout <<endl;
}

239
Tant que la fin de ligne n'est pas détectée, la boucle continue de récupérer un par un tous les
caractères. Une fois que le caractère de fin de ligne est détecté, alors on génère sur le flux
sortant une fin de ligne et on vide le buffer avec le manipulateur endl.

13.10.5.2 La fonction getline()


Le prototype de la fonction getline est le suivant :

istream& get ( char* buffer, int n, char delim ='\n');

Cette fonction place dans buffer les caractères reçus sur le flux entrant cin et s'arrête
lorsqu'on a lu n-1 caractères (le dernier caractère que l'on placera dans le buffer sera le
caractère nul '\0') ou lorsqu'on a reçu le caractère de délimitation. Dans l'exemple, c'est le
retour à la ligne.

Exercice 13.7 : écrire un programme basé sur getline qui récupère au plus 19 caractères, et
qui termine la saisie lorsqu'on détecte un point.

13.10.5.3 La fonction read()


Les différents prototypes de la fonction read sont :

istream& read (char* buffer, int n);


istream& read(unsigned char*buffer, int n);

Cette fonction transfère dans buffer les n premiers caractères du flux d'entrée. On n'ajoute pas
de caractère nul '\0'.

Exercice 13.8 : Soit le code suivant.


char buffer[]="??????????"
cin.read(buffer,4);
cout << buffer <<endl;
cin.read(buffer,2);
cout << buffer <<endl;

Si l'on tape ABCDEFG, qu'obtient-on ?

240
13.10.6 Fonctions de sortie non formatées
On retrouve les fonctions symétriques à celles que l'on a pour l'entrée.

13.10.6.1 La fonction put


Son prototype est le suivant : ostream & put(char c);

cout.put('a');

place le caractère 'a' sur le flux de sortie cout.

Exercice 13.9 : soit la ligne de code suivante.


cout.put('s').put('r').put('e').put('v').put('n').put('e').put('\n');

Qu'obtient on ?

13.10.6.2 La fonction write


La fonction est symétrique de la fonction read. Son prototype est :
ostream& write(const char* buffer, int n); ou
ostream& write(const unsigned char* buffer, int n);

Cette fonction écrit sur le flux de sortie les n premiers octets de buffer.

13.10.7 Les fonctions de manipulations évoluées


Voici quelques fonctions complémentaires dont le fonctionnement n'est pas décrit en détail.
On se reportera à l'aide en ligne :
• ignore(); Cette fonction permet d'ignorer un certain nombre de caractères dans le flux
entrant.
• peek(); lit le caractère du flux d'entrée sans le retirer du flux d'entrée. Ainsi, plusieurs
appels consécutifs à peek() retourneront le même caractère, contrairement à la fonction
get() qui à chaque fois extrait et retourne le caractère.
• putback(); elle place un caractère dans le flux d'entrée.

241
13.11 Bibliographie

[1] Cours de l’Ecole Polytechnique Fédérale de Lausanne


http://ltiwww.epfl.ch/Cxx/

[2] Programmation en C++ de John Hubbard MacGraw-Hill 1997

[3] Introduction à la programmation objet


http://www.commentcamarche.net/poo/poointro.php3

[4] Introduction au langage C++


http://www.commentcamarche.net/cpp/cppintro.php3

[5] Introduction à la conception objet et à C++ de Philippe Dosch - Université Nancy2


Institut Universitaire de Technologie
http://www.developpez.com/c/cours dans la rubrique "langage C++"

242

Vous aimerez peut-être aussi