Académique Documents
Professionnel Documents
Culture Documents
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
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.
Information : texte, son, image, données binaires (produites par un système électronique,
utilisées par un système électronique), …
Applications Logicielles
Interface de programmation
Logiciels (software)
Système d’exploitation
Interface matériel/logiciel
Dispositif électronique
ordinateur
matériel (hardware)
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 binaire (base 2) :
Le nombre 10101 = 1*24 + 0*23 + 1*22 + 0*21 + 1*20 = 1*16 + 1*4 + 1*1
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
multiple valeur
Kilo 210 = 1024
Méga 220 = 1048576 = 1024 kilo
Giga 230 = 1073741824 = 1024 méga
Tera 240 = 1099511627776 = 1024 giga
3
Exemples :
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
1 x 25 + 0 x 24 + 1 x 23 + 1 x 22 + 1 x 21 + 1 x 20 = 4710
4
• Hexadécimal vers binaire
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.
1.4 L’ordinateur
Un ordinateur :
• Traite l’information grâce à un programme qu’il mémorise,
• Communique et archive des informations.
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.
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, …
Caractéristiques :
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.
Multi-Utilisateurs
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,
• …
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.
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, …
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 :
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.
../../bin/ nom_de_fichier
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.
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
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.
• 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.
13
Exemple d’informations retournées par une commande dir sous Windows NT :
Répertoire de D:\users\dut_info
6 fichier(s) 41 octets
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.
Sous Unix. Chaque utilisateur fait partie d’un groupe de travail. Les protections d’un fichier
concernent :
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).
d : directory
user group taille Nom
l : lien symbolique
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
Format des noms 8.3 (255 avec 8.3 (255 avec 255 255
VFAT) VFAT)
[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.
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.
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 é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
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 ».
problème
analyse
algorithme
programmation
Compilation/assemblage tests
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.
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 ! ! !
Avec ces éléments, vous serez à même d’écrire des programmes élémentaires.
22
3. Bases du langage C
Une variable peut contenir plusieurs types de données : nombre entier, nombre réel, caractère,
…
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.
Au moment de la déclaration.
int n = 0, p = 100;
n = 0;
Exemple :
int n, p;
n = 10;
p = 2*n – 3;
instructions n p commentaire
C’est après l’exécution de l’instruction que la variable à gauche du signe = change de valeur.
24
Exemple 1 : a = b;
Mathématiquement, cette expression signifie que a est égal à b pendant toute la durée du
problème.
Exemple 2 :
instructions a b commentaire
Exemple 3 : a = a + 1;
25
Exemple 4 : a + 5 = 3;
En informatique, cette expression est fausse. On ne peut attribuer une valeur qu’à une
variable et pas à une expression.
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 */
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
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.
max - (négation)
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.
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);
}
Les flottants s’écrivent en C sous la forme : mantisse + exposant (avec un point décimal et
pas une virgule) tels que :
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);
}
30
Exemple :
int n, p;
float x, 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.
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).
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;
...
}
32
Les constantes de type char s’écrivent sous la forme :
char n, o, p;
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.
33
int getchar(void)
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 ».
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.
main()
{
int c;
c=getchar();
putchar(c);
#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 :
Mais il est aussi possible de remplacer le clavier comme entrée standard (StdIn) par un fichier
en utilisant la commande :
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>&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 :
#include <stdio.h>
#include <ctype.h>
main()
{
int c, cm;
c=getchar();
while (c != EOF)
{
cm = tolower(c) ;
putchar(cm);
c=getchar();
}
}
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.
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 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 :
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("\n\tTerminé\n\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
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 ↵
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);
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é.
main()
{
int n, p;
42
if (n < p)
printf("croissant\n");
else
printf("non croissant\n");
printf("au revoir\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;
43
if (n < p) {
maxi = p;
printf("croissant\n");
}
else {
maxi = n;
printf("non croissant\n");
}
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.
ou le contraire.
44
if (n < p)
printf("croissant\n");
else {
maxi = n;
printf("non 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");
if (condition)
instruction_1
[else
instruction_2]
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 :
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;
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.
opérateur signification
&& et
|| ou
! non
Quelques exemples :
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.
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 */
}
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 €.
main()
{
int 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");
}
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;
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");
}
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.
main()
{
int n;
do {
printf("Donnez un nombre entier : ");
scanf("%d", &n);
printf("voici son carré : %d\n", n*n);
}
while (n != 0);
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);
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);
52
main()
{
int n;
while (n != 0) {
printf("Donnez un nombre entier : ");
scanf("%d", &n);
printf("voici son carré : %d\n", n*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
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.
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 ");
}
i = 0;
Le nombre de tours dans la boucle n’est plus connu à l’écriture du programme, mais
seulement au moment de son exécution.
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 ");
}
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).
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.
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.
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.
TABLE DES 4
4 x 1 = 4
4 x 2 = 8
...
4 x 10 = 40
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 :
0 C1 1
1 C1→C2m 2
2 C1→C3m C2 3
3 C1→C4m C2→C21m C3 5
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
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.
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).
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.
1. /* prototype de la fonction */
2. int puissance(int m, int n);
3. void main()
4. {
5. int x = 2, n = 5, res;
19. puis = 1;
60
20. for (i = 0; i < y; i++)
21. puis = puis * x;
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.
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.
Seuls les types des paramètres sont nécessaires et pas les noms. La ligne suivante est
équivalente à la précédente.
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);
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 ?
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);
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.
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);
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 :
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 :
au lieu de (printf retourne le nombre de caractères affichés, ou une valeur négative en cas
d’erreur d’affichage) :
main()
{
void meteo(void); /* prototype local */
nb_fois = 2;
meteo();
nb_fois = 3;
meteo();
}
void meteo(void)
{
int i; /* variable locale */
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();
}
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.
main()
{
int n; /* cette variable locale cache la variable globale
n tandis que p est toujours accessible */
}
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);
}
compteur++ ;
printf("appel n°%d\n", compteur) ;
}
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.
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 */
}
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).
main()
{
int n = 1, p = 2 ;
tmp = b;
b = a;
a = tmp;
printf("fin échange : n = %d, p = %d\n", a, b);
}
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.
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).
scanf("%d", &a);
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
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;
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.
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;
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;
• 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
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;
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 :
En supprimant les espaces, on supprime aussi les ambiguïtés. Il est clair alors que l’* ne
s’applique qu’à la variable suivante.
• 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.
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;
*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);
}
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.
#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.
main()
{
float x =3.14;
void inverse(float y)
{
printf("adresse de y = %x\n", &y);
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.
main()
{
83
float x =3.14;
84
5. Les tableaux
float tab[5];
tab[0]
tab[1]
tab[2]
tab[3]
tab[4]
ou bien
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.
ce qui donne :
1 2 3 4 5
a b c
ce qui donne :
1 2 3
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) :
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 :
x[0] = 1;
x[1] = 2;
x[2] = 3;
x[3] = 4;
1 2 3 4
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
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;
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;
2. figurer dans une expression arithmétique. Comme pour toute variable, les notations
suivantes sont possibles :
3. être utilisé dans une instruction de lecture (comme un scanf) ou dans une instruction
d’écriture (comme un printf). Exemples :
int x[4];
&x[i] étant l’adresse du ième élément du tableau x. Bien entendu, il est possible
d’écrire :
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
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;
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
scanf("%d", &dim);
...
}
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] = ...
• 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;
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];
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;
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];
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;
...
float tab2[4][2];
93
colonne 0 colonne 1
Evidemment, il est tout à fait possible de voir ce tableau sous la forme colonnes x lignes :
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}};
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.
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;
...
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;
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.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;
main()
{
int tableau[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, i;
raz(tableau);
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 :
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.
main()
{
int t1[10], t2[5];
raz(t1, 10);
raz(t2, 5);
...
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).
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);
...
}
99
Par contre, il faut obligatoirement spécifier les dimensions de tab2. Le programme suivant ne
se compile pas.
main()
{
int tab2x3[2][3];
raz(tab2x3);
...
}
t = &tab[0];
u = t + 2;
100
t u un entier = 4 octets
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.
main() {
int *p, *q, *r, *s;
int tab[N];
p = &tab[0];
q = p + (N-1);
r = &tab[N-1];
s = r - (N-1);
}
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};
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];
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];
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.
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.
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.
#include <stdlib.h>
main()
{
int *ptr, taille, i;
scanf("%d", &taille);
104
for (i = 0; i < taille; i++)
scanf("%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 :
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.
• 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 :
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.
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 :
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 :
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 :
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
" " ^ \0
0x20 0x00
"" \0
0x00
"\"" " \0
0x22 0x00
"\0" \0 \0
0x00 0x00
"c:\\tmp" c : \ t m p \0
mais c’est assez lourd à l’usage. Heureusement, le C propose une facilité en utilisant
directement une chaîne littérale :
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).
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.
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 :
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 :
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.
#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;
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);
}
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);
}
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);
}
main()
{
char *s1;
s1 = malloc(NB_CHAR * sizeof(char));
scanf("%s", s1);
printf("%s\n", s1);
}
main()
{
char *s1 = "bonjour";
int 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.
main()
{
char s1[100] = "essai";
char s2[100] = "s1 = %s";
printf(s2, s1);
}
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.
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;
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;
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(????);
/* création du fichier */
fp = fopen(nom_de_fichier, "w");
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);
}
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
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);
}
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 */
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,
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;
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 :
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);
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);
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);
}
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);
}
}
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.
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 :
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
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 :
main()
{
124
char ligne[] = "coucou";
On peut aussi passer directement une chaîne littérale (c’est ce qui est fait dans printf) :
imprime("essai");
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[])
main()
{
char ligne[] = "coucou";
125
imprime(ligne); /* appel de la fonction */
}
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.
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,
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 :
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 :
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 :
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 :
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
130
Après avoir tapé la commande :
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.
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 {
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;
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 :
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.
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;
};
int main() {
struct modele_local {
int numero;
int quantite;
float prix;
};
/* ... */
}
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;
};
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]
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;
/* ... */
}
truc.date_embauche.annee = 1990;
truc.date_embauche = machin.date_poste;
137
#include <stdio.h>
struct str {
int a;
int b;
};
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);
}
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);
}
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 :
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.
nom du
mot clé déclaration nouveau type
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 :
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 :
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;
/* .. */
}
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 :
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 :
r lecture seule
w écriture seule
r+ lecture et écriture
w+ lecture et écriture
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);
}
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.
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";
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.
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.
lit un caractère dans un flux et retourne son code ASCII dans un entier. La fonction :
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");
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.
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");
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.
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");
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";
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 :
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);
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 :
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;
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 :
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 :
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 :
La fonction rewind :
fseek(fp, 0, SEEK_SET);
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.
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.
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
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 :
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( ??? ) ;
}
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 ?
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)
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 :
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};
donnera la valeur 11 à ESPAGNE et 12 à SUISSE. Les caractères en C étant des entiers, il est
aussi possible d’écrire :
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 :
rep = OUI;
rep = OUI;
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.
affectera à i la valeur 6 et à n la valeur 0, alors que dans les mêmes conditions initiales,
l’expression :
n = ++i - 5;
on met le caractère lu au clavier dans tab[i], puis on incrémente i alors que dans
tab[++i] = getchar();
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
i = (j = 2, 1);
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 :
z = (a > b) ? a : b;
168
Dans l’expression conditionnelle :
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.
En réalité, pour le préprocesseur, il s’agit de macros sans paramètres au même titre que
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 :
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 :
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 ;) :
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
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.
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.
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.13 : écrivez la définition de la macro ROUND(x) qui arrondit un nombre flottant
à l’entier le plus proche.
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.
• 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.
/* fichier exemple.h */
#ifndef EXEMPLE_H
#define 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
#endif
#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);
}
...
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]
****/
176
#ifndef _INC_LIMITS
#define _INC_LIMITS
#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 */
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_
#endif /* POSIX */
#endif /* _INC_LIMITS */
178
11. Edition de lien
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);
}
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>
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,
PTC_TYPE putchar_ptr;
putchar_ptr = putchar;
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>
void main(void)
{
double pi = 3.1415926535;
double x;
x = pi / 2;
affiche(x, sin);
affiche(x, sinh);
affiche(x, cos);
affiche(x, cosh);
}
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.
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);
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).
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
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.
184
#include <stdio.h>
main()
{
int auto_var1[1024];
int auto_var2[1024];
int *ptr1, *ptr2;
static int stat_var1[1024];
static int stat_var2[1024];
Les adresses suivantes sont obtenues pour les différentes fonctions et variables :
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).
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
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
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>
main()
{
static char str[] = "coucou\n";
affiche(str);
}
qui appelle une fonction affiche contenue dans un autre fichier source ESSfct.c :
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 :
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
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 :
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
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
...
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
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.
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)
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.
196
pile pile pile
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é :
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) :
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 *);
affiche_dyn(str);
dlclose(handle);
}
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 :
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.
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.
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.
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
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
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
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
Recherche et tri :
bsearch recherche d’un élément dans un tableau
qsort tri d’un tableau par ordre croissant
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.
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 :
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.
« 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 »
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.
#include <malloc.h>
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 *));
// Définition de la couleur
RComp=128 ;
GComp=0;
BComp=36;
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"
PtRect->XTaille=XTaille;
PtRect->YTaille=YTaille;
PtRect->XPosition=XPosition;
PtRect->YPosition=YPosition;
211
for(CbLigne=0 ; CbLigne<YTaille ; CbLigne ++)
PtRect->Image[CbLigne]=(PIXEL *)malloc(XTaille*sizeof(PIXEL *));
// Initialisation de la couleur
ColorRectangle( PtRect,Couleur);
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);
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.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.
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.
215
MaClasse ::Fonction1(){
// Code source de la fonction
} ;
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 ;
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 : :Fonction3(){
// Code source de la fonction
} ;
Une classe étant définie, on peut à partir de celle-ci créer et manipuler des objets.
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.
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] ();
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 :
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 :
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) ;
L’opérateur delete ne doit être utilisé que dans le cas où l’objet a été créé avec
l’opérateur new.
Ou
PointeurSurObjet->NomVariable
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
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.
Objet.var1=(unsigned char)Objet.var3 ;
PtObjet.var2=Objet.var2 ;
220
Objet.Fonction3 ;
}
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.
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
};
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.
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 ;
} ;
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;
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 +.
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>
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.
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{
};
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 ;
…
}
Si on déclare alors un objet Objet2D, on accédera aux données membres de la façon suivante :
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
classY :public X{
public :
void f() {a=500 ;}
unsigned int a ;
} ;
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 ;
class ClasseBase{
ClasseBase(int a) ;
~ClasseBase() ;
int X;
…
} ;
ClasseBase::ClasseBase(int a){
x=a ;
}
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;
…
} ;
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) ;
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;
…
} ;
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.
} ;
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();
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
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 >>.
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 ;
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;
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
Pour annuler des drapeaux, nous avons la fonction unsetf(). On pourra par exemple
écrire :
cout.unsetf(ios::uppercase | ios::fixed );
237
et la modification :
cout.precision(int);
cout.fill(char) ;
cout.width(int);
238
Exercice 13.6 : on considère le code suivant :
int n ,p;
cin >> n >> p;
// première sortie
cout << 2200. <<"\n"<<54<<endl;
// deuxième sortie
cout << 2200. <<"\n"<<54<<endl;
// Troisième sortie
cout << 2200. <<"\n"<<54<<endl;
// Quatrième sortie
cout << 2200. <<"\n"<<54<<endl;
Qu'obtient on ?
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.
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.
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'.
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.
cout.put('a');
Qu'obtient on ?
Cette fonction écrit sur le flux de sortie les n premiers octets de buffer.
241
13.11 Bibliographie
242