Chapitre 4
Les fonctions
Le meilleure façon de rédiger un programme complexe est de le construire à partir d'éléments
ou de modules indépendants, qu'on assemble un peu comme des briques (On peut aussi penser au
proverbe : diviser pour régner lorsqu'il s'agit de maîtriser une tâche complexe). Chaque langage de
programmation a une façon à lui de désigner et d'organiser ces modules ; en C++, on ne dispose que
d'une seule sorte de module, la fonction. Il existe des fonctions « standards », préprogrammées, qui
économisent bien du travail, et les «fonctions utilisateur», écrite par le programmeur. Je rappelle
que l'ensemble des modules ou fonctions présentes dans un programme est régi par une fonction
principale, qui s'appelle justement « main».
4.1 Les fonctions mathématiques
Toutes les fonctions mathématiques courantes sont prédéfinies en C (un avantage certain sur
Pascal). Les plus courantes sont rassemblées dans le tableau suivant.
Fonction Description Exemple
sqrt(x) racine carrée sqrt(121.0)
¡! 11.0
exp(x) exponentielle,
e x exp(2) ¡! 7.389056
log(x) logarithme à base
e log(7.389056) ¡! 2.0
log10(x) logarithme à base 10 log10(2.0)
¡! 0.30103
fabs(x) valeur absolue fabs(-2.0)
¡! 2.0
ceil(x) arrondi au plus petit ceil(3.2)
¡! 3.0
entier non inférieur à
x ceil(-4.7) ¡! -4.0
floor(x) arrondi au plus grand floor(7.6)
¡! 7.0
entier non supérieur à
x floor(-8.
¡! -9.0
pow(x,y) puissance,
x y pow(2,10) ¡! 1024.0
pow(27.0,0.3333)
¡! 3.0
sin(x) sinus (argument en radian) sin(1.5707963)
¡! 1.0
cos(x) cosinus (idem) cos(1.5707963)
¡! 0.0
tan(x) tangente (idem) tan(0.0)
¡! 0.0
Comme pour la lecture et l'écriture, ces fonctions ne font pas partie du langage C++ au sens
strict : il faut donc prévenir le compilateur que l'on souhaite les utiliser, en insérant au début la
directive
#include <cmath> . Ceci n'est pas suffisant pour certains systèmes ; il faudra encore, au
moment de la compilation, indiquer que l'on veut utiliser une librairie de fonctions mathématiques
« -lm » pour Linux).
Les fonctions mathématiques convertissent automatiquement leur argument dans le «type
double» (double précision, 13 chiffres significatifs, 8 octets) et renvoient un résultat de même
20
Intro_C
21
type. La notion de «type» sera détaillée un peu plus loin.
Pour C++, le générateur de nombres aléatoires n'est pas une fonction mathématique. La fonction
correspondante s'appelle
rand() (sans arguments). Le résultat est un nombre entier aléatoire
compris entre 0 et
RAND_MAX (en majuscules). Ces deux entités sont définies dans un autre fichier
d'entête, « cstdlib».
4.2 Fonctions de l'utilisateur
J'aborde maintenant l'écriture de fonctions propres à l'utilisateur. Toute fonction doit en principe
correspondre à une tâche précise et bien définie et son nom doit refléter cette tâche. De plus,
une fonction bien conçue est appelée à être réutilisée souvent et donc à économiser du temps de
programmeur. Voici un premier exemple simpliste, une fonction qui calcule le cube d'un entier.
Elle fait partie d'un programme qui dresse la table des cubes des dix premiers entiers.
1
/ ? f_cube . cpp : exemple de f onc t i on ? /
2
#include <ios t ream>
3
#include <c s t d l i b >
4
using namespace s td ;
5
int cube ( int ) ;
6
int main ( void ){
7
int x ;
8
for ( x = 1 ; x <= 1 0 ; x++)
9 cout << x << "\ t " << cube ( x ) << endl ;
10 system( " pause " ) ;
11
return 0 ;
12 }
13
int cube ( int y ){
14
return y ? y ? y ;
15 }
On a vu, dans les chapitres précédents, que ce programme, grâce à l'entête
<iostream> , pouvait
utiliser les fonctions d'écriture comme
cout << . La ligne 5 joue un peu le même rôle : elle prévient
le compilateur que je vais utiliser une fonction recevant un argument entier, dont le résultat sera
aussi un entier et qui s'appellera cube. Le compilateur pourra ainsi vérifier que, tout au long du
programme, mes instructions seront conformes à cette définition. La ligne 5 est le « prototype »
de la fonction cube ou encore sa déclaration. On peut mentionner des noms de variables dans la
déclaration ; le compilateur n'en tient aucun compte, mais cela peut aider à la compréhension du
programme. Si la fonction ne renvoyait aucune information vers le programme principal (comme
par exemple une fonction destinée à afficher un message), il faudrait la déclarer de type
void .
La fonction principale commence à la ligne 6 ; elle contient essentiellement une boucle « for»
qui répète 10 fois la ligne 9, où se fait tout le travail. Cette ligne contient un «appel» de la fonction
cube
, avec son «argument effectif», x.
La fonction
cube elle-même est définie lignes 13-15 ; elle se présente de façon assez semblable à
main
: une entête où sont précisés le type de la fonction, son nom, le type et le nom de l'argument. Le
corps de la fonction (entre accolades) est ici réduit à peu de chose : le résultat ligne 14. Remarquez
que le prototype est suivi d'un point-virgule, alors que l'entête est suivie d'une accolade ouvrante
qui marque le début du corps.
Le fonctionnement du programme est simple. L'ordinateur exécute l'une après l'autre les instructions
de
main . Lorsqu'il parvient à la ligne 9, il imprime la valeur de x , puis exécute les
instructions contenues dans la fonction (une seule ici), en remplaçant « l'argument formel»
y par
la valeur courante de
x . L'identificateur cube contient la valeur de x 3 . L'ordinateur reprend la suite
des instructions de
main , c'est-à-dire qu'il imprime la valeur de x 3 , incrémente x et recommence
tout, tant que la condition de contrôle est vérifiée.
Intro_C
22
Voici un deuxième exemple, la recherche du maximum de trois nombres.
1
/ ? max3 . cpp : maximum de t r o i s nombres ? /
2
#include <ios t ream>
3
#include <c s t d l i b >
4
using namespace s td ;
5
int maximum( int x , int y , int z ){
6
int max = x ;
7
i f ( y > max) max = y ;
8
i f ( z > max) max = z ;
9
return max ;
10 }
11
int main ( void ){
12
int a , b , c ;
13 cout << "donnez t r o i s e n t i e r s : " ;
14 c in >> a >> b >> c ;
15 cout << "Le plus grand e s t : " << maximum( a , b , c ) << endl ;
16 system( " pause " ) ;
17
return 0 ;
18 }
J'ai adopté ici une présentation différente. De même que l'on peut déclarer et initialiser une variable
en seule instruction (
int uvw = 321 ), on peut déclarer et définir une fonction en une fois. Dans
ce cas le prototype est inutile et disparaît. Il semble que la majorité des programmeurs préfère la
première présentation (déclaration et définition séparées), peut-être parce que l'ensemble est plus
lisible si le corps de la fonction est volumineux.
4.3 Types et conversion de type
4.3.1 des types différents
Toutes les variables d'un programme en C++ doivent avoir un type ; celui-ci peut être soit
prédéfini par le langage soit défini par l'utilisateur. On peut comparer la mémoire de l'ordinateur à
une bibliothèque, avec des casiers de tailles différentes : des petits casiers pour les livres de poche,
des gros casiers pour les atlas. En ce qui concerne les variables numériques, il existe 8 types prédéfinis,
auxquels on peut encore rattacher le type « char», très voisin d'un entier. Ils sont listés dans
le tableau ci-dessous, par ordre de taille décroissante (et donc de nombre de chiffres significatifs
décroissant).
spécification spécification
Type pour printf pour scanf
long double %Lf %Lf
double %f %lf
float %f %f
unsigned long int %lu %lu
long int %ld %ld
unsigned int %u %u
int %d %d
short %hd %hd
char %c %c
Tous les compilateurs ne reconnaissent pas tous ces types, en particulier « long double». J'ai
indiqué dans le même tableau les codes de formattage utilisés en C pour les entrées-sorties ; remar
Intro_
C
23
quez le piège classique : la spécification de format des nombres en double précision, très commun
en calcul scientifique, est différente pour la lecture et l'écriture ! Les utilisateurs de C++ n'ont pas
à tenir compte de cette remarque, puisque
cin et cout formattent automatiquement les données
qui leur sont soumises.
4.3.2 promotion
J'ai déjà dit que les fonctions de la bibliothèque mathématique attendaient un argument de
type « double» ; je peux quand même calculer la racine carrée de 4 par l'expression
sqrt(4) . Dans
ce cas, il y a « promotion (conversion) automatique» de l'argument en son équivalent
double , soit
4.00, cela sans perte d'information (je peux ranger une livre de poche dans le casier destiné à une
encyclopédie). Le résultat sera donné en double précision.
La même opération de promotion a lieu chaque fois que j'écrit une expression en mélangeant
des types ; tous les arguments sont temporairement convertis dans le type le plus précis. Ainsi, si
x = 1.0
et y sont des float , alors que a = 2 est un int , l'affectation y = x + a + 1; donnera
à
y la valeur 4.00. Il est toutefois peu prudent de faire aveuglément confiance à ce mécanisme. De
plus, le compilateur va protester si j'écris simplement
y = a .
D'autre part, il faut faire attention aux divisions entre entiers. Ainsi, le fragment de programme
int a = 2, b = 3, c = 7;
cout << a/b << '\t' << c/a << endl;
affichera
0 3 Pour obtenir des résultats plus précis (mais fractionnaires), il faut convertir l'un
(au moins) des facteurs en un nombre fractionnaire. Ceci se fait proprement par un «transtypage»
(«cast» en anglais) :
int a = 2, b = 3, c = 7;
cout << (double)a/b << '\t' << (double)c/(double)a << endl;
Cette écriture est conforme à la norme C et acceptée en C++. En C++, il faut en principe écrire
static_cast<double>(a)/b
.
4.3.3 dégradation
La conversion vers un type moins précis est en fait une dégradation (comme si je voulais
absolument faire pénétrer un dictionnaire dans le casier d'un livre de poche). Si j'appelle la fonction
cube
du paragraphe précédent (qui attend un argument entier) avec un argument fractionnaire,
cube(2.7)
, celui-ci sera tronqué à sa partie entière et j'obtiendrais le résultat 8 au lieu de 19.683.
Pour prendre volontairement la partie entière, il existe les fonctions
floor et ceil , décrites plus
haut.
4.4 Passage des arguments
Il y a en principe deux façons simples de transmettre un (ou des) argument(s) à une fonction :
le « passage par valeur» et le « passage par référence» (on dit aussi « passage par adresse »). En
C++, sauf recours à un formalisme spécial, les arguments simples (type entier, flottant, double,
caractère) sont passés par valeur, ce qui veut dire qu'une copie de l'argument d'appel est transmise
à la fonction. Ceci a un avantage évident : si l'argument est modifié dans le corps de la fonction, cela
n'affecte pas la variable du programme principal. Dans d'autres langages (Fortran), on pratique
l'appel par référence : toute modification de l'argument dans la fonction appelée est répercutée dans
le programme principal. La convention du C++ a aussi un inconvénient : comme la fonction ne peut
(par l'intermédiaire du mot réservé
return ) renvoyer qu'une valeur unique, comment pourrais-je
construire une fonction dont le résultat serait un ensemble de valeurs (composantes d'un vecteur
par exemple) ? La solution sera abordée dans un prochain chapitre.
Intro_C
24
4.5 portée des variables
À partir du moment on l'on commence à décomposer un programme en blocs et en fonctions,
on doit se demander quel est le domaine de validité de chaque variable ou dans quelle portion du
programme chaque variable est définie.
Si un programme se compose de plusieurs blocs, il est possible de définir des variables à l'intérieur
de chaque bloc, ces variables étant invisibles à l'extérieur de leur bloc de définition, comme
dans l'exemple un peu artificiel qui suit.
1
// por t e e1 . cpp : p o r t é e des v a r i a b l e s
2
#include <ios t ream>
3
#include <c s t d l i b >
4
using namespace s td ;
5
void fonc ( int ) ;
6
int nb = 1000 , val = 128;
7
int main ( void ){
8 cout << " d i v e r s e s v a l e ur s des v a r i a b l e s : \n" ;
9 cout << "\t
¡ nb dans main : " << nb << "\n\n" ;
10 cout << "\t
¡ val dans main : " << val << "\n\n" ;
11 fonc ( 1 0 0 ) ;
12 cout << "\t
¡ nb apr e s fonc : " << nb << "\n" ;
13 system( " pause " ) ;
14
return 0 ;
15 }
16
void fonc ( int nb){
17 cout << "nb au debut de fonc : " << nb << endl << endl ;
18 cout << " val au debut de fonc : " << val << endl << endl ;
19 {
20 nb = 2 0 ;
21 cout << "nb dans l e premier bloc de fonc : " << nb << "\n\n" ;
22 }
23 {
24 nb = 3 0 ;
25 cout << "nb dans l e deuxieme bloc de fonc : " << nb <<endl << endl ;
26 cout << " val dans l e deuxieme bloc de fonc : " << val << "\n\n" ;
27 }
28 }
J'ai défini « au niveau global» ou encore « à la profondeur 0», c'est à dire en dehors de toute fonction
ou bloc, des entiers
nb = 1000, val = 128 . Ces objets (identificateurs) sont visibles de tout le
programme, sauf s'ils sont masqués par une définition ultérieure (voir plus loin). On peut définir
des variables dans une fonction ou à l'intérieur d'un bloc (entre accolades). Ces définitions sont
locales au bloc ou à la fonction. Les variables déclarées dans un bloc (fonction) ne sont visibles que
dans ce bloc (fonction) et dans les sous-blocs qu'il (ou elle) contient. D'autre part, une déclaration
masque toutes les déclarations d'une variable de même nom à une profondeur inférieure.
L'exécution de portée1.exe donne comme résultat
Intro_C
25
diverses valeurs des variables:
- nb dans main: 1000
- val dans main: 128
nb au debut de fonc: 100
val au debut de fonc: 128
nb dans le premier bloc de fonc: 20
nb dans le deuxieme bloc de fonc: 30
val dans le deuxieme bloc de fonc: 128
- nb apres fonc: 1000
La logique de ce programme peut être représentée par le dessin ci-contre. Le programme est
contenu dans le fichier
portee1.cpp , analogue à une grande boite et qui constitue l'espace de travail
du compilateur. À l'intérieur, on trouve quatre objets,
nb, val,main et fonc , dont deux sont aussi
des boites. Ces déclarations sont globales. Dans
main , on s'intéresse aux deux variables nb et val .
Comme on ne trouve pas de déclaration dans
main , ce sont les initialisations précédentes qui sont
valables.
main appelle fonc avec l'argument nb = 100 et c'est cette valeur qui a cours à l'intérieur
de la boite
fonc . Seulement, fonc contient deux autres boites, anonymes et qui contiennent chacune
une définition de
nb . C'est cette définition qui est la bonne dans la boite où elle se trouve.
Intro_C
26
Intro_C
portee1.cpp
nb = 1000 ; val = 128
main
cout
<< nb ; cout << val ;
fonc(100) ;
cout
<< nb ;
fonc(nb)
cout
<< nb ; cout << val ;
nb = 20 ; cout
<< nb ;
nb = 30 ; cout
<< nb ; cout << val
Intro_C
27
Les déclarations de fonctions sont toujours globales (à la différence de Pascal, où l'on peut définir
une fonction à l'intérieur d'une autre fonction. Voici encore un exemple, à peine plus compliqué.
Pour gagner de la place, j'ai fait figurer plusieurs instructions sur la même ligne, ce qui ne favorise
pas la lisibilité ; elles sont bien sûr lues et exécutées de gauche à droite.
1
// por t e e2 . cpp : aut r e exemple de p o r t e e des v a r i a b l e s
2
#include <ios t ream>
3
#include <c s t d l i b >
4
using namespace s td ;
5
void a ( void ) ; void b( void ) ; void c ( void ) ;
6
int x = 1 ;
7
int main ( void ){
8
int x = 8 ;
9 cout << "x , d e f i n i dans main , vaut : " << x << endl ;
10 a ( ) ; b ( ) ; c ( ) ;
11 a ( ) ; b ( ) ; c ( ) ;
12 cout << "x , d e f i n i dans main , vaut : " << x << endl ;
13 system( " pause " ) ;
14
return 0 ;
15 }
16
void a ( void ){
17
int x = 2 5 ;
18 cout << "x au debut de a : " << x << endl ;
19 x++;
20 cout << "x a l a f i n de a : "<< x << endl ;
21 }
22
void b( void ){
23
s tat ic int x = 5 0 ;
24 cout << "x ( s t a t i c ) au debut de b : "<< x << endl ;
25 x++;
26 cout << "x ( s t a t i c ) a l a f i n de b : " << x << endl ;
27 }
28
void c ( void ){
29 cout << "x ( g l o b a l ) au debut de c : " << x << endl ;
30 x
? = 1 0 ;
31 cout << "x ( g l o b a l ) a l a f i n de c : " << x << endl ;
32 }
J'ai introduit une nouveauté, le type de variable
static ; une telle variable conserve sa valeur
entre deux appels de la fonction. Essayez de prévoir ce que fera ce programme avant de regarder
le résultat.
Intro_C
28
x, defini dans main, vaut : 8
x au debut de a: 25
x a la fin de a: 26
x(static) au debut de b: 50
x(static) a la fin de b: 51
x(global) au debut de c: 1
x(global) a la fin de c: 10
x au debut de a: 25
x a la fin de a: 26
x(static) au debut de b: 51
x(static) a la fin de b: 52
x(global) au debut de c: 10
x(global) a la fin de c: 100
x, defini dans main, vaut : 8
4.6 La récurrence
Beaucoup d'objets mathématiques peuvent être définis de manière récursive, par une relation
de récurrence. Le langage C++ permet de même des définitions de fonctions par récurrence. La
fonction factorielle est l'exemple traditionnel dans ce domaine. Sa définition mathématique explicite
est
n
! = n ¢ ( n ¡ 1) ¢ ( n ¡ 2) ¢ ¢ ¢ 2 ¢ 1 :
Je peux aussi utiliser la définition récursive équivalente
n
! = n ¢ ( n ¡ 1)!
En C++, j'écrirai soit un fragment de programme itératif :
fact = 1;
for( cptr = n; cptr >= 1; cptr--)
fact *= cptr;
soit un programme appelant une fonction définie par récurrence :
1
/ ? facto_r . cpp : f a c t o r i e l l e , forme r é c u r s i v e ? /
2
#include <ios t ream>
3
#include <c s t d l i b >
4
using namespace s td ;
5
int f a c t ( int ) ;
6
int main ( void ){
7
int i ;
8
for ( i = 1 ; i <= 1 0 ; i++)
9 cout << i << " ! = " << f a c t ( i ) << endl ;
10
return 0 ;
11 }
12
int f a c t ( int x ){
13
i f ( x <= 1)
14
return 1 ;
15
el se
16
return ( x ? f a c t (x ¡ 1) ) ;
17 }
La programmation récursive peut être très élégante et concise. Elle souffre de deux inconvénients.
Les risques d'erreur spectaculaire sont grands. Si je me trompe dans la condition d'arrêt ou
Intro_C
29
dans la définition de la fonction, la récurrence peut devenir infinie : il n'y a plus qu'à arracher la
prise de courant. De plus, le temps de calcul est souvent élevé, car l'ordinateur doit effectivement
évaluer toutes les valeurs intermédiaires de la fonction récurrente,
fact ici.
4.7 Passage d'arguments par référence
On a vu que les arguments «simples» d'une fonction (nombres, caractères) étaient transmis
«par valeur» : la fonction reçoit une copie de l'argument. Si cette pratique accroît la sécurité de
la programmation (il est impossible de modifier, depuis la fonction, une variable du programme
principal), elle ne facilite pas les communications entre fonctions. Si une fonction calcule des valeurs,
comment renvoyer ces données dans le programme principal ? C'est possible pour une valeur
unique : l'intruction
return permet justement d'affecter à l'identificateur de la fonction une valeur
visible du programme appelant.
Les tableaux sont traités de façon diamètralement opposée : ils sont transmis «par référence» ou
«par adresse». Tout se passe comme si la fonction et le programme appelant partageait les mêmes
données : toute modification apportée à un élément du tableau dans la fonction est immédiatement
répercutée dans
main . L'utilisation de tableaux comme arguments de fonctions est détaillée dans
le chapitre 6.
Il serait commode de pouvoir transmettre plusieurs valeurs de nature différente d'une fonction
à une autre : comme elles sont de nature différente, elles ne peuvent pas être des éléments d'un tableau.
C++ offre cependant cette possibilité, appelée «transmission par référence». Un paramètre
par référence est un pseudonyme (un alias) de l'argument correspondant. Pour passer un paramètre
par référence, il suffit de faire suivre le type du paramètre (dans l'entête et dans le prototype de la
fonction) par une esperluette (&). Les connaisseurs du C peuvent considérer que ce mécanisme est
une version simplifiée du passage par adresse, à l'aide d'un pointeur. Le programme suivant met
en oeuvre le passage normal (par valeur) et le passage par référence.
1
// pas sag e . cpp : pas sag e d ' argument par v a l e u r e t par r e f e r enc e
2
#include <ios t ream>
3
#include <c s t d l i b >
4
using namespace s td ;
5
int cubeVal ( int ) ;
6
void cubeRef ( int &);
7
int main ( void ){
8
int a = 3 , b = ¡ 5;
9 cout << "a avant cubeVal : " << a << endl ;
10 cout << " r e s u l t a t de cubeVal : " << cubeVal ( a ) << endl ;
11 cout << "a apr e s cubeVal : " << a << endl ;
12 cout << "b avant cubeRef : " << b << endl ;
13 cubeRef (b ) ;
14 cout << "b apr e s cubeRef : " << b << endl ;
15 system( " pause " ) ;
16
return 0 ;
17 }
18
int cubeVal ( int aval ){
19
return aval ? = aval ? aval ;
20 }
21
void cubeRef ( int & aRef ){
22 aRef
? = aRef ? aRef ;
23 }
avec le résultat :
Intro_C
30
a avant cubeVal: 3
resultat de cubeVal: 27
a apres cubeVal: 3
b avant cubeRef: -5
b apres cubeRef: -125
Remarquez, ligne 21, que la manipulation d'un paramètre passé par référence est identique à
celle d'un paramètre normal.
Il est possible (encore que d'un intérêt faible) d'utiliser un alias dans le corps d'une fonction,
comme dans l'extrait ci-dessous.
int i = 1;
int &iRef = i;
++iRef; //i est incrémenté par l'intermédiaire de son pseudo
Toute variable qui en référence une autre doit être initialisée au moment de sa déclaration ; on
peut comprendre cette contrainte en remarquant que
iRef , par exemple est déclarée comme une
copie, mais une copie de quoi demande le compilateur ? On doit répondre immédiatement à cette
question.
Chapitre 5
Entrées et sorties
5.1 Généralités
Un programme, écrit par exemple en C++, doit souvent lire des données pour pouvoir fonctionner
; il écrit fréquemment des résultats.
On dit que l'échange de données entre un programme et l'extérieur fait intervenir un « flot »,
c'est à dire une suite d'octets. Pour l'utilisateur (mais pas pour la machine), chacun de ces octets a
une signification : lettre, nombre, pixel, échantillon de son... Pendant une opération de lecture (plus
généralement d'entrée de données), les octets passent d'un dispositif extérieur (clavier, disquette,
modem) à la mémoire centrale. Pendant une opération d'écriture (sortie de données), les octets
passent de la mémoire centrale vers un dispositif extérieur (écran, imprimante, disque).
On distingue les E/S (entrées/sorties) de bas niveau (non-formattées), où l'on se contente de
spécifier le nombre d'octets à transmettre entre tel et tel partenaire, et les E/S de haut niveau
(formattées), où les octets sont regroupés en ensembles significatifs pour l'utilisateur (nombres
entiers, fractionnaires, chaînes de caractères). Je ne parlerai pas des premières, les secondes suffisant
à toutes les applications habituelles.
Les manuels emploient souvent les expressions de «dispositif standard de sortie», l'écran, et
de «dispositif standard d'entrée», le clavier. Vous avez déjà constaté que l'on pouvait capter des
données tapées au clavier et afficher des résultats à l'écran très simplement à l'aide des «opérateurs»
cin
et cout . Dans ce chapitre, je vais détailler les propriétés de ces opérateurs, je montrerai
comment on peut personnaliser les affichages et j'expliquerai comment on peut lire et écrire dans un
fichier sur disque ou sur disquette. Ce dernier point est évidemment important pour les applications,
mais aussi dans le cadre de l'enseignement. Pendant la mise au point d'un programme, il est
fastidieux de retaper les données à chaque essai et bien plus commode de les lire sur le disque dur.
Une dernière remarque générale. Les concepteurs du C++ se sont donné du mal pour que ces
opérations de lecture et d'écriture soient «robustes», ce qui signifie que tous les types classiques de
données sont lus ou écrits correctement, sans précaution particulière (ce qui est loin d'être le cas
en C pur).
5.2 Les opérateurs d'insertion et d'extraction
L'écriture à l'écran se fait, comme nous le savons, à l'aide de « l'opérateur»
<< , qui insère les
éléments à afficher dans le flot de sortie représenté par
cout (dans le sens des flèches). Je rappelle
que l'on peut écrire indifféremment
cout << "Bienvenue à tous" << endl;
\\
cout << "Bienvenue à tous\n";
31
Intro_C
32
\\
cout << "Bienvenue";
cout << " à";
cout << " tous";
cout << endl;
\\
cout << "Bienvenue" << " à"
<< " tous" << '\n';
(associativité de gauche à droite et équivalence du caractère d'échappement
\n avec le « manipulateur
de flot»,
endl ).
Les mêmes règles s'appliquent à l'affichage des nombres, entiers ou fractionnaires ; l'opérateur
<<
est « assez malin » pour savoir à quel type de donnée il a affaire et pour agir en conséquence.
Symmétriquement, la lecture des données se fait au moyen de « l'opérateur d'extraction du flot
d'entrée»,
>> , dans la direction des flèches.
Les opérateurs
<< et >> ont une priorité élevée : il ne faut donc pas hésiter à utiliser des
parenthèses, comme dans le morceau de code suivant, pour être sûr de l'interprétation.
cout << "donnez deux entiers: ";
cin >> x >> y;
cout << x << (x == y ? "est " : "n'est pas ") << "égal à" << y ;
qui n'est pas correctement compilé si l'on enlève les parenthèses.
Lorsque l'on veut lire une série de données en nombre inconnu, on peut utiliser une boucle «
while», comme ceci
cout << "entrez un nombre (fin-de-fichier pour arrêter): ";
while( cin >> nb){
.............
cout << "entrez un nombre (fin-de-fichier pour arrêter): ";
}
La lecture s'interrompra lorsque l'utilisateur tapera « ctrl-Z» (fin de fichier). L'extraction fournit
un résultat nul, interprété comme faux.
On parvient au même résultat en examinant un par un les caractères entrés et en interrompant
la lecture dès qu'on détecte le caractère fin-de-fichier (EOF) :
char c;
while ( (c = cin.get()) != EOF) {
.........
}
5.3 Opérateurs de mise en forme des nombres
Les entêtes de la plupart des fonctions décrites dans ce paragraphe se trouvent dans le fichier
« iomanip», qu'il faut appeler par
#include <iomanip> ; cette bibliothèque contient <iostream> ,
il est donc en principe inutile d'appeler cette dernière, mais tous les compilateurs ne sont pas au
courant.
5.3.1 nombre de chiffres significatifs
Pour les applications scientifiques et techniques, il est commode de pouvoir choisir le nombre
de chiffres après la virgule ; il existe deux méthodes pratiques de le faire, montrées dans l'exemple
ci-dessous.
Intro_C
33
1
// decimal . cpp : nombre de de c imal e s
2
#include <iomanip>
3
#include <c s t d l i b >
4
using namespace s td ;
5
int main ( void )
6 {
7
double r2 = s q r t ( 2 ) ;
8
int nbchs ;
9 cout << " spontanement : " << r2 << endl ;
10 cout << " avec l a f o n c t i o n cout . p r e c i s i o n : " << endl ;
11
for ( nbchs = 1 ; nbchs <= 1 0 ; nbchs++){
12 cout . p r e c i s i o n ( nbchs ) ;
13 cout << r2 << endl ;
14 }
15 cout << " spontanement : " << r2 << endl ;
16 cout << " avec l e manipulateur s e t p r e c i s i o n : " << endl ;
17
for ( nbchs = 1 ; nbchs <= 1 0 ; nbchs++)
18 cout << s e t p r e c i s i o n ( nbchs ) << r2 << endl ;
19 cout << " spontanement : " << r2 << endl ;
20 system( " pause " ) ;
21
return 0 ;
22 }
avec le résultat
spontanement: 1.41421
avec la fonction cout.precision:
1
1.4
1.41
1.414
1.4142
1.41421
1.414214
1.4142136
1.41421356
1.414213562
spontanement: 1.414213562
avec le manipulateur setprecision:
1
1.4
1.41
1.414
1.4142
1.41421
1.414214
1.4142136
1.41421356
1.414213562
spontanement: 1.414213562
Spontanément, C++ affiche
p 2 avec 5 chiffres après la virgule ; j'ai modifié ce comportement
d'abord à l'aide de la fonction
cout.precision(n) , dont l'argument est le nombre de chiffres puis
avec le « manipulateur»
setprecision(n) . Remarquez que l'effet produit sur le nombre de chiffres
Intro_C
34
est permanent, tant qu'une nouvelle instruction ne vient pas le modifier.
5.3.2 largeur de champ
Il est possible de choisir la largeur de la zone (champ) où va apparaître une donnée, grâce à la
fonction
cout.width ou en insérant le manipulateur setwidth(n) .
1
//l_champ . cpp
2
#include <iomanip>
3
#include <c s t d l i b >
4
using namespace s td ;
5
int main ( void ){
6
double mi l l e = 1000 , c e n tmi l l e = 1 0 0 0 0 0 . 5 ;
7 cout << " spontanement : " << endl
8 << mi l l e << '
? ' << c e n tmi l l e << ' ? ' <<endl << endl ;
9
for ( int nbchs = 3 ; nbchs <= 8 ; nbchs++){
10 cout . width ( nbchs ) ;
11 cout << mi l l e << '
? '<< c e n tmi l l e << ' ? ' << endl ;
12 }
13 cout << endl ;
14 cout << " spontanement : " << endl
15 << mi l l e << '
? '<< c e n tmi l l e << ' ? ' << endl << endl ;
16
for ( int nbchs = 3 ; nbchs <= 8 ; nbchs++)
17 cout << setw ( nbchs ) << mi l l e << '
? ' << c e n tmi l l e << ' ? ' << endl ;
18 cout << endl ;
19 cout << " spontanement : " << endl
20 << mi l l e << '
? ' << c e n tmi l l e << ' ? ' << endl ;
21 system( " pause " ) ;
22 }
Avec le compilateur que j'utilise, j'obtiens le même résultat dans les deux cas. Au contraire des
modifications de précision, ces instructions n'ont pas d'effet permanent : il faut les renouveler pour
chaque objet à imprimer.
Intro_C
35
spontanement:
1000*100000*
1000*100000*
1000*100000*
1000*100000*
1000*100000*
1000*100000*
1000*100000*
spontanement:
1000*100000*
1000*100000*
1000*100000*
1000*100000*
1000*100000*
1000*100000*
1000*100000*
spontanement:
1000*100000*
C++ est très économe sur les zéros qu'il affiche : le nombre 1.2 sera affiché tel quel, même
si la précision demandée est de 6 chiffres après la virgule. D'autre part, C++ fait de son mieux
pour corriger les erreurs de programmation :
mille et centmille seront affichés tant bien que mal,
même si vous ne réservez que deux caractères pour le faire. Le cas de
centmille (100000.5) est
particulier : si on n'indique pas la précision, C++ imprime 6 chiffres (100000), la largeur du champ
ne fait rien à l'affaire. Enfin, vous remarquez que le nombre 1000 apparaît à droite du champ. Cous
trouverez dans les manuels plus détaillés des instructions pour modifier ce cadrage.
5.3.3 notation scientifique
Pour les nombres très grands ou très petits, il est préférable d'employer la notation scientifique.
C++ sait le faire, à condition de lui demander.
1
// s c i e n t i f . cpp : format s pour l e s nombres
2
#include <ios t ream>
3
#include <c s t d l i b >
4
using namespace s td ;
5
int main ( void ){
6
double r2 = s q r t ( 2 ) , micro = 1.234 e ¡ 9, mega = 1.234 e9 ;
7
int nb ;
8 cout << " spontanement : " << ' \ t ' << ' \ t ' << r2 << " " << micro
9 << " " << mega << endl ;
10 cout . s e t f ( i o s : : s c i e n t i f i c ) ;
11 cout << " no t a t i on s c i e n t i f i q u e : " << ' \ t ' << r2 << ' \ t '
12 << micro << ' \ t ' << mega << endl ;
13 cout . uns e t f ( i o s : : s c i e n t i f i c ) ;
14 cout . s e t f ( i o s : : f i x e d ) ;
15 cout << " v i r g u l e f i x e : " << ' \ t ' << ' \ t ' << r2 << " " << micro
16 << " " << mega << endl ;
17 cout . uns e t f ( i o s : : f i x e d ) ;
18 cout << " spontanement : " << ' \ t ' << ' \ t ' << r2 << " "<< micro
Intro_C
36
19 << " " << mega << endl ;
20 system( " pause " ) ;
21 }
Ce programme utilise des «drapeaux» (flags) que l'on peut installer (
set ) ou désinstaller ( unset ).
Ils se trouvent dans la bibliothèque
ios ; plutôt que de lire celle-ci en entier, avec un include , je
vais chercher les objets qui m'intéressent, avec l'opérateur de résolution de portée
::
spontanement: 1.41421 1.234e-09 1.234e+09
notation scientifique: 1.414214e+00 1.234000e-09 1.234000e+09
virgule fixe: 1.414214 0.000000 1234000000.000000
spontanement: 1.41421 1.234e-09 1.234e+09
Toutes les fonctions et manipulateurs précédents peuvent se combiner dans un même programme,
avec des résultats que je vous laisse le soin de découvrir.
5.3.4 un parfum de classe
Vous avez du remarquer que les identificateurs de nombreuses fonctions spécialisées de C++
sont de la forme nom1.nom2(), comme
cin.get() ou cout.width() . On voit apparaître ici une
trace de ce qui fait l'originalité du C++, la possibilité de définir des classes (ou des objets). Je peux
donner une idée simpliste de ce dont il s'agit par analogie avec le type « record (enregistrement)»
du Pascal. Cette structure de donnée est commode lorsque l'on veut rassembler des données de
nature différente ayant un point commun (nom, prénom, âge, taille et sexe d'un même individu
par exemple). On définit alors un enregistrement à plusieurs champs, auxquels on accède par
des identificateurs comme
rec1.nom , rec1.prenom , rec1.age . La construction correspondante
existe en C/C++, elle s'appelle une
struct , tout simplement. Le C++ pousse l'idée un cran
plus loin : une classe (en simplifiant) est une sorte d'enregistrement qui contient non seulement
des données mais encore des définitions de fonctions, capable d'opérer sur ces données. Ainsi, la
fonction
cout.width() est un « membre» de la classe cout .
5.4 Les fichiers
Les programmes traitent souvent de grandes quantités de données, et peuvent aussi produire
énormément de résultats. Il serait tout à fait impossible de saisir à la main ces monceaux d'octets
ou de les analyser en temps réel sur l'écran. Pour assembler, archiver ou analyser beaucoup de
données, on a recours à des fichiers. Un fichier réside sur un organe de stockage, disque, disquette,
CD, etc. Je donne ici quelques éléments d'un vaste sujet.
Nous allons nous intéresser aux fichiers « séquentiels», où les octets sont rangés en file, sans
structure spéciale autre que celle prévue par le programmeur. Il existe aussi des fichiers « à accès
aléatoire», où l'on peut aller chercher une information connaissant son rang (comme les plages d'un
disque). Ils ne sont pas abordés ici.
La manipulation d'un fichier en C++ ressemble beaucoup à celle des entrées-sorties habituelles,
et c'est normal : tout est fait pour qu'un quelconque périphérique se présente comme un fichier.
L'ensemble des fonctions utiles est déclaré dans le fichier
fstream qu'il faut donc inclure au début
du programme.
Le processus de déclaration d'un fichier est à peu près le même quelque soit le langage. On commence
par établir une relation entre le nom du fichier tel qu'il est ou sera connu sur le périphérique
(disque par exemple) et une variable qui le représente dans le programme (
ASSIGN en Pascal). On
précise ensuite s'il s'agit de lire, créer ou ajouter dans un fichier. Il ne reste plus qu'à lire, écrire
ou rajouter. Les choses vont paraître un peu mystérieuses parce que je ne veux pas entrer dans
Intro_C
37
le détail des opérations sur les classes et que je ne présente que ce qui ressemble à des fonctions
tordues.
5.4.1 Création d'un fichier
Examinons le petit programme qui suit.
1
// c r e_f i ch . cpp : c r é a t i on d 'un f i c h i e r s é q u e n t i e l .
2
#include <ios t ream>
3
#include <f s t ream>
4
#include <c s t d l i b >
5
using namespace s td ;
6
int main ( void ){
7
int nb1 = 1234; double nb2 = 5 . 6 7 8 9 ;
8
char msg [ ] = " bi en l e bonjour chez vous ! " ;
9 of s t r eam f i c h ( "D: / CoursC/ g l o b a l / e s s a i . dta " , i o s : : out ) ;
10 f i c h << msg << endl ;
11 f i c h << nb1 << ' \ t ' << nb2 << endl ;
12 cout << " f i n " << endl ;
13 system( " pause " ) ;
14
return 0 ;
15 }
Il crée un fichier et y inscrit une phrase et deux nombres ; on peut vérifier que le contenu de
essai.dta
est bien
bien le bonjour chez vous!
1234 5.6789
À la ligne 9 je définis l'objet
fich et je l'initialise pour qu'il corresponde au fichier extérieur
D:\CoursC\global\essai.dta
. J'ajoute ios::out pour indiquer que je veux écrire dans le fichier
(cette mention est facultative). Si le fichier n'existe pas, il sera créé ; s'il existe, son contenu sera
écrasé. On peut éviter cette issue fâcheuse en remplaçant
ios::out par ios::app , qui demande
que les données soient ajoutées à la fin du fichier existant. On pourrait procéder en deux étapes :
établir la relation d'abord et ouvrir le fichier plus tard, par les lignes suivantes.
ofstream fich;
.........
fich.open("D:\CoursC\global\essai.dta", ios::out);
Le fichier ainsi créé est, comme on dit, un fichier texte : il peut être lu par n'importe quel
éditeur de texte, ce qui est commode. Le programme précédent pourrait être amélioré en prévoyant
la conduite à tenir si on ne parvient pas à ouvrir le fichier.
Le fichier
fich sera fermé automatiquement lorsque le programme se terminera (à la différence
du C et du Pascal). On peut fermer explicitement les fichiers dont on n'a plus l'usage par
fich.close();
L'imprimante est considérée comme un fichier dans lequel on peut écrire ; il est donc possible
d'imprimer en remplaçant dans le programme
"D:\CoursC\global\essai.dta" par le nom de
fichier de l'imprimante, souvent
LPT1: . Ceci est vrai pour un fonctionnement dans une fenêtre
DOS, mais pas sous Windows ni sans doute lorsque l'imprimante est accessible par l'intermédiaire
d'un réseau.
Intro_C
38
5.4.2 Lecture d'un fichier
Le procédé, très voisin du précédent, est illustré ci-dessous. Avec un éditeur de texte ou avec
un programme, j'ai créé le fichier param.txt dont le contenu figure ci-dessous.
mardi
123
0.0258
jeudi
654
10.98
Le programme
lec_fich.cpp
1
// l e c_f i c h . cpp : l e c t u r e d 'un f i c h i e r s é q u e n t i e l .
2
#include <ios t ream>
3
#include <f s t ream>
4
#include <c s t d l i b >
5
using namespace s td ;
6
int main ( void ){
7
int nb1 ; double nb2 ; char j our [ 2 0 ] ;
8 i f s t r e am f i c h ( "D: / CoursC/ g l o b a l /param. txt " , i o s : : in ) ;
9
for ( int i = 1 ; i <= 2 ; i++){
10 f i c h >> j our >> nb1 >> nb2 ;
11 cout << j our << ' \ t ' << nb1 << ' \ t ' << nb2 << endl ;
12 }
13 cout << " f i n " << endl ;
14 system( " pause " ) ;
15
return 0 ;
16 }
produit alors le résultat
mardi 123 0.0258
jeudi 654 10.98
fin
Le fichier est fermé automatiquement en fin de programme, mais il pourrait l'être sur demande
(
fich.close() ).
En conclusion de cette brève introduction aux fichiers, il faut retenir que ces objets sont extrêmement
commodes et que leur emploi n'est pas plus compliqué que celui d'une imprimante.
5.5 «string»
Vous avez remarqué que j'utilisais beaucoup de mots ou de phrases dans les programmes qui
illustrent ce cours. Nous venons de voir une application (les fichiers) où les noms jouaient un
rôle plus important que celui d'un simple exemple. Dans le jargon de l'informatique, un mot
ou une phrase constituent une «chaîne de caractères». Les chaînes se manipulent facilement en
Pascal et de façon plus tortueuse en C (comme décrit dans un chapitre suivant). C++ (pas C)
comporte cependant une bibliothèque de fonctions spécialisées dans la manipulation aisée de chaînes
particulières que je vais décrire sommairement. Pour éviter des confusions, j'emploierai le mot
«string» plutôt que «chaîne» qui sera réservé aux chaînes de caractères de style C. D'autre part,
les strings sont des objets que l'on doit manipuler avec les règles de la programmation objet ; pour
Intro_C
39
simplifier, je vais masquer cet aspect des choses. Pour utiliser ces ressources, il faut appeler la
bibliothèque :
# include <string>
5.5.1 déclaration et initialisation
La déclaration d'une string est banale :
string s, str, nom_fich, titre;
Une string peut être initialisée de façon normale
s = "programmation";
titre = 'X';
str = s;
Il existe plusieurs méthodes de déclaration et initialisation couplées :
string nom1("Dupont");
string nom2 = "Durand";
string etoiles (10,'*'); // une rangée de 10 étoiles
Vous avez du remarquer qu'à aucun moment je n'ai précisé la longueur de la «string». Celle-ci est
arbitraire et n'est limitée que par la taille de la mémoire de l'ordinateur, un avantage considérable
par rapport aux chaînes de caactères du C ou du Pascal.
5.5.2 lecture et écriture
La lecture et l'écriture de strings sont aussi conventionnelles
cout << s << endl;
cin >> nom_fich;
La lecture s'arrête au premier blanc. Pour lire tout jusqu'au passage à la ligne :
getline (cin,titre);
On pourrait lire ou écrire dans un fichier en remplaçant simplement
cin par le nom complet du
fichier.
5.5.3 quelques opérations
Il est très facile de concaténer des strings, à l'aide des opérateurs + et +=. Ainsi
string s1 = "Du", s2 = "pont ", s3 = "de Nemours", s;
s = s1 + s2;
s += s3;
cout << s;
affichera le résultat
Dupont de Nemours .
À chaque string, on peut associer les fonctions
size et length qui renvoient la longueur de
l'objet ; le fragment
int n1 = s1.length();
int n2 = s2.size();
associé aux déclarations précédentes crée deux entiers de valeurs respectives 2 et 4.
Les caractères individuels d'une «string» sont accessibles, comme les éléments d'un tableau
(voir chapitre suivant), ce qui ne signifie pas qu'une string est représentée par un tableau. La numérotation
commence à zéro.
L'instruction cout << s3[3] produit N et l'affectation s2[3] = 'd'
crée la chaîne
pond .
Intro_C
40
5.5.4 un exemple
Le programme qui suit crée un fichier et y dépose une phrase ; l'utilisateur n'est pas obligé de
taper le nom complet du fichier, le répertoire et le suffixe sont ajoutés par le programme. Certains
logiciels anciens redoutent les noms de fichiers comportant plus de 8 caractères, d'où l'affichage de
la longueur du nom.
12
// f i c h_s t r . cpp : s t r i n g comme nom de f i c h i e r
3
#include <f s t ream>
4
#include <s t r ing>
5
#include <c s t d l i b >
6
using namespace s td ;
7
int main ( void ){
8 s t r i n g nom_fich , base = "D: / CoursC/ g l o b a l /" ;
9 cout << "Nom du Fi c h i e r a c r e e r : " ; c in >> nom_fich ;
10 cout << " l e nom de vot r e f i c h i e r comporte " << nom_fich . s i z e ( )
11 << " c a r a c t è r e s \n" ;
12 nom_fich = base + nom_fich + " . dta " ;
13 of s t r eam f i c h ( nom_fich . c_str ( ) , i o s : : out ) ;
14 f i c h << " que l beau programme ! " << endl ;
15 system( " pause " ) ;
16 }
Attention :
Une «string» ne peut pas être utilisée telle quelle dans une déclaration de fichier ;
il faut la convertir en chaîne de style C; c'est ce que fait la fonction
c_str() affectée à l'objet
nom_fich