Programmer
Programmer
Programmer
2
les élèves ingénieurs
2
. . . ou les collégiens
2
débutants
2
. . . ou confirmés
1 Préambule 7
1.1 Pourquoi savoir programmer ? . . . . . . . . . . . . . . . . . . . . . . . . 10
1.2 Comment apprendre ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.2.1 Choix du langage . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.2.2 Choix de l’environnement . . . . . . . . . . . . . . . . . . . . . . . 11
1.2.3 Principes et conseils . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2 Bonjour, Monde ! 15
2.1 L’ordinateur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.1.1 Le micro-processeur . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.1.2 La mémoire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.1.3 Autres Composants . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.2 Système d’exploitation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
2.3 La Compilation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
2.4 L’environnement de programmation . . . . . . . . . . . . . . . . . . . . . 25
2.4.1 Noms de fichiers . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
2.4.2 Debuggeur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
2.5 Le minimum indispensable . . . . . . . . . . . . . . . . . . . . . . . . . . 26
2.5.1 Pour comprendre le TP . . . . . . . . . . . . . . . . . . . . . . . . 26
2.5.2 Un peu plus. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
2.5.3 Le debuggeur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
2.5.4 TP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
3 Premiers programmes 31
3.1 Tout dans le main() ! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
3.1.1 Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
3.1.2 Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
3.1.3 Boucles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
3.1.4 Récréations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
3.2 Fonctions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
3.2.1 Retour . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
3.2.2 Paramètres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
3.2.3 Passage par référence . . . . . . . . . . . . . . . . . . . . . . . . . 46
3.2.4 Portée, Déclaration, Définition . . . . . . . . . . . . . . . . . . . . 48
3.2.5 Variables locales et globales . . . . . . . . . . . . . . . . . . . . . . 49
3.2.6 Surcharge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
3.3 TP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
3.4 Fiche de référence . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
TABLE DES MATIÈRES TABLE DES MATIÈRES
4 Les tableaux 55
4.1 Premiers tableaux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
4.2 Initialisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
4.3 Spécificités des tableaux . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
4.3.1 Tableaux et fonctions . . . . . . . . . . . . . . . . . . . . . . . . . . 58
4.3.2 Affectation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
4.4 Récréations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
4.4.1 Multi-balles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
4.4.2 Avec des chocs ! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
4.4.3 Mélanger les lettres . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
4.5 TP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
4.6 Fiche de référence . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
5 Les structures 71
5.1 Révisions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
5.1.1 Erreurs classiques . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
5.1.2 Erreurs originales . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
5.1.3 Conseils . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
5.2 Les structures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
5.2.1 Définition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
5.2.2 Utilisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
5.3 Récréation : TP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
5.4 Fiche de référence . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
6 Plusieurs fichiers ! 79
6.1 Fichiers séparés . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
6.1.1 Principe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
6.1.2 Avantages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
6.1.3 Utilisation dans un autre projet . . . . . . . . . . . . . . . . . . . . 82
6.1.4 Fichiers d’en-têtes . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
6.1.5 À ne pas faire. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
6.1.6 Implémentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
6.1.7 Inclusions mutuelles . . . . . . . . . . . . . . . . . . . . . . . . . . 86
6.1.8 Chemin d’inclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
6.2 Opérateurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
6.3 Récréation : TP suite et fin . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
6.4 Fiche de référence . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
7 La mémoire 93
7.1 L’appel d’une fonction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
7.1.1 Exemple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
7.1.2 Pile des appels et débuggeur . . . . . . . . . . . . . . . . . . . . . 95
7.2 Variables Locales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
7.2.1 Paramètres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
7.2.2 La pile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
7.3 Fonctions récursives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
7.3.1 Pourquoi ça marche ? . . . . . . . . . . . . . . . . . . . . . . . . . . 98
7.3.2 Efficacité . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
2
TABLE DES MATIÈRES TABLE DES MATIÈRES
3
TABLE DES MATIÈRES TABLE DES MATIÈRES
4
TABLE DES MATIÈRES TABLE DES MATIÈRES
B Imagine++ 227
B.1 Common . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 227
B.2 Graphics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 228
B.3 Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 229
B.4 LinAlg . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 230
B.5 Installation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 230
5
TABLE DES MATIÈRES TABLE DES MATIÈRES
6
1. Préambule
Chapitre 1
Préambule
(Ce premier chapitre tente surtout de motiver les élèves ingénieurs dans leur apprentissage
de la programmation. Les enfants qui se trouveraient ici pour apprendre à programmer sont
sûrement déjà motivés et peuvent sauter au chapitre suivant ! Profitons-en pour tenir des propos
qui ne les concernent pas. . . )
— M.P. : "La précision est indispensable pour communiquer avec une machine. C’est
à l’Homme de s’adapter. Tu dois faire un effort. En contre-partie tu deviendras
son maître. Réjouis-toi. Bientôt, tu pourras créer ces êtres obéissants que sont les
programmes."
— A.P. : "Bien, Maître. . . " Quel vieux fou ! Pour un peu, il se prendrait pour Dieu. La
vérité, c’est qu’il parle aux machines parce qu’il ne sait pas parler aux hommes. Il comble
avec ses ordinateurs son manque de contact humain. L’informaticien type. . . Il ne lui
manque plus que des grosses lunettes et les cheveux gras 4 . "Maître, je ne suis pas sûr
d’en avoir envie. Je n’y arriverai pas. Ne le prenez pas mal, mais je crois être
davantage doué pour les Mathématiques ! Et puis, à quoi savoir programmer me
servira-t-il ?"
— M.P. : "Les vrais problèmes qui se poseront à toi, tu ne pourras toujours les ré-
soudre par les Mathématiques. Savoir programmer, tu devras !"
— A.P. : "J’essaierai. . . " Je me demande s’il a vraiment raison ! Je suis sûr qu’il doit être nul
en Maths. Voilà la vérité !
— ...
8
1. Préambule
— ne réalise pas que ses élèves ont un niveau avancé en maths parce qu’ils en
font depuis plus de dix ans, et qu’il leur faudra du temps pour apprendre ne
serait-ce que les bases de la programmation. Du temps. . . et de la pratique,
car, si programmer est effectivement simple en regard de ce que ses élèves
savent faire en maths, il nécessite une tournure d’esprit complètement diffé-
rente et beaucoup de travail personnel devant la machine.
— oublie qu’il a le plus souvent appris seul quand il était plus jeune, en pro-
grammant des choses simples et ludiques 7 . Il devrait donc faire venir ses
élèves à la programmation par le côté ludique, et non avec les mêmes sem-
piternels exemples 8 .
— L’élève :
— ne se rend pas compte que savoir programmer lui sera utile. Il s’agit pour-
tant d’une base qui se retrouve dans tous les langages et même dans la plu-
part des logiciels modernes 9 . Et puis, considéré comme "le jeune" donc le
moins "allergique" aux ordinateurs, il se verra vraisemblablement confier à
son premier poste la réalisation de quelques petits programmes en plus de
ses attributions normales.
— s’arrange un peu trop facilement d’un mépris de bon ton pour la program-
mation. Il lui est plus aisé d’apprendre une n-ième branche des mathéma-
tiques que de faire l’effort d’acquérir par la pratique une nouvelle tournure
d’esprit.
On l’aura compris, il est à la fois facile et difficile d’apprendre à programmer. Pour
l’ingénieur, cela demandera de la motivation et un peu d’effort : essentiellement de
mettre ses maths de côté et de retrouver le goût des choses basiques. Pour un collé-
gien, motivation et goût de l’effort seront au rendez-vous. Il lui restera malgré tout à
acquérir quelques bases d’arithmétique et de géométrie. Comme annoncé par le titre
de ce cours, collégien et ingénieur en sont au même point pour l’apprentissage de la
programmation. De plus, et c’est un phénomène relativement nouveau, il en est de
même pour le débutant et le "geek 10 ". Expliquons-nous : le passionné d’informatique
a aujourd’hui tellement de choses à faire avec son ordinateur qu’il sera en général in-
collable sur les jeux, internet, les logiciels graphiques ou musicaux, l’installation ou
la configuration de son système, l’achat du dernier gadget USB à la mode, etc. mais
qu’en contrepartie il sera mauvais programmeur. Il y a quelques années, il y avait peu
à faire avec son ordinateur sinon programmer. Programmer pour combler le manque
de possibilités de l’ordinateur. Aujourd’hui, faire le tour de toutes les possibilités d’un
ordinateur est une occupation à plein temps ! Ainsi, le "fana info" passe-t-il sa journée à
se tenir au courant des nouveaux logiciels 11 et en oublie qu’il pourrait lui aussi en créer.
7. C’est une erreur fréquente de croire qu’il intéressera ses élèves en leur faisant faire des pro-
grammes centrés sur les mathématiques ou le calcul scientifique. De tels programmes leur seront peut-
être utiles plus tard, mais ne sont pas forcément motivants. L’algèbre linéaire ou l’analyse numérique
sont des domaines passionnants à étudier. . . mais certainement pas à programmer. Il faut admettre sans
complexe que programmer un flipper, un master-mind ou un labyrinthe 3D est tout aussi formateur et
plus motivant qu’inverser une matrice creuse.
8. La liste est longue, mais tellement vraie : quel cours de programmation ne rabâche pas les célèbres
"factorielle", "suites de Fibonacci", "Quick Sort", etc ?
9. Savoir programmer ne sert pas seulement à faire du C++ ou du Java, ni même du Scilab, du Matlab
ou du Maple : une utilisation avancée d’Excel ou du Word demande parfois de la programmation !
10. Une récompense à qui me trouve un substitut satisfaisant à cette expression consacrée.
11. Sans même d’ailleurs avoir le temps d’en creuser convenablement un seul !
9
1.1. Pourquoi savoir programmer ? 1. Préambule
10
1. Préambule 1.2. Comment apprendre ?
4. Enfin, certains aspects pratiques et pourtant simples de C++ ont disparu dans
Java 15 .
Depuis quelques années, un langage qui s’impose de plus en plus est le Python. La
raison est qu’il est portable, puissant et facile d’accès. Cependant, il présente des in-
convénients. Il est en constante évolution, non standardisé, et la compatibilité entre les
versions n’est pas garantie 16 . De plus, les structures de données de Python, certes très
utiles, cachent la complexité qu’il y a derrière du point de vue de la gestion mémoire,
et il est important pour un ingénieur d’être conscient de ce qui se passe en coulisse.
Encore une fois, répétons que le choix du langage n’est pas le plus important et que
l’essentiel est d’apprendre à programmer.
11
1.2. Comment apprendre ? 1. Préambule
compilateur C++ par défaut 19 , mais en installant la version de Qt pour MinGW on bé-
néficie justement du compilateur MinGW 20 . Un autre compilateur pour Windows est
celui de Microsoft qui vient avec VisualStudio, qui est certes gratuit mais ne l’installez
que si ça ne vous dérange pas de donner à Microsoft des informations personnelles qui
ne le concernent en rien 21 .
19. sous Linux, le compilateur usuel est GCC (GNU Compiler Collection) et Clang sous Mac, soutenu
par Apple. Le projet GNU développe de nombreux outils indispensables et libres pour compléter le
système d’exploitation. Clang s’inspire fortement de GCC pour l’utilisation, mais dispose d’une licence
différente, qui ne ferme pas la porte à des extensions non libres.
20. Minimal GNU for Windows, une adaptation de GCC pour Windows.
21. Cette intrusion n’est plus de mise avec VisualStudio 2015, enfin !
22. Le vocabulaire n’est pas choisi au hasard : un programme est une suite d’ordres, de commandes ou
d’instructions. On voit bien qui est le chef !
12
1. Préambule 1.2. Comment apprendre ?
Gardons bien présents ces quelques principes car il est maintenant temps de. . .
13
2. Bonjour, Monde !
Chapitre 2
Bonjour, Monde !
(Si certains collégiens sont arrivés ici, ils sont bien courageux ! Lorsque je disais tout à
l’heure qu’ils pouvaient facilement apprendre à programmer, je le pensais vraiment. Par contre,
c’est avec un peu d’optimisme que j’ai prétendu qu’ils pouvaient le faire en lisant un polycopié
destiné à des ingénieurs. Enfin, je suis pris à mon propre piège ! Alors, à tout hasard, je vais
tenter d’expliquer au passage les mathématiques qui pourraient leur poser problème.)
i n t main ( )
{
cout << " Hello , World ! " << endl ;
return 0;
}
Eh bien, allons-y ! Décortiquons-le ! Dans ce programme, qui affiche à l’écran 1 le texte
"Hello, World!", les lignes 1 et 2 sont des instructions magiques 2 qui servent à pou-
voir utiliser dans la suite cout et endl. La ligne 4 int main() définit une fonction appelée
main(), qui renvoie 3 un nombre entier. Cette fonction est spéciale car c’est la fonction
principale d’un programme C++, celle qui est appelée automatiquement 4 quand le
1. Cette expression, vestige de l’époque où les ordinateurs étaient dotés d’un écran capable de n’affi-
cher que des caractères et non des graphiques (courbes, dessins, etc.), signifie aujourd’hui que l’affichage
se fera dans une fenêtre simulant l’écran d’un ordinateur de cette époque. Cette fenêtre est appelée ter-
minal, console, fenêtre de commande, fenêtre DOS, xterm, etc. suivant les cas. Souvenons nous avec
un minimum de respect que c’était déjà un progrès par rapport à la génération précédente, dépourvue
d’écran et qui utilisait une imprimante pour communiquer avec l’homme. . . ce qui était relativement
peu interactif !
2. Entendons par là des instructions que nous n’expliquons pas pour l’instant. Il n’y a (mal ?)-
heureusement rien de magique dans la programmation.
3. On dit aussi retourne. À qui renvoie-t-elle cet entier ? Mais à celui qui l’a appelée, voyons !
4. Voilà, maintenant vous savez qui appelle main(). Dans un programme, les fonctions s’appellent
les unes les autres. Mais main() n’est appelée par personne puisque c’est la première de toutes. (Du
moins en apparence car en réalité le programme a plein de choses à faire avant d’arriver dans main()
2. Bonjour, Monde !
programme est lancé 5 . Délimitée par les accolades ({ ligne 5 et } ligne 8), la fonction
main() se termine ligne 7 par return 0; qui lui ordonne de retourner l’entier 0. Notons
au passage que toutes les instructions se terminent par un point-virgule ;. Enfin, à la
ligne 6, seule ligne "intéressante", cout << "Hello, World!"<< endl; affiche, grâce à la
variable 6 cout qui correspond à la sortie console 7 , des données séparées par des <<. La
première de ces données est la chaîne de caractères 8 "Hello, World!". La deuxième, endl,
est un retour à la ligne 9 .
C’est toute cette approche qui est négligée quand on commence comme nous venons
de le faire. Donc. . .
et il commence par plusieurs autres fonctions que le programmeur n’a pas à connaître et qui finissent
par appeler main(). D’ailleurs, si personne ne l’appelait, à qui main() retournerait-elle un entier ?)
5. Je savais bien que vouloir expliquer tous les barbarismes propres aux informaticiens m’interrom-
prait souvent. Mais bon. Donc, un programme démarre ou est lancé. Après quoi, il s’exécute ou tourne.
Enfin, il se termine ou meurt.
6. Les données sont rangées ou stockées dans des variables qui mémorisent des valeurs. Ces variables ne
sont d’ailleurs pas toujours variables au sens usuel, puisque certaines sont constantes !
7. Qu’est-ce que je disais ! On affiche dans une fenêtre console !
8. En clair, un texte.
9. Ce qui signifie que la suite de l’affichage sur la console se fera sur une nouvelle ligne.
16
2. Bonjour, Monde! 2.1. L’ordinateur
2.1 L’ordinateur
Pour savoir ce qu’un ordinateur sait vraiment faire, il faut commencer par son or-
gane principal : le micro-processeur.
2.1.1 Le micro-processeur
Quel qu’il soit 12 et quelle que soit sa vitesse 13 , un micro-processeur ne sait faire que
des choses relativement basiques. Sans être exhaustif, retenons juste ceci :
— Il sait exécuter une suite ordonnée d’instructions.
— Il possède un petit nombre de mémoires internes appelées registres.
— Il dialogue avec le monde extérieur via de la mémoire 14 en plus grande quantité
que ses registres.
— Cette mémoire contient, sous forme de nombres, les instructions à exécuter et les
données sur lesquelles travailler.
— Les instructions sont typiquement :
— Lire ou écrire un nombre dans un registre ou en mémoire.
— Effectuer des calculs simples : addition, multiplication, etc.
— Tester ou comparer des valeurs et décider éventuellement de sauter à une
autre partie de la suite d’instructions.
10. Un computer, quoi !
11. Cette notion est évidemment dépendante de notre savoir faire informatique à l’instant présent.
Les premiers langages étaient plus éloignés de l’Homme car plus proches de la machine qui était alors
rudimentaire, et l’on peut envisager que les futurs langages seront plus proches de l’Homme.
12. Pentium ou autre
13. Plus exactement la fréquence à laquelle il exécute ses instructions. Aujourd’hui l’horloge va envi-
ron à 3GHz. (Mais attention : une instruction demande plus d’un cycle d’horloge !)
14. Aujourd’hui, typiquement 1Go (giga-octets), soit 1024 × 1024 × 1024 mémoires de 8 bits (mémoires
pouvant stocker des nombres entre 0 et 255).
17
2.1. L’ordinateur 2. Bonjour, Monde !
Voici par exemple ce que doit faire le micro-processeur quand on lui demande
d’exécuter "c=3∗a+2∗b;" en C++, où a,b,c sont trois variables entières :
00415A61 8B 45 F8
00415A64 6B C0 03
00415A67 8B 4D EC
00415A6A 8D 14 48
00415A6D 89 55 E0
A part encore une fois la colonne de gauche, chaque suite de nombres 15 correspond
évidemment à une instruction précise. C’est tout de suite moins compréhensible 16 !
Notons que chaque micro-processeur à son jeu d’instructions ce qui veut dire que la tra-
duction de c=3∗a+2∗b; en la suite de nombres 8B45F86BC0038B4DEC8D14488955E0
est propre au Pentium que nous avons utilisé pour notre exemple :
Remarquons aussi que les concepteurs du Pentium ont décidé de créer une instruction
spécifique pour calculer edx=eax+ecx∗2 en une seule fois car elle est très fréquente. Si
on avait demandé c=3∗a+3∗b;, notre programme serait devenu :
18
2. Bonjour, Monde ! 2.1. L’ordinateur
2.1.2 La mémoire
La mémoire interne du micro-processeur est gérée dans des registres, un peu comme
les variables du C++, mais en nombre prédéfini. Pour stocker 17 la suite d’instructions à
lui fournir, on utilise de la mémoire en quantité bien plus importante, désignée en gé-
néral par la mémoire de l’ordinateur. Il s’agit des fameuses "barrettes" 18 de mémoire que
l’on achète pour augmenter la capacité de sa machine et dont les prix fluctuent assez
fortement par rapport au reste des composants d’un ordinateur. Cette mémoire est dé-
coupée en octets. Un octet 19 correspond à un nombre binaire de 8 bits 20 , soit à 28 = 256
valeurs possibles. Pour se repérer dans la mémoire, il n’est pas question de donner des
noms à chaque octet. On numérote simplement les octets et on obtient ainsi des adresses
mémoire. Les nombres 00415A61, etc. vus plus haut sont des adresses ! Au début, ces
nombres étaient écrits en binaire, ce qui était exactement ce que comprenait le micro-
processeur. C’est devenu déraisonnable quand la taille de la mémoire a dépassé les
quelques centaines d’octets. Le contenu d’un octet de mémoire étant lui aussi donné
sous la forme d’un nombre, on a opté pour un système adapté au fait que ces nombres
sont sur 8 bits : plutôt que d’écrire les nombre en binaire, le choix de la base 16 per-
mettait de représenter le contenu d’un octet sur deux chiffres (0,1,. . . ,9,A,B,C,D,E,F).
Le système hexadécimal 21 était adopté. . . Les conversions de binaire à hexadécimal sont
très simples, chaque chiffre hexadécimal valant pour un paquet de 4 bits, alors qu’entre
binaire et décimal, c’est moins immédiat. Il est aujourd’hui encore utilisé quand on dé-
signe le contenu d’un octet ou une adresse 22 . Ainsi, notre fameux c=3∗a+2∗b; devient
en mémoire :
17. Encore un anglicisme. . .
18. Aujourd’hui, typiquement une ou plusieurs barrettes pour un total de 1 ou 2Go, on l’a déjà dit.
Souvenons nous avec une larme à l’oeil des premiers PC qui avaient 640Ko (kilo-octet soit 1024 octets),
voire pour les plus agés d’entre nous des premiers ordinateurs personnels avec 4Ko, ou même des
premières cartes programmables avec 256 octets !
19. byte en anglais. Attention donc à ne pas confondre byte et bit, surtout dans des abréviations comme
512kb/s données pour le débit d’un accès internet. . . b=bit, B=byte=8 bits
20. Le coin des collégiens : en binaire, ou base 2, on compte avec deux chiffres au lieu de dix d’ha-
bitude (c’est à dire en décimal ou base 10). Cela donne : 0, 1, 10, 11, 100, 101, 110, 111, . . . Ainsi, 111 en
binaire vaut 7 . Chaque chiffre s’appelle un bit. On voit facilement qu’avec un chiffre on compte de 0 à
1 soit deux nombres possibles ; avec deux chiffres, de 0 à 3, soit 4 = 2 × 2 nombres ; avec 3 chiffres, de
0 à 7, soit 8 = 2 × 2 × 2 nombres. Bref avec n bits, on peut coder 2n (2 multiplié par lui-même n fois)
nombres. Je me souviens avoir appris la base 2 en grande section de maternelle avec des cubes en bois !
Étrange programme scolaire. Et je ne dis pas ça pour me trouver une excuse d’être devenu informaticien.
Quoique. . .
21. Coin des collégiens (suite) : en base 16, ou hexadécimal, on compte avec 16 chiffres. Il faut inven-
ter des chiffres au delà de 9 et on prend A,B,C,D,E,F. Quand on compte, cela donne : 0, 1, 2, . . . , 9, A, B,
C, D, E, F, 10, 11, 12, 13, . . . , 19, 1A, 1B, 1C, . . . Ainsi 1F en hexadécimal vaut 31. Avec 1 chiffre, on compte
de 0 à 15 soit 16 nombres possibles ; avec 2 chiffres, de 0 à 255 soit 256 = 16 × 16 nombres possibles, etc.
Un octet peut s’écrire avec 8 bits en binaire, ou 2 nombres en hexadécimal et va de 0 à 255, ou 11111111
en binaire, ou FF en hexadécimal.
22. Dans ce cas, sur plus de 2 chiffres : 8 pour les processeurs 32 bits, 16 pour les processeurs 64 bits.
19
2.1. L’ordinateur 2. Bonjour, Monde !
20
2. Bonjour, Monde ! 2.1. L’ordinateur
Types de mémoire
La mémoire dont nous parlions jusqu’ici est de la mémoire vive ou RAM. Elle est
rapide 28 mais a la mauvaise idée de s’effacer quand on éteint l’ordinateur. Il faut donc
aussi de la mémoire morte ou ROM, c’est-à-dire de la mémoire conservant ses données
quand l’ordinateur est éteint mais qui en contre-partie ne peut être modifiée 29 . Cette
mémoire contient en général le minimum pour que l’ordinateur démarre et exécute
une tâche prédéfinie. Initialement, on y stockait les instructions nécessaires pour que le
programmeur puisse remplir ensuite la RAM avec les instructions de son programme.
Il fallait retaper le programme à chaque fois 30 ! On a donc rapidement eu recours à des
moyens de stockage pour sauver programmes et données à l’extinction de l’ordinateur. Il
suffisait alors de mettre en ROM le nécessaire pour gérer ces moyens de stockages.
Moyens de stockage
Certains permettent de lire des données, d’autres d’en écrire, d’autres les deux à
la fois. Certains ne délivrent les données que dans l’ordre, de manière séquentielle,
d’autres, dans l’ordre que l’on veut, de manière aléatoire. Ils sont en général bien plus
lents que la mémoire et c’est sûrement ce qu’il faut surtout retenir ! On recopie donc en
RAM la partie des moyens de stockage sur laquelle on travaille.
21
2.2. Système d’exploitation 2. Bonjour, Monde !
Périphériques
On appelle encore périphériques différents appareils reliés à l’ordinateur : clavier,
souris, écran, imprimante, modem, scanner, etc. Ils étaient initialement là pour servir
d’interface avec l’Homme, comme des entrées et des sorties entre le micro-processeur
et la réalité. Maintenant, il est difficile de voir encore les choses de cette façon. Ainsi
les cartes graphiques, qui pouvaient être considérées comme un périphérique allant
avec l’écran, sont-elles devenues une partie essentielle de l’ordinateur, véritables puis-
sances de calcul, à tel point que certains programmeur les utilisent pour faire des cal-
culs sans même afficher quoi que ce soit. Plus encore, c’est l’ordinateur qui est parfois
juste considéré comme maillon entre différents appareils. Qui appellerait périphérique
un caméscope qu’on relie à un ordinateur pour envoyer des vidéos sur internet ou les
transférer sur un DVD ? Ce serait presque l’ordinateur qui serait un périphérique du
caméscope !
22
2. Bonjour, Monde ! 2.3. La Compilation
process) en train de s’exécuter. Il doit pour cela essentiellement faire face à deux pro-
blèmes 40 :
1. Faire travailler le processeur successivement par petites tranches sur les diffé-
rents programmes. Il s’agit de donner la main de manière intelligente et équi-
table, mais aussi de replacer un process interrompu dans la situation qu’il avait
quittée lors de son interruption.
2. Gérer la mémoire dédiée à chaque process. En pratique, une partie ajustable de
la mémoire est réservée à chaque process. La mémoire d’un process devient mé-
moire virtuelle : si un process est déplacé à un autre endroit de la mémoire physique
(la RAM), il ne s’en rend pas compte. On en profite même pour mettre tempo-
rairement hors RAM (donc sur disque dur) un process en veille. On peut aussi
utiliser le disque dur pour qu’un process utilise plus de mémoire que la mémoire
physique : mais attention, le disque étant très lent, ce process risque de devenir
lui aussi très lent.
Lorsqu’un process à besoin de trop de mémoire, il utilise, sans préve-
nir, le disque dur à la place de la mémoire et peut devenir très lent.
On dit qu’il swappe (ou pagine). Seule sa lenteur (et le bruit du disque
dur !) permet en général de s’en rendre compte (on peut alors s’en as-
surer avec le gestionnaire de tâche du système).
Autre progrès : on gère maintenant la mémoire virtuelle de façon à séparer les
process entre eux et, au sein d’un même process, la mémoire contenant les ins-
tructions de celle contenant les données. Il est rigoureusement impossible qu’un
process buggé puisse modifier ses instructions ou la mémoire d’un autre process
en écrivant à un mauvais endroit de la mémoire 41 .
Avec l’arrivée des systèmes d’exploitation, les fichiers exécutables ont dû s’adapter
pour de nombreuse raisons de gestion et de partage de la mémoire. En pratique, un
programme exécutable linux ne tournera pas sous Windows et réciproquement, même
s’ils contiennent tous les deux des instructions pour le même processeur.
Un fichier exécutable est spécifique, non seulement à un processeur donné,
mais aussi à un système d’exploitation donné.
Au mieux, tout comme les versions successives d’une famille de processeur essaient
de continuer à comprendre les instructions de leurs prédécesseurs, tout comme les
versions successives d’un logiciel essaient de pouvoir lire les données produites avec
les versions précédentes, les différentes versions d’un système d’exploitation essaient
de pouvoir exécuter les programmes faits pour les versions précédentes. C’est la com-
patibilité ascendante, que l’on paye souvent au prix d’une complexité et d’une lenteur
accrues.
2.3 La Compilation
Tout en essayant de comprendre ce qui se passe en dessous pour en tirer des infor-
mations utiles comme la gestion de la mémoire, nous avons entrevu que transformer
40. Les processeurs ont évidemment évolué pour aider le système d’exploitation à faire cela
efficacement.
41. Il se contente de modifier anarchiquement ses données, ce qui est déjà pas mal !
23
2.3. La Compilation 2. Bonjour, Monde !
un programme C++ en un fichier exécutable est un travail difficile mais utile. Cer-
tains logiciels disposant d’un langage de programmation comme Maple ou Scilab ne
transforment pas leurs programmes en langage machine. Le travail de traduction est
fait à l’exécution du programme qui est alors analysé au fur et à mesure 42 : on parle
alors de langage interprété. L’exécution alors est évidemment très lente. D’autres lan-
gages, comme Java, décident de résoudre les problèmes de portabilité, c’est-à-dire de
dépendance au processeur et au système, en plaçant une couche intermédiaire entre
le processeur et le programme : la machine virtuelle. Cette machine, évidemment écrite
pour un processeur et un système donnés, peut exécuter des programmes dans un
langage machine virtuel 43 , le "byte code". Un programme Java est alors traduit en son
équivalent dans ce langage machine. Le résultat peut être exécuté sur n’importe quelle
machine virtuelle Java. La contrepartie de cette portabilité est évidemment une perte
d’efficacité.
La traduction en code natif ou en byte code d’un programme s’appelle la compila-
tion 44 . Un langage compilé est alors à opposer à un langage interprété. Dans le cas du C++
et de la plupart des langages compilés (Fortran, C, etc), la compilation se fait vers du
code natif. On transforme un fichier source, le programme C++, en un fichier objet, suite
d’instructions en langage machine.
Cependant, le fichier objet ne se suffit pas à lui-même. Des instructions supplémen-
taires sont nécessaires pour former un fichier exécutable complet :
— de quoi lancer le main() ! Plus précisément, tout ce que le process doit faire avant
et après l’exécution de main().
— des fonctions ou variables faisant partie du langage et que le programmeur uti-
lise sans les reprogrammer lui-même, comme l’objet cout, la fonction min(), etc.
L’ensemble de ces instructions constitue ce qu’on appelle une bibliothèque 45 .
— des fonctions ou variables programmées par le programmeur lui-même dans
d’autres fichiers source compilés par ailleurs en d’autres fichiers objet, mais qu’il
veut utiliser dans son programme actuel.
La synthèse de ces fichiers en un fichier exécutable s’appelle l’édition des liens. Le pro-
gramme qui réalise cette opération est plus souvent appelé linker qu’éditeur de liens. . .
24
2. Bonjour, Monde ! 2.4. L’environnement de programmation
2.4.2 Debuggeur
Lorsqu’un programme ne fait pas ce qu’il faut, on peut essayer de comprendre ce
qui ne va pas en truffant son source d’instructions pour imprimer la valeur de certaines
données ou simplement pour suivre son déroulement. Ça n’est évidemment pas très
pratique. Il est mieux de pouvoir suivre son déroulement instruction par instruction et
d’afficher à la demande la valeur des variables. C’est le rôle du debuggeur 48 .
Lorsqu’un langage est interprété, il est relativement simple de le faire s’exécuter pas
à pas car c’est le langage lui-même qui exécute le programme. Dans le cas d’un langage
compilé, c’est le micro-processeur qui exécute le programme et on ne peut pas l’arrêter
à chaque instruction ! Il faut alors mettre en place des points d’arrêt en modifiant tem-
porairement le code machine du programme pour que le processeur s’arrête lorsqu’il
atteint l’instruction correspondant à la ligne de source à debugger. Si c’est compliqué
à mettre au point, c’est très simple à utiliser, surtout dans un environnement de pro-
grammation graphique.
Nous verrons au fur et à mesure des TP comment le debuggeur peut aussi inspecter
les appels de fonctions, espionner la modification d’une variable, etc.
25
2.5. Le minimum indispensable 2. Bonjour, Monde !
-- Configuring done
-- Generating done
-- Build files have been written to: /home/pascal/TEMP/Build
26
2. Bonjour, Monde ! 2.5. Le minimum indispensable
27
2.5. Le minimum indispensable 2. Bonjour, Monde !
A noter que le terminal intégré de QtCreator ne supporte pas l’entrée par l’utilisateur.
Il faut donc lancer avec un terminal extérieur, en allant dans l’onglet Projects, rubrique
Run et cliquer le bouton “Run in terminal”.
Enfin, le TP utilise la commande conditionnelle if - else.
2.5.3 Le debuggeur
Il est important de pouvoir suivre pas à pas le programme au cours de son exécu-
tion, consulter la valeur des variables, etc. Pour cela, il faut compiler dans un mode
spécial, dit Debug. Cela se fait avec une variable de CMake, qu’on peut modifier di-
rectement dans le fichier CMakeCache.txt, ou mieux sans quitter QtCreator : dans
l’onglet Projects, passer comme argument à CMake -DCMAKE_BUILD_TYPE=Debug,
puis recompiler (voir Figure 2.2). À noter que les versions récentes de QtCreator per-
mettent de passer en mode Debug plus simplement en cliquant sur le bon bouton. La
touche F9 permet de mettre un point d’arret à la ligne courante.
2.5.4 TP
Vous devriez maintenant aller faire le TP en annexe A.1. Si la pratique est essen-
tielle, en retenir quelque chose est indispensable ! Vous y trouverez aussi comment ins-
taller les outils sur votre ordinateur (lien http://imagine.enpc.fr/~monasse/
Imagine++ mentionné à la fin du TP). Voir en Figure 2.3 ce qu’il faut retenir du TP.
—
Nous en savons maintenant assez pour apprendre un peu de C++. . .
Nous commençons maintenant à nous fabriquer une “fiche de référence” qui servira
d’aide mémoire lorsqu’on est devant la machine. Nous la compléterons après chaque
chapitre avec ce qui est vu dans le chapitre. Les nouveautés par rapport à la fiche
précédente sont en rouge. La fiche finale est en annexe D.
28
2. Bonjour, Monde ! 2.5. Le minimum indispensable
29
3. Premiers programmes
Chapitre 3
Premiers programmes
3.1.1 Variables
Types
Les variables sont des mémoires dans lesquelles sont stockées des valeurs (ou don-
nées). Une donnée ne pouvant être stockée n’importe comment, il faut à chaque fois dé-
cider de la place prise en mémoire (nombre d’octets) et du format, c’est-à-dire de la façon
dont les octets utilisés vont représenter les valeurs prises par la variable. Nous avons
1. La contre-partie de cette présentation est que ce polycopié, s’il est fait pour être lu dans l’ordre, est
peut-être moins adapté à servir de manuel de référence. .
2. Et bien des élèves, dès que le professeur n’est plus derrière !
3.1. Tout dans le main() ! 3. Premiers programmes
déjà rencontré les int qui sont le plus souvent aujourd’hui stockés sur quatre octets,
soit 32 bits, et pouvant prendre 232 = 4294967296 valeurs possibles 3 . Par convention,
les int stockent les nombres entiers relatifs 4 , avec autant de nombres négatifs que de
nombres positifs 5 , soit, dans le cas de 32 bits 6 , de −2147483648 à 2147483647 suivant
une certaine correspondance avec le binaire 7 .
Dire qu’une variable est un int, c’est préciser son type. Certains langages n’ont pas
la notion de type ou essaient de deviner les types des variables. En C++, c’est initia-
lement pour préciser la mémoire et le format des variables qu’elles sont typées. Nous
verrons que le compilateur se livre à un certain nombre de vérifications de cohérence
de type entre les différentes parties d’un programme. Ces vérifications, pourtant bien
pratiques, n’étaient pas faites dans les premières versions du C, petit frère du C++, car
avant tout, répétons-le :
32
3. Premiers programmes 3.1. Tout dans le main() !
Dans ce programme :
— Les lignes 1 et 2 définissent une variable nommée i 8 de type int puis affecte
2 à cette variable. La représentation binaire de 2 est donc stockée en mémoire
là où le compilateur décide de placer i. Ce qui suit le "double slash" ( // ) est une
remarque : le compilateur ignore toute la fin de la ligne, ce qui permet de mettre
des commentaires aidant à la compréhension du programme.
— La ligne 3 affiche la valeur de i puis un espace (sans aller à la ligne)
— Les lignes 4, 5 et 6 définissent un int nommé j , recopie la valeur de i, soit 2,
dans j , puis mémorise 1 dans i. Notez bien que i et j sont bien deux variables
différentes : i passe à 1 mais j reste à 2 !
— La ligne 8 nous montre comment définir simultanément plusieurs variables du
même type.
— La ligne 9 nous apprend que l’on peut affecter des variables simultanément à une
même valeur.
— A la ligne 12, des variables sont définies et affectées en même temps. En fait,
on parle plutôt de variables initialisées : elles prennent une valeur initiale en
même temps qu’elles sont définies. Notez que, pour des raisons d’efficacité, les
variables ne sont pas initialisées par défaut : tant qu’on ne leur a pas affecté une
valeur et si elles n’ont pas été initialisées, elles valent n’importe quoi 9 !
— Attention toutefois, il est inutile de tenter une initialisation simultanée. C’est in-
terdit. La ligne 14 provoque une erreur.
— Enfin, on peut rajouter const devant le type d’une variable : celle-ci devient alors
constante et on ne peut modifier son contenu. La ligne 15 définit une telle variable
et la ligne 16 est une erreur.
En résumé, une fois les lignes 14 et 16 supprimées, ce (passionnant !) programme af-
fiche 10 :
2 1 2 3 3 4 5 5 2147483647
Les noms de variable sont composés uniquement des caractères a à z (et majus-
cules), chiffres et underscore _ (évitez celui-ci, il n’est pas très esthétique), mais ne
peuvent pas commencer par un chiffre. N’utilisez pas de caractères accentués, car cela
pose des problèmes de portabilité.
Portée
Dans l’exemple précédent, les variables ont été définies au fur et à mesure des be-
soins. Ce n’est pas une évidence. Par exemple, le C ne permettait de définir les variables
que toutes d’un coup au début du main(). En C++, on peut définir les variables en cours
de route, ce qui permet davantage de clarté. Mais attention :
8. Le nom d’une variable est aussi appelé identificateur. Les messages d’erreur du compilateur utilise-
ront plutôt ce vocabulaire !
9. Ainsi, un entier ne vaut pas 0 lorsqu’il est créé et les octets où il est mémorisé gardent la valeur qu’il
avaient avant d’être réquisitionnés pour stocker l’entier en question. C’est une mauvaise idée d’utiliser
la valeur d’une variable qui vaut n’importe quoi et un compilateur émettra généralement un warning si
on utilise une variable avant de lui fournir une valeur !
10. du moins sur une machine 32 bits, cf. remarque précédente sur INT_MAX
33
3.1. Tout dans le main() ! 3. Premiers programmes
les variables "n’existent" (et ne sont donc utilisables) qu’à partir de la ligne
où elles sont définies. Elles ont une durée de vie limitée et meurent dès que
l’on sort du bloc limité par des accolades auquel elles appartiennent a . C’est
ce qu’on appelle la portée d’une variable.
a. C’est un peu plus compliqué pour les variables globales. Nous verrons ça aussi. . .
Ainsi, en prenant un peu d’avance sur la syntaxe des tests, que nous allons voir tout
de suite, le programme suivant provoque des erreurs de portée aux lignes 2 et 8 :
int i ;
i= j ; / / Erreur : j n ’ e x i s t e pas encore !
i n t j =2;
i f ( j >1) {
i n t k=3;
j =k ;
}
i =k ; / / E r r e u r : k n ’ e x i s t e p l u s .
Autres types
Nous verrons les différents types au fur et à mesure. Voici malgré tout les plus
courants :
i n t i =3; // Entier r e l a t i f
double x = 1 2 . 3 ; // Nombre r é e l ( d o u b l e p r é c i s i o n )
char c= ’A ’ ; // Caractère
s t r i n g s= " hop " ; // Chaîne de c a r a c t è r e s
bool t = t r u e ; // B o o l é e n ( v r a i ou f a u x )
Les nombres réels sont en général approchés par des variables de type double ("double
précision", ici sur 8 octets). Les caractères sont représentés par un entier sur un oc-
tet (sur certaines machines de -128 à 127, sur d’autres de 0 à 255), la correspondance
caractère/entier étant celle du code ASCII (65 pour A, 66 pour B, etc.), qu’il n’est heu-
reusement pas besoin de connaître puisque la syntaxe ’A’ entre simples guillemets est
traduite en 65 par le compilateur, etc. Les doubles guillemets sont eux réservés aux
"chaînes" de caractères 11 . Enfin, les booléens sont des variables qui valent vrai (true)
ou faux ( false ).
34
3. Premiers programmes 3.1. Tout dans le main() !
float valent au plus FLT_MAX (ici, environ 3.4e+38 12 ) et que leur valeur la plus
petite strictement positive est FLT_MIN (ici, environ 1.2e−38), de même que pour
les double les constantes DBL_MAX et DBL_MIN valent ici environ 1.8e+308 et
2.2e−308),
— les unsigned int, entiers positifs utilisés pour aller plus loin que les int dans les
positifs (de 0 à UINT_MAX, soit 4294967295 dans notre cas),
— les unsigned char, qui vont de 0 à 255,
— les signed char, qui vont de -128 à 127,
— et enfin les nombres complexes 13 .
3.1.2 Tests
Tests simples
Les tests servent à exécuter telle ou telle instruction en fonction de la valeur d’une
ou de plusieurs variables. Ils sont toujours entre parenthèses. Le “et” s’écrit &&, le
“ou” ||, la négation !, l’égalité ==, la non-égalité !=, et les inégalités >, >=, < et <=.
Si plusieurs instructions doivent être exécutées quand un test est vrai ( if ) ou faux
(else), on utilise des accolades pour les regrouper. Tout cela se comprend facilement
sur l’exemple suivant :
i f ( i ==0) / / i e s t − i l n u l ?
cout << " i e s t nul " << endl ;
...
i f ( i >2) / / i e s t − i l p l u s g r a n d que 2?
j =3;
else
j =5; / / S i on e s t i c i , c ’ e s t que i <=2
...
/ / Cas p l u s c o m p l i q u é !
i f ( i ! = 3 || ( j ==2 && k ! = 3 ) || ! ( i > j && i >k ) ) {
/ / I c i , i e s t d i f f é r e n t d e 3 ou a l o r s
/ / j v a u t 2 e t k e s t d i f f é r e n t d e 3 ou a l o r s
/ / on n ’ a p a s i p l u s g r a n d a l a f o i s d e j e t d e k
cout << " Une première i n s t r u c t i o n " << endl ;
cout << " Une deuxième i n s t r u c t i o n " << endl ;
}
Les variables de type booléen servent à mémoriser le résultat d’un test :
bool t = ( ( i ==3)||( j = = 4 ) ) ;
if ( t )
k=5;
12. Coin des collégiens : 1038 ou 1e+38 vaut 1 suivi de 38 zéros, 10−38 ou 1e−38 vaut 0.000 . . . 01 avec
37 zéros avant le 1. En compliquant : 3.4e+38 vaut 34 suivis de 37 zéros (38 chiffres après le 3) et 1.2e−38
vaut 0.00 . . . 012 toujours avec 37 zéros entre la virgule et le 1 (le 1 est à la place 38).
13. Il est trop tôt pour comprendre la syntaxe "objet" de cette définition mais il nous parait important
de mentionner dès maintenant que les complexes existent en C++.
Coin des collégiens : pas de panique ! Vous apprendrez ce que sont les nombres complexes plus tard.
Ils ne seront pas utilisés dans ce livre.
35
3.1. Tout dans le main() ! 3. Premiers programmes
Enfin, une dernière chose très importante : penser à utiliser == et non = sous peine
d’avoir des surprises. 14 C’est peut-être l’erreur la plus fréquente chez les débutants.
Elle est heureusement signalée aujourd’hui par un warning. . .
Le "switch"
On a parfois besoin de faire telle ou telle chose en fonction des valeurs possibles
d’une variable. On utilise alors souvent l’instruction switch pour des raisons de clarté
de présentation. Chaque cas possible pour les valeurs de la variable est précisé avec
case et doit se terminer par break 15 . Plusieurs case peuvent être utilisés pour préciser
un cas multiple. Enfin, le mot clé default, à placer en dernier, correspond aux cas non
précisés. Le programme suivant 16 réagit aux touches tapées au clavier et utilise un
switch pour afficher des commentaires passionnants !
1 # i n c l u d e <iostream >
2 using namespace s t d ;
3 # i n c l u d e <conio . h> / / Non s t a n d a r d !
4
5 i n t main ( )
6 {
7 bool f i n i = f a l s e ;
8 char c ;
9 do {
10 c=_ g e t c h ( ) ; / / Non s t a n d a r d !
11 s w i tch ( c ) {
12 case ’ a ’ :
13 cout << " Vous avez tapé ’ a ’ ! " << endl ;
14 break ;
15 case ’ f ’ :
16 cout << " Vous avez tapé ’ f ’ . Au r e v o i r ! " << endl ;
17 f i n i =true ;
18 break ;
19 case ’ e ’ :
20 case ’ i ’ :
21 case ’o ’ :
22 case ’u ’ :
23 case ’y ’ :
24 cout << " Vous avez tapé une a u t r e v o y e l l e ! " << endl ;
14. Faire if ( i=3) affecte 3 à i puis renvoie 3 comme résultat du test, ce qui est considéré comme vrai
car la convention est qu’un booléen est en fait un entier, faux s’il est nul et vrai s’il est non nul !
15. C’est une erreur grave et fréquente d’oublier le break. Sans lui, le programme exécute aussi les
instructions du cas suivant !
16. Attention, un cin >> c, instruction que nous verrons plus loin, lit bien un caractère au clavier
mais ne réagit pas à chaque touche : il attend qu’on appuie sur la touche Entrée pour lire d’un coup
toutes les touches frappées ! Récupérer juste une touche à la console n’est malheureusement pas stan-
dard et n’est plus très utilisé dans notre monde d’interfaces graphiques. Sous Windows, il faudra utiliser
_getch() après avoir fait un #include <conio.h> (cf. lignes 3 et 10) et sous Unix getch() après avoir fait
un #include <curses.h>.
36
3. Premiers programmes 3.1. Tout dans le main() !
25 break ;
26 default :
27 cout << " Vous avez tapé a u t r e chose ! " << endl ;
28 break ;
29 }
30 } while ( ! f i n i ) ;
31 return 0;
32 }
Si vous avez tout compris, le switch précédant ceci est équivalent à 17 :
i f ( c== ’ a ’ )
cout << " Vous avez tapé ’ a ’ ! " << endl ;
e l s e i f ( c== ’ f ’ ) {
cout << " Vous avez tapé ’ f ’ . Au r e v o i r ! " << endl ;
f i n i =true ;
} e l s e i f ( c== ’ e ’ || c== ’ i ’ || c== ’ o ’ || c== ’ u ’ || c== ’ y ’ )
cout << " Vous avez tapé une a u t r e v o y e l l e ! " << endl ;
else
cout << " Vous avez tapé a u t r e chose ! " << endl ;
Avant tout, rappelons la principale source d’erreur du switch :
Vous avez pu remarquer cette ligne 2 un peu cryptique. Un namespace est un pré-
fixe pour certains objets. Le préfixe des objets standard du langage est std. Ainsi cout
et endl ont pour nom complet std :: cout et std :: endl. La ligne 2 permet d’omettre ce
préfixe.
3.1.3 Boucles
Il est difficile de faire un programme qui fait quelque chose sans avoir la possibilité
d’exécuter plusieurs fois la même instruction. C’est le rôle des boucles. La plus utili-
sée est le for () , mais ça n’est pas la plus simple à comprendre. Commençons par le
do ... while, qui "tourne en rond" tant qu’un test est vrai. Le programme suivant at-
tend que l’utilisateur tape au clavier un entier entre 1 et 10, et lui réitère sa question
jusqu’à obtenir un nombre correct :
1 # i n c l u d e <iostream >
2 using namespace s t d ;
3
4 i n t main ( )
5 {
6 int i ;
7 do { / / Début d e l a b o u c l e
8 cout << "Un nombre e n t r e 1 e t 1 0 , SVP : " ;
17. On voit bien que le switch n’est pas toujours plus clair ni plus court. C’est comme tout, il faut
l’utiliser à bon escient. . . Et plus nous connaîtrons de C++, plus nous devrons nous rappeler cette règle
et éviter de faire des fonctions pour tout, des structures de données pour tout, des objets pour tout, des
fichiers séparés pour tout, etc.
37
3.1. Tout dans le main() ! 3. Premiers programmes
9 c i n >> i ;
10 } while ( i <1 || i > 1 0 ) ; / / R e t o u r n e au d é b u t d e l a b o u c l e s i
11 / / ce t e s t est vrai
12 cout << " Merci ! Vous avez tapé " << i << endl ;
13 return 0;
14 }
Notez la ligne 9 qui met dans i un nombre tapé au clavier. La variable cin est le pendant
en entrée ("console in") de la sortie cout.
Vient ensuite le while qui vérifie le test au début de la boucle. Le programme sui-
vant affiche les entiers de 1 à 100 :
i n t i =1;
while ( i <=100) {
cout << i << endl ;
i = i +1;
}
Enfin, on a crée une boucle spéciale tant elle est fréquente : le for () qui exécute
une instruction avant de démarrer, effectue un test au début de chaque tour, comme le
while, et exécute une instruction à la fin de chaque boucle. Instruction initiale, test et
instruction finale sont séparées par un ;, ce qui donne le programme suivant, absolu-
ment équivalent au précédent :
int i ;
f o r ( i = 1 ; i <=100; i = i +1) {
cout << i << endl ;
}
En général, le for () est utilisé comme dans l’exemple précédent pour effectuer une
boucle avec une variable (un indice) qui prend une série de valeurs dans un certain
intervalle. On trouvera en fait plutôt :
f o r ( i n t i = 1 ; i <=100; i ++)
cout << i << endl ;
quand on sait que :
— On peut définir la variable dans la première partie du for () . Attention, cette va-
riable admet le for () pour portée : elle n’est plus utilisable en dehors du for () 18 .
— i++ est une abbréviation de i=i+1
— Puisqu’il n’y a ici qu’une seule instruction dans la boucle, les accolades étaient
inutiles.
On utilise aussi la virgule , pour mettre plusieurs instructions 19 dans l’instruction fi-
nale du for. Ainsi, le programme suivant part de i=1 et j=100, et augmente i de 2 et
diminue j de 3 à chaque tour jusqu’à ce que leurs valeurs se croisent 20 :
18. Les vieux C++ ne permettaient pas de définir la variable dans la première partie du for () . Des C++
un peu moins anciens permettaient de le faire mais la variable survivait au for () !
19. Pour les curieux : ça n’a en fait rien d’extraordinaire, car plusieurs instructions séparées par une
virgule deviennent en C++ une seule instruction qui consiste à exécuter l’une après l’autre les différentes
instructions ainsi rassemblées.
20. Toujours pour les curieux, il s’arrête pour i=39 et j=43.
38
3. Premiers programmes 3.1. Tout dans le main() !
f o r ( i n t i =1 , j = 1 0 0 ; j > i ; i = i +2 , j = j −3)
cout << i << " " << j << endl ;
Notez aussi qu’on peut abréger i=i+2 en i+=2 et j =j−3 en j −=3.
3.1.4 Récréations
Nous pouvons déjà faire de nombreux programmes. Par exemple, jouer au juste
prix. Le programme choisit le prix, et l’utilisateur devine :
1 # i n c l u d e <iostream >
2 # include <cstdlib >
3 using namespace s t d ;
4
5 i n t main ( )
6 {
7 i n t n=rand ( ) % 1 0 0 ; / / nombre à d e v i n e r e n t r e 0 e t 99
8 int i ;
9 do {
10 cout << " Votre p r i x : " ;
11 c i n >> i ;
12 i f ( i >n )
13 cout << "C ’ e s t moins " << endl ;
14 e l s e i f ( i <n )
15 cout << "C ’ e s t plus " << endl ;
16 else
17 cout << " Gagne ! " << endl ;
18 } while ( i ! = n ) ;
19 return 0;
20 }
Seule la ligne 7 a besoin d’explications :
— la fonction rand() fournit un nombre entier au hasard entre 0 et RAND_MAX. On
a besoin de rajouter #include <cstdlib> pour l’utiliser
— % est la fonction modulo 21 .
C’est évidemment plus intéressant quand c’est le programme qui devine. Pour cela,
il va procéder par dichotomie, afin de trouver au plus vite :
1 # i n c l u d e <iostream >
2 using namespace s t d ;
3
4 i n t main ( )
5 {
6 cout << " C h o i s i s s e z un nombre e n t r e 1 e t 100 " << endl ;
7 cout << " Repondez par + , − ou = " << endl ;
8 i n t a =1 , b = 1 0 0 ; / / V a l e u r s e x t r è m e s
21. Coin des collégiens : compter "modulo N", c’est retomber à 0 quand on atteint N. Modulo 4, cela
donne : 0,1,2,3,0,1,2,3,0,. . . . Par exemple 12%10 vaut 2 et 11%3 aussi ! Ici, le modulo 100 sert à retomber
entre 0 et 99.
39
3.1. Tout dans le main() ! 3. Premiers programmes
9 bool trouve= f a l s e ;
10 do {
11 i n t c =( a+b ) / 2 ; / / On p r o p o s e l e m i l i e u
12 cout << " S e r a i t −ce " << c << " ? : " ;
13 char r ;
14 do
15 c i n >> r ;
16 while ( r ! = ’ = ’ && r ! = ’ + ’ && r ! = ’− ’ ) ;
17 i f ( r== ’ = ’ )
18 trouve= t r u e ;
19 e l s e i f ( r== ’− ’ )
20 b=c −1; / / C ’ e s t moins , on e s s a i e e n t r e a e t c −1
21 else
22 a=c + 1 ; / / C ’ e s t p l u s , on e s s a i e e n t r e c +1 e t b
23 } while ( ! trouve && ( a<=b ) ) ;
24 i f ( trouve )
25 cout << " Quel boss j e s u i s ! " << endl ;
26 else
27 cout << " Vous avez t r i c h é ! " << endl ;
28 return 0;
29 }
40
3. Premiers programmes 3.2. Fonctions
24 }
25 endGraphics ( ) ;
26 return 0;
27 }
Notez ce endGraphics() dont la fonction est d’attendre un clic de l’utilisateur avant
de terminer le programme, de manière à laisser la fenêtre visible le temps nécessaire.
Cette fonction n’est pas standard, et elle est dans le namespace Imagine. La ligne 2
permet de l’appeler sans utiliser son nom complet Imagine::endGraphics(). Les autres
fonctions appelées dans ce petit programme (openWindow, fillRect et milliSleep) sont
aussi fournies par Imagine.
3.2 Fonctions
Lorsqu’on met tout dans le main() on réalise très vite que l’on fait souvent des
copier/coller de bouts de programmes. Si des lignes de programmes commencent à se
ressembler, c’est qu’on est vraisemblablement devant l’occasion de faire des fonctions.
On le fait pour des raisons de clarté, mais aussi pour faire des économies de frappe au
clavier !
En fait, pouvoir réutiliser le travail déjà fait est le fil conducteur d’une bonne program-
mation. Pour l’instant, nous nous contentons, grâce aux fonctions, de réutiliser ce que
nous venons de taper quelques lignes plus haut. Plus tard, nous aurons envie de réuti-
liser ce qui aura été fait dans d’autres programmes, ou longtemps auparavant, ou dans
les programmes d’autres personnes, et nous verrons alors comment faire.
Prenons le programme suivant, qui dessine des traits et des cercles au hasard, et
dont la figure 3.1 montre un résultat :
1 # i n c l u d e <Imagine/Graphics . h>
2 using namespace Imagine ;
3 # include <cstdlib >
4 using namespace s t d ;
5
6 i n t main ( )
7 {
8 openWindow ( 3 0 0 , 2 0 0 ) ;
9 f o r ( i n t i = 0 ; i < 1 5 0 ; i ++) {
10 i n t x1=rand ( ) % 3 0 0 ; / / P o i n t i n i t i a l
11 i n t y1=rand ( ) % 2 0 0 ;
41
3.2. Fonctions 3. Premiers programmes
12 i n t x2=rand ( ) % 3 0 0 ; / / P o i n t f i n a l
13 i n t y2=rand ( ) % 2 0 0 ;
14 Color c=Color ( rand ( ) % 2 5 6 , rand ( ) % 2 5 6 , rand ( ) % 2 5 6 ) ; / / RVB
15 drawLine ( x1 , y1 , x2 , y2 , c ) ; / / T r a c é d e s e g m e n t
16 i n t xc=rand ( ) % 3 0 0 ; / / C e n t r e du c e r c l e
17 i n t yc=rand ( ) % 2 0 0 ;
18 i n t r c =rand ( ) % 1 0 ; / / Rayon
19 Color c c =Color ( rand ( ) % 2 5 6 , rand ( ) % 2 5 6 , rand ( ) % 2 5 6 ) ; / / RVB
20 f i l l C i r c l e ( xc , yc , rc , c c ) ; / / C e r c l e
21 }
22 endGraphics ( ) ;
23 return 0;
24 }
La première chose qui choque 22 , c’est l’appel répété à rand() et à modulo pour tirer
un nombre au hasard. On aura souvent besoin de tirer des nombres au hasard dans
un certain intervalle et il est naturel de le faire avec une fonction. Au passage, nous
corrigeons une deuxième chose qui choque : les entiers 300 et 200 reviennent souvent.
Si nous voulons changer les dimensions de la fenêtre, il faudra remplacer dans le pro-
gramme tous les 300 et tous les 200. Il vaudrait mieux mettre ces valeurs dans des
variables et faire dépendre le reste du programme de ces variables. C’est un défaut
constant de tous les débutants et il faut le corriger tout de suite.
Il faut dès le début d’un programme repérer les paramètres constants uti-
lisés à plusieurs reprises et les placer dans des variables dont dépendra le
programme. On gagne alors beaucoup de temps a quand on veut les modi-
fier par la suite.
a. Encore la règle du moindre effort. . . Si on fait trop de copier/coller ou de remplacer avec
l’éditeur, c’est mauvais signe !
22. à part évidemment la syntaxe "objet" des variables de type Color pour lesquelles on se permet un
Color(r,v,b) bien en avance sur ce que nous sommes censés savoir faire. . .
42
3. Premiers programmes 3.2. Fonctions
i n t main ( )
{
c o n s t i n t w=300 , h = 2 0 0 ;
openWindow (w, h ) ;
f o r ( i n t i = 0 ; i < 1 5 0 ; i ++) {
i n t x1=hasard (w) , y1=hasard ( h ) ; / / P o i n t i n i t i a l
i n t x2=hasard (w) , y2=hasard ( h ) ; / / P o i n t f i n a l
Color c=Color ( hasard ( 2 5 6 ) , hasard ( 2 5 6 ) , hasard ( 2 5 6 ) ) ;
drawLine ( x1 , y1 , x2 , y2 , c ) ; / / T r a c é d e s e g m e n t
i n t xc=hasard (w) , yc=hasard ( h ) ; / / C e n t r e du c e r c l e
i n t r c =hasard (w/ 2 0 ) ; / / Rayon
Color c c =Color ( hasard ( 2 5 6 ) , hasard ( 2 5 6 ) , hasard ( 2 5 6 ) ) ;
f i l l C i r c l e ( xc , yc , rc , c c ) ; / / C e r c l e
}
endGraphics ( ) ;
return 0;
}
On pourrait penser que hasard(w) est aussi long à taper que rand()%w et que notre
fonction est inutile. C’est un peu vrai. Mais en pratique, nous n’avons alors plus à
nous souvenir de l’existence de la fonction rand() ni de comment on fait un modulo.
C’est même mieux que ça : nous devenons indépendant de ces deux fonctions, et si
vous voulions tirer des nombres au hasard avec une autre fonction 23 , nous n’aurions
plus qu’à modifier la fonction hasard(). C’est encore une règle importante.
3.2.1 Retour
Nous venons de définir sans l’expliquer une fonction hasard() qui prend un para-
mètre n de type int et qui retourne un résultat, de type int lui aussi. Il n’y a pas grand
chose à savoir de plus, si ce n’est que :
23. Pourquoi vouloir le faire ? Dans notre cas parce que la fonction rand() utilisée est suffisante pour
des applications courantes mais pas assez précise pour des applications mathématiques. Par exemple,
faire un modulo ne répartit pas vraiment équitablement les nombres tirés. Enfin, nous avons oublié
d’initialiser le générateur aléatoire. Si vous le permettez, nous verrons une autre fois ce que cela signifie
et comment le faire en modifiant juste la fonction hasard().
43
3.2. Fonctions 3. Premiers programmes
1. Une fonction peut ne rien renvoyer. Son type de retour est alors void et il n’y a
pas de return à la fin. Par exemple :
void dis_bonjour_a_la_dame ( s t r i n g nom_de_la_dame ) {
cout << " Bonjour , Mme " << nom_de_la_dame << " ! " << endl ;
}
...
dis_bonjour_a_la_dame ( " Germaine " ) ;
dis_bonjour_a_la_dame ( " F i t z g e r a l d " ) ;
2. Une fonction peut comporter plusieurs instructions return 24 . Cela permet de sor-
tir quand on en a envie, ce qui est bien plus clair et plus proche de notre façon de
penser :
i n t s i g n e _ a v e c _ u n _ s e u l _ r e t u r n ( double x ) {
int s ;
i f ( x ==0)
s =0;
e l s e i f ( x <0)
s =−1;
else
s =1;
return s ;
}
i n t s i g n e _ p l u s _ s i m p l e ( double x ) {
i f ( x <0)
r e t u r n −1;
i f ( x >0) / / N o t e z l ’ a b s e n c e d e e l s e , d e v e n u i n u t i l e !
return 1;
return 0;
}
3. Pour une fonction void, on utilise return sans rien derrière pour un retour en
cours de fonction :
void t e l e p h o n e r _ a v e c _ u n _ s e u l _ r e t u r n ( s t r i n g nom) {
i f ( j_ai_le_telephone ) {
i f (mon telephone_marche ) {
i f ( e s t _ d a n s _ l _ a n n u a i r e (nom ) ) {
i n t numero=numero_telephone (nom ) ;
composer ( numero ) ;
i f ( ca_decroche ) {
parler ( ) ;
raccrocher ( ) ;
}
}
}
}
24. Contrairement à certains vieux langages, comme le Pascal
44
3. Premiers programmes 3.2. Fonctions
}
void t e l e p h o n e r _ p l u s _ s i m p l e ( s t r i n g nom) {
i f ( ! j_ai_le_telephone )
return ;
i f ( ! mon telephone_marche )
return ;
i f ( ! e s t _ d a n s _ l _ a n n u a i r e (nom ) )
return ;
i n t numero=numero_telephone (nom ) ;
composer ( numero ) ;
i f ( ! ca_decroche )
return ;
parler ( ) ;
raccrocher ( ) ;
}
3.2.2 Paramètres
Nous n’avons vu que des fonctions à un seul paramètre. Voici comment faire pour
en passer plusieurs ou n’en passer aucun :
/ / Nombre e n t r e a e t b
i n t hasard2 ( i n t a , i n t b )
{
r e t u r n a +( rand ( ) % ( b−a + 1 ) ) ;
}
/ / Nombre e n t r e 0 e t 1
double hasard3 ( )
{
r e t u r n rand ( ) / double (RAND_MAX) ;
}
...
i n t a=hasard2 ( 1 , 1 0 ) ;
double x=hasard3 ( ) ;
...
Attention à bien utiliser x=hasard3() et non simplement x=hasard3 pour appeler cette
fonction sans paramètre. Ce simple programme est aussi l’occasion de parler d’une
erreur très fréquente : la division de deux nombres entiers donne un nombre entier !
Ainsi, écrire double x=1/3; est une erreur car le C++ commence par calculer 1/3 avec
des entiers, ce qui donne 0, puis convertit 0 en double pour le ranger dans x. Il ne sait
pas au moment de calculer 1/3 qu’on va mettre le résultat dans un double ! Il faut alors faire
en sorte que le 1 ou le 3 soit une double et écrire double x=1.0/3; ou double x=1/3.0;.
Si, comme dans notre cas, on a affaire à deux variables de type int, il suffit de convertir
une de ces variables en double avec la syntaxe double (...) que nous verrons plus tard.
45
3.2. Fonctions 3. Premiers programmes
46
3. Premiers programmes 3.2. Fonctions
echanger2 ( a , b ) ;
cout << a << " " << b << endl ;
...
Ce programme affiche 2 3 3 2, echanger1() ne marchant pas.
Une bonne façon de comprendre le passage par référence est de considérer que les
variables x et y de echanger1 sont des variables vraiment indépendantes du a et du
b de la fonction appelante, alors qu’au moment de l’appel à echanger2, le x et le y
de echanger2 deviennent des "liens" avec a et b. A chaque fois que l’on utilise x dans
echanger2, c’est en fait a qui est utilisée. Pour encore mieux comprendre allez voir le
premier exercice du TP 2 (A.2.1) et sa solution.
En pratique,
on utilise aussi les références pour faire des fonctions retournant plusieurs
valeurs à la fois,
47
3.2. Fonctions 3. Premiers programmes
24 openWindow (w, h ) ;
25 f o r ( i n t i = 0 ; i < 1 5 0 ; i ++) {
26 i n t x1 , y1 ; / / P o i n t i n i t i a l
27 un_point (w, h , x1 , y1 ) ;
28 i n t x2 , y2 ; / / P o i n t f i n a l
29 un_point (w, h , x2 , y2 ) ;
30 Color c=une_couleur ( ) ;
31 drawLine ( x1 , y1 , x2 , y2 , c ) ; / / T r a c é d e s e g m e n t
32 i n t xc , yc ; / / C e n t r e du c e r c l e
33 un_point (w, h , xc , yc ) ;
34 i n t r c =hasard (w/ 2 0 ) ; / / Rayon
35 Color c c =une_couleur ( ) ;
36 f i l l C i r c l e ( xc , yc , rc , c c ) ; / / C e r c l e
37 }
38 endGraphics ( ) ;
39 return 0;
40 }
Avec le conseil suivant :
Il devient même :
26 i n t x1 , y1 ; / / P o i n t i n i t i a l
27 un_point (w, h , x1 , y1 ) ;
28 i n t x2 , y2 ; / / P o i n t f i n a l
29 un_point (w, h , x2 , y2 ) ;
30 drawLine ( x1 , y1 , x2 , y2 , une_couleur ( ) ) ; / / T r a c é d e s e g m e n t
31 i n t xc , yc ; / / C e n t r e du c e r c l e
32 un_point (w, h , xc , yc ) ;
33 i n t r c =hasard (w/ 2 0 ) ; / / Rayon
34 f i l l C i r c l e ( xc , yc , rc , une_couleur ( ) ) ; / / C e r c l e
comme les variables, les fonctions ont une portée et ne sont connues que
dans les lignes de source qui lui succèdent.
48
3. Premiers programmes 3.2. Fonctions
4 return 0;
5 }
6 void f ( ) {
7 }
car à la ligne 3, f () n’est pas connue. Il suffit ici de mettre les lignes 6 et 7 avant le main()
pour que le programme compile. Par contre, il est plus difficile de faire compiler :
void f ( )
{
g ( ) ; / / Erreur : g ( ) inconnue
}
void g ( ) {
f ();
}
puisque les deux fonctions on besoin l’une de l’autre, et qu’aucun ordre ne conviendra.
Il faut alors connaître la règle suivante :
Notre programme précédent peut donc se compiler avec une ligne de plus :
void g ( ) ; / / D é c l a r a t i o n de g
void f ( )
{
g(); / / OK: f o n c t i o n d é c l a r é e
}
void g ( ) { / / D é f i n i t i o n d e g
f ();
}
On parle alors de variables locales à la fonction. Ainsi, le programme suivant est in-
terdit :
49
3.2. Fonctions 3. Premiers programmes
void f ( )
{
int x ;
x =3;
}
void g ( ) {
int y ;
y=x ; / / E r r e u r : x i n c o n n u
}
Si vraiment deux fonctions utilisent des variables communes, il faut alors les "sortir"
des fonctions. Elles deviennent alors des variables globales, dont voici un exemple :
1 int z ; / / globale
2
3 void f ( )
4 {
5 int x ; / / loc ale
6 ...
7 i f ( x<z )
8 ...
9 }
10
11 void g ( )
12 {
13 int y ; / / loc ale
14 ...
15 z=y ;
16 ...
17 }
L’utilisation de variables globales est tolérée et parfois justifiée. Mais elle constitue une
solution de facilité dont les débutants abusent et il faut combattre cette tentation dès le
début :
les variables globales sont à éviter au maximum car
— elles permettent parfois des communications abusives entre fonctions,
sources de bugs a .
— les fonctions qui les utilisent sont souvent peu réutilisables dans des
contextes différents.
En général, elles sont le signe d’une mauvaise façon de traiter le problème.
a. C’est pourquoi les variables globales non constantes ne sont pas tolérées chez le débu-
tant. Voir le programme précédent où g() parle à f () au travers de z.
3.2.6 Surcharge
Il est parfois utile d’avoir une fonction qui fait des choses différentes suivant le type
d’argument qu’on lui passe. Pour cela on peut utiliser la surcharge :
50
3. Premiers programmes 3.3. TP
Deux fonctions qui ont des listes de paramètres différentes peuvent avoir le
même nom a . Attention : deux fonctions aux types de retour différents mais
aux paramètres identiques ne peuvent avoir le même nom b .
a. Car alors la façon de les appeler permettra au compilateur de savoir laquelle des fonc-
tions on veut utiliser
b. Car alors le compilateur ne pourra différencier leurs appels.
Ainsi, nos fonctions "hasard" de tout à l’heure peuvent très bien s’écrire :
1 / / Nombre e n t r e 0 e t n−1
2 i n t hasard ( i n t n ) {
3 r e t u r n rand ()%n ;
4 }
5 / / Nombre e n t r e a e t b
6 i n t hasard ( i n t a , i n t b ) {
7 r e t u r n a +( rand ( ) % ( b−a + 1 ) ) ;
8 }
9 / / Nombre e n t r e 0 e t 1
10 double hasard ( ) {
11 r e t u r n rand ( ) / double (RAND_MAX) ;
12 }
13 ...
14 i n t i =hasard ( 3 ) ; // entre 0 et 2
15 i n t j =hasard ( 2 , 4 ) / / e n t r e 2 e t 4
16 double k=hasard ( ) ; / / e n t r e 0 e t 1
3.3 TP
Nous pouvons maintenant aller faire le deuxième TP donné en annexe A.2 afin de
mieux comprendre les fonctions et aussi pour obtenir un mini jeu de tennis (figure 3.2).
51
3.4. Fiche de référence 3. Premiers programmes
52
3. Premiers programmes 3.4. Fiche de référence
53
4. Les tableaux
Chapitre 4
Les tableaux
Tout en continuant à utiliser les fonctions pour les assimiler, nous allons rajouter les ta-
bleaux qui, sinon, nous manqueraient rapidement. Nous n’irons pas trop vite et ne verrons
pour l’instant que les tableaux à une dimension et de taille fixe. Nous étudierons dans un autre
chapitre les tableaux de taille variable et les questions de mémoire ("pile" et "tas").
double x [ 1 0 0 ] , y [ 1 0 0 ] , z [ 1 0 0 ] ;
...
. . . / / i c i , l e s x [ i ] et y [ i ] prennent des valeurs
...
f o r ( i n t i = 0 ; i < 1 0 0 ; i ++)
z [ i ]= x [ i ]+ y [ i ] ;
...
. . . / / i c i , on u t i l i s e l e s z [ i ]
...
Il y deux choses essentielles à retenir.
1. D’abord :
les indices d’un tableau t de taille n vont de 0 à n-1. Tout accès à t[n]
peut provoquer une erreur grave pendant l’exécution du programme.
C’ EST UNE DES ERREURS LES PLUS FRÉQUENTES EN C++. Soit on va
lire ou écrire dans un endroit utilisé pour une autre variable a , soit on
accède à une zone mémoire illégale et le programme peut "planter" b .
a. Dans l’exemple ci-dessus, si on remplaçait la boucle pour que i aille de 1 à 100,
x[100] irait certainement chercher y[0] à la place. De même, z[100] irait peut-être
chercher la variable i de la boucle, ce qui risquerait de faire ensuite des choses étranges,
i valant n’importe quoi !
b. Ci-dessus, z[i] avec n’importe quoi pour i irait écrire en dehors de la zone ré-
servée aux données, ce qui stopperait le programme plus ou moins délicatement !
Dans le dernier exemple, on utilise x[0] à x[99]. L’habitude est de faire une boucle
avec i<100 comme test, plutôt que i<=99, ce qui est plus lisible. Mais attention à
ne pas mettre i<=100 !
2. Ensuite :
un tableau doit avoir une taille fixe connue à la compilation. Cette taille
peut être un nombre ou une variable constante, mais pas une variable.
56
4. Les tableaux 4.2. Initialisation
Connaissant ces deux points, on peut très facilement utiliser des tableaux. Attention
toutefois :
ne pas utiliser de tableau quand c’est inutile, notamment quand on traduit
une formule mathématique.
P100 1
Je m’explique. Si vous devez calculer s = i=1 f (i) pour f donnée , par exemple
f (i) = 3i + 4, n’allez pas écrire, comme on le voit parfois :
1 double f [ 1 0 0 ] ;
2 f o r ( i n t i = 1 ; i <=100; i ++)
3 f [ i ]=3∗ i + 4 ;
4 double s ;
5 f o r ( i n t i = 1 ; i <=100; i ++)
6 s=s+ f [ i ] ;
ni, même, ayant corrigé vos bugs :
5 double f [ 1 0 0 ] ; / / S t o c k e f ( i ) d a n s f [ i −1]
6 f o r ( i n t i = 1 ; i <=100; i ++)
7 f [ i −1]=3∗ i + 4 ; / / A t t e n t i o n aux i n d i c e s !
8 double s = 0 ; / / Ca va mieux comme c a !
9 f o r ( i n t i = 1 ; i <=100; i ++)
10 s=s+ f [ i − 1 ] ;
mais plutôt directement sans tableau :
5 double s = 0 ;
6 f o r ( i n t i = 1 ; i <=100; i ++)
7 s=s +(3∗ i + 4 ) ; / / Ou mieux : s +=3∗ i +4
ce qui épargnera, à la machine, un tableau (donc de la mémoire et des calculs), et à
vous des bugs (donc vos nerfs !). Notez qu’ici on utilise la relation de récurrence
k
X
sk = f (i) = sk−1 + f (k)
i=1
pour calculer s100 . Comme pour calculer sk on n’a besoin que de garder en mémoire
sk−1 , on peut se contenter d’une seule variable s qu’on met à jour.
4.2 Initialisation
Tout comme une variable, un tableau peut être initialisé :
int t [4]={1 ,2 ,3 ,4};
s t r i n g s [ 2 ] = { " hip " , " hop " } ;
Attention, la syntaxe utilisée pour l’initialisation ne marche pas pour une affecta-
tion 2 :
int t [ 2 ] ;
t ={1 ,2}; / / Erreur !
57
4.3. Spécificités des tableaux 4. Les tableaux
— Un tableau est toujours passé par référence bien qu’on n’utilise pas le
’&’ a .
— Une fonction ne peut pas retourner un tableau b .
a. Un void f(int& t[4]) ou toute autre syntaxe est une erreur.
b. On comprendra plus tard pourquoi, par souci d’efficacité, les concepteurs du C++ ont
voulu qu’un tableau ne soit ni passé par valeur, ni retourné.
donc :
1 / / R a p p e l : c e c i ne marche p a s
2 void a f f e c t e 1 ( i n t x , i n t v a l ) {
3 x= v a l ;
4 }
5 / / R a p p e l : c ’ e s t c e c i q u i marche !
6 void a f f e c t e 2 ( i n t& x , i n t v a l ) {
7 x= v a l ;
8 }
9 / / Une f o n c t i o n q u i marche s a n s ’& ’
10 void r e m p l i t ( i n t s [ 4 ] , i n t v a l ) {
11 f o r ( i n t i = 0 ; i < 4 ; i ++)
12 s [ i ]= v a l ;
13 }
14 ...
15 i n t a =1;
3. Il est du coup de plus en plus fréquent que les programmeurs utilisent directement des variables
de type vector qui sont des objets implémentant les fonctionnalités des tableaux tout en se comportant
davantage comme des variables standard. Nous préférons ne pas parler dès maintenant des vector
car leur compréhension nécessite celle des objets et celle des "template". Nous pensons aussi que la
connaissance des tableaux, même si elle demande un petit effort, est incontournable et aide à la compré-
hension de la gestion de la mémoire.
58
4. Les tableaux 4.3. Spécificités des tableaux
16 affecte1 (a , 0 ) ; / / a ne s e r a p a s mis à 0
17 cout << a << endl ; // vérification
18 affecte2 (a , 0 ) ; / / a s e r a b i e n mis à 0
19 cout << a << endl ; // vérification
20 int t [ 4 ] ;
21 remplit ( t , 0 ) ; / / Met l e s t [ i ] à 0
22 affiche ( t ) ; / / V é r i f i e que l e s t [ i ] v a l e n t 0
et aussi :
1 / / Somme d e deux t a b l e a u x q u i ne c o m p i l e même p a s
2 / / Pour r e t o u r n e r un t a b l e a u
3 i n t somme1 ( i n t x [ 4 ] , i n t y [ 4 ] ) [ 4 ] { / / on p e u t i m a g i n e r m e t t r e l e
4 / / [ 4 ] i c i ou a i l l e u r s :
5 // rien n ’y f a i t !
6 int z [ 4 ] ;
7 f o r ( i n t i = 0 ; i < 4 ; i ++)
8 z [ i ]= x [ i ]+ y [ i ] ;
9 return z ;
10 }
11 / / En p r a t i q u e , on f e r a donc comme ç a !
12 / / Somme d e deux t a b l e a u x q u i marche
13 void somme2 ( i n t x [ 4 ] , i n t y [ 4 ] , i n t z [ 4 ] )
14 f o r ( i n t i = 0 ; i < 4 ; i ++)
15 z [ i ]= x [ i ]+ y [ i ] ; / / OK: ’ z ’ e s t p a s s é p a r r é f é r e n c e !
16 }
17
18 int a [4] ,b [ 4 ] ;
19 ... / / r e m p l i s s a g e de a e t b
20 int c [ 4 ] ;
21 c=somme1 ( a , b ) ; / / ERREUR
22 somme2 ( a , b , c ) ; / / OK
Enfin, et c’est utilisé tout le temps,
Une fonction n’est pas tenue de travailler sur une taille de tableau
unique. . . mais il est impossible de demander à un tableau sa taille !
On utilise la syntaxe int t [] dans les paramètres pour un tableau dont on ne précise
pas la taille. Comme il faut bien parcourir le tableau dans la fonction et qu’on ne peut
retrouver sa taille, on la passe en paramètre en plus du tableau :
1 / / Une f o n c t i o n q u i ne marche p a s
2 void a f f i c h e 1 ( i n t t [ ] ) {
3 f o r ( i n t i = 0 ; i <TAILLE ( t ) ; i ++) / / TAILLE ( t ) n ’ e x i s t e p a s !
4 cout << t [ i ] << endl ;
5 }
6 / / Comment on f a i t en p r a t i q u e
7 void a f f i c h e 2 ( i n t t [ ] , i n t n ) {
8 f o r ( i n t i = 0 ; i <n ; i ++)
9 cout << t [ i ] << endl ;
59
4.4. Récréations 4. Les tableaux
10 }
11 ...
12 int t1 [ 2 ] = { 1 , 2 } ;
13 int t2 [ 3 ] = { 3 , 4 , 5 } ;
14 a f f i c h e 2 ( t1 , 2 ) ; / / OK
15 a f f i c h e 2 ( t2 , 3 ) ; / / OK
4.3.2 Affectation
C’est simple :
Ainsi, le programme :
int s [4]={1 ,2 ,3 ,4} , t [ 4 ] ;
t =s ; / / ERREUR d e c o m p i l a t i o n
ne marche pas et on est obligé de faire :
int s [4]={1 ,2 ,3 ,4} , t [ 4 ] ;
f o r ( i n t i = 0 ; i < 4 ; i ++)
t [ i ]= s [ i ] ; / / OK
Le problème, c’est que :
Affecter un tableau ne marche jamais mais ne génère pas toujours une er-
reur de compilation, ni même un warning. C’est le cas entre deux para-
mètres de fonction. Nous comprendrons plus tard pourquoi et l’effet exact
d’une telle affectation. . .
.
1 / / F o n c t i o n q u i ne marche p a s
2 / / Mais q u i c o m p i l e t r è s b i e n !
3 void s e t 1 ( i n t s [ 4 ] , i n t t [ 4 ] ) {
4 t =s ; / / Ne f a i t p a s c e qu ’ i l f a u t !
5 / / m a i s c o m p i l e s a n s warning !
6 }
7 / / F o n c t i o n q u i marche ( e t q u i c o m p i l e ! −)
8 void s e t 2 ( i n t s [ 4 ] , i n t t [ 4 ] ) {
9 f o r ( i n t i = 0 ; i < 4 ; i ++)
10 t [ i ]= s [ i ] ; / / OK
11 }
12 ...
13 int s [4]={1 ,2 ,3 ,4} , t [ 4 ] ;
14 s e t 1 ( s , t ) ; / / Sans e f f e t
15 s e t 2 ( s , t ) ; / / OK
60
4. Les tableaux 4.4. Récréations
F IGURE 4.1 – Des balles qui rebondissent. . . (momentanément figées ! Allez sur la page
du cours pour un programme animé !)
4.4 Récréations
4.4.1 Multi-balles
Nous pouvons maintenant reprendre le programme de la balle qui rebondit, donné
à la section 3.1.4, puis amélioré avec des fonctions et de constantes lors du TP de l’an-
nexe A.2. Grâce aux tableaux, il est facile de faire se déplacer plusieurs balles à la fois.
Nous tirons aussi la couleur et la position et la vitesse initiales des balles au hasard.
Plusieurs fonctions devraient vous être inconnues :
— L’initialisation du générateur aléatoire avec srand((unsigned int)time(0)), qui est
expliquée dans le TP 3 (annexe A.3)
— Les fonctions noRefreshBegin et noRefreshEnd qui servent à accélérer l’affichage
de toutes les balles (voir documentation de Imagine++ annexe B).
Voici le listing du programme (exemple d’affichage (malheureusement statique !) fi-
gure 4.1) :
1 # i n c l u d e <Imagine/Graphics . h>
2 using namespace Imagine ;
3 # include <cstdlib >
4 # i n c l u d e <ctime >
5 using namespace s t d ;
6 // / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
7 / / C o n s t a n t e s du programme
8 c o n s t i n t width = 2 5 6 ; / / Largeur de l a f e n e t r e
9 const i n t height =256; / / Hauteur d e l a f e n e t r e
10 const i n t b a l l _ s i z e =4; / / Rayon d e l a b a l l e
11 const i n t nb_balls =30; / / Nombre d e b a l l e s
12 // / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
13 / / Generateur a l e a t o i r e
14 / / A n ’ a p p e l e r qu ’ une f o i s , a v a n t Random ( )
61
4.4. Récréations 4. Les tableaux
15 void InitRandom ( )
16 {
17 srand ( ( unsigned i n t ) time ( 0 ) ) ;
18 }
19 / / Entre a e t b
20 i n t Random ( i n t a , i n t b )
21 {
22 r e t u r n a +( rand ( ) % ( b−a + 1 ) ) ;
23 }
24 // / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
25 // Position et vitesse aleatoire
26 void I n i t B a l l e ( i n t &x , i n t &y , i n t &u , i n t &v , Color &c ) {
27 x=Random ( b a l l _ s i z e , width−b a l l _ s i z e ) ;
28 y=Random ( b a l l _ s i z e , height −b a l l _ s i z e ) ;
29 u=Random ( 0 , 4 ) ;
30 v=Random ( 0 , 4 ) ;
31 c=Color ( byte ( Random ( 0 , 2 5 5 ) ) ,
32 byte ( Random ( 0 , 2 5 5 ) ) ,
33 byte ( Random ( 0 , 2 5 5 ) ) ) ;
34 }
35 // / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
36 / / A f f i c h a g e d ’ une b a l l e
37 void D e s s i n e B a l l e ( i n t x , i n t y , Color c o l ) {
38 f i l l R e c t ( x−b a l l _ s i z e , y−b a l l _ s i z e , 2 ∗ b a l l _ s i z e +1 ,2∗ b a l l _ s i z e +1 , c o l ) ;
39 }
40 // / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
41 / / D e p l a c e m e n t d ’ une b a l l e
42 void BougeBalle ( i n t &x , i n t &y , i n t &u , i n t &v ) {
43 / / Rebond s u r l e s b o r d s g a u c h e e t d r o i t
44 i f ( x+u>width−b a l l _ s i z e || x+u< b a l l _ s i z e )
45 u=−u ;
46 / / Rebond s u r l e s b o r d s h a u t e t b a s e t c o m p t a g e du s c o r e
47 i f ( y+v< b a l l _ s i z e || y+v>height −b a l l _ s i z e )
48 v=−v ;
49 / / Mise a j o u r d e l a p o s i t i o n
50 x+=u ;
51 y+=v ;
52 }
53 // / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
54 / / Fonction p r i n c i p a l e
55 i n t main ( )
56 {
57 / / Ouverture de l a f e n e t r e
58 openWindow ( width , h e i g h t ) ;
59 / / Position et v i t e s s e des b a l l e s
60 i n t xb [ n b _ b a l l s ] , yb [ n b _ b a l l s ] , ub [ n b _ b a l l s ] , vb [ n b _ b a l l s ] ;
61 Color cb [ n b _ b a l l s ] ; / / C o u l e u r s d e s b a l l e s
62 InitRandom ( ) ;
62
4. Les tableaux 4.4. Récréations
63 f o r ( i n t i = 0 ; i < n b _ b a l l s ; i ++) {
64 I n i t B a l l e ( xb [ i ] , yb [ i ] , ub [ i ] , vb [ i ] , cb [ i ] ) ;
65 D e s s i n e B a l l e ( xb [ i ] , yb [ i ] , cb [ i ] ) ;
66 }
67 / / Boucle p r i n c i p a l e
68 while ( t r u e ) {
69 milliSleep (25);
70 noRefreshBegin ( ) ;
71 f o r ( i n t i = 0 ; i < n b _ b a l l s ; i ++) {
72 D e s s i n e B a l l e ( xb [ i ] , yb [ i ] , White ) ;
73 BougeBalle ( xb [ i ] , yb [ i ] , ub [ i ] , vb [ i ] ) ;
74 D e s s i n e B a l l e ( xb [ i ] , yb [ i ] , cb [ i ] ) ;
75 }
76 noRefreshEnd ( ) ;
77 }
78 endGraphics ( ) ;
79 return 0;
80 }
63
4.4. Récréations 4. Les tableaux
27 y=Random ( b a l l _ s i z e , height −b a l l _ s i z e ) ;
28 u=Random ( 0 , 4 ) ;
29 v=Random ( 0 , 4 ) ;
30 c=Color ( byte ( Random ( 0 , 2 5 5 ) ) ,
31 byte ( Random ( 0 , 2 5 5 ) ) ,
32 byte ( Random ( 0 , 2 5 5 ) ) ) ;
33 }
34 // / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
35 / / A f f i c h a g e d ’ une b a l l e
36 void D e s s i n e B a l l e ( double x , double y , Color c o l ) {
37 f i l l R e c t ( i n t ( x)− b a l l _ s i z e , i n t ( y)− b a l l _ s i z e ,
38 2∗ b a l l _ s i z e +1 ,2∗ b a l l _ s i z e +1 , c o l ) ;
39 }
40 // / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
41 / / Choc e l a s t i q u e d e deux b a l l e s s p h e r i q u e s
42 / / c f l a b o . n t i c . org
43 # i n c l u d e <cmath>
44 void C h o c B a l l e s ( double&x1 , double&y1 , double&u1 , double&v1 ,
45 double&x2 , double&y2 , double&u2 , double&v2 )
46 {
47 / / Distance
48 double o2o1x=x1−x2 , o2o1y=y1−y2 ;
49 double d= s q r t ( o2o1x ∗ o2o1x+o2o1y ∗ o2o1y ) ;
50 i f ( d==0) r e t u r n ; / / Même c e n t r e ?
51 / / R e p è r e ( o2 , x , y )
52 double Vx=u1−u2 , Vy=v1−v2 ;
53 double V= s q r t ( Vx∗Vx+Vy∗Vy ) ;
54 i f (V==0) r e t u r n ; / / Même v i t e s s e
55 / / R e p è r e s u i v a n t V ( o2 , i , j )
56 double i x =Vx/V, i y =Vy/V, j x =−iy , j y = i x ;
57 / / Hauteur d ’ a t t a q u e
58 double H=o2o1x ∗ j x +o2o1y ∗ j y ;
59 / / Angle
60 double th=acos (H/d ) , c=cos ( th ) , s= s i n ( th ) ;
61 / / V i t e s s e a p r è s c h o c d a n s ( o2 , i , j )
62 double v 1 i =V∗ c ∗ c , v 1 j =V∗ c ∗ s , v 2 i =V∗ s ∗ s , v 2 j=−v 1 j ;
63 / / Dans r e p è r e d ’ o r i g i n e (O, x , y )
64 u1= v 1 i ∗ i x + v 1 j ∗ j x +u2 ;
65 v1= v 1 i ∗ i y + v 1 j ∗ j y +v2 ;
66 u2+= v 2 i ∗ i x + v 2 j ∗ j x ;
67 v2+= v 2 i ∗ i y + v 2 j ∗ j y ;
68 }
69 // / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
70 / / D e p l a c e m e n t d ’ une b a l l e
71 void BougeBalle ( double x [ ] , double y [ ] , double u [ ] , double v [ ] , i n t i )
72 { / / Rebond s u r l e s b o r d s g a u c h e e t d r o i t
73 i f ( x [ i ]+u [ i ] > width−b a l l _ s i z e || x [ i ]+u [ i ] < b a l l _ s i z e )
74 u [ i ]=−u [ i ] ;
64
4. Les tableaux 4.4. Récréations
75 / / Rebond s u r l e s b o r d s h a u t e t b a s e t c o m p t a g e du s c o r e
76 i f ( y [ i ]+ v [ i ] < b a l l _ s i z e || y [ i ]+ v [ i ] > height −b a l l _ s i z e )
77 v [ i ]=−v [ i ] ;
78 f o r ( i n t j = 0 ; j < n b _ b a l l s ; j ++) {
79 i f ( j == i )
80 continue ;
81 i f ( abs ( x [ i ]+u [ i ]−x [ j ] ) < 2 ∗ b a l l _ s i z e
82 && abs ( y [ i ]+ v [ i ]−y [ j ] ) < 2 ∗ b a l l _ s i z e ) {
83 ChocBalles ( x [ i ] , y [ i ] , u[ i ] , v [ i ] , x [ j ] , y [ j ] , u[ j ] , v [ j ] ) ;
84 }
85 }
86 / / Mise a j o u r d e l a p o s i t i o n
87 x [ i ]+=u [ i ] ;
88 y [ i ]+=v [ i ] ;
89 }
90 // / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
91 / / Fonction p r i n c i p a l e
92 i n t main ( )
93 {
94 / / Ouverture de l a f e n e t r e
95 openWindow ( width , h e i g h t ) ;
96 / / Position et v i t e s s e des b a l l e s
97 double xb [ n b _ b a l l s ] , yb [ n b _ b a l l s ] , ub [ n b _ b a l l s ] , vb [ n b _ b a l l s ] ;
98 Color cb [ n b _ b a l l s ] ; / / C o u l e u r s d e s b a l l e s
99 InitRandom ( ) ;
100 f o r ( i n t i = 0 ; i < n b _ b a l l s ; i ++) {
101 I n i t B a l l e ( xb [ i ] , yb [ i ] , ub [ i ] , vb [ i ] , cb [ i ] ) ;
102 D e s s i n e B a l l e ( xb [ i ] , yb [ i ] , cb [ i ] ) ;
103 }
104 / / Boucle p r i n c i p a l e
105 while ( t r u e ) {
106 milliSleep (25);
107 noRefreshBegin ( ) ;
108 f o r ( i n t i = 0 ; i < n b _ b a l l s ; i ++) {
109 D e s s i n e B a l l e ( xb [ i ] , yb [ i ] , White ) ;
110 BougeBalle ( xb , yb , ub , vb , i ) ;
111 D e s s i n e B a l l e ( xb [ i ] , yb [ i ] , cb [ i ] ) ;
112 }
113 noRefreshEnd ( ) ;
114 }
115 endGraphics ( ) ;
116 return 0;
117 }
65
4.4. Récréations 4. Les tableaux
66
4. Les tableaux 4.5. TP
39
40 i n t main ( )
41 {
42 const i n t n=11;
43 s t r i n g phrase [ n ] = { " C e t t e " , " p e t i t e " , " phrase " , " d e v r a i t " , " e t r e " ,
44 " encore " , " l i s i b l e " , " pour " , " v o t r e " , " pauvre " ,
45 " cerveau " } ;
46
47 InitRandom ( ) ;
48 f o r ( i n t i = 0 ; i <n ; i ++)
49 cout << Melanger ( phrase [ i ] , 3 ) << " " ;
50 cout << endl ;
51
52 return 0;
53 }
4.5 TP
Nous pouvons maintenant aller faire le troisième TP donné en annexe A.3 afin de
mieux comprendre les tableaux et aussi pour obtenir un master mind (voir figure 4.2
le résultat d’une partie intéressante !).
67
4.6. Fiche de référence 4. Les tableaux
Comme promis, nous complétons, en rouge, la "fiche de référence" avec ce qui a été
vu pendant ce chapitre et son TP.
68
4. Les tableaux 4.6. Fiche de référence
69
5. Les structures
Chapitre 5
Les structures
Les fonctions et les boucles nous ont permis de regrouper des instructions identiques. Les
tableaux permettent de grouper des variables de même type, mais pour manipuler plusieurs va-
riables simultanément, il est tout aussi indispensable des fabriquer des structures de données. . .
5.1 Révisions
Avant cela, il est utile de nous livrer à une petite révision, qui prendra la forme d’un
inventaire des erreurs classiques commises par de nombreux débutants. . . et même de
celles, plus rares mais plus originales, constatées chez certains ! Enfin, nous répéterons,
encore et toujours, les mêmes conseils.
1. Ne me faites pas dire ce que je n’ai pas dit ! Les informaticiens théoriques considèrent parfois les
programmes comme des formules, mais ça n’a rien à voir !
72
5. Les structures 5.2. Les structures
Il est compréhensible que le débutant puisse être victime de son manque de savoir,
d’une mauvaise assimilation des leçons précédentes, de la confusion avec un autre
langage, ou de son imagination débordante ! Toutefois, il faut bien comprendre qu’un
langage est finalement lui aussi un programme, limité et conçu pour faire des choses
bien précises. En conséquence, il est plus raisonnable d’adopter la conduite suivante :
Tout ce qui n’a pas été annoncé comme possible est impossible !
5.1.3 Conseils
— Indenter. Indenter. Indenter !
— Cliquer sur les messages d’erreurs et de warnings pour aller directement à la
bonne ligne !
— Ne pas laisser de warning.
— Utiliser le debuggeur.
73
5.2. Les structures 5. Les structures
Color c o u l e u r ;
};
Cercle C;
C. centre . x =12.;
C. centre . y =13.;
C . rayon = 1 0 . 4 ;
C . c o u l e u r =Red ;
L’intérêt des structures est évident et il faut
Regrouper dans des structures des variables dès qu’on repère qu’elles sont
logiquement liées. Si un programme devient pénible parce qu’on passe
systématiquement plusieurs paramètres identiques à de nombreuses fonc-
tions, alors il est vraisemblable que les paramètres en question puissent
être avantageusement regroupés en une structure. Ce sera plus simple et
plus clair.
5.2.2 Utilisation
Les structures se manipulent comme les autres types 2 . La définition, l’affectation,
l’initialisation, le passage en paramètre, le retour d’une fonction : tout est semblable
au comportement des types de base. Seule nouveauté : on utilise des accolades pour
préciser les valeurs des champs en cas d’initialisation 3 . On peut évidemment faire
des tableaux de structures. . . et même définir un champ de type tableau ! Ainsi, les
lignes suivantes se comprennent facilement :
P o i n t a = { 2 . 3 , 3 . 4 } , b=a , c ; // Initialisations
c=a ; // Affectations
C e r c l e C= { { 1 2 , 1 3 } , 1 0 . 4 , Red } ; / / I n i t i a l i s a t i o n
...
double d i s t a n c e ( P o i n t a , P o i n t b ) { / / Passage par valeur
r e t u r n s q r t ( ( a . x−b . x ) ∗ ( a . x−b . x ) + ( a . y−b . y ) ∗ ( a . y−b . y ) ) ;
}
void a g r a n d i r ( C e r c l e& C , double e c h e l l e ) { / / Par r é f é r e n c e
C . rayon=C . rayon ∗ e c h e l l e ; / / M o d i f i e l e r a y o n
}
Point milieu ( Point a , Point b ) { // retour
P o i n t M;
M. x =( a . x+b . x ) / 2 ;
M. y =( a . y+b . y ) / 2 ;
r e t u r n M;
}
...
Point P [ 1 0 ] ; / / Tableau de s t r u c t u r e s
f o r ( i n t i = 0 ; i < 1 0 ; i ++) {
P [ i ] . x= i ;
2. D’ailleurs, nous avions bien promis que seuls les tableaux avaient des particularités (passage par
référence, pas de retour possible et pas d’affectation.
3. Comme pour un tableau !
74
5. Les structures 5.2. Les structures
P [ i ] . y= f ( i ) ;
}
...
/ / Un d é b u t d e j e u d e Yam ’ s
s t r u c t Tirage { / /
i n t de [ 5 ] ; / / champ d e t y p e t a b l e a u
};
Tirage lancer ( ) {
Tirage t ;
f o r ( i n t i = 0 ; i < 5 ; i ++)
t . de [ i ]=1+ rand ( ) % 6 ; / / Un d é d e 1 à 6
return t ;
}
...
Tirage t ;
t=lancer ( ) ;
Attention, tout comme pour les tableaux, la syntaxe utilisée pour l’initialisation ne
marche pas pour une affectation 4 :
Point P ;
P={1 ,2}; / / Erreur !
D’ailleurs, répétons-le :
Tout ce qui n’a pas été annoncé comme possible est impossible !
75
5.3. Récréation : TP 5. Les structures
5.3 Récréation : TP
Nous pouvons maintenant aller faire le TP de l’annexe A.4 afin de mieux com-
prendre les structures. Nous ferons même des tableaux de structures 5 ! Nous obtien-
drons un projectile naviguant au milieu des étoiles puis un duel dans l’espace (fi-
gure 5.1) !
76
5. Les structures 5.4. Fiche de référence
77
5.4. Fiche de référence 5. Les structures
78
6. Plusieurs fichiers !
Chapitre 6
Plusieurs fichiers !
Lors du dernier TP, nous avons réalisé deux projets quasiment similaires dont seuls les
main() étaient différents. Modifier après coup une des fonctions de la partie commune aux
deux projets nécessiterait d’aller la modifier dans les deux projets. Nous allons voir maintenant
comment factoriser cette partie commune dans un seul fichier, de façon à en simplifier les éven-
tuelles futures modifications. Au passage 1 nous verrons comment définir un opérateur sur de
nouveaux types.
1. Toujours cette idée que nous explorons les différentes composantes du langages quand le besoin
s’en fait sentir.
6.1. Fichiers séparés 6. Plusieurs fichiers !
D’ailleurs il est aussi préférable d’éviter les accents pour les noms de variables et de
fonctions, tant pis pour la correction du français. . .
6.1.1 Principe
Jusqu’à présent un seul fichier source contenait notre programme C++. Ce fichier
source était transformé en fichier objet par le compilateur puis le linker complétait le
fichier objet avec les bibliothèques du C++ pour en faire un fichier exécutable. En fait,
un projet peut contenir plusieurs fichiers sources. Il suffit pour cela de rajouter un
fichier .cpp à la liste des sources du projet :
— Dans QtCreator, ouvrir le menu File/New File or Project ou faire Ctrl+N,
choisir comme modèle C++ Source File, lui donner un nom et bien s’assurer
qu’on le met dans le dossier des sources (et non dans le dossier de build).
— Rajouter ce fichier dans le CMakeLists.txt :
a d d _e xe cu ta bl e (Hop main . cpp hop . cpp )
une fonction n’est pas "connue" en dehors de son fichier. Pour l’utiliser dans
un autre fichier, il faut donc l’y déclarer !
80
6. Plusieurs fichiers ! 6.1. Fichiers séparés
— Fichier hop.cpp :
// Définitions
void f ( i n t x ) {
...
}
int g () {
...
}
/ / Autres f o n c t i o n s
...
— Fichier main.cpp :
// Déclarations
void f ( i n t x ) ;
int g ( ) ;
...
i n t main ( ) {
...
// Utilisation
i n t a=g ( ) ;
f (a );
...
Nous pourrions aussi évidemment déclarer dans hop.cpp certaines fonctions de main.cpp
pour pouvoir les utiliser. Attention toutefois : si des fichiers s’utilisent de façon croisée,
c’est peut-être que nous sommes en train de ne pas découper les sources convenable-
ment.
6.1.2 Avantages
Notre motivation initiale était de mettre une partie du code dans un fichier séparé
pour l’utiliser dans un autre projet. En fait, découper son code en plusieurs fichiers a
d’autres intérêts :
— Rendre le code plus lisible et évitant les fichiers trop longs et en regroupant les
fonctions de façon structurée.
— Accélérer la compilation. Lorsqu’un programme devient long et complexe, le
temps de compilation n’est plus négligeable. Or, lorsque l’on régénère un projet,
l’environnement de programmation ne recompile que les fichiers sources qui ont
été modifiés depuis la génération précédente. Il serait en effet inutile de recompi-
ler un fichier source non modifié pour ainsi obtenir le même fichier objet 3 ! Donc
changer quelques lignes dans un fichier n’entraînera pas la compilation de tout
le programme mais seulement du fichier concerné.
Attention toutefois à ne pas séparer en de trop nombreux fichiers ! Il devient alors plus
compliqué de s’y retrouver et de naviguer parmi ces fichiers.
3. C’est en réalité un peu plus compliqué : un source peu dépendre, via des inclusions (cf sec-
tion 6.1.4), d’autres fichiers, qui, eux, peuvent avoir été modifiés ! Il faut alors recompiler un fichier
dont une dépendance a été modifiée. Ces dépendances sont gérées automatiquement par Cmake.
81
6.1. Fichiers séparés 6. Plusieurs fichiers !
F IGURE 6.2 – Même source dans deux projets : hop.cpp et hop.h sont partagés par
les deux projets, même s’ils se trouvent dans le dossier Projet
Il s’agit bien de remplacer par le texte complet du fichier nom comme avec un simple
copier/coller. Cette opération est faite avant la compilation par un programme dont
nous n’avions pas parlé : le pré-processeur. Les lignes commençant par un # lui seront
destinées. Nous en verrons d’autres. Attention : jusqu’ici nous utilisions une forme
légèrement différente : #include <nom>, qui va chercher le fichier nom dans les dossiers
des bibliothèques C++ 5 .
Grâce à cette possibilité du pré-processeur, il nous suffit de mettre les déclarations
se rapportant au fichier séparé dans un troisième fichier et de l’inclure dans les fichiers
principaux. Il est d’usage de prendre pour ce fichier supplémentaire le même nom que
le fichier séparé, mais avec l’extension .h : on appelle ce fichier un fichier d’en-tête 6 .
Pour créer ce fichier, faire comme pour le source, mais en choisissant "C++ Header File"
au lieu de "C++ Source File". Voila ce que cela donne :
— Fichier hop.cpp :
// Définitions
void f ( i n t x ) {
4. Toujours le moindre effort. . .
5. Les fichiers d’en-tête iostream, etc. sont parfois appelés en-têtes système. Leur nom ne se termine
pas toujours par .h (voir après), mais rien ne les distingue fondamentalement d’un fichier d’en-tête
habituel. Certains noms de headers système commencent par la lettre c, comme cmath, ce sont ceux
hérités du C.
6. .h comme header, on voit aussi parfois .hpp pour les distinguer des headers du C.
82
6. Plusieurs fichiers ! 6.1. Fichiers séparés
...
}
int g () {
...
}
/ / Autres f o n c t i o n s
...
— Fichier hop.h :
// Déclarations
void f ( i n t x ) ;
int g ( ) ;
En fait, pour être sûr que les fonctions définies dans hop.cpp sont cohérentes avec leur
déclaration dans hop.h, et bien que ça ne soit pas obligatoire, on inclut aussi l’en-tête
dans le source, ce qui donne :
— Fichier hop.cpp :
# i n c l u d e " hop . h "
...
// Définitions
void f ( i n t x ) {
...
}
int g () {
7. On peut aussi préciser au compilateur une liste de dossiers où il peut aller chercher les fichiers
d’en-tête, voir la section 6.1.8.
83
6.1. Fichiers séparés 6. Plusieurs fichiers !
...
}
/ / Autres f o n c t i o n s
...
En pratique, le fichier d’en-tête ne contient pas seulement les déclarations des fonc-
tions mais aussi les définitions des nouveaux types (comme les structures) utilisés par
le fichier séparé. En effet, ces nouveaux types doivent être connus du fichier séparé,
mais aussi du fichier principal. Il faut donc vraiment :
— Fichier vect.cpp :
# include " vect . h" / / Fonctions e t t y p e s
// Définitions
double norme ( Vecteur V) {
...
}
Vecteur plus ( Vecteur A, Vecteur B ) {
...
}
/ / Autres f o n c t i o n s
...
84
6. Plusieurs fichiers ! 6.1. Fichiers séparés
6.1.6 Implémentation
Finalement, la philosophie de ce système est que
8. On devrait même tout faire pour bien les cacher et pour interdire au fichier principal de les utiliser.
Il serait possible de le faire dès maintenant, mais nous en reparlerons plutôt quand nous aborderons les
objets. . .
9. Une même fonction peut alors se retrouver définie plusieurs fois : dans le fichier séparé et dans le
fichier principal qui l’inclut. Or, s’il est possible de déclarer autant de fois que nécessaire une fonction, il
est interdit de la définir plusieurs fois (ne pas confondre avec la surcharge qui rend possible l’existence
de fonctions différentes sous le même nom—cf. section 3.2.6)
85
6.1. Fichiers séparés 6. Plusieurs fichiers!
Certains compilateurs peuvent ne pas connaître #pragma once. On utilise alors une
astuce que nous donnons sans explication :
— Choisir un nom unique propre au fichier d’en-tête. Par exemple VECT_H pour le
fichier vect.h.
— Placer #ifndef VECT_H et #define VECT_H au début du fichier vect.h et #endif
à la fin.
Cela utilise la commande if du préprocesseur. Notons un autre usage parfois utile en
cours de développement pour que le compilateur ne regarde pas tout un bloc de code :
#if 0
N’ importe quoi i c i , ce s e r a i g n o r é par l e c o m p i l a t e u r .
# endif
86
6. Plusieurs fichiers ! 6.2. Opérateurs
<Imagine_DIR>/include
6.2 Opérateurs
Le C++ permet de définir les opérateurs +, -, etc. quand les opérandes sont de nou-
veaux types. Voici très succinctement comment faire. Nous laissons au lecteur le soin
de découvrir seul quels sont les opérateurs qu’il est possible de définir.
Considérons l’exemple suivant qui définit un vecteur 12 2D et en implémente l’ad-
dition :
s t r u c t vect {
double x , y ;
};
v e c t plus ( v e c t m, v e c t n ) {
v e c t p={m. x+n . x ,m. y+n . y } ;
return p ;
}
i n t main ( ) {
vect a ={1 ,2} , b = { 3 , 4 } ;
v e c t c=plus ( a , b ) ;
return 0;
}
Voici comment définir le + entre deux vect et ainsi remplacer la fonction plus() :
s t r u c t vect {
double x , y ;
};
v e c t o p e r a t o r +( v e c t m, v e c t n ) {
v e c t p={m. x+n . x ,m. y+n . y } ;
return p ;
}
i n t main ( ) {
vect a ={1 ,2} , b = { 3 , 4 } ;
v e c t c=a+b ;
return 0;
}
Nous pouvons aussi définir un produit par un scalaire, un produit scalaire 13 , etc 14 .
12. Coin des collégiens : vous ne savez pas ce qu’est un vecteur. . . mais vous êtes plus forts en pro-
grammation que les "vieux". Alors regardez les sources qui suivent et vous saurez ce qu’est un vecteur
2D !
13. Dans ce cas, on utilise a*b et non a.b, le point n’étant pas définissable car réservé à l’accès aux
champs de la structure
14. On peut en fait définir ce qui existe déjà sur les types de base. Attention, il est impossible de
redéfinir les opérations des types de base ! Pas question de donner un sens différent à 1+1.
87
6.3. Récréation : TP suite et fin 6. Plusieurs fichiers !
/ / P r o d u i t p a r un s c a l a i r e
v e c t o p e r a t o r ∗ ( double s , v e c t m) {
v e c t p={ s ∗m. x , s ∗m. y } ;
return p ;
}
/ / Produit s c a l a i r e
double o p e r a t o r ∗ ( v e c t m, v e c t n ) {
r e t u r n m. x∗n . x+m. y∗n . y ;
}
i n t main ( ) {
vect a ={1 ,2} , b = { 3 , 4 } ;
v e c t c =2∗a ;
double s=a ∗b ;
return 0;
}
Remarquez que les deux fonctions ainsi définies sont différentes bien que de même
nom (operator∗) car elles prennent des paramètres différents (cf surcharge section 3.2.6).
La fiche habituelle. . .
88
6. Plusieurs fichiers ! 6.4. Fiche de référence
89
6.4. Fiche de référence 6. Plusieurs fichiers !
90
6. Plusieurs fichiers ! 6.4. Fiche de référence
91
7. La mémoire
Chapitre 7
La mémoire
Il est grand temps de revenir sur la mémoire et son utilisation. Nous pourrons alors mieux
comprendre les variables locales, comment marche exactement l’appel d’une fonction, les fonc-
tions récursives, etc. Après cela, nous pourrons enfin utiliser des tableaux de taille variable
(sans pour autant rentrer vraiment dans la notion délicate de pointeur).
7.1.1 Exemple
Considérons le programme suivant :
1 # i n c l u d e <iostream >
2 using namespace s t d ;
3
4 void v e r i f i e ( i n t p , i n t q , i n t quo , i n t r e s ) {
5 i f ( re s <0 || re s >=q || q∗quo+ r e s ! = p )
6 cout << " Tiens , c ’ e s t b i z a r r e ! " << endl ;
7 }
8
9 i n t d i v i s e ( i n t a , i n t b , i n t& r ) {
10 int q ;
11 q=a/b ;
12 r=a−q∗b ;
13 verifie (a ,b,q, r );
14 return q ;
15 }
16 i n t main ( )
17 {
18 i n t num, denom ;
19 do {
7.1. L’appel d’une fonction 7. La mémoire
Les fonctions s’appelant les unes les autres, on se retrouve avec des appels de fonc-
tions imbriqués les uns dans les autres : main() appelle divise () qui lui-même appelle
verifie () 2 . Plus précisément, cette imbrication est un empilement et on parle de pile
1. par exemple 24a et 24b
2. Et d’ailleurs main() a lui-même été appelé par une fonction a laquelle il renvoie un int.
94
7. La mémoire 7.1. L’appel d’une fonction
des appels. Pour mieux comprendre cette pile, nous allons utiliser le debuggeur. Avant
cela, précisons ce qu’un informaticien entend par pile.
Pile/File
— Une pile est une structure permettant de mémoriser des données dans laquelle
celles-ci s’empilent de telle sorte que celui qui est rangé en dernier dans la pile en
est extrait en premier. En anglais, une pile (stack) est aussi appelée LIFO (last in
first out 3 ). On y empile (push) et on y dépile (pop) les données. Par exemple, après
un push(1), un push(2) et un push(3), le premier pop() donnera 3, le deuxième
pop() donnera 2 et un dernier pop() donnera 1.
— Pour une file (en anglais queue), c’est la même chose mais le premier arrivé est
le premier sorti (FIFO). Par exemple, après un push(1), un push(2) et un push(3),
le premier pop() donnera 1, le deuxième pop() donnera 2 et un dernier pop()
donnera 3.
95
7.1. L’appel d’une fonction 7. La mémoire
(a)
(b)
(c)
(d)
(e)
(f)
(g)
96
7. La mémoire 7.2. Variables Locales
F IGURE 7.2 – Pile et variables locales. De gauche à droite : étape (b) (ligne 12), étape (c)
(ligne 5) et étape (g) (ligne 25/26).
(f) Nous exécutons maintenant la suite jusqu’à nous retrouver en ligne 24 au re-
tour de divise () . Pour cela, on peut faire du pas-à-pas détaillé, ou simplement
deux fois de suite un pas-à-pas sortant 4 (Maj-F11) pour relancer jusqu’à sortir de
verifie () , puis jusqu’à sortir de divise () . On voit bien quotient, qui est encore
non défini, et aussi la valeur de retour de divise () , non encore affectée à quotient.
(g) Un pas-à-pas de plus et nous sommes en 25/26. La variable quotient vaut enfin
7.
7.2.1 Paramètres
Pour les paramètres, c’est simple :
Les paramètres sont en fait des variables locales ! Leur seule spécificité est
d’être initialisés dès le début de la fonction avec les valeurs passées à l’appel
de la fonction.
4. Step Out ou Maj-F11 ou . Notez aussi la possibilité de continuer le programme jusqu’à une
certaine ligne sans avoir besoin de mettre un point d’arrêt temporaire sur cette ligne mais simplement
en cliquant sur la ligne avec le bouton de droite et en choisissant "Run to line. . . ", ( )
97
7.3. Fonctions récursives 7. La mémoire
7.2.2 La pile
Les variables locales (et donc les paramètres) ne sont pas mémorisées à des adresses
fixes en mémoire 5 , décidées à la compilation. Si on faisait ça, les adresses mémoire
en question devraient être réservées pendant toute l’exécution du programme : on ne
pourrait y ranger les variables locales d’autres fonctions. La solution retenue est beau-
coup plus économe en mémoire 6 :
Ainsi, au fur et à mesure des appels, les variables locales s’empilent : la mémoire est
utilisée juste pendant le temps nécessaire. La figure 7.2 montre trois étapes de la pile
pendant l’exécution de notre exemple.
5. Souvenons-nous du chapitre 2.
6. Et permettra de faire des fonctions récursives, cf section suivante !
7. Coin des collégiens : La factorielle d’un nombre entier n s’écrit n! et vaut n! = 1 × 2 × . . . × n.
8. Le fait de pouvoir mettre des return au milieu des fonctions est ici bien commode !
98
7. La mémoire 7.3. Fonctions récursives
Ligne nf act1(3) retf act1(3) nf act1(2) retf act1(2) nf act1(1) retf act1(1)
5f act1(3) 3
9af act1(3) 3
5f act1(2) 3 2
9af act1(2) 3 2
5f act1(1) 3 2 1
8f act1(1) 3 2 1 1
10f act1(1) 3 2 1
9bf act1(2) 3 2 2 1
10f act1(2) 3 2
9bf act1(3) 3 6 2
10f act1(3) 6
Ce tableau devient difficile à écrire maintenant qu’on sait que les variables locales ne
dépendent pas que de la fonction mais changent à chaque appel ! On est aussi obligé
de préciser, pour chaque numéro de ligne, quel appel de fonction est concerné. Si on
visualise la pile, on comprend mieux pourquoi ça marche. Ainsi, arrivés en ligne 8 de
fact1 (1) pour un appel initial à fact1 (3), la pile ressemble à :
pile variable valeur
place libre
top→ nf act1(1) 1
nf act1(2) 2
nf act1(3) 3
ce que l’on peut aisément vérifier avec le débuggeur. Finalement :
Les fonctions récursives ne sont pas différentes des autres. C’est le système
d’appel des fonctions en général qui rend la récursivité possible.
7.3.2 Efficacité
Une fonction récursive est simple et élégante à écrire quand le problème s’y prête 9 .
Nous venons de voir qu’elle n’est toujours pas facile à suivre ou à debugger. Il faut
aussi savoir que
la pile des appels n’est pas infinie et même relativement limitée.
Ainsi, le programme suivant
22 / / Fait déborder la pile
23 i n t fact3 ( int n)
24 {
25 i f ( n==1)
26 return 1;
27 r e t u r n n∗ f a c t 3 ( n + 1 ) ; / / e r r e u r !
28 }
dans lequel une erreur s’est glissée va s’appeler théoriquement à l’infini et en pratique
s’arrêtera avec une erreur de dépassement de la pile des appels 10 . Mais la vraie raison
qui fait qu’on évite parfois le récursif est qu’
9. C’est une erreur classique de débutant que de vouloir abuser du récursif.
10. Sous Linux, il s’arrête pour n = 260, 000 environ.
99
7.3. Fonctions récursives 7. La mémoire
Lorsque le corps d’une fonction est suffisamment petit pour que le fait d’appeler cette
fonction ne soit pas négligeable devant le temps passé à exécuter la fonction elle-même,
il est préférable d’éviter ce mécanisme d’appel 11 . Dans le cas d’une fonction récursive,
on essaie donc s’il est nécessaire d’écrire une version dérécursivée (ou itérative) de la
fonction. Pour notre factorielle, cela donne :
/ / Version i t é r a t i v e
int fact2 ( int n)
{
i n t f =1;
f o r ( i n t i = 2 ; i <=n ; i ++)
f ∗= i ;
return f ;
}
ce qui après tout n’est pas si terrible.
Enfin, il arrive qu’écrire une fonction sous forme récursive ne soit pas utilisable
pour des raisons de complexité. Une exemple classique est la suite de Fibonacci définie
par :
f0 = f1 = 1
fn = fn−1 + fn−2
et qui donne : 1, 1, 2, 3, 5, 8,. . . En version récursive :
32 / / T r è s l e n t !
33 i n t f i b 1 ( i n t n ) {
34 i f ( n <2)
35 return 1;
36 r e t u r n f i b 1 ( n−2)+ f i b 1 ( n − 1 ) ;
37 }
cette fonction a la mauvaise idée de s’appeler très souvent : n = 10 appelle n = 9
et n = 8, mais n = 9 appelle lui aussi n = 8 de son côté en plus de n = 7, n = 7
qui lui-même est appelé par tous les n = 8 lancés, etc. Bref, cette fonction devient
rapidement très lente. Ainsi, pour n = 40, elle s’appelle déjà 300.000.000 de fois elle-
même, ce qui prend un certain temps ! Il est donc raisonnable d’en programmer une
version dérécursivée :
39 / / Dérécursivée
40 i n t fib2 ( int n) {
41 i n t fnm2 =1 , fnm1 = 1 ;
42 f o r ( i n t i = 2 ; i <=n ; i ++) {
43 i n t fn=fnm2+fnm1 ;
44 fnm2=fnm1 ;
45 fnm1=fn ;
46 }
47 r e t u r n fnm1 ;
48 }
11. Nous verrons dans un autre chapitre les fonctions inline qui répondent à ce problème.
100
7. La mémoire 7.4. Le tas
Mentionnons aussi qu’il existe des fonctions suffisamment tordues pour que leur ver-
sion récursive ne se contente pas de s’appeler un grand nombre de fois en tout, mais
un grand nombre de fois en même temps, ce qui fait qu’indépendamment des questions
d’efficacité, leur version récursive fait déborder la pile d’appels !
7.4 Le tas
La pile n’est pas la seule zone de mémoire utilisée par les programmes. Il y a aussi
le tas (heap en anglais).
7.4.1 Limites
La pile est limitée en taille. La pile d’appel n’étant pas infinie et les variables locales
n’étant pas en nombre illimité, il est raisonnable de réserver une pile de relativement
petite taille. Essayez donc le programme :
32 i n t main ( )
33 {
34 const i n t n=500000;
35 int t [n ] ;
36 ...
37 }
Il s’exécute avec une erreur : "stack overflow". La variable locale t n’est pas trop grande
pour l’ordinateur 12 : elle est trop grande pour tenir dans la pile. Jusqu’à présent, on
savait qu’on était limité aux tableaux de taille constante. En réalité, on est aussi limité
aux petits tableaux. Il est donc grand temps d’apprendre à utiliser le tas !
1. Remplacer int t[n] par int* t=new int[n] (ou l’équivalent pour
un autre type que int)
2. Lorsque le tableau doit mourir (en général en fin de fonction), rajouter
la ligne delete[] t;
Le non respect de la règle 2 fait que le tableau reste en mémoire jusqu’à la fin du pro-
gramme, ce qui entraine en général une croissance anarchique de la mémoire utilisée
(on parle de fuite de mémoire). Pour le reste, on ne change rien. Programmer un tableau
de cette façon fait qu’il est mémorisé dans le tas et non plus dans la pile. On fait donc
ainsi :
12. 500000x4 soit 2Mo seulement !
13. Et le débutant oublie toujours la deuxième, ce qui a pour conséquence des programmes qui gros-
sissent en quantité de mémoire occupée. . .
101
7.4. Le tas 7. La mémoire
102
7. La mémoire 7.5. L’optimiseur
Ce qui suit n’est pas essentiel pour un débutant mais peut éventuellement répondre à ses
interrogations. S’il comprend, tant mieux, sinon, qu’il oublie et se contente pour l’instant de la
règle précédente !
Pour avoir accès à toute la mémoire de l’ordinateur 14 , on utilise le tas. Le tas est une
zone mémoire que le programme possède et qui peut croître s’il en fait la demande au
système d’exploitation (et s’il reste de la mémoire de libre évidemment). Pour utiliser le
tas, on appelle une fonction d’allocation à laquelle on demande de réserver en mémoire
de la place pour un certain nombre de variables. C’est ce que fait new int[n].
Cette fonction retourne l’adresse de l’emplacement mémoire qu’elle a réservé. Nous
n’avons jamais rencontré de type de variable capable de mémoriser une adresse. Il
s’agit des pointeurs dont nous reparlerons plus tard. Un pointeur vers de la mémoire
stockant des int est de type int∗. D’où le int∗ t pour mémoriser le retour du new.
Ensuite, un pointeur peut s’utiliser comme un tableau, y compris comme paramètre
d’une fonction.
Enfin, il ne faut pas oublier de libérer la mémoire au moment où le tableau de taille
constante aurait disparu : c’est ce que fait la fonction delete [] t qui libère la mémoire
pointée par t.
7.5 L’optimiseur
Mentionnons ici un point important qui était négligé jusqu’ici, mais que nous allons
utiliser en TP.
Il y a plusieurs façons de traduire en langage machine un source C++. Le résultat
de la compilation peut donc être différent d’un compilateur à l’autre. Au moment de
compiler, on peut aussi rechercher à produire un exécutable le plus rapide possible :
on dit que le compilateur optimise le code. En général, l’optimisation nécessite un plus
grand travail mais aussi des transformations qui font que le code produit n’est plus
facilement débuggable. On choisit donc en pratique entre un code debuggable et un
code optimisé.
Jusqu’ici, nous utilisions toujours le compilateur en mode "Debug". Lorsqu’un pro-
gramme est au point (et seulement lorsqu’il l’est), on peut basculer le compilateur en
mode "Release" pour avoir un programme plus performant. Dans certains cas, les gains
peuvent être considérables. Un programmeur expérimenté fait même en sorte que l’op-
timiseur puisse efficacement faire son travail. Ceci dit, il faut respecter certaines règles :
— Rester en mode Debug le plus longtemps possible pour bien mettre au point le
programme.
14. Plus exactement à ce que le système d’exploitation veut bien attribuer au maximum à chaque pro-
gramme, ce qui est en général réglable mais en tout cas moins que la mémoire totale, bien que beaucoup
plus que la taille de la pile.
103
7.6. Assertions 7. La mémoire
7.6 Assertions
Voici une fonction très utile pour faire des programmes moins buggés ! La fonction
assert () prévient quand un test est faux. Elle précise le fichier et le numéro de ligne où
elle se trouve, offre la possibilité de debugger le programme, etc. Elle ne ralentit pas
les programmes car elle disparaît à la compilation en mode Release. C’est une fonction
peu connue des débutants, et c’est bien dommage ! Par exemple :
Si l’utilisateur entre une valeur négative, les conséquences pourraient être fâcheuses.
En particulier une valeur négative de n serait interprétée comme un grand entier (car le
[] attend un entier non signé, ainsi -1 serait compris comme le plus grand int possible)
et le new serait probablement un échec. A noter que si n==0, un tableau nul, l’allocation
marche. Mais dans ce cas t [0] n’existe même pas ! La seule chose qu’on peut donc
faire avec un tableau nul c’est le désallouer avec delete [] t ;. Il est toujours utile de se
prémunir contre une telle exception en vérifiant que la valeur est raisonnable.
c o n s t i n t m=5 , n = 3 ;
s t r u c t Tableau { double t a b [m] [ n ] ; } ;
void f ( Tableau& t ) {
f o r ( i n t i = 0 ; i <m; i ++)
f o r ( i n t j = 0 ; j <n ; j ++)
t . t a b [ i ] [ j ] = cos ( M_PI∗ i /m) ∗ s i n ( M_PI∗ j /n ) ;
cout << t . t a b [m−1][n−1] << endl ;
}
Notons le double crochet pour accéder à un élément, et bien sûr que le premier élement
est tab [0][0] et le dernier élément tab[m−1][n−1].
104
7. La mémoire 7.9. Fiche de référence
105
7.9. Fiche de référence 7. La mémoire
Chapitre 8
Allocation dynamique
Nous revenons une fois de plus sur l’utilisation du tas pour gérer des tableaux de taille
variable. Après avoir mentionné l’existence de tableaux bidimensionnels de taille fixe, nous dé-
taillons l’allocation dynamique 1 déjà vue en 7.4.2 et expliquons enfin les pointeurs, du moins
partiellement. A travers l’exemple des matrices (et des images en TP) nous mélangeons struc-
tures et allocation dynamique. Il s’agira là de notre structure de donnée la plus complexe avant
l’arrivée tant attendue - et maintenant justifiée - des objets. . .
1 i n t A[ 2 ] [ 3 ] ; B[0] 1 2 3
2 f o r ( i n t i = 0 ; i < 2 ; i ++)
3 f o r ( i n t j = 0 ; j < 3 ; j ++)
4 A[ i ] [ j ]= i + j ; B[1] 4 5 6
5 int B[2][3]={{1 ,2 ,3} ,{4 ,5 ,6}};
6 c o n s t i n t M=2 ,N= 3 ; Tableau 2D. Notez que B[0] est
7 i n t C[M] [N] ; le tableau 1D {1,2,3} et B[1] le
tableau 1D {4,5,6} .
La figure ci-dessus montre le tableau B. À noter que B[0] et B[1] sont des tableaux
1D représentant les lignes de B.
8.1.2 Limitations
Vis-à-vis des fonctions, les particularités sont les mêmes qu’en 1D :
— Impossible de retourner un tableau 2D.
— Passage uniquement par variable.
mais avec une restriction supplémentaire :
108
8. Allocation dynamique 8.1. Tableaux bidimensionnels
11 A[ i ] [ j ]= x ;
12 }
8.1.3 Solution
En pratique, dès que l’on doit manipuler des tableaux de dimension 2 (ou plus !) de
différentes tailles, on les mémorise dans des tableaux 1D en stockant par exemple les
colonnes les unes après les autres pour profiter des avantages des tableaux 1D. Ainsi,
on stockera une matrice A de m lignes de n colonnes dans un tableau T de taille mn
en plaçant l’élément A(i, j) en T (i + mj). La Fig. 8.1 montre le tableau B de l’exemple
précédent stocké comme tableau 1D. On peut alors écrire :
1 void s e t ( double A[ ] , i n t m, i n t n ) {
2 f o r ( i n t i = 0 ; i <m; i ++)
3 f o r ( i n t j = 0 ; j <n ; j ++)
4 A[ i +m∗ j ]= i + j ;
5 }
6 ...
7 double F [ 2 ∗ 3 ] ;
8 set (F , 2 , 3 ) ;
9 double G[ 3 ∗ 5 ] ;
10 s e t (G, 3 , 5 ) ;
ou par exemple, ce produit matrice vecteur dans lequel les vecteurs et les matrices sont
stockés dans des tableaux 1D :
1 / / y=Ax
2 void p r o d u i t ( double A[ ] , i n t m, i n t n , double x [ ] , double y [ ] )
3 {
4 f o r ( i n t i = 0 ; i <m; i ++) {
5 y[ i ]=0;
6 f o r ( i n t j = 0 ; j <n ; j ++)
7 y [ i ]+=A[ i +m∗ j ] ∗ x [ j ] ;
8 }
9 }
10
11 ...
12 double P [ 2 ∗ 3 ] , x [ 3 ] , y [ 2 ] ;
13 ...
109
8.2. Allocation dynamique 8. Allocation dynamique
14 // P=... x=...
15 produit (P , 2 , 3 , x , y ) ; / / y=Px
110
8. Allocation dynamique 8.2. Allocation dynamique
— une fonction f ( int s []) est conçue pour qu’on lui passe une adresse s
— elle marche évidemment avec les tableaux alloués dynamiquement qui ne
sont finalement que des adresses
— c’est plutôt l’appel f ( t ), avec t tableau de taille fixe, qui s’adapte en passant
à f l’adresse où se trouve le tableau.
— logiquement, on devrait même déclarer f par f ( int∗ s) au lieu de f ( int s []) .
Les deux sont en fait possibles et synonymes.
Vous pouvez donc maintenant programmer, en comprenant, ce genre de choses :
1 double somme( double ∗ t , i n t n ) { / / S y n t a x e " p o i n t e u r "
2 double s = 0 ;
3 f o r ( i n t i = 0 ; i <n ; i ++)
4 s+= t [ i ] ;
5 return s ;
6 }
7 ...
8 int t1 [ 4 ] ;
9 ...
10 double s1=somme( t1 , 4 ) ;
11 ...
12 i n t ∗ t 2 =new i n t [ n ] ;
13 ...
14 double s2=somme( t2 , n ) ;
15 ...
16 delete [ ] t2 ;
2. Oublier de désallouer :
void f ( i n t n ) {
i n t ∗ t =new i n t [ n ] ;
...
} / / On o u b l i e d e l e t e [ ] t ;
/ / Chaque a p p e l à f ( ) va p e r d r e n i n t d a n s l e t a s !
111
8.2. Allocation dynamique 8. Allocation dynamique
...
s= t ; / / A i e ! Du coup , s c o n t i e n t l a même a d r e s s e que t
/ / (On n ’ a p a s r e c o p i é l a z o n e p o i n t é e p a r t d a n s c e l l e
// p o i n t é e par s ! )
...
d e l e t e [ ] t ; / / OK
d e l e t e [ ] s ; / / C a t a : Non s e u l e m e n t on ne l i b è r e p a s l a mémoire
/ / i n i t i a l e m e n t m é m o r i s é e d a n s s , m a i s en p l u s on
/ / d é s a l l o u e à nouveau c e l l e q u i v i e n t d ’ ê t r e l i b é r é e !
8.2.3 Conséquences
Quand libérer ?
Maintenant que vous avez compris new et delete, vous imaginez bien qu’on n’at-
tend pas toujours la fin de l’existence du tableau pour libérer la mémoire. Le plus tôt
est le mieux et on libère la mémoire dès que le tableau n’est plus utilisé :
1 void f ( ) {
2 int t [10];
3 i n t ∗ s=new int [n ] ;
4 ...
5 delete [ ] s ; / / s i s ne s e r t p l u s d a n s l a s u i t e . . .
6 / / Autant l i b é r e r m a i n t e n a n t . . .
7 ...
8 } / / Par c o n t r e , t a t t e n d c e t t e l i g n e pour mourir .
En fait, le tableau dont l’adresse est mémorisée dans s est alloué ligne 3 et libéré ligne
5. La variable s qui mémorise son adresse, elle, est créée ligne 3 et meurt ligne 8 !
Pointeurs et fonctions
Il est fréquent que le new et le delete ne se fassent pas dans la même fonction (atten-
tion, du coup, aux oublis !). Ils sont souvent intégrés dans des fonctions. A ce propos,
lorsque des fonctions manipulent des variables de type pointeur, un certain nombre de
questions peuvent se poser. Il suffit de respecter la logique :
— Une fonction qui retourne un pointeur se déclare int∗ f ();
1 int ∗ alloue ( int n) {
2 r e t u r n new i n t [ n ] ;
3 }
4 ....
5 int ∗ t=alloue ( 1 0 ) ;
6 ...
— Un pointeur passé en paramètre à une fonction l’est par valeur. Ne pas mélan-
ger avec le fait qu’un tableau est passé par référence ! Considérez le programme
suivant :
1 void f ( i n t ∗ t , i n t n ) {
112
8. Allocation dynamique 8.3. Structures et allocation dynamique
2 ....
3 t [ i ] = . . . ; / / On m o d i f i e t [ i ] m a i s p a s t !
4 t = . . . / / Une t e l l e l i g n e ne c h a n g e r a i t p a s ’ s ’
5 / / dans l a f o n c t i o n a p p e l a n t e
6 }
7 ...
8 i n t ∗ s=new i n t [m] ;
9 f ( s ,m) ;
En fait, c’est parce qu’on passe l’adresse d’un tableau qu’on peut modifier ses élé-
ments. Par ignorance, nous disions que les tableaux étaient passés par référence
en annonçant cela comme une exception. Nous pouvons maintenant rectifier :
Un tableau est en fait passé via son adresse. Cette adresse est passée par
valeur. Mais ce mécanisme permet à la fonction appelée de modifier le
tableau. Dire qu’un tableau est passé par référence était un abus de
langage simplificateur.
Bizzarerie ? Les lignes 7 et 8 ci-dessus auraient pu s’écrire int∗ t ,n;. En fait, il faut
remettre une étoile devant chaque variable lorsqu’on définit plusieurs pointeurs en
même-temps. Ainsi, int ∗t , s,∗u; définit deux pointeurs d’int (les variables t et u) et
un int (la variable s).
3. Coin des enfants : les matrices et les vecteurs vous sont inconnus. Ca n’est pas grave. Comprenez
le source quand même et rattrapez vous avec le TP qui, lui, joue avec des images.
113
8.3. Structures et allocation dynamique 8. Allocation dynamique
1 # i n c l u d e <iostream >
2 # include <string >
3 using namespace s t d ;
4
5 / / ==================================================
6 / / f o n c t i o n s sur l e s m a t r i c e s
7 / / p o u r r a i e n t e t r e d a n s un m a t r i c e . h e t un m a t r i c e . cpp
8
9 s t r u c t Matrice {
10 i n t m, n ;
11 double ∗ t ;
12 };
13
14 M a t r i c e c r e e ( i n t m, i n t n ) {
15 M a t r i c e M;
16 M.m=m;
17 M. n=n ;
18 M. t =new double [m∗n ] ;
19 r e t u r n M;
20 }
21
22 void d e t r u i t ( M a t r i c e M) {
23 d e l e t e [ ] M. t ;
24 }
25
26 M a t r i c e p r o d u i t ( M a t r i c e A, M a t r i c e B ) {
27 i f (A. n ! =B .m) {
28 cout << " E r r e u r ! " << endl ;
29 exit (1);
30 }
31 M a t r i c e C= c r e e (A.m, B . n ) ;
32 f o r ( i n t i = 0 ; i <A.m; i ++)
33 f o r ( i n t j = 0 ; j <B . n ; j ++) {
34 / / C i j =Ai0 ∗ B 0 j+Ai1 ∗ B 1 j + . . .
35 C . t [ i +C .m∗ j ] = 0 ;
36 f o r ( i n t k = 0 ; k<A. n ; k++)
37 C . t [ i +C .m∗ j ]+=A. t [ i +A.m∗k ] ∗ B . t [ k+B .m∗ j ] ;
38
39 }
40 return C;
41 }
42
43 void a f f i c h e ( s t r i n g s , M a t r i c e M) {
44 cout << s << " = " << endl ;
45 f o r ( i n t i = 0 ; i <M.m; i ++) {
46 f o r ( i n t j = 0 ; j <M. n ; j ++)
47 cout << M. t [ i +M.m∗ j ] << " " ;
48 cout << endl ;
114
8. Allocation dynamique 8.3. Structures et allocation dynamique
49 }
50 }
51
52 / / ==================================================
53 // Utilisateur
54
55 i n t main ( )
56 {
57 M a t r i c e A= c r e e ( 2 , 3 ) ;
58 f o r ( i n t i = 0 ; i < 2 ; i ++)
59 f o r ( i n t j = 0 ; j < 3 ; j ++)
60 A. t [ i +2∗ j ]= i + j ;
61 a f f i c h e ( "A" ,A ) ;
62 M a t r i c e B= c r e e ( 3 , 5 ) ;
63 f o r ( i n t i = 0 ; i < 3 ; i ++)
64 f o r ( i n t j = 0 ; j < 5 ; j ++)
65 B . t [ i +3∗ j ]= i + j ;
66 a f f i c h e ( "B" ,B ) ;
67 M a t r i c e C= p r o d u i t (A, B ) ;
68 a f f i c h e ( "C" ,C ) ;
69 d e t r u i t (C ) ;
70 detruit (B ) ;
71 d e t r u i t (A ) ;
72 return 0;
73 }
L’utilisateur n’a maintenant plus qu’à savoir qu’il faut allouer et libérer les matrices
en appelant des fonctions mais il n’a pas à savoir ce que font ces fonctions. Dans cette
logique, on pourra rajouter des fonctions pour qu’il n’ait pas non plus besoin de savoir
comment les éléments de la matrice sont mémorisés. Il n’a alors même plus besoin de
savoir que les matrices sont des structures qui ont un champ t ! (Nous nous rappro-
chons vraiment de la programmation objet. . . ) Bref, on rajoutera en général :
10 double g e t ( M a t r i c e M, i n t i , i n t j ) {
11 r e t u r n M. t [ i +M.m∗ j ] ;
12 }
13
14 void s e t ( M a t r i c e M, i n t i , i n t j , double x ) {
15 M. t [ i +M.m∗ j ]= x ;
16 }
que l’utilisateur pourra appeler ainsi :
51 f o r ( i n t i = 0 ; i < 2 ; i ++)
52 f o r ( i n t j = 0 ; j < 3 ; j ++)
53 s e t (A, i , j , i + j ) ;
et que celui qui programme les matrices pourra aussi utiliser pour lui :
39 void a f f i c h e ( s t r i n g s , M a t r i c e M) {
40 cout << s << " = " << endl ;
41 f o r ( i n t i = 0 ; i <M.m; i ++) {
115
8.4. Boucles et continue 8. Allocation dynamique
F IGURE 8.2 – Attention au double delete : le code A=B fait pointer deux fois sur la
même zone mémoire alors qu’il n’y a plus de pointeur sur le tableau du haut (donc
une fuite mémoire puisqu’il n’est plus possible de la libérer). Le detruit(B) libère
une zone mémoire qui l’avait déjà été, avec des conséquences fâcheuses. . .
42 f o r ( i n t j = 0 ; j <M. n ; j ++)
43 cout << g e t (M, i , j ) << " " ;
44 cout << endl ;
45 }
46 }
Attention, il reste facile dans ce contexte :
— D’oublier d’allouer.
— D’oublier de désallouer.
— De ne pas désallouer ce qu’il faut si on fait A=B entre deux matrices. (C’est alors
deux fois la zone allouée initialement pour B qui est désallouée lorsqu’on libère
A et B tandis que la mémoire initiale de A ne le sera jamais, comme on peut le
voir sur la Fig. 8.2).
La programmation objet essaiera de faire en sorte qu’on ne puisse plus faire ces erreurs.
Elle essaiera aussi de faire en sorte que l’utilisateur ne puisse plus savoir ce qu’il n’a pas
besoin de savoir, de façon à rendre vraiment indépendantes la conception des matrices
et leur utilisation.
116
8. Allocation dynamique 8.5. TP
i f ( ! A) {
...
i f ( ! B) {
...
}
}
}
Ceci est à rapprocher de l’utilisation du return en milieu de fonction pour évacuer les
cas particuliers (section 7.3).
F IGURE 8.3 – Deux images et différents traitements de la deuxième (négatif, flou, relief,
déformation, contraste et contours).
8.5 TP
Le TP que nous proposons en A.7 est une illustration de cette façon de manipuler
des tableaux bidimensionnels dynamiques à travers des structures de données. Pour
changer de nos passionnantes matrices, nous travaillerons avec des images (figure 8.3).
117
8.6. Fiche de référence 8. Allocation dynamique
118
8. Allocation dynamique 8.6. Fiche de référence
119
8.6. Fiche de référence 8. Allocation dynamique
120
9. Premiers objets
Chapitre 9
Premiers objets
Nous abordons maintenant notre dernière étape dans la direction d’une meilleure organisa-
tion des programmes. Tantôt nous structurions davantage les instructions (fonctions, fichiers),
tantôt nous nous intéressions aux données (structures, tableaux). Nous allons maintenant pen-
ser données et instructions simultanément : c’est là l’idée première des objets, même s’ils pos-
sèdent de nombreux autres aspects 1 . Enfin, nous justifierons l’emploi des objets par la notion
d’"interface" 2 .
9.1 Philosophie
Réunir les instructions en fonctions ou fichiers est une bonne chose. Réunir les don-
nées en tableaux ou structures aussi. Il arrive que les deux soient liées. C’est d’ailleurs
ce que nous avons constaté naturellement dans les exemples des chapitres précédents,
dans lesquels un fichier regroupait souvent une structure et un certain nombre de fonc-
tions s’y rapportant. C’est dans ce cas qu’il faut faire des objets.
L’idée est simple : un objet est un type de donnée possédant un certain nombre de
fonctionnalités propres 3 . Ainsi :
Ce ne sont plus les fonctions qui travaillent sur des données. Ce sont les
données qui possèdent des fonctionnalités.
Ces "fonctionnalités" sont souvent appelées les méthodes de l’objet. En pratique, l’uti-
lisation d’un objet remplacera ce genre d’instructions :
obj a ;
int i=f ( a ) ; // fonction f () appliquée à a
par :
1. Le plus important étant l’héritage, que nous ne verrons pas dans ce cours, préférant nous consacrer
à d’autres aspects du C++ plus indispensables et négligés jusqu’ici. . .
2. Nous exposerons une façon simple de créer des interfaces. Un programmeur C++ expérimenté
utilisera plutôt de l’héritage et des fonctions virtuelles pures, ce qui dépasse largement ce cours !
3. Il arrive même parfois qu’un objet regroupe des fonctionnalités sans pour autant stocker la
moindre donnée. Nous n’utiliserons pas ici cette façon de présenter les choses, dont le débutant pourrait
rapidement abuser.
9.2. Exemple simple 9. Premiers objets
obj a ;
i n t i =a . f ( ) ; / / a p p e l à l a méthode f ( ) de a
Vous l’avez compris, il s’agit ni plus ni moins de "ranger" les fonctions dans les
objets. Attention, crions tout de suite haut et fort qu’
il ne faut pas abuser des objets, surtout lorsqu’on est débutant. Les dangers
sont en effet :
— de voir des objets là où il n’y en n’a pas. Instructions et données ne
sont pas toujours liées.
— de mal penser l’organisation des données ou des instructions en ob-
jets.
Un conseil donc : quand ça devient trop compliqué pour vous, abandonnez
les objets.
Ce qui ne veut pas dire qu’un débutant ne doit pas faire d’objets. Des petits objets
dans des cas simples sont toujours une bonne idée. Mais seule l’expérience permet
de correctement organiser son programme, avec les bons objets, les bonnes fonctions,
etc. Un exemple simple : lorsqu’une fonction travaille sur deux types de données, le
débutant voudra souvent s’acharner à en faire malgré tout une méthode de l’un des
deux objets, et transformer :
obj1 a ;
obj2 b ;
int i=f (a , b ) ; // f () appliquée à a et b
en :
obj1 a ;
obj2 b ;
i n t i =a . f ( b ) ; / / méthode f ( ) de a a p p l i q u é e à b
/ / Est−c e b i e n l a c h o s e à f a i r e ????
Seuls un peu de recul et d’expérience permettent de rester simple quand il le faut. Le
premier code était le plus logique : la fonction f () n’a souvent rien à faire chez a, ni
chez b.
122
9. Premiers objets 9.2. Exemple simple
i n t main ( ) {
obj a ;
a . x =3;
i n t i =a . f ( ) ;
i n t j =a . g ( 2 ) ;
...
Il y a juste un détail, mais d’importance : la définition de la structure obj ci-dessus ne
fait que déclarer les méthodes. Elles ne sont définies nulle part dans le code précédent.
Pour les définir, on fait comme pour les fonctions habituelles, sauf que
123
9.3. Visibilité 9. Premiers objets
...
9.3 Visibilité
Il y a une règle que nous n’avons pas vue sur les espaces de nom mais que nous
pouvons facilement comprendre : quand on est "dans" un espace de nom, on peut
utiliser toutes les variables et fonctions de cet espace sans préciser l’espace en question.
Ainsi, ceux qui ont programmé cout et endl ont défini l’espace std puis se sont "placés
à l’intérieur" de cet espace pour programmer sans avoir à mettre std:: partout devant
cout, cin, endl et les autres. . . C’est suivant cette même logique, que
dans ses méthodes, un objet accède directement à ses champs et à ses autres
méthodes, c’est-à-dire sans rien mettre devant a !
a. Vous verrez peut-être parfois traîner le mot clé this qui est utile parfois en C++ et que
les programmeurs venant de Java mettent partout, en se trompant d’ailleurs sur son type.
Vous ne devriez pas en avoir besoin.
si un objet n’utilise pas ses champs dans une méthode, c’est probablement
qu’on est en train de ranger dans cet objet une fonction qui n’a rien à voir
avec lui (cf abus mentionné plus haut)
124
9. Premiers objets 9.4. Exemple des matrices
# i n c l u d e <iostream >
# include <string >
using namespace s t d ;
/ / ==================================================
/ / f o n c t i o n s sur l e s m a t r i c e s
/ / p o u r r a i e n t e t r e d a n s un m a t r i c e . h e t m a t r i c e . cpp
/ / ========= d e c l a r a t i o n s ( d a n s l e . h )
s t r u c t Matrice {
i n t m, n ;
double ∗ t ;
void c r e e ( i n t m1, i n t n1 ) ;
void d e t r u i t ( ) ;
double g e t ( i n t i , i n t j ) ;
void s e t ( i n t i , i n t j , double x ) ;
void a f f i c h e ( s t r i n g s ) ;
};
M a t r i c e o p e r a t o r ∗ ( M a t r i c e A, M a t r i c e B ) ;
/ / ========= d é f i n i t i o n s ( d a n s l e . cpp )
void M a t r i c e : : c r e e ( i n t m1, i n t n1 ) {
/ / N o t e z que l e s p a r a m e t r e s ne s ’ a p p e l l e n t p l u s m e t n
/ / p o u r ne p a s m é l a n g e r a v e c l e s champs !
m=m1 ;
n=n1 ;
t =new double [m∗n ] ;
}
void M a t r i c e : : d e t r u i t ( ) {
delete [ ] t ;
}
double M a t r i c e : : g e t ( i n t i , i n t j ) {
r e t u r n t [ i +m∗ j ] ;
}
void M a t r i c e : : s e t ( i n t i , i n t j , double x ) {
t [ i +m∗ j ]= x ;
}
void M a t r i c e : : a f f i c h e ( s t r i n g s ) {
cout << s << " = " << endl ;
f o r ( i n t i = 0 ; i <m; i ++) {
f o r ( i n t j = 0 ; j <n ; j ++)
cout << g e t ( i , j ) << " " ;
cout << endl ;
}
125
9.4. Exemple des matrices 9. Premiers objets
M a t r i c e o p e r a t o r ∗ ( M a t r i c e A, M a t r i c e B ) {
i f (A. n ! =B .m) {
cout << " E r r e u r ! " << endl ;
exit (1);
}
Matrice C;
C . c r e e (A.m, B . n ) ;
f o r ( i n t i = 0 ; i <A.m; i ++)
f o r ( i n t j = 0 ; j <B . n ; j ++) {
/ / C i j =Ai0 ∗ B 0 j+Ai1 ∗ B 1 j + . . .
C. set ( i , j , 0 ) ;
f o r ( i n t k = 0 ; k<A. n ; k++)
C. set ( i , j ,
C . g e t ( i , j )+A. g e t ( i , k ) ∗ B . g e t ( k , j ) ) ;
}
return C;
}
126
9. Premiers objets 9.5. Cas des opérateurs
En clair, le programme :
s t r u c t objA {
...
};
s t r u c t objB {
...
};
i n t o p e r a t o r +( objA A, objB B ) {
...
}
...
i n t main ( ) {
objA A;
objB B ;
i n t i =A+B ; / / a p p e l l e o p e r a t o r +(A, B )
...
peut aussi s’écrire :
s t r u c t objA {
...
i n t o p e r a t o r +( objB B ) ;
};
s t r u c t objB {
...
};
i n t objA : : o p e r a t o r +( objB B ) {
...
}
...
i n t main ( ) {
objA A;
objB B ;
i n t i =A+B ; / / a p p e l l e m a i n t e n a n t A . o p e r a t o r +(B )
...
ce qui pour nos matrices donne :
s t r u c t Matrice {
...
Matrice operator ∗( Matrice B ) ;
};
...
127
9.5. Cas des opérateurs 9. Premiers objets
/ / A∗B a p p e l l e A . o p e r a t o r ∗ ( B ) donc t o u s
/ / l e s champs e t f o n c t i o n s u t i l i s é s d i r e c t e m e n t
/ / c o n c e r n e n t ce qui é t a i t p r é f i x é précédemment par A.
Matrice Matrice : : operator ∗( Matrice B) {
/ / On e s t d a n s l ’ o b j e t A du A∗B a p p e l é
i f ( n ! =B .m) { / / Le n d e A
cout << " E r r e u r ! " << endl ;
exit (1);
}
Matrice C;
C . c r e e (m, B . n ) ;
f o r ( i n t i = 0 ; i <m; i ++)
f o r ( i n t j = 0 ; j <B . n ; j ++) {
/ / C i j =Ai0 ∗ B 0 j+Ai1 ∗ B 1 j + . . .
C. set ( i , j , 0 ) ;
f o r ( i n t k = 0 ; k<n ; k++)
/ / g e t ( i , j ) s e r a c e l u i de A
C. set ( i , j ,
C . g e t ( i , j )+ g e t ( i , k ) ∗ B . g e t ( k , j ) ) ;
}
return C;
}
Notez aussi que l’argument de l’opérateur n’a en fait pas besoin d’être un objet.
Ainsi pour écrire le produit B=A∗2, il suffira de créer la méthode :
M a t r i c e M a t r i c e : : o p e r a t o r ∗ ( double lambda ) {
...
}
...
B=A∗ 2 ; / / A p p e l l e A . o p e r a t o r ∗ ( 2 )
Par contre, pour écrire B=2∗A, on ne pourra pas créer :
M a t r i c e double : : o p e r a t o r ∗ ( M a t r i c e A) / / IMPOSSIBLE c a r d o u b l e
/ / n ’ e s t p a s un o b j e t !
car cela reviendrait à définir une méthode pour le type double, qui n’est pas un ob-
jet 4 . Il faudra simplement se contenter d’un opérateur standard, qui, d’ailleurs, sera
bien inspiré d’appeler la méthode Matrice::operator∗(double lambda) si elle est déjà
programmée :
M a t r i c e o p e r a t o r ∗ ( double lambda , M a t r i c e A) {
r e t u r n A∗lambda ; / / d é f i n i précé demm ent , r i e n à r e p r o g r a m m e r !
}
...
B=2∗A; / / a p p e l l e o p e r a t o r ∗ ( 2 ,A) q u i a p p e l l e à s o n t o u r
/ / A. o p e r a t o r ∗(2)
Nous verrons au chapitre suivant d’autres opérateurs utiles dans le cas des objets. . .
4. et de toute façon n’appartient pas au programmeur !
128
9. Premiers objets 9.6. Interface
9.6 Interface
s t r u c t Matrice {
void c r e e ( i n t m1, i n t n1 ) ;
void d e t r u i t ( ) ;
double g e t ( i n t i , i n t j ) ;
void s e t ( i n t i , i n t j , double x ) ;
void a f f i c h e ( s t r i n g s ) ;
Matrice operator ∗( Matrice B ) ;
};
intéresse l’utilisateur. Que les dimensions soient dans des champs int m et int n et que
les éléments soient dans un champ double∗ t ne le concerne plus : c’est le problème de
celui qui programme les matrices. Si ce dernier trouve un autre moyen 5 de stocker un
tableau bidimensionnel de double, libre à lui de le faire. En fait
5. Et il en existe ! Par exemple pour stocker efficacement des matrices creuses, c’est-à-dire celles dont
la plupart des éléments sont nuls. Ou bien, en utilisant des objets implémentant déjà des tableaux de fa-
çon sûre et efficace, comme il en existe déjà en C++ standard ou dans des bibliothèques complémentaires
disponibles sur le WEB. Etc, etc.
129
9.7. Protection 9. Premiers objets
9.7 Protection
9.7.1 Principe
Tout cela est bien beau, mais les détails d’implémentation ne sont pas entièrement
cachés : la définition de la structure dans le fichier d’en-tête fait apparaître les champs
utilisés pour l’implémentation. Du coup, l’utilisateur peut être tenté de les utiliser !
Rien ne l’empêche en effet de faire des bêtises :
M a t r i c e A;
A. c r e e ( 3 , 2 ) ;
A.m= 4 ; / / A i e ! L e s a c c è s v o n t ê t r e f a u x !
ou tout simplement de préférer ne pas s’embêter en remplaçant
f o r ( i n t i = 0 ; i < 3 ; i ++)
f o r ( i n t j = 0 ; j < 2 ; j ++)
A. s e t ( i , j , 0 ) ;
par
f o r ( i n t i = 0 ; i < 6 ; i ++)
A. t [ i ] = 0 ; / / H o r r e u r ! Et s i on i m p l é m e n t e a u t r e m e n t ?
Dans ce cas, l’utilisation n’est plus indépendante de l’implémentation et on a perdu
une grande partie de l’intérêt de la programmation objet. . . C’est ici qu’intervient la
possibilité d’empêcher l’utilisateur d’accéder à certains champs ou même à certaines
méthodes. Pour cela :
1. Remplacer struct par class : tous les champs et les méthodes de-
viennent privés : seules les méthodes de l’objet lui-même ou de tout
autre objet du même type a peuvent les utiliser.
2. Placer la déclaration public: dans la définition de l’objet pour débu-
ter la zone b à partir de laquelle seront déclarés les champs et méthodes
publics, c’est-à-dire accessibles à tous.
a. Bref, les méthodes de la classe en question !
b. On pourrait à nouveau déclarer des passages privés avec private:, puis publics, etc. Il
existe aussi des passages protégés, notion qui dépasse ce cours. . .
Voici un exemple :
c l a s s obj {
int x , y ;
void a_moi ( ) ;
public :
int z ;
void pour_tous ( ) ;
void une_autre ( o b j A ) ;
};
void o b j : : a_moi ( ) {
x=..; / / OK
..=y; / / OK
130
9. Premiers objets 9.7. Protection
z=..; / / OK
}
void o b j : : pour_tous ( ) {
x=..; / / OK
a_moi ( ) ; / / OK
}
void o b j : : une_autre ( o b j A) {
x=A. x ; / / OK
A. a_moi ( ) ; / / OK
}
...
i n t main ( ) {
o b j A, B ;
A. x = . . ; / / NON!
A. z = . . ; / / OK
A. a_moi ( ) ; / / NON!
A. pour_tous ( ) ; / / OK
A. une_autre ( B ) ; / / OK
Dans le cas de nos matrices, que nous avions déjà bien programmées, il suffit de les
définir comme suit :
c l a s s Matrice {
i n t m, n ;
double ∗ t ;
public :
void c r e e ( i n t m1, i n t n1 ) ;
void d e t r u i t ( ) ;
double g e t ( i n t i , i n t j ) ;
void s e t ( i n t i , i n t j , double x ) ;
void a f f i c h e ( s t r i n g s ) ;
Matrice operator ∗( Matrice B ) ;
};
pour empêcher une utilisation dépendante de l’implémentation.
9.7.3 Accesseurs
Les méthodes get () et set () qui permettent d’accéder en lecture (get) ou en écriture
(set) à notre classe, sont appelées accesseurs. Maintenant que nos champs sont tous pri-
vés, l’utilisateur n’a plus la possibilité de retrouver les dimensions d’une matrice. On
rajoutera donc deux accesseurs en lecture vers ces dimensions :
6. sans compter qu’ils les déclarent souvent comme en C avec d’inutiles typedef. Mais bon, ceci ne
devrait pas vous concerner !
131
9.8. TP 9. Premiers objets
i n t M a t r i c e : : nbLin ( ) {
r e t u r n m;
}
i n t M a t r i c e : : nbCol ( ) {
return n ;
}
i n t main ( ) {
...
f o r ( i n t i = 0 ; i <A. nbLin ( ) ; i ++)
f o r ( i n t j = 0 ; j <A. nbCol ( ) ; j ++)
A. s e t ( i , j , 0 ) ;
mais pas en écriture, ce qui est cohérent avec le fait que changer m en cours de route
rendrait fausses les fonctions utilisant t [ i+m∗j] !
9.8 TP
Vous devriez maintenant pouvoir faire le TP en A.8 qui dessine quelques courbes
fractales (figure 9.1) en illustrant le concept d’objet..
132
9. Premiers objets 9.9. Fiche de référence
133
9.9. Fiche de référence 9. Premiers objets
134
9. Premiers objets 9.9. Fiche de référence
135
10. Constructeurs et Destructeurs
Chapitre 10
Constructeurs et Destructeurs
Dans ce long chapitre, nous allons voir comment le C++ offre la possibilité d’intervenir
sur ce qui se passe à la naissance et à la mort d’un objet. Ce mécanisme essentiel repose sur la
notion de constructeur et de destructeur. Ces notions sont très utiles, même pour le débutant
qui devra au moins connaître leur forme la plus simple. Nous poursuivrons par un aspect bien
pratique du C++, tant pour l’efficacité des programmes que pour la découverte de bugs à la
compilation : une autre utilisation du const. Enfin, pour les plus avancés, nous expliquerons
aussi comment les problèmes de gestion du tas peuvent être ainsi automatisés.
10.1 Le problème
Avec l’apparition des objets, nous avons transformé :
s t r u c t point {
int x , y ;
};
...
point a ;
a . x =2; a . y=3;
i =a . x ; j =a . y ;
en :
c l a s s point {
int x , y ;
public :
void g e t ( i n t&X , i n t&Y ) ;
void s e t ( i n t X , i n t Y ) ;
};
...
point a ;
a . set (2 ,3);
a . get ( i , j ) ;
Conséquence :
point a = { 2 , 3 } ;
10.2. La solution 10. Constructeurs et Destructeurs
est maintenant impossible. On ne peut remplir les champs privés d’un objet, même à
l’initialisation, car cela permettrait d’accéder en écriture à une partie privée 1 !
10.2 La solution
La solution est la notion de constructeur :
c l a s s point {
int x , y ;
public :
point ( i n t X, i n t Y ) ;
};
p o i n t : : p o i n t ( i n t X , i n t Y) {
x=X ;
y=Y ;
}
...
point a ( 2 , 3 ) ;
Un constructeur est une méthode dont le nom est le nom de la classe elle-
même. Il ne retourne rien mais son type de retour n’est pas void : il n’a pas
de type de retour. Le constructeur est appelé à la création de l’objet et ses
paramètres sont passés avec la syntaxe ci-dessus. Il est impossible d’appeler
un constructeur sur un objet déjà créé.
Ici, c’est le constructeur point :: point(int X,int Y) qui est défini. Notez bien qu’il
est impossible d’appeler un constructeur sur un objet déjà contruit :
p o i n t a ( 1 , 2 ) ; / / OK! V a l e u r s i n i t i a l e s
/ / On ne f a i t p a s comme ç a p o u r c h a n g e r l e s champs d e a .
a . p o i n t ( 3 , 4 ) ; / / ERREUR!
/ / Mais p l u t ô t comme ç a .
a . set (3 ,4); / / OK!
138
10. Constructeurs et Destructeurs 10.3. Cas général
obj : : obj ( ) {
cout << " h e l l o " << endl ;
}
...
obj a ; / / a p p e l l e l e c o n s t r u c t e u r par d é f a u t
affiche "hello".
Ainsi, le programme :
# i n c l u d e <iostream >
using namespace s t d ;
c l a s s obj {
public :
obj ( ) ;
};
obj : : obj ( ) {
cout << " o b j " ;
}
void f ( o b j d ) {
}
obj g ( ) {
obj e ;
cout << 6 << " " ;
return e ;
}
i n t main ( )
{
cout << 0 << " " ;
obj a ;
cout << 1 << " " ;
f o r ( i n t i = 2 ; i <=4; i ++) {
obj b ;
cout << i << " " ;
}
f (a );
cout << 5 << " " ;
a=g ( ) ;
139
10.3. Cas général 10. Constructeurs et Destructeurs
return 0;
}
affiche :
0 obj 1 obj 2 obj 3 obj 4 5 obj 6
Bien repérer les deux objets non construits avec obj :: obj () : le paramètre d de f () , copie
de a, et la valeur de retour de g(), copie de e.
Si on ne définit aucun constructeur, tout se passe comme s’il n’y avait qu’un
constructeur vide ne faisant rien. Mais attention : dès qu’on définit soi-même
un constructeur, le constructeur vide n’existe plus, sauf si on le redéfinit soi-
même.
Par exemple, le programme :
c l a s s point {
int x , y ;
};
...
point a ;
a . set (2 ,3);
point b ; / / OK
devient, avec un constructeur, un programme qui ne se compile plus :
c l a s s point {
int x , y ;
public :
140
10. Constructeurs et Destructeurs 10.3. Cas général
point ( i n t X, i n t Y ) ;
};
p o i n t : : p o i n t ( i n t X , i n t Y) {
x=X ;
y=Y ;
}
...
p o i n t a ( 2 , 3 ) ; / / c o n s t r u i t a v e c p o i n t (X, Y)
point b ; / / ERREUR! p o i n t ( ) n ’ e x i s t e p l u s
et il faut alors rajouter un constructeur vide, même s’il ne fait rien :
c l a s s point {
int x , y ;
public :
point ( ) ;
point ( i n t X, i n t Y ) ;
};
point : : point ( ) {
}
p o i n t : : p o i n t ( i n t X , i n t Y) {
x=X ;
y=Y ;
}
...
p o i n t a ( 2 , 3 ) ; / / c o n s t r u i t a v e c p o i n t (X, Y)
point b ; / / OK! c o n s t r u i t a v e c p o i n t ( )
141
10.4. Objets temporaires 10. Constructeurs et Destructeurs
Ainsi, le programme :
void f ( p o i n t p ) {
...
}
point g ( ) {
point e ( 1 , 2 ) ; / / pour l e r e t o u r n e r
return e ;
}
...
point a ( 3 , 4 ) ; / / uniquement pour p o u v o i r a p p e l e r f ( )
f (a );
point b ;
b=g ( ) ;
point c ( 5 , 6 ) ; / / on p o u r r a i t a v o i r e n v i e d e f a i r e
b=c ; / / ça pour m e t t r e b à ( 5 , 6 )
peut largement s’alléger, en ne stockant pas dans des variables les points pour lesquels
ce n’était pas utile :
1 void f ( p o i n t p ) {
2 ...
3 }
4 point g ( ) {
5 return point ( 1 , 2 ) ; / / r e t o u r n e d i r e c t e m e n t
6 / / l ’ objet temporaire point (1 ,2)
7 }
8 ...
9 f ( p o i n t ( 3 , 4 ) ) ; / / P a s s e d i r e c t e m e n t l ’ o b j . temp . p o i n t ( 3 , 4 )
10 point b ;
11 b=g ( ) ;
12 b= p o i n t ( 5 , 6 ) ; / / a f f e c t e directement b à l ’ objet
13 / / temporaire point (5 ,6)
Attention à la ligne 12 : elle est utile quand b existe déjà mais bien comprendre qu’on
construit un point (5,6) temporaire qui est ensuite affecté à b. On ne remplit pas b
directement avec (5,6) comme on le ferait avec un b. set (5,6) .
Attention aussi à l’erreur suivante, très fréquente. Il ne faut pas écrire
p o i n t p= p o i n t ( 1 , 2 ) ; / / NON! ! ! ! ! ! !
mais plutôt
point p ( 1 , 2 ) ; / / OUI !
142
10. Constructeurs et Destructeurs 10.5. TP
10.5 TP
Nous pouvons faire une pause et aller faire le TP que nous proposons en A.8. Il
s’agit de programmer le jeu de motos de Tron (figure 10.1).
143
10.6. Références Constantes 10. Constructeurs et Destructeurs
...
};
/ / r é s o u t AX=B
void s o l v e ( m a t r i c e A, v e c t e u r B , v e c t e u r& X) {
...
}
...
vecteur b , x ;
matrice a ;
...
s o l v e ( a , b , x ) ; / / r é s o u t ax=b
les variables A et B de la fonction solve() sont des copies des objets a et b de la fonction
appelante. Notez bien que, passé par référence, le paramètre X n’est pas une copie car
il s’agit juste d’un lien vers la variable x.
La recopie de a dans A n’est pas une très bonne chose. La variable a fait dans notre
cas pas moins de 8 millions d’octets : les recopier dans A prend du temps ! Même pour
des objets un peu moins volumineux, si une fonction est appelée souvent, cette recopie
peut ralentir le programme. Lorsqu’une fonction est courte, il n’est pas rare non plus
que ce temps de recopie soit supérieur à celui passé dans la fonction !
L’idée est alors, pour des objets volumineux, de les passer eux-aussi par référence,
même si la fonction n’a pas à les modifier ! Il suffit donc de définir la fonction solve()
ainsi :
void s o l v e ( m a t r i c e& A, v e c t e u r& B , v e c t e u r& X) {
...
pour accélérer le programme.
Cependant, cette solution n’est pas sans danger. Rien ne garantit en effet que solve
ne modifie pas ses paramètres A et B. Il est donc possible, suivant la façon dont solve
est programmée, qu’en sortie de solve(a,b,x), a et b eux-mêmes aient été modifiés,
alors que précédemment c’étaient leurs copies A et B qui l’étaient. C’est évidemment
gênant ! Le C++ offre heureusement la possibilité de demander au compilateur de vérifier
qu’une variable passée par référence n’est pas modifiée par la fonction. Il suffit de rajouter
const au bon endroit :
void s o l v e ( c o n s t m a t r i c e& A, c o n s t v e c t e u r& B , v e c t e u r& X) {
...
Si quelque part dans solve (ou dans les sous-fonctions appelées par solve !), la variable
A ou la variable B est modifiée, alors il y aura erreur de compilation. La règle est donc :
144
10. Constructeurs et Destructeurs 10.6. Références Constantes
Bref, notre premier programme ne se compilerait pas non plus car l’appel g(y) avec
const int& y impose que g() soit déclarée void g(const int& x). Le bon programme est
donc :
void g ( c o n s t i n t& x ) {
cout << x << endl ;
}
void f ( c o n s t i n t& y ) {
double z=y ; / / OK ne m o d i f i e p a s y
g(y ) ; / / OK! Pas b e s o i n d ’ a l l e r r e g a r d e r d a n s g ( )
}
...
i n t a =1;
f (a );
Avec les objets, nous avons besoin d’une nouvelle notion. En effet, considérons
maintenant :
void f ( c o n s t o b j& o ) {
o . g ( ) ; / / OK?
}
Il faut indiquer au compilateur si la méthode g() modifie ou non l’objet o. Cela se fait
avec la syntaxe suivante :
145
10.7. Destructeur 10. Constructeurs et Destructeurs
c l a s s obj {
...
void g ( ) c o n s t ;
...
};
void o b j : : g ( ) c o n s t {
...
}
void f ( c o n s t o b j& o ) {
o . g ( ) ; / / OK! Méthode c o n s t a n t e
}
Cela n’est finalement pas compliqué :
On précise qu’une méthode est constante, c’est-à-dire qu’elle ne modifie pas
son objet, en plaçant const derrière les parenthèses de sa déclaration et de
sa définition.
On pourrait se demander si toutes ces complications sont bien nécessaires, notre
point de départ étant juste le passage rapide de paramètres en utilisant les références.
En réalité, placer des const dans les méthodes est une très bonne chose. Il ne faut pas
le vivre comme une corvée de plus, mais comme une façon de préciser sa pensée :
"suis-je ou non en train d’ajouter une méthode qui modifie l’objets ?". Le compilateur
va ensuite vérifier pour nous la cohérence de ce const avec tout le reste. Ceci a deux
effets importants :
— Découverte de bugs à la compilation. (On pensait qu’un objet n’était pas modifié
et il l’est.)
— Optimisation du programme 2 .
—
La fin du chapitre peut être considérée comme difficile. Il est toutefois recommandé de la com-
prendre, même si la maîtrise et la mise en application de ce qui s’y trouve est laissée aux plus
avancés.
—
10.7 Destructeur
Lorsqu’un objet meurt, une autre de ses méthodes est appelée : le destructeur.
Le destructeur :
— est appelé quand l’objet meurt.
— porte le nom de la classe précédé de ˜.
— comme les constructeurs, n’a pas de type.
— n’a pas de paramètres (Il n’y a donc qu’un seul destructeur par classe.)
2. Lorsque le compilateur sait qu’un objet reste constant pendant une partie du programme, il peut
éviter d’aller le relire à chaque fois. Le const est donc une information précieuse pour la partie optimi-
sation du compilateur.
146
10. Constructeurs et Destructeurs 10.7. Destructeur
c l a s s obj {
public :
obj ( ) ;
~obj ( ) ;
};
obj : : obj ( ) {
cout << " o b j " ;
}
obj : : ~ obj ( ) {
cout << " ~ " ;
}
void f ( o b j d ) {
}
obj g ( ) {
obj e ;
cout << 6 << " " ;
return e ;
}
i n t main ( )
{
cout << 0 << " " ;
obj a ;
cout << 1 << " " ;
f o r ( i n t i = 2 ; i <=4; i ++) {
obj b ;
cout << i << " " ;
}
f (a );
cout << 5 << " " ;
a=g ( ) ;
return 0;
}
Il affiche maintenant :
0 obj 1 obj 2 ~ obj 3 ~ obj 4 ~ ~ 5 obj 6 ~ ~ ~
Repérez bien à quel moment les objets sont détruits. Constatez aussi qu’il y a plus
d’appels au destructeur (7) qu’au constructeur (5) : nous n’avons pas encore parlé du
constructeur pour les objets qui sont construits par copie. . .
147
10.8. Destructeurs et tableaux 10. Constructeurs et Destructeurs
Attention : il est possible d’écrire delete t sans les []. C’est une erreur !
Cette syntaxe est réservée à une autre utilisation du new/delete. L’utiliser
ici a pour conséquence de bien désallouer le tas, mais d’oublier d’appeler les
destructeurs sur les t[i]
.
Le constructeur de copie :
— Se déclare : obj::obj(const obj& o);
— Est utilisé évidemment par :
obj a;
obj b(a); // b à partir de a
— Mais aussi par :
obj a;
obj b=a; // b à partir de a, synonyme de b(a)
à ne pas confondre avec :
obj a,b;
b=a; // ceci n’est pas un constructeur!
— Et aussi pour construire les paramètres des fonctions et leur valeur de
retour.
Notre programme exemple est enfin complet. En rajoutant :
o b j : : o b j ( c o n s t o b j& o ) {
cout << " copy " ;
}
148
10. Constructeurs et Destructeurs 10.10. Affectation
il affiche :
0 o b j 1 o b j 2 ~ o b j 3 ~ o b j 4 ~ copy ~ 5 o b j 6 copy ~ ~ ~
Nous avons enfin autant d’appels (7) aux constructeurs qu’au destructeur !
Il reste malgré tout à savoir une chose sur ce constructeur, dont nous comprendrons
l’importance par la suite :
Lorsqu’il n’est pas programmé explicitement, le constructeur par copie re-
copie tous les champs de l’objet à copier dans l’objet construit.
Remarquez aussi que lorsqu’on définit soi-même un constructeur, le constructeur vide
par défaut n’existe plus mais le constructeur de copie par défaut existe toujours !
10.10 Affectation
Il reste en fait une dernière chose qu’il est possible de reprogrammer pour un objet :
l’affectation. Si l’affectation n’est pas reprogrammée, alors elle se fait naturellement par
recopie des champs. Pour la reprogrammer, on a recours à l’opérateur =. Ainsi a=b, se
lit a.operator=(b) si jamais celui-ci existe. Rajoutons donc :
void o b j : : o p e r a t o r =( c o n s t o b j&o ) {
cout << " = " ;
}
à notre programme, et il affiche :
0 o b j 1 o b j 2 ~ o b j 3 ~ o b j 4 ~ copy ~ 5 o b j 6 copy ~ = ~ ~
On raffine en général un peu. L’instruction a=b=c; entre trois entiers marche pour
deux raisons :
— Elle se lit a=(b=c);
— L’instruction b=c affecte c à b et retourne la valeur de c
Pour pouvoir faire la même chose entre trois objets, on reprogrammera plutôt l’affec-
tation ainsi :
o b j o b j : : o p e r a t o r =( c o n s t o b j&o ) {
cout << " = " ;
return o ;
}
...
obj a , b , c ;
a=b=c ; / / OK c a r a =( b=c )
ou même ainsi, ce qui dépasse nos connaissances actuelles, mais que nous préconisons
car cela évite de recopier un objet au moment du return :
c o n s t o b j& o b j : : o p e r a t o r =( c o n s t o b j&o ) {
cout << " = " ;
return o ;
}
...
obj a , b , c ;
a=b=c ; / / OK c a r a =( b=c )
149
10.11. Objets avec allocation dynamique 10. Constructeurs et Destructeurs
Un dernier conseil :
c l as s vect {
int n;
double ∗ t ;
public :
void a l l o u e ( i n t N) ;
void l i b e r e ( ) ;
};
void v e c t : : a l l o u e ( i n t N) {
n=N;
t =new double [ n ] ;
}
void v e c t : : l i b e r e ( ) {
delete [ ] t ;
}
i n t main ( )
{
vect v ;
v . alloue ( 1 0 ) ;
...
v. libere ( ) ;
return 0;
}
150
10. Constructeurs et Destructeurs 10.11. Objets avec allocation dynamique
# i n c l u d e <iostream >
using namespace s t d ;
c l as s vect {
int n;
double ∗ t ;
public :
v e c t ( i n t N) ;
~vect ( ) ;
};
v e c t : : v e c t ( i n t N) {
n=N;
t =new double [ n ] ;
}
vect : : ~ vect ( ) {
delete [ ] t ;
}
i n t main ( )
{
vect v ( 1 0 ) ;
...
return 0;
}
10.11.2 Problèmes !
Le malheur est que cette façon de faire va nous entraîner assez loin pour des débu-
tants. Nous allons devoir affronter deux types de problèmes.
Un problème simple
Puisqu’il n’y a qu’un seul destructeur pour plusieurs constructeurs, il va falloir faire
attention à ce qui se passe dans le destructeur. Rajoutons par exemple un constructeur
vide :
vect : : vect ( ) {
}
alors la destruction d’un objet créé à vide va vouloir désallouer un champ t absurde. Il
faudra donc faire, par exemple :
vect : : vect ( ) {
n=0;
}
151
10.11. Objets avec allocation dynamique 10. Constructeurs et Destructeurs
vect : : ~ vect ( ) {
i f (n!=0)
delete [ ] t ;
}
i n t main ( )
{
v e c t v ( 1 0 ) ,w( 1 0 ) ;
w=v ;
return 0;
}
Pourquoi ? Parce que l’affectation par défaut recopie les champs de v dans ceux de
w. Du coup, v et w se retrouvent avec les mêmes champs t ! Non seulement ils iront
utiliser les mêmes valeurs, d’où certainement des résultats faux, mais en plus une même
zone du tas va être désallouée deux fois, tandis qu’une autre ne le sera pas 3 !
Il faut alors reprogrammer l’affectation, ce qui n’est pas trivial. On décide en géné-
ral de réallouer la mémoire et de recopier les éléments du tableau :
c o n s t v e c t& v e c t : : o p e r a t o r =( c o n s t v e c t& v ) {
i f (n!=0)
d e l e t e [ ] t ; / / On s e d e s a l l o u e s i n e c e s s a i r e
n=v . n ;
i f (n!=0) {
t =new double [ n ] ; / / R e a l l o c a t i o n e t r e c o p i e
f o r ( i n t i = 0 ; i <n ; i ++)
t [ i ]= v . t [ i ] ;
}
return v ;
}
Cette version ne marche d’ailleurs pas si on fait v=v car alors v est désalloué avant
d’être recopié dans lui-même, ce qui provoque une lecture dans une zone qui vient
d’être désallouée 4 .
10.11.3 Solution !
Des problèmes identiques se posent pour le constructeur de copie. . . Ceci dit, en
factorisant le travail à faire dans quelques petites fonctions privées, la solution n’est
pas si compliquée. Nous vous la soumettons en bloc. Elle peut même servir de schéma
pour la plupart des objets similaires 5 :
3. Ne pas désallouer provoque évidemment des fuites de mémoire. Désallouer deux fois provoque
dans certains cas une erreur.
4. Il suffit de rajouter un test (&v==this) pour repérer ce cas, ce qui nous dépasse un petit peu. . .
5. Ceci n’est que le premier pas vers une série de façon de gérer les objets. Doit-on recopier les ta-
bleaux ? Les partager en faisant en sorte que le dernier utilisateur soit chargé de désallouer ? Etc, etc.
152
10. Constructeurs et Destructeurs 10.11. Objets avec allocation dynamique
1 # i n c l u d e <iostream >
2 using namespace s t d ;
3
4 c l a s s vect {
5 / / champs
6 int n;
7 double ∗ t ;
8 // fonctions privées
9 void a l l o c ( i n t N) ;
10 void k i l l ( ) ;
11 void copy ( c o n s t v e c t& v ) ;
12 public :
13 // constructeurs " obligatoires "
14 vect ( ) ;
15 v e c t ( c o n s t v e c t& v ) ;
16 // destructeur
17 ~vect ( ) ;
18 // affectation
19 c o n s t v e c t& o p e r a t o r =( c o n s t v e c t& v ) ;
20 / / constructeurs supplémentaires
21 v e c t ( i n t N) ;
22 };
23
24 void v e c t : : a l l o c ( i n t N) {
25 n=N;
26 i f (n!=0)
27 t =new double [ n ] ;
28 }
29
30 void v e c t : : k i l l ( ) {
31 i f (n!=0)
32 delete [ ] t ;
33 }
34
35 void v e c t : : copy ( c o n s t v e c t& v ) {
36 alloc (v.n ) ;
37 f o r ( i n t i = 0 ; i <n ; i ++) / / OK même s i n==0
38 t [ i ]= v . t [ i ] ;
39 }
40
41 vect : : vect ( ) {
42 alloc (0);
43 }
44
45 v e c t : : v e c t ( c o n s t v e c t& v ) {
46 copy ( v ) ;
47 }
48
153
10.12. Fiche de référence 10. Constructeurs et Destructeurs
49 vect : : ~ vect ( ) {
50 kill ();
51 }
52
53 c o n s t v e c t& v e c t : : o p e r a t o r =( c o n s t v e c t& v ) {
54 i f ( t h i s !=&v ) {
55 kill ();
56 copy ( v ) ;
57 }
58 return v ;
59 }
60
61 v e c t : : v e c t ( i n t N) {
62 a l l o c (N) ;
63 }
64
65 / / Pour t e s t e r c o n s t r u c t e u r d e c o p i e
66 vect f ( vect a ) {
67 return a ;
68 }
69 / / Pour t e s t e r l e r e s t e
70 i n t main ( )
71 {
72 vect a , b ( 1 0 ) , c ( 1 2 ) ,d ;
73 a=b ;
74 a=a ;
75 a=c ;
76 a=d ;
77 a= f ( a ) ;
78 b= f ( b ) ;
79 return 0;
80 }
154
10. Constructeurs et Destructeurs 10.12. Fiche de référence
155
10.12. Fiche de référence 10. Constructeurs et Destructeurs
156
10. Constructeurs et Destructeurs 10.12. Fiche de référence
Chapitre 11
Nous commençons avec ce chapitre un tour de tout ce qui est utile et même souvent in-
dispensable et que nous n’avons pas encore vu : chaînes de caractères, fichiers, plus quelques
fonctionnalités utiles. Encore une fois, nous ne verrons pas tout de manière exhaustive, mais
les fonctions les plus couramment utilisées.
— Attention c’est le type size_t 1 qui est utilisé et non int. Considérez-le
comme un entier mais pour lequel C++ choisit lui-même sur combien d’oc-
tets il faut le mémoriser. . .
— Si le caractère n’est pas trouvé, find retourne string::npos (une constante,
dont la valeur importe peu).
3. On peut aussi chercher une sous-chaîne :
s i z e _ t i =s . f i n d ( " hop " ) ; / / où e s t " hop " d a n s s ?
s i z e _ t j =s . f i n d ( " hop " , 3 ) ; / / où e s t " hop " d a n s s à p a r t i r
/ / d e l a p o s i t i o n 3?
7. Convertir une string en une chaîne au format C : le C mémorise ses chaînes dans
des tableaux de caractères terminés par un 0. Certaines fonctions prennent encore
en paramètre un char∗ ou un const char∗ 2 . Il faudra alors leur passer s . c_str ()
pour convertir une variable s de type string (cf section 11.2.2).
1. En réalité, il faut utiliser le type string::size_type.
2. Nous n’avons pas encore vu le rôle de const avec les tableaux.
160
11. Chaînes de caractères, fichiers 11.2. Fichiers
11.2 Fichiers
11.2.1 Principe
Pour lire et écrire dans un fichier, on procède exactement comme avec cout et cin.
On crée simplement une variable de type ofstream pour écrire dans un fichier, ou de
type ifstream pour lire. . .
1. Voici comment faire :
# i n c l u d e <fstream >
using namespace s t d ;
...
o f s t r e a m f ( " hop . t x t " ) ;
f << 1 << ’ ’ << 2 . 3 << ’ ’ << " s a l u t " << endl ;
f . close ( ) ;
161
11.2. Fichiers 11. Chaînes de caractères, fichiers
5. Moins fréquent, mais très utile à connaître : on peut écrire dans un fichier direc-
tement la suite d’octets en mémoire qui correspond à une variable ou un tableau.
Le fichier est alors moins volumineux, l’écriture et la lecture plus rapides (pas
besoin de traduire un nombre en une suite de caractères ou l’inverse !)
double x [ 1 0 ] ;
double y ;
o f s t r e a m f ( " hop . bin " , i o s : : b i n a r y ) ;
f . w r i t e ( ( c o n s t char ∗ ) x , 1 0 ∗ s i z e o f ( double ) ) ;
f . w r i t e ( ( c o n s t char ∗)&y , s i z e o f ( double ) ) ;
f . close ( ) ;
...
i f s t r e a m g ( " hop . bin " , i o s : : b i n a r y ) ;
g . read ( ( char ∗ ) x , 1 0 ∗ s i z e o f ( double ) ) ;
g . read ( ( c o n s t char ∗)&y , s i z e o f ( double ) ) ;
g . close ( ) ;
Attention à ne pas oublier le "mode d’ouverture" ios :: binary
2. Pour lire une chaîne avec des espaces, même chose qu’avec cin :
getline (g , s ) ;
getline (g , s , ’ : ’ ) ;
3. Enfin, un peu technique mais très pratique : les stringstream qui sont des chaînes
simulant des fichiers virtuels. On les utilise notamment pour convertir une chaîne
en nombre ou l’inverse :
# i n c l u d e <sstream >
using namespace s t d ;
s t r i n g s= " 12 " ;
162
11. Chaînes de caractères, fichiers 11.2. Fichiers
stringstream f ;
int i ;
/ / Chaîne v e r s e n t i e r
f << s ; / / On é c r i t l a c h a î n e
f >> i ; / / On r e l i t un e n t i e r ! ( i v a u t 1 2 )
i ++;
/ / Entier vers chaîne
f . c l e a r ( ) ; / / Ne p a s o u b l i e r s i on a d é j à u t i l i s é f
f << i ; / / On é c r i t un e n t i e r
f >> s ; / / On r e l i t une c h a î n e ( s v a u t " 1 3 " )
Notons dans ce dernier cas une fonction plus commode pour convertir une valeur
numérique (int, double, . . .) en string (depuis C++ 2011) : s = std :: to_string ( i ).
s t r u c t point {
int x , y ;
};
3. Ils ont l’air un peu pénibles à utiliser pour le programmeur habitué au printf et scanf du C. On
voit ici enfin leur puissance !
163
11.3. Valeurs par défaut 11. Chaînes de caractères, fichiers
void g ( ) {
f (12); / / Appelle f (12 ,0 ,0);
f (10 ,2); / / Appelle f (10 ,2 ,0);
f (1 ,2 ,3); / / Appelle f (1 ,2 ,3);
}
S’il y a déclaration puis définition, on ne précise les valeurs par défaut que dans la
déclaration :
void f ( i n t a , i n t b = 0 ) ; / / d é c l a r a t i o n
void g ( ) {
f (12); / / Appelle f (12 ,0);
f (10 ,2); / / Appelle f (10 ,2);
}
void f ( i n t a , i n t b ) { / / ne p a s r e −p r é c i s e r i c i l e b p a r d é f a u t . . .
// ...
}
11.3.2 Utilité
En général, on part d’une fonction :
int f ( int a , int b) {
...
}
Puis, on veut lui rajouter un comportement spécial dans un certain cas :
i n t f ( i n t a , i n t b , bool s p e c i a l ) {
...
}
Plutôt que de transformer tous les anciens appels à f (.,.) en f (.,., false ), il suffit de
faire :
i n t f ( i n t a , i n t b , bool s p e c i a l = f a l s e ) {
...
}
pour laisser les anciens appels inchangés, et uniquement appeler f (.,., true) dans les
futurs cas particuliers qui vont se présenter.
164
11. Chaînes de caractères, fichiers 11.4. Accesseurs
11.4 Accesseurs
Voici, en cinq étapes, les points utiles à connaître pour faire des accesseurs pratiques
et efficaces.
165
11.4. Accesseurs 11. Chaînes de caractères, fichiers
r e t u r n i ; / / r é f é r e n c e v e r s une v a r i a b l e q u i va m o u r i r !
/ / C ’ EST GRAVE!
}
...
f ( ) = 3 ; / / NON! ! ! Le i n ’ e x i s t e p l u s . Que va−t− i l s e p a s s e r ? !
11.4.2 Utilisation
Même si un objet n’est pas une variable globale, un champ de cet objet ne meurt
pas en sortant d’une de ses méthodes ! On peut, partant du programme :
c l a s s point {
double x [N] ;
public :
void s e t ( i n t i , double v ) ;
};
void p o i n t : : s e t ( i n t i , double v ) {
x [ i ]= v ;
}
...
point p ;
p. set ( 1 , 2 . 3 ) ;
le transformer en :
c l a s s point {
double x [N] ;
public :
double& element ( i n t i ) ;
};
double& p o i n t : : element ( i n t i ) {
return x [ i ] ;
}
...
point p ;
p . element ( 1 ) = 2 . 3 ;
11.4.3 operator()
Etape suivante : ceci devient encore plus utile quand on connaît operator() qui per-
met de redéfinir les parenthèses :
c l a s s point {
double x [N] ;
public :
double& o p e r a t o r ( ) ( i n t i ) ;
};
double& p o i n t : : o p e r a t o r ( ) ( i n t i ) {
return x [ i ] ;
166
11. Chaînes de caractères, fichiers 11.4. Accesseurs
}
...
point p ;
p ( 1 ) = 2 . 3 ; / / J o l i , non ?
Notez que l’on peut passer plusieurs paramètres, ce qui est utile par exemple pour
les matrices :
c l a s s mat {
double x [M∗N] ;
public :
double& o p e r a t o r ( ) ( i n t i , i n t j ) ;
};
double& mat : : o p e r a t o r ( ) ( i n t i , i n t j ) {
r e t u r n x [ i +M∗ j ] ;
}
...
mat A;
A( 1 , 2 ) = 2 . 3 ;
167
11.4. Accesseurs 11. Chaînes de caractères, fichiers
}
void f ( mat& A) {
A( 1 , 1 ) = 2 ; / / OK, a p p e l l e l e p r e m i e r o p e r a t o r ( )
}
void f ( c o n s t mat& A) {
double x=A( 1 , 1 ) ; / / OK, a p p e l l e l e d e u x i è m e
}
11.4.5 "inline"
Principe
Dernière étape : appeler une fonction et récupérer sa valeur de retour est un mécanisme
complexe, donc long. Appeler A(i, j ) au lieu de faire A.x[i+M∗j] est une grande perte de
temps : on passe plus de temps à appeler la fonction A.operator()(i , j ) et à récupérer
sa valeur de retour, qu’à exécuter la fonction elle-même ! Cela pourrait nous conduire à
retourner aux structures en oubliant les classes ! 4
Il existe un moyen de supprimer ce mécanisme d’appel en faisant en sorte que le
corps de la fonction soit recopié dans le code appelant lui-même. Pour cela, il faut
déclarer la fonction inline. Par exemple :
i n l i n e double s q r ( double x ) {
r e t u r n x∗x ;
}
...
double y= s q r ( z − 3 ) ;
fait exactement comme si on avait écrit y=(z−3)(z−3), sans qu’il n’y ait d’appel de
fonction !
Précautions
168
11. Chaînes de caractères, fichiers 11.5. Assertions
a. Contrairement à ce qu’il faut faire en Java ! Encore une source de mauvaises habitudes
pour le programmeur Java qui se met à C++. . .
11.5 Assertions
Rappelons l’existence de la fonction assert () vue en 7.6. Il ne faut pas hésiter à s’en
servir car elle facilite la compréhension du code (répond à la question “quels sont les
présupposés à ce point du programme ?”) et facilite le diagnostic des erreurs. Sachant
qu’elle ne coûte rien en mode Release (car non compilée), il ne faut pas se priver de
l’utiliser. Voici par exemple comment rendre sûrs nos accesseurs :
# include <cassert >
c l a s s mat {
double x [M∗N] ;
public :
i n l i n e double& o p e r a t o r ( ) ( i n t i , i n t j ) {
a s s e r t ( i >=0 && i <M && j >=0 && j <N) ;
r e t u r n x [ i +M∗ j ] ;
}
i n l i n e double o p e r a t o r ( ) ( i n t i , i n t j ) c o n s t {
a s s e r t ( i >=0 && i <M && j >=0 && j <N) ;
r e t u r n x [ i +M∗ j ] ;
}
169
11.6. Types énumérés 11. Chaînes de caractères, fichiers
};
mais il est maladroit de faire ainsi ! Il vaut mieux connaître l’existence des types énumé-
rés :
enum Dir { nord , e s t , sud , o u e s t } ;
void avance ( Dir d i r e c t i o n ) ;
Il s’agit bien de définir un nouveau type, qui, en réalité, masque des entiers. Une pré-
cision : on peut forcer certaines valeurs si besoin. Comme ceci :
enum Code { C10 =200 ,
C11 =231 ,
C12 =240 ,
C13 , / / Vaudra 241
C14 } ; / / " 242
Voilà. C’est tout pour aujourd’hui ! Nous continuerons au prochain chapitre. Il est donc
temps de retrouver notre célèbre fiche de référence. . .
170
11. Chaînes de caractères, fichiers 11.7. Fiche de référence
171
11.7. Fiche de référence 11. Chaînes de caractères, fichiers
172
11. Chaînes de caractères, fichiers 11.7. Fiche de référence
174
12. Fonctions et classes paramétrées (templates)
Chapitre 12
Nous continuons dans ce chapitre un inventaire de diverses choses utiles. Parmi elles, les
structures de données de la STL (Standard Template Library) nécessiteront la compréhension
des template. Nous aborderons donc cet aspect intéressant du C++.
12.1 template
12.1.1 Principe
Considérons la fonction classique pour échanger deux variables :
void echange ( i n t& a , i n t& b ) {
i n t tmp ;
tmp=a ;
a=b ;
b=tmp ;
}
...
int i , j ;
...
echange ( i , j ) ;
Si nous devions maintenant échanger deux variables de type double, il faudrait ré-
écrire une autre fonction echange(), identique aux définitions de type près. Heureuse-
ment, le C++ offre la possibilité de définir une fonction avec un type générique, un peu
comme un type variable, que le compilateur devra "instancier" au moment de l’appel
de la fonction en un type précis. Cette "programmation générique" se fait en définissant
un "template" :
/ / Echange deux v a r i a b l e s d e n ’ i m p o r t e q u e l t y p e T
t e m p l a t e <typename T>
void echange ( T& a , T& b ) {
T tmp ;
12.1. template 12. Fonctions et classes paramétrées (templates)
tmp=a ;
a=b ;
b=tmp ;
}
...
i n t a =2 , b = 3 ;
double x = 2 . 1 , y = 2 . 3 ;
echange ( a , b ) ; / / " i n s t a n c i e " T en i n t
echange ( x , y ) ; / / " i n s t a n c i e " T en d o u b l e
...
Autre exemple :
/ / C h e r c h e e 1 d a n s l e t a b l e a u t a b 1 e t met
/ / d a n s e 2 l ’ e l e m e n t d e t a b 2 d e meme i n d i c e
/ / R e n v o i e f a l s e s i non t r o u v é
t e m p l a t e <typename T1 , typename T2>
bool cherche ( T1 e1 , T2& e2 , c o n s t T1∗ tab1 , c o n s t T2∗ tab2 , i n t n ) {
f o r ( i n t i = 0 ; i <n ; i ++)
i f ( t a b 1 [ i ]== e1 ) {
e2= t a b 2 [ i ] ;
return true ;
}
return f a l s e ;
}
...
s t r i n g noms [ 3 ] = { " j e a n " , " p i e r r e " , " paul " } ;
i n t ages [ 3 ] = { 2 1 , 2 5 , 1 5 } ;
...
s t r i n g nm= " p i e r r e " ;
i n t ag ;
i f ( cherche (nm, ag , noms , ages , 3 ) )
cout << nm << " a " << ag << " ans " << endl ;
...
176
12. Fonctions et classes paramétrées (templates) 12.1. template
12.1.3 Classes
Il est fréquent qu’une définition de classe soit encore plus utile si elle est générique.
C’est possible. Mais attention ! Dans le cas des fonctions, c’est le compilateur qui dé-
termine tout seul quels types sont utilisés. Dans le cas des classes, c’est l’utilisateur qui
doit préciser en permanence avec la syntaxe obj<type> le type utilisé :
/ / P a i r e d e deux v a r i a b l e s d e t y p e T
t e m p l a t e <typename T>
class paire {
T x[2];
public :
// constructeurs
paire ( ) ;
p a i r e ( T A, T B ) ;
// accesseurs
T operator ( ) ( i n t i ) const ;
T& o p e r a t o r ( ) ( i n t i ) ;
};
t e m p l a t e <typename T>
p a i r e <T > : : p a i r e ( ) {
}
t e m p l a t e <typename T>
p a i r e <T > : : p a i r e ( T A, T B ) {
x [ 0 ] =A; x [ 1 ] = B ;
}
t e m p l a t e <typename T>
T p a i r e <T > : : o p e r a t o r ( ) ( i n t i ) c o n s t {
a s s e r t ( i ==0 || i = = 1 ) ;
1. Ceci est gênant et va à l’encontre du principe consistant à mettre les déclarations dans le .h et à
masquer les définitions dans le .cpp. Cette remarque a déjà été formulée pour les fonctions inline. Le
langage prévoit une solution avec le mot clé export, mais les compilateurs actuels n’implémentent pas
encore cette fonctionnalité !
177
12.1. template 12. Fonctions et classes paramétrées (templates)
return x [ i ] ;
}
t e m p l a t e <typename T>
T& p a i r e <T > : : o p e r a t o r ( ) ( i n t i ) {
a s s e r t ( i ==0 || i = = 1 ) ;
return x [ i ] ;
}
...
paire <int > p ( 1 , 2 ) , r ;
i n t i =p ( 1 ) ;
p a i r e <double > q ;
q(1)=2.2;
...
Dans le cas de la classe très simple ci-dessus, on aura recours aux fonctions inline vues
en 11.4.5 :
/ / P a i r e d e deux v a r i a b l e s d e t y p e T
/ / F o n c t i o n s c o u r t e s e t r a p i d e s en i n l i n e
t e m p l a t e <typename T>
class paire {
T x[2];
public :
// constructeurs
inline paire ( ) { }
i n l i n e p a i r e ( T A, T B ) { x [ 0 ] =A; x [ 1 ] = B ; }
// accesseurs
i n l i n e T operator ( ) ( i n t i ) const {
a s s e r t ( i ==0 || i = = 1 ) ;
return x [ i ] ;
}
i n l i n e T& o p e r a t o r ( ) ( i n t i ) {
a s s e r t ( i ==0 || i = = 1 ) ;
return x [ i ] ;
}
};
Lorsque plusieurs types sont génériques, on les sépare par une virgule :
/ / P a i r e d e deux v a r i a b l e s d e t y p e s d i f f é r e n t s
t e m p l a t e <typename S , typename T>
class paire {
public :
/ / Tout en p u b l i c p o u r s i m p l i f i e r
S x;
T y;
// constructeurs
inline paire ( ) { }
i n l i n e p a i r e ( S X , T Y) { x=X ; y=Y ; }
};
178
12. Fonctions et classes paramétrées (templates) 12.1. template
...
p a i r e < i n t , double > P ( 1 , 2 . 3 ) ;
p a i r e < s t r i n g , i n t > Q;
Q. x= " p i e r r e " ;
Q. y = 2 5 ;
...
/ / n−u p l e t d e v a r i a b l e s d e t y p e T
/ / A t t e n t i o n : c h a q u e n u p l e t <T , N> s e r a un t y p e d i f f é r e n t
t e m p l a t e <typename T , i n t N>
c l a s s nuplet {
T x [N] ;
public :
// accesseurs
i n l i n e T operator ( ) ( i n t i ) const {
a s s e r t ( i >=0 && i <N) ;
return x [ i ] ;
}
i n l i n e T& o p e r a t o r ( ) ( i n t i ) {
a s s e r t ( i >=0 && i <N) ;
return x [ i ] ;
}
};
...
nuplet < i n t ,4 > A;
A( 1 ) = 3 ;
nuplet < s t r i n g ,2 > B ;
B(1)= " pierre " ;
...
t e m p l a t e <typename T , i n t N>
T somme( nuplet <T ,N> u ) {
T s=u ( 0 ) ;
f o r ( i n t i = 1 ; i <N; i ++)
s+=u ( i ) ;
return s ;
}
...
nuplet <double ,3 > C ;
...
cout << somme(C) << endl ;
...
Au regard de tout ça, on pourrait être tenté de mettre des template partout. Et bien,
non !
179
12.1. template 12. Fonctions et classes paramétrées (templates)
12.1.4 STL
Les template sont délicats à programmer, mais pas à utiliser. Le C++ offre un certain
nombre de fonctions et de classes utilisant les template. Cet ensemble est communé-
ment désigné sous le nom de STL (Standard Template Library). Vous en trouverez la
documentation complète sur Internet. Nous exposons ci-dessous quelques exemples
qui devraient pouvoir servir de point de départ et faciliter la compréhension de la do-
cumentation.
Des fonctions simples comme min et max sont définies de façon générique :
i n t i =max ( 1 , 3 ) ;
double x=min ( 1 . 2 , 3 . 4 ) ;
Attention : une erreur classique consiste à appeler max(1,2.3) : le compilateur l’inter-
prète comme le max d’un int et d’un double ce qui provoque une erreur ! Il faut taper
max(1.,2.3).
Les complexes sont eux-aussi génériques, laissant variable le choix du type de leurs
parties réelle et imaginaire :
# i n c l u d e <complex>
using namespace s t d ;
...
complex<double > z1 ( 1 . 1 , 3 . 4 ) , z2 ( 1 , 0 ) , z3 ;
z3=z1+z2 ;
cout << z3 << endl ;
double a=z3 . r e a l ( ) , b=z3 . imag ( ) ;
double m=abs ( z3 ) ; / / module
double th=arg ( z3 ) ; / / argument
Les couples sont aussi offerts par la STL :
p a i r < i n t , s t r i n g > P ( 2 , " hop " ) ;
P . f i r s t =3;
P . second= " hop " ;
Enfin, un certain nombre de structures de données sont fournies et s’utilisent suivant
un même schéma. Voyons l’exemple des listes :
# include < l i s t >
using namespace s t d ;
...
l i s t <int > l ; / / l =[]
180
12. Fonctions et classes paramétrées (templates) 12.1. template
l . p u sh _ f ro n t ( 2 ) ; // l =[2]
l . p u sh _ f ro n t ( 3 ) ; // l =[3 ,2]
l . push_back ( 4 ) ; // l =[3 ,2 ,4]
l . p u sh _ f ro n t ( 5 ) ; // l =[5 ,3 ,2 ,4]
l . p u sh _ f ro n t ( 2 ) ; // l =[2 ,5 ,3 ,2 ,4]
Pour désigner un emplacement dans une liste, on utilise un itérateur. Pour désigner un
emplacement en lecture seulement, on utilise un itérateur constant. Le ’∗’ sert ensuite
à accéder à l’élément situé à l’emplacement désigné par l’itérateur. Seule difficulté : le
type de ces itérateurs est un peu compliqué à taper 2 :
l i s t <int > : : c o n s t _ i t e r a t o r i t ;
i t = l . begin ( ) ; / / P o i n t e v e r s l e d é b u t d e l a l i s t e
cout << ∗ i t << endl ; / / a f f i c h e 2
i t = l . f i n d ( 3 ) ; / / P o i n t e v e r s l ’ e n d r o i t ou s e t r o u v e
/ / l e premier 3
i f ( i t ! = l . end ( ) )
cout << " 3 e s t dans l a l i s t e " << endl ;
l i s t <int > : : i t e r a t o r i t 2 ;
i t 2 = l . f i n d ( 3 ) ; / / P o i n t e v e r s l ’ e n d r o i t ou s e t r o u v e
/ / l e premier 3
∗ i t =6; / / maintenant l =[2 ,5 ,6 ,2 ,4]
Les itérateurs servent également à parcourir les listes (d’où leur nom !) :
/ / P a r c o u r t e t a f f i c h e une l i s t e
t e m p l a t e <typename T>
void a f f i c h e ( l i s t <T> l ) {
cout << " [ " ;
f o r ( l i s t <T > : : c o n s t _ i t e r a t o r i t = l . begin ( ) ; i t ! = l . end ( ) ; i t ++)
cout << ∗ i t << ’ ’ ;
cout << ’ ] ’ << endl ;
}
/ / R e m p l a c e a p a r b d a n s une l i s t e
t e m p l a t e <typename T>
void remplace ( l i s t <T>& l , T a , T b ) {
f o r ( l i s t <T > : : i t e r a t o r i t = l . begin ( ) ; i t ! = l . end ( ) ; i t ++)
i f ( ∗ i t ==a )
∗ i t =b ;
}
...
affiche ( l ) ;
remplace ( l , 2 , 1 ) ; / / m a i n t e n a n t l = [ 1 , 5 , 3 , 1 , 4 ]
...
Enfin, on peut appeler des algorithmes comme le tri de la liste :
l . sort ( ) ;
affiche ( l ) ;
2. Nous n’avons pas vu comment définir de nouveaux types cachés dans des classes ! C’est ce qui est
fait ici. . .
181
12.2. Opérateurs binaires 12. Fonctions et classes paramétrées (templates)
Sur le même principe que les listes, vous trouverez dans la STL :
— Les piles ou stack (Last In First Out).
— Les files ou queue (First In First Out).
— Les ensembles ou set (pas deux fois le même élément).
— Les vecteurs ou vector (tableaux de taille variable).
— Les tas ou heap (arbres binaires de recherche).
— Les tables ou map (table de correspondance clé/valeur).
— Et quelques autres encore. . .
Le reste de ce chapitre regroupe quelques notions utiles mais non fondamentales.
Elles vous serviront probablement plus pour comprendre des programmes déjà écrits
que dans vos propres programmes.
182
12. Fonctions et classes paramétrées (templates) 12.3. Valeur conditionnelle
Remarques :
— Ces instructions sont particulièrement rapides car simples pour le processeur.
— Le fait que a^b existe est aussi source de bugs (il ne s’agit pas de la fonction
puissance !)
— Le résultat de ~ dépend en fait du type : si par exemple i est un entier non signé
sur 8 bits valant 13, alors ~i vaut 242, car ~00001101 vaut 11110010.
En pratique, tout cela ne sert pas à faire joli ou savant, mais à manipuler les nombres
bit par bit. Ainsi, il arrive souvent qu’on utilise un int pour mémoriser un certain
nombre de propriétés en utilisant le moins possible de mémoire avec la convention
que la propriété n est vraie ssi le neme bit de l’entier est à 1. Un seul entier de 32 bits
pourra par ainsi mémoriser 32 propriétés là où il aurait fallu utiliser 32 variables de
type bool. Voici comment on utilise les opérateurs ci-dessus pour manipuler les bits en
question :
Il existe aussi d’autres utilisations fréquentes des opérateurs binaires, non pour des
raisons de gain de place, mais pour des raisons de rapidité :
183
12.4. Boucles et break 12. Fonctions et classes paramétrées (templates)
184
12. Fonctions et classes paramétrées (templates) 12.5. Variables statiques
/ / en l i g n e 10 ( p a s en l i g n e 1 2 )
...
}
...
}
...
Le danger est alors que tout le reste du programme voie cette variable globale et l’uti-
lise ou la confonde avec une autre variable globale. Il est possible de cacher cette variable
dans la fonction grâce au mot clé static placé devant la variable :
/ / F o n c t i o n random q u i a p p e l l e s r a n d ( ) t o u t e s e u l e
/ / au p r e m i e r a p p e l . . . a v e c s a v a r i a b l e g l o b a l e
/ / masquée à l ’ i n t é r i e u r
double random ( ) {
s t a t i c bool f i r s t = t r u e ; / / Ne p a s o u b l i e r s t a t i c !
if ( first ) {
first=false ;
srand ( ( unsigned i n t ) time ( 0 ) ) ;
}
r e t u r n double ( rand ( ) ) /RAND_MAX;
}
Attention : il s’agit bien d’une variable globale et non d’une variable locale. Une
variable locale mourrait à la sortie de la fonction, ce qui dans l’exemple précédent
donnerait un comportement non désiré !
NB : Il est aussi possible de cacher une variable globale dans une classe, toujours
grâce à static . Nous ne verrons pas comment et renvoyons le lecteur à la documenta-
tion du C++.
185
12.6. const et tableaux 12. Fonctions et classes paramétrées (templates)
Nous avons vu malgré nous const char ∗ comme paramètre de certaines fonctions
(ouverture de fichier par exemple). Il nous faut donc l’expliquer : il ne s’agit pas d’un
pointeur de char qui serait constant mais d’un pointeur vers des char qui sont constants ! Il
faut donc retenir que :
placé devant un tableau, const signifie que ce sont les éléments du tableau
qui ne peuvent être modifiés.
Cette possibilité de préciser qu’un tableau ne peut être modifié est d’autant plus im-
portante qu’un tableau est toujours passé en référence : sans le const, on ne pourrait
assurer cette préservation des valeurs :
void f ( i n t t [ 4 ] ) {
...
}
void g ( c o n s t i n t t [ 4 ] ) {
...
}
void h ( c o n s t i n t ∗ t , i n t n ) {
...
}
...
int a [ 4 ] ;
f (a ); / / m o d i f i e p e u t −ê t r e a [ ]
g(a ) ; / / ne m o d i f i e p a s a [ ]
h ( a , 4 ) ; / / ne m o d i f i e p a s a [ ]
...
186
12. Fonctions et classes paramétrées (templates) 12.7. Fiche de référence
187
12.7. Fiche de référence 12. Fonctions et classes paramétrées (templates)
188
12. Fonctions et classes paramétrées (templates) 12.7. Fiche de référence
189
12.7. Fiche de référence 12. Fonctions et classes paramétrées (templates)
190
12. Fonctions et classes paramétrées (templates) 12.7. Fiche de référence
191
12.7. Fiche de référence 12. Fonctions et classes paramétrées (templates)
192
A. Travaux Pratiques
Annexe A
Travaux Pratiques
Note : les corrigés seront disponibles sur la page web du cours après chaque TP.
4. Génération : Appuyez sur l’outil marteau en bas à droite pour générer le pro-
gramme. Regardez dans l’onglet 4 “Compile Output”, il vous dit qu’il a créé un
fichier Tp1.cpp.o. Constatez avec un gestionnaire de fichiers que le fichier est
bien dans un sous-dossier du “build”. Observez aussi que le programme Tp1 est
aussi présent.
5. Exécution :
(a) Lancez le programme avec la flèche verte dans QtCreator, observez le ré-
sultat dans l’onglet 3 “Application Output”.
(b) Vérifiez qu’on a en fait créé un programme indépendant qu’on peut lancer
dans une fenêtre de commande :
— Ouvrez un terminal (sous Windows : "Démarrer/Exécuter", lancez
la commande “cmd”)
— Sous Windows, tapez "D:", 3 puis
"cd \Documents and Settings\login\Bureau\build-Tp1...\Tp1"
(apprenez à profiter de la complétion automatique avec la touche TAB).
Sous Linux, le chemin ne sera pas le même, et remplacez les \ par des /.
— Vérifiez la présence de Tp1.exe ou Tp1 avec la commande "dir" (Win-
dows) ou "ls" (Linux).
— Tapez "./Tp1".
6. Compression :
En cliquant à droite sur le répertoire Tp1, fabriquer une archive comprimée Tp1.zip
(ou Tp1.7z suivant la machine). Une telle archive, comprenant tous les fichiers
nécessaires à la compilation, sera le format utilisé pour déposer vos exercices et
rendus de TP sur Educnet.
Notez bien qu’avec Cmake nous avons deux dossiers :
— Le dossier source contenant les fichiers Tp1.cpp et CMakeLists.txt ;
— Le dossier build que vous avez choisi au démarrage de Cmake.
Le plus important est le premier, puisque le deuxième peut toujours être régénéré
avec Cmake. N’envoyez à votre enseignant que le répertoire source, il recompi-
lera lui-même. Votre build lui est probablement inutile car il n’utilise pas le même
système que vous. Ainsi, quand vous avez terminé un projet ou un TP, n’hési-
tez pas à nettoyer en supprimant votre dossier build, mais gardez précieusement
votre dossier source, c’est celui-ci qui représente le résultat de votre travail. Bien
que Cmake autorise d’utiliser un même répertoire pour les deux, c’est à éviter,
pour bien séparer les sources et les fichiers générés automatiquement. Pour com-
prendre un peu comment tous ces outils se coordonnent, on peut se reporter à
l’annexe C du polycopié.
194
A. Travaux Pratiques A.1. L’environnement de programmation
7. Vérifiez qu’on peut tout recommencer : quittez QtCreator, effacez vos dos-
siers source et build, extrayez le source de votre archive ; s’il reste un fichier
CMakeLists.txt.user, créé par QtCreator, supprimez-le avant de lancer
QtCreator et d’ouvrir le projet.
2. Erreurs de compilation
Provoquer, constater et apprendre à reconnaître quelques erreurs de compilation :
(a) includ au lieu de include
(b) iostrem au lieu de iostream
(c) Oublier le ; après std
(d) inte au lieu de int
(e) cou au lieu de cout
(f) Oublier les guillemets " fermant la chaîne "Hello ... "
(g) Rajouter une ligne i=3; avant le return.
A ce propos, il est utile de découvrir que :
195
A.1. L’environnement de programmation A. Travaux Pratiques
(a) Rajouter une ligne f(2); avant le return et générer. C’est pour l’instant une
erreur de compilation (il ne sait pas ce que f désigne).
(b) Corriger l’erreur de compilation en rajoutant une ligne (pour l’instant "ma-
gique")
void f ( int i ); avant la ligne avec main. Générer le programme : le linker
constate l’absence d’une fonction f() utilisée par la fonction main() qu’il ne
trouve nulle part.
(c) Effacez maintenant l’appel à f dans main(). Tout remarche à nouveau. Re-
nommez main en main en main2. L’absence de fonction main sera remar-
quée par le linker, qui refusera de générer le programme.
4. Indentations :
Avec toutes ces modifications, le programme ne doit plus être correctement "in-
denté". C’est pourtant essentiel pour une bonne compréhension et repérer d’éven-
tuelle erreur de parenthèses, accolades, etc. Le menu Edit/Advanced fournit de
quoi bien indenter.
Pour repérer des erreurs, toujours bien indenter. Ctrl+I = indenter la
zone sélectionnée. Ctrl+A,Ctrl+I = indenter tout le fichier.
5. Warnings du compilateur
En modifiant le main(), provoquer les warnings suivants : 4
(a) int i ;
i =2.5;
cout << i << endl;
Exécuter pour voir le résultat.
(b) int i ;
i=4;
if ( i=3) cout << "salut" << endl;
Exécuter !
(c) Ajouter exit; comme première instruction de main. Appeler une fonction
en oubliant les arguments arrive souvent ! Exécuter pour voir. Corriger en
mettant exit (0); . (La fonction exit () quitte le programme en urgence !)
Certains warnings sont anodins (faux positifs) et ne sont pas forcément des er-
reurs. Le problème est que ne pas les corriger peut noyer les vrais positifs, il faut
donc s’efforcer de tous les corriger.
Il est très formellement déconseillé de laisser passer des warnings ! Il
faut les corriger au fur et à mesure. Une option du compilateur propose
même de les considérer comme des erreurs !
A.1.3 Debugger
Savoir utiliser le debuggeur est essentiel. Il doit s’agir du premier réflexe en
présence d’un programme incorrect. C’est un véritable moyen d’investiga-
tion, plus simple et plus puissant que de truffer son programme d’instruc-
tions supplémentaires destinées à espionner son déroulement.
4. Aller dans le mode “Projects” de QtCreator , rubrique CMake, clicker sur le bouton “Ad-
vanced” et ajouter “-Wall -Wextra” à la variable CMAKE_CXX_FLAGS.
196
A. Travaux Pratiques A.1. L’environnement de programmation
F5 = = Debug
Touches utiles : F10 = = Step over
F11 = = Step inside
197
A.1. L’environnement de programmation A. Travaux Pratiques
198
A. Travaux Pratiques A.2. Variables, boucles, conditions, fonctions
2. Debugger :
Exécuter le programme pas à pas et étudier la façon dont les variables changent.
Vous devez utiliser le programme compilé en mode Debug.
199
A.2. Variables, boucles, conditions, fonctions A. Travaux Pratiques
1. Programme de départ :
Etudier le programme du projet Tennis dont voici le source :
1 # i n c l u d e <Imagine/Graphics . h>
2 using namespace Imagine ;
3 ...
4
5 // / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
6 / / Fonction p r i n c i p a l e
7 i n t main ( )
8 {
9 / / Ouverture de l a f e n e t r e
10 openWindow ( 2 5 6 , 2 5 6 ) ;
11 / / P o s i t i o n e t v i t e s s e de l a b a l l e
12 i n t xb =128 ,
13 yb =20 ,
14 ub =2 ,
15 vb = 3 ;
16 / / Boucle p r i n c i p a l e
17 while ( t r u e ) {
18 / / A f f i c h a g e de l a b a l l e
19 f i l l R e c t ( xb −3 ,yb − 3 , 7 , 7 , Red ) ;
20 / / Temporisation
21 milliSleep (20);
22 / / E f f a c e m e n t de l a b a l l e
23 f i l l R e c t ( xb −3 ,yb − 3 , 7 , 7 , White ) ;
24 / / Rebond
25 i f ( xb+ub >253)
26 ub=−ub ;
27 / / Mise a j o u r d e l a p o s i t i o n d e l a b a l l e
28 xb+=ub ;
29 yb+=vb ;
30 }
31 endGraphics ( ) ;
32 return 0;
33 }
200
A. Travaux Pratiques A.2. Variables, boucles, conditions, fonctions
201
A.2. Variables, boucles, conditions, fonctions A. Travaux Pratiques
202
A. Travaux Pratiques A.3. Tableaux
A.3 Tableaux
Dans ce TP, nous allons programmer un jeu de Mastermind, où l’utilisateur doit
deviner une combinaison générée aléatoirement par l’ordinateur. Le joueur dispose
d’un nombre déterminé d’essais. A chaque essai d’une combinaison, l’ordinateur four-
nit deux indices : le nombre de pions correctement placés et le nombre de pions de la
bonne couleur mais incorrectement positionnés.
203
A.3. Tableaux A. Travaux Pratiques
204
A. Travaux Pratiques A.3. Tableaux
205
A.3. Tableaux A. Travaux Pratiques
6. Sous QtCreator, sélectionnez-le avec l’icône représentant un écran sur la gauche de la fenêtre.
206
A. Travaux Pratiques A.4. Structures
A.4 Structures
Avertissement : Dans ce TP, nous allons faire évoluer des corps soumis à la gravita-
tion, puis leur faire subir des chocs élastiques. Il s’agit d’un long TP qui nous occupera
plusieurs séances. En fait, le TP suivant sera une réorganisation de celui-ci. Les sec-
tions 11 et 15 ne sont données que pour les élèves les plus à l’aise et ne seront abordées
qu’en deuxième semaine. En section A.4.2 sont décrites quelques-unes des fonctions à
utiliser, et en A.4.3 leur justification physique.
A.4.1 Etapes
Mouvement de translation
1. Pour commencer, étudier le projet :
Télécharger le fichier Tp4_Initial.zip sur la page habituelle, le décompresser
et lancer QtCreator. Parcourir le projet, en s’attardant sur les variables globales
et la fonction main (inutile de regarder le contenu des fonctions déjà définies
mais non utilisées). Le programme fait évoluer un point (x, y) selon un mouve-
ment de translation constante (vx, vy), et affiche régulièrement un disque centré
en ce point. Pour ce faire, afin de l’effacer, on retient la position du disque au
dernier affichage (dans ox et oy) ; par ailleurs, deux instructions commençant
par noRefresh sont placées autour des instructions graphiques afin de fluidi-
fier l’affichage.
2. Utiliser une structure :
Modifier le programme de façon à utiliser une structure Balle renfermant toute
l’information sur le disque (position, vitesse, rayon, couleur).
3. Fonctions d’affichage :
Créer (et utiliser) une fonction void AfficheBalle(Balle D) affichant le disque D,
et une autre
void EffaceBalle(Balle D) l’effaçant.
4. Faire bouger proprement le disque :
Pour faire évoluer la position du disque, remplacer les instructions correspon-
dantes déjà présentes dans main par un appel à une fonction qui modifie les
coordonnées d’une Balle, en leur ajoutant la vitesse de la Balle multipliée par
un certain pas de temps défini en variable globale (dt = 1 pour l’instant).
Gravitation
207
A.4. Structures A. Travaux Pratiques
7. Accélération gravitationnelle :
Créer (et utiliser à la place de la gravitation uniforme) une fonction qui prend en
argument la planète et le soleil, et qui fait évoluer la position de la planète. Rappel
de physique : l’accélération à prendre en compte est −G mS /r3 → −
r , avec ici G = 1
(Vous aurez sans doute besoin de la fonction double sqrt(double x), qui retourne
la racine carrée de x). Ne pas oublier le facteur dt. . . Faire tourner et observer.
Essayez diverses initialisations de la planète (par exemple x = largeur/2, y =
hauteur/3, vx = 1 , vy = 0). Notez que l’expression de l’accélération devient
très grande lorsque r s’approche de 0 ; on prendra donc garde à ne pas utiliser ce
terme lorsque r devient trop petit.
8. Initialisation aléatoire :
Créer (et utiliser à la place des conditions initiales données pour le soleil) une
fonction initialisant une Balle, sa position étant dans la fenêtre, sa vitesse nulle,
son rayon entre 5 et 15, et sa masse valant le rayon divisé par 20. Vous aurez
probablement besoin de la fonction Random. . .
9. Des soleils par milliers. . .
Placer 10 soleils aléatoirement (et en tenir compte à l’affichage, dans le calcul du
déplacement de l’astéroïde. . . ).
10. Diminuer le pas de temps de calcul :
Afin d’éviter les erreurs dues à la discrétisation du temps, diminuer le pas de
temps dt, pour le fixer à 0.01 (voire à 0.001 si la machine est assez puissante).
Régler la fréquence d’affichage en conséquence (inversement proportionnelle à
dt). Lancer plusieurs fois le programme.
Jeu de tir
(figure A.3 droite)
12. Ouvrir un nouveau projet :
Afin de partir dans deux voies différentes et travailler proprement, nous allons
ajouter un nouveau projet Imagine++, appelé Duel, dans cette même solution.
Recopier (par exemple par copier/coller) intégralement le contenu du fichier
Tp4.cpp dans un fichier Duel.cpp. Une fois cette copie faite, modifier le fichier
CMakeLists.txt en ajoutant deux lignes indiquant que l’exécutable Duel dé-
pend de Duel.cpp et utilise la bibliothèque Graphics d’Imagine++.
208
A. Travaux Pratiques A.4. Structures
Chocs élastiques
(figure A.3 gauche)
16. Tout faire évoluer, tout faire rebondir :
On retourne dans le projet Gravitation. Tout faire bouger, y compris les soleils.
Utiliser, pour les chocs élastiques, la fonction Chocs (qui fait rebondir les deux
corps). Faire en sorte que lors de l’initialisation les soleils ne s’intersectent pas.
A.4.2 Aide
Fonctions fournies :
209
A.4. Structures A. Travaux Pratiques
void InitRandom ( ) ;
est à exécuter une fois avant le premier appel à Random.
double Random ( double a , double b ) ;
renvoie un double aléatoirement entre a et b (compris). Exécuter une fois InitRandom();
avant la première utilisation de cette fonction.
void ChocSimple ( double x , double y , double &vx , double &vy , double m,
double x2 , double y2 , double vx2 , double vy2 ) ;
fait rebondir la première particule, de coordonnées (x, y), de vitesse (vx, vy) et de
masse m, sur la deuxième, de coordonnées (x2, y2) et de vitesse (vx2, vy2), sans dé-
placer la deuxième.
void Choc ( double x , double y , double &vx , double &vy , double m,
double x2 , double y2 , double &vx2 , double &vy2 , double m2 ) ;
fait rebondir les deux particules l’une contre l’autre.
bool C o l l i s i o n ( double x1 , double y1 , double vx1 , double vy1 , double r1 ,
double x2 , double y2 , double vx2 , double vy2 , double r 2 ) ;
renvoie true si le corps de coordonnées (x1, y1), de vitesse (vx1, vy1) et de rayon r1
est sur le point d’entrer en collision avec le corps de coordonnées (x2, y2), de vitesse
(vx2, vy2) et de rayon r2, et false sinon.
Accélération
La somme des forces exercées sur un corps A est égale au produit de sa masse par
l’accélération de son centre de gravité.
X→ −
F i/A = mA →
−
a G(A)
i
Gravitation universelle
Soient deux corps A et B. Alors A subit une force d’attraction
→
− 1 −
F B/A = −GmA mB 2 → u B→A .
dA,B
Chocs élastiques
Soient A et B deux particules rentrant en collision. Connaissant tous les paramètres
avant le choc, comment déterminer leur valeur après ? En fait, seule la vitesse des par-
ticules reste à calculer, puisque dans l’instant du choc, les positions ne changent pas.
Durant un choc dit élastique, trois quantités sont conservées :
210
A. Travaux Pratiques A.4. Structures
→
−
1. la quantité de mouvement P = mA →
−
v A + mB →
−
vB
2. le moment cinétique M = mA →
−rA×→ −v A + mB →
−
rB×→
−
v B (qui est un réel dans le
cas d’un mouvement plan).
3. l’énergie cinétique Ec = 21 mA vA2 + 12 mB vB2 .
Ce qui fait 4 équations pour 4 inconnues.
Résolution du choc
On se place dans le référentiel du centre de masse. On a alors, à tout instant :
→
−
1. P = 0 (par définition de ce référentiel), d’où mA →
−
v A = −mB → −
v B.
2. M = ( r A − r B ) × mA v A , d’où, en notant ∆ r = r A − r B , M = ∆→
→
− →
− →
− →
− →
− →
− −r × mA →
−
v A.
mA
3. 2Ec = mA (1 + mB
)vA2 .
La constance de Ec nous informe que dans ce repère, la norme des vitesses est
conservée, et la constance du moment cinétique que les vitesses varient parallèlement
à ∆→−r . Si l’on veut que les vitesses varient effectivement, il ne nous reste plus qu’une
possibilité : mutliplier par −1 la composante des → −v i selon ∆→
−
r . Ce qui fournit un algo-
rithme simple de rebond.
N (u) = (→
−
r A (t) − →
−
r B (t) + (u − t)(→
−
v A (t) − →
−
v B (t)))2
N (u) = ∆→
−
r (t)2 + 2(u − t)∆→
−
r (t) · ∆→
−
v (t) + (u − t)2 ∆→
−
v (t)2
La norme, toujours positive, est minimale au point u tel que ∂u N (u) = 0, soit :
∆→
−
r (t) · ∆→
−
v (t)
(tm − t) = − →
−
∆ v (t)2
Donc :
1. si tm < t, le minimum est atteint en t,
2. si t < tm < t + dt, le minimum est atteint en tm ;
3. sinon, t + dt < tm , le minimum est atteint en t + dt.
Ce qui nous donne explicitement et simplement la plus petite distance atteinte entre
les deux corps entre t et t + dt.
211
A.5. Fichiers séparés A. Travaux Pratiques
A.5.2 Vecteurs
4. Structure Vector :
Créer dans un nouveau fichier vector.h une structure représentant un vecteur
du plan, avec deux membres de type double. Ne pas oublier le mécanisme de
protection contre la double inclusion. Déclarer (et non définir) les opérateurs et
fonction suivants :
Vector o p e r a t o r +( Vector a , Vector b ) ; / / Somme
Vector o p e r a t o r −( Vector a , Vector b ) ; // Différence
212
A. Travaux Pratiques A.5. Fichiers séparés
8. Fonctions associées :
Déplacer toutes les fonctions annexes prenant des Balle en paramètres dans un
nouveau fichier Balle.cpp. Il ne devrait plus rester dans Gravitation.cpp
d’autre fonction que main. Déclarer dans Balle.h les fonctions définies dans
Balle.cpp. Ajouter les #include nécessaires dans ce dernier fichier et dans Gravitation.cp
et faire les adaptations nécessaires (par exemple, si des fonctions utilisent largeur
ou hauteur, comme ces constantes ne sont définies que dans Gravitation.cpp,
il faut les passer en argument. . . )
213
A.5. Fichiers séparés A. Travaux Pratiques
Jeu de tir
214
A. Travaux Pratiques A.5. Fichiers séparés
Chocs élastiques
215
A.6. Images A. Travaux Pratiques
A.6 Images
F IGURE A.5 – Deux images et différents traitements de la deuxième (négatif, flou, relief,
déformation, contraste et contours).
Dans ce TP, nous allons jouer avec les tableaux bidimensionnels statiques (mais
stockés dans des tableaux 1D) puis dynamiques. Pour changer de nos passionnantes
matrices, nous travaillerons avec des images (figure A.5).
A.6.1 Allocation
1. Récupérer le projet :
Télécharger le fichier Tp6_Initial.zip sur la page habituelle, le décompresser
et lancer votre environnement de développement préféré, QtCreator.
2. Saturer la mémoire :
Rien à voir avec ce qu’on va faire après mais il faut l’avoir fait une fois. . . Faire,
dans une boucle infinie, des allocations de 1000000 entiers sans désallouer, cha-
cune suivie d’une pause de 0.5 seconde, et regarder la taille du process grandir.
(Utiliser Ctrl+Shift+Echap pour accéder au gestionnaire de tâches sous Win-
dows). Compiler en mode Release pour utiliser la “vraie” gestion du tas (Le mode
Debug utilise une gestion spécifique qui aide à trouver les bugs et se comporte
différemment. . . )
216
A. Travaux Pratiques A.6. Images
allant de 0 pour le noir à 255 pour le blanc. L’origine est en haut à gauche, i est
l’horizontale et j la verticale. Dans un tableau de byte mono-dimensionnel t de
taille W*H mémorisant le pixel (i,j) en t[i+W*j] :
— Stocker une image noire et l’afficher avec putGreyImage(0,0,t,W,H).
— Idem avec une image blanche.
— Idem avec un dégradé du noir au blanc (attention aux conversions entre
byte et double).
— Idem avec t(i, j) = 128 + 128 sin(4πi/W ) sin(4πj/H) (cf figure A.5). Utiliser
# i n c l u d e <cmath>
pour avoir les fonctions et les constantes mathématiques : M_PI vaut π.
4. Couleurs :
Afficher, avec putColorImage(0,0,r,g,b,W,H), une image en couleur sto-
ckée dans trois tableaux r, g et b (rouge, vert, bleu). Utiliser la fonction click()
pour attendre que l’utilisateur clique avec la souris entre l’affichage précédent et
ce nouvel affichage.
A.6.5 Fonctions
8. Découper le travail :
On ne garde plus que la partie noir et blanc du programme. Faire des fonctions
pour allouer, détruire, afficher et charger les images :
byte ∗ AlloueImage ( i n t W, i n t H) ;
void DetruitImage ( byte ∗ I ) ;
void AfficheImage ( byte ∗ I , i n t W, i n t H) ;
byte ∗ ChargeImage ( char ∗ name , i n t &W, i n t &H) ;
217
A.6. Images A. Travaux Pratiques
9. Fichiers :
Créer un image.cpp et un image.h en conséquence. . .
A.6.6 Structure
10. Principe :
Modifier le programme précédent pour utiliser une structure :
s t r u c t Image {
byte ∗ t ;
i n t w, h ;
};
AlloueImage() et ChargeImage() pourront retourner des Image.
11. Indépendance :
Pour ne plus avoir à savoir comment les pixels sont stockés, rajouter :
byte Get ( Image I , i n t i , i n t j ) ;
void S e t ( Image I , i n t i , i n t j , byte g ) ;
12. Traitements :
Ajouter dans main.cpp différentes fonctions de modification des images
Image N e g a t i f ( Image I ) ;
Image Flou ( Image I ) ;
Image R e l i e f ( Image I ) ;
Image Contours ( Image I , double s e u i l ) ;
Image Deforme ( Image I ) ;
et les utiliser :
(a) Negatif : changer le noir en blanc et vise-versa par une transformation
affine.
(b) Flou : chaque pixel devient la moyenne de lui-même et de ses 8 voisins.
Attention aux pixels du bords qui n’ont pas tous leurs voisins (on pourra ne
pas moyenner ceux-là et en profiter pour utiliser l’instruction continue !).
(c) Relief : la dérivée suivant une diagonale donne une impression d’ombres
projetées par une lumière rasante.
— Approcher cette dérivée par différence finie : elle est proportionnelle à
I(i + 1, j + 1) − I(i − 1, j − 1).
— S’arranger pour en faire une image allant de 0 à 255.
(d) Contours : calculer par différences finies la dérivée horizontale dx = (I(i +
1, j) − I(i
p 2− 1, j))/2 et la dérivée verticale dy , puis la norme du gradient
2
|∇I| = dx + dy et afficher en blanc les points où cette norme est supérieure
à un seuil.
(e) Deforme : Construire une nouvelle image sur le principe J(i, j) = I(f (i, j))
avec f bien choisie. On pourra utiliser un sinus pour aller de 0 à W-1 et de 0
à H-1 de façon non linéaire.
218
A. Travaux Pratiques A.6. Images
219
A.7. Premiers objets et dessins de fractales A. Travaux Pratiques
Dans ce TP, nous allons nous essayer à la programmation objet. Nous allons trans-
former une structure vecteur en une classe et l’utiliser pour dessiner des courbes frac-
tales (figure A.6).
220
A. Travaux Pratiques A.7. Premiers objets et dessins de fractales
221
A.7. Premiers objets et dessins de fractales A. Travaux Pratiques
222
A. Travaux Pratiques A.8. Tron
A.8 Tron
Dans ce TP, nous allons programmer le jeu TRON. Il s’agit d’un jeu à 2 joueurs,
dans lequel chaque joueur pilote un mobile qui se déplace à vitesse constante et laisse
derrière lui une trace infranchissable. Le premier joueur qui percute sa propre trace ou
celle de son adversaire a perdu. Ce TP est assez ambitieux et s’approche d’un mini-
projet. Il nous occupera plusieurs séances.
A.8.1 Serpent
Nous allons procéder en deux temps. D’abord programmer un jeu de Serpent à un
joueur. Le programme serpent.exe (sous Windows) vous donne une idée du résul-
tat recherché. Dans ce jeu, le joueur pilote un Serpent qui s’allonge petit à petit (d’un
élément tous les x tours, avec la convention que la longueur totale est bornée à nmax
éléments). Il s’agit de ne pas se rentrer dedans ni de percuter les murs.
Il s’agit ici de concevoir un objet Serpent doté des méthodes adéquates, plus une
fonction jeu_1p exploitant les capacités du Serpent pour reproduire le comportement
désiré. On pourra dans un premier temps ne pas gérer les collisions (avec le bord et
avec lui-même), et ne les rajouter que dans un second temps. Votre travail se décom-
pose en 6 étapes :
1. (sur papier) Définir l’interface de la classe Serpent (c’est-à-dire lister toutes les
fonctionnalités nécessaires).
2. (sur papier) Réfléchir à l’implémentation de la classe Serpent : comment stocker
les données ? comment programmer les différentes méthodes ? (lire en prélimi-
naire les remarques du paragraphe suivant).
3. Dans un fichier serpent.h, écrire la déclaration de votre classe Serpent : ses
membres, ses méthodes, ce qui est public, ce qui ne l’est pas.
4. Soumettre le résultat de vos réflexions à votre enseignant pour valider avec lui les choix
retenus.
223
A.8. Tron A. Travaux Pratiques
A.8.2 Tron
A partir du jeu de Serpent réalisé précédemment, nous allons facilement pouvoir
implémenter le jeu Tron. Le programme tron.exe vous donne une idée du résultat
recherché. Le principe de ce jeu est que chaque joueur pilote une moto qui laisse der-
rière elle une trace infranchissable. Le but est de survivre plus longtemps que le joueur
adverse.
1. Passage à deux joueurs.
A partir de la fonction jeu_1p, créer une fonction jeu_2p implémentant un
jeu de serpent à 2 joueurs. On utilisera pour ce joueur les touches S, X, D et F. 8
La fonction Clavier() renverra donc les entiers int ( ’S’ ), int ( ’X’), int ( ’D’) et
int ( ’F’). Remarque : on ne gèrera qu’une touche par tour, soit un seul appel à la
fonction Clavier() par tour, sinon les serpents seront en concurrence pour le
clavier.
2. Ultimes réglages
(a) Gérer la collision entre les deux serpents.
(b) Le principe de Tron est que la trace des mobiles reste. Pour implémenter
cela, il suffit d’allonger nos serpents à chaque tour.
7. Moralement, (x + 3)%4 = (x − 1)%4, mais pour le C++ −1%4 = −1
8. Les touches A, Z, Q et W ne sont pas dans la bonne configuration sur un clavier Qwerty, les éviter
donc.
224
A. Travaux Pratiques A.8. Tron
A.8.3 Graphismes
Petit bonus pour les rapides : nous allons voir comment gérer des graphismes un
peu plus sympas que les rectangles uniformes que nous avons utilisés jusqu’ici. L’ob-
jectif est de remplacer le carré de tête par une image que l’on déplace à chaque tour.
Nous allons utiliser pour cela les NativeBitmap d’Imagine++, qui sont des images
à affichage rapide. Pour charger une image dans une NativeBitmap on procède ainsi :
/ / E n t i e r s p a s s é s p a r r é f é r e n c e l o r s du c h a r g e m e n t d e l ’ i m a g e p o u r
/ / qu ’ y s o i e n t s t o c k é e s l a l a r g e u r e t l a h a u t e u r d e l ’ i m a g e
i n t w, h ;
/ / Chargement d e l ’ i m a g e
byte ∗ rgb ;
loadColorImage ( " n o m _ f i c h i e r . bmp" , rgb ,w, h ) ;
/ / D é c l a r a t i o n de l a NativeBitmap
NativeBitmap ma_native_bitmap (w, h ) ;
/ / On p l a c e l ’ i m a g e d a n s l a N a t i v e B i t m a p
ma_native_bitmap . setColorImage ( 0 , 0 , rgb ,w, h ) ;
L’affichage d’une NativeBitmap à l’écran se fait alors en utilisant la fonction
d’Imagine++ :
void putNativeBitmap ( i n t x , i n t y , NativeBitmap nb )
225
B. Imagine++
Annexe B
Imagine++
B.1 Common
Le module Common définit entre autres la classe Color codée par un mélange de
rouge, vert et bleu, la quantité de chacun codée par un entier entre 0 et 255 :
Color n o i r = Color ( 0 , 0 , 0 ) ;
Color b l a n c = Color ( 2 5 5 , 2 5 5 , 2 5 5 ) ;
Color rouge = Color ( 2 5 5 , 0 , 0 ) ;
Un certain nombre de constantes de ce type sont déjà définies : BLACK, WHITE, RED,
GREEN, BLUE, CYAN, MAGENTA, YELLOW.
Un type byte (synonyme de unsigned char) est défini pour coder une valeur
entière entre 0 et 255.
B.2. Graphics B. Imagine++
Très pratique, srcPath fait précéder la chaîne de caractère argument par le chemin
complet du répertoire contenant le fichier source. L’équivalent pour un argument de
type string est stringSrcPath :
c o n s t char ∗ f i c h i e r = s r c P a t h ( " m o n _ f i c h i e r . t x t " ) ;
s t r i n g s = " mon_fichier . t x t " ;
s = stringSrcPath ( s ) ;
En d’autres termes, le fichier sera trouvé quel que soit l’emplacement de l’exécutable.
La classe template FArray s’utilise pour des tableaux de taille petite et connue à
la compilation (allocation statique). Pour des tableaux de taille non connue à la com-
pilation (allocation dynamique), utiliser Array. Pour les matrices et vecteurs, utiliser
FMatrix et FVector, utilisant l’allocation statique comme indiqué par le préfixe F
(fixed). Les équivalents dynamiques sont dans LinAlg.
Pratique aussi, l’appel de fonction intRandom(5,10) renvoie un nombre entier aléa-
toire dans l’intervalle [5, 10]. Pour ne pas obtenir à chaque exécution du programme la
même suite de nombres aléatoires, il faut faire appel à initRandom(), fonction qui ne
doit être appelée qu’une fois, typiquement au début du main.
B.2 Graphics
Le module Graphics propose du dessin en 2D et 3D. Les coordonnées 2D sont en
pixel, l’axe des x est vers la droite et l’axe des y vers le bas (attention, ce n’est pas le
sens mathématique usuel !). Le point (0,0) est donc le coin haut-gauche de la fenêtre
(pour les fonctions de tracé) ou de l’écran (pour openWindow).
openWindow ( 5 0 0 , 5 0 0 ) ; / / T a i l l e d e f e n ê t r e
drawRect ( 1 0 , 1 0 , 4 8 0 , 4 8 0 ,RED ) ; / / Coin haut −g a u c h e ( 1 0 , 1 0 ) ,
/ / l a r g e u r 4 8 0 , h a u t e u r 480
drawLine ( 1 0 , 1 0 , 4 9 0 , 4 9 0 ,BLUE ) ; / / D i a g o n a l e
Window w = openWindow ( 1 0 0 , 1 0 0 ) ; / / A u t r e f e n ê t r e
setActiveWindow (w) ; / / S é l e c t i o n p o u r l e s p r o c h a i n s t r a c é s
drawString ( 1 0 , 1 0 , "Du t e x t e " , MAGENTA) ; / / M e t t r e du t e x t e
endGraphics ( ) ; / / A t t e n d un c l i c s o u r i s a v a n t d e f e r m e r l e s f e n ê t r e s
Si on a beaucoup de dessins à faire à la suite et qu’on veut n’afficher que le résultat
final (c’est plus esthétique), on encadre le code de tracé par :
noRefreshBegin ( ) ;
...
noRefreshEnd ( ) ;
Pour faire une animation, il est utile de faire une petite pause entre les images pour
réguler la cadence :
m i l l i S l e e p ( 5 0 ) ; / / Temps en m i l l i s e c o n d e s
Attention cependant à ne pas intercaler une telle commande entre un noRefreshBegin
et un noRefreshEnd, car rien ne s’afficherait pendant cette pause.
On peut charger une image (loadGreyImage, loadColorImage) ou sauvegar-
der (saveGreyImage, saveColorImage) dans un fichier. Attention, ces fonctions
allouent de la mémoire qu’il ne faut pas oublier de libérer après usage.
228
B. Imagine++ B.3. Images
byte ∗ g ;
i n t l a r g e u r , hauteur ;
i f ( ! loadGreyImage ( s r c P a t h ( " image . j p g " ) , g , l a r g e u r , hauteur ) ) {
c e r r << " I m p o s s i b l e d ’ o u v r i r l e f i c h i e r "
<< s r c P a t h ( " image . j p g " ) << endl ;
exit (1);
}
/ / D e s s i n e a v e c c o i n h a u t g a u c h e en ( 0 , 0 ) :
putGreyImage ( 0 , 0 , g , l a r g e u r , hauteur ) ;
d e l e t e [ ] g ; / / Ne p a s o u b l i e r !
A noter srcPath, défini dans Common, qui indique de chercher dans le dossier conte-
nant les fichiers source.
En fait, pour éviter de gèrer soi-même la mémoire des images, il existe une classe
dédiée à cela :
B.3 Images
Le module Images gère le chargement, la manipulation et la sauvegarde des images.
Image<byte > im ; / / Image en n i v e a u x d e g r i s
i f ( ! load ( im , s r c P a t h ( " f i c h i e r _ i m a g e . png " ) ) ) {
c e r r << " I m p o s s i b l e d ’ o u v r i r l e f i c h i e r "
<< s r c P a t h ( f i c h i e r _ i m a g e . png ’ ’ ) << endl ;
exit (1);
}
d i s p l a y ( im ) ; / / D e s s i n e d a n s l a f e n ê t r e a c t i v e
im ( 0 , 0 ) = 1 2 8 ; / / Met l e p i x e l en g r i s
save ( im , " f i c h i e r _ i m a g e 2 . png " ) ; / / S a u v e g a r d e l ’ i m a g e d a n s un f i c h i e r
Attention : la recopie et l’affectation (opérateur =) sont des opérations peu coû-
teuses qui en fait ne font qu’un lien entre les images, sans réelle copie. Ce qui fait que
la modification de l’une affecte l’autre :
Image<Color > im1 ( 1 0 0 , 1 0 0 ) ;
Image<Color > im2 = im1 ; / / C o n s t r u c t e u r p a r r e c o p i e
im1 ( 1 0 , 1 0 ) = CYAN;
a s s e r t ( im2 ( 1 0 , 1 0 ) == CYAN) ; / / im2 a é t é a f f e c t é e
Pour faire une vraie copie plutôt qu’un lien, on utilise :
im2 = im1 . c l o n e ( ) ;
im1 ( 1 0 , 1 0 ) = CYAN; / / N ’ a f f e c t e p a s im2
Ainsi, si on passe une image comme paramètre d’une fonction, puisque c’est le
constructeur par copie qui est appelé, tout se passe comme si on avait passé par réfé-
rence :
void f ( Image<Color > im ) { / / P a s s a g e p a r v a l e u r
im ( 1 0 , 1 0 ) = CYAN;
}
229
B.4. LinAlg B. Imagine++
Une erreur courante est de chercher à lire ou écrire à des coordonnées au-delà des
bornes du tableau, typiquement une erreur dans un indice de boucle.
B.4 LinAlg
Le module LinAlg propose l’algèbre linéaire avec des classes matrice et vecteur.
Matrix < f l o a t > I ( 2 , 2 ) ; / / T a i l l e 2 x2
I . f i l l (0.0 f ) ; / / Matrice nulle
I (0 ,0)= I (1 ,1)=1.0 f ; / / Matrice i d e n t i t é
cout << " det ( I )= " << det ( I ) << endl ; / / D é t e r m i n a n t
Les opérateurs d’addition (matrice+matrice, vecteur+vecteur), soustraction (matrice-
matrice, vecteur-vecteur) et multiplication (matrice*matrice, matrice*vecteur) sont bien
sûr définis.
Comme pour les images, attention de ne pas sortir des bornes en accédant aux
éléments des matrices et vecteurs !
Une fonction très utile est linSolve pour résoudre un système linéaire.
B.5 Installation
Examinons le fichier CMakeLists.txt d’un programme utilisant Imagine++.
cmake_minimum_required(VERSION 2.6)
find_package(Imagine REQUIRED)
project(Balle)
add_executable(Balle Balle.cpp)
ImagineUseModules(Balle Graphics)
Pour que le find_package fonctionne, il est parfois nécessaire de définir la variable
Image_DIR. Cette commande va charger le fichier
/usr/share/Imagine++/CMake/ImagineConfig.cmake
quit contient des commandes CMake, dont ImagineUseModules. Cette dernière fait
deux choses :
— Indique où chercher lors des #include, comme par exemple dans le dossier
/usr/share/Imagine++/include
Ainsi, l’instruction
#include "Imagine/Graphics.h"
inclura le fichier
/usr/share/Imagine++/include/Imagine/Graphics.h
— Indique qu’il faut lier la bibliothèque libImageGraphics.a lors de l’édition de
liens (link) ; elle se trouve dans
/usr/share/Imagine++/lib
230
C. Compilateur, CMake, Linker, Qt. . . Au secours !
Annexe C
D’accord, tout cela a l’air bien compliqué, et en effet ce n’est pas si simple. Néanmoins, il
faut en passer par là, et nous allons voir que ce n’est pas si difficile à comprendre, malgré l’effroi
que nous pouvons ressentir face à une telle accumultation d’outils. La lecture de cette annexe
n’est pas nécessaire pour les utiliser, mais elle devrait aider à comprendre comment tout cela
s’imbrique.
C.1 Compilation
C.1.1 Compilateur et linker
Commençons par le principe de base : pour passer du code source, c’est-à-dire des
fichiers d’extension .h et .cpp 1 , à un programme exécutable, il faut lancer le build, qu’on
appelle souvent abusivement compilation, alors que cette compilation proprement dite
n’est qu’une partie du processus 2 . Celle-ci est composée en fait de deux phases, com-
pilation (faite par le compilateur) suivie d’édition de liens (faite par le linker). Le rôle
du linker est de rassembler les produits de la phase de compilation, les fichiers objet
(d’extension .o ou .obj), et d’éventuelles bibliothèques (libraries en anglais, que les in-
formaticiens francisent souvent à tort en librairies), qui ne sont en fait que des fichiers
de type archive (comme un zip) de fichiers objet. Ceci est résumé par le schéma de la
Figure C.1.
Plusieurs choses sont à remarquer :
— Le compilateur est lancé indépendamment sur chaque source .cpp, au point que
ces compilations peuvent être lancées en parallèle. Une conséquence importante
est que la modification d’un .cpp ne nécessite pas la recompilation des autres,
d’où gain de temps du build.
1. Parfois on remplace l’extension .h par .hpp pour éviter de confondre avec les fichiers header du
langage C. On rencontre aussi les conventions .hxx et .cxx, car le + pourrait être mal géré dans le nom
d’un fichier par certains systèmes d’exploitation, et finalement le x n’est qu’un + tourné.
2. On fait donc là une synecdoque. Comme quoi faire de l’ingénierie ne prive pas d’avoir du goût
pour les figures littéraires !
C.1. Compilation C. Compilateur, CMake, Linker, Qt. . . Au secours !
libImagineGraphics.a
libQtCore.so
F IGURE C.1 – Phases de compilation et d’édition de liens. Les extensions sont les
conventions des systèmes Unix (Linux et Mac), sous Windows les fichiers objet sont
en .obj, les bibliothèques en .lib et le programme en .exe.
— Les fichier header en .h ne sont pas passés directement au compilateur, mais ils le
sont indirectement par les .cpp qui font des #include de ces .h. Comme plu-
sieurs .cpp peuvent faire un #include "fic1.h", ce fichier passe plusieurs
fois par le compilateur. Il est donc essentiel que le compilateur ne génère pas de
symboles dus à fic1.h, comme des variables globales (à éviter tant que possible
de toute façon) ou des définitions de fonction, mais seulement des déclarations
(de classes, de fonctions, de constantes) 3 . Sinon, lors de l’édition de liens, il va
échouer en signalant : Multiply defined symbol...
— Le rôle de l’éditeur de liens est de vérifier que chaque fonction ou méthode de
classe utilisée 4 soit définie une et une seule fois, c’est-à-dire dans un unique .o
ou bibliothèque. De plus, il vérifie que la fonction d’entrée du programme, le
main, est bien défini également (une seule fois).
— Les bibliothèques se présentent sous deux formes : statiques (extension .a) et dy-
namiques (extension .so, comme shared object). Lors de l’édition de liens,
les symboles des bibliothèques statiques utilisés sont inclus dans le programme
résultant, alors que ceux des bibliothèques dynamiques ne le sont pas, l’éditeur
de liens vérifie juste leur présence. L’avantage de ces dernières est que le pro-
gramme est moins volumineux en mémoire, que plusieurs programmes utilisant
la même bibliothèque ne nécessitent qu’un unique chargement en mémoire de
cette bibliothèque lors de leur lancement, et que la correction d’une faille de sécu-
rité ou d’un bug dans une bibliothèque dynamique ne nécessite pas de recompi-
ler tous les programmes l’utilisant et de les redistribuer aux utilisateurs (pourvu
qu’on prenne garde à seulement corriger ces bugs sans changer les arguments des
fonctions, etc). La contrepartie, c’est que le programme exécutable ne peut pas se
lancer directement car il est incomplet, il a besoin de trouver les bibliothèques
dynamiques dont il dépend.
En fait, sous Windows, les bibliothèques dynamiques sont scindées en deux : le .lib
sert lors de l’édition de liens (penser à lui comme à une sorte de header pour la bi-
bliothèque) tandis que la partie .dll sert lors du lancement du programme. Le pro-
3. Les exceptions sont les fonctions inline, les méthodes de classes définies directement dans la
classe, qui sont en fait aussi inline, et les fonctions ou méthodes template, qui sont très particu-
lières car elles ne peuvent être réellement compilées que lorsque les types des paramètres template sont
connus, on parle d’instantiation de ces templates.
4. Une fonction ou méthode déclarée mais non appelée ne provoque pas d’erreur mais est simplement
considérée comme superflue par l’éditeur de liens.
232
C. Compilateur, CMake, Linker, Qt. . . Au secours ! C.2. En ligne de commande
gramme ne peut se lancer s’il ne trouve pas les .dll dont il a besoin 5 . On peut voir les
bibliothèques dynamiques dont dépendent un programme exécutable avec les outils
ldd (Linux), otool (Mac) et depend.exe (Windows).
5. Il cherche pour cela dans le dossier contenant le programme et dans la liste des dossiers indiqués
par la variable d’environnement PATH. Sous Linux et Mac, la variable équivalente pour chercher les .so
s’intitule LD_LIBRARAY_PATH.
233
C.3. Make et CMake C. Compilateur, CMake, Linker, Qt. . . Au secours !
234
C. Compilateur, CMake, Linker, Qt. . . Au secours ! C.4. Utilisation d’un IDE
seul test.cpp. S’il y avait plusieurs .cpp, on les ajouterait à la ligne séparés par
des espaces. On peut aussi ajouter les .h, bien que ceux-ci ne soient pas compilés di-
rectement : ils seront juste visibles dans la partie headers si on demande à CMake de
créer un projet (voir les IDE). La dernière ligne indique que notre programme a besoin
de la bibliothèque Graphics d’Imagine++, ce qui indique les chemins des headers et
des bibliothèques. Le fait que cette bibliothèque dépende de Qt est automatiquement
ajouté. Un exemple typique d’utilisation de CMake en ligne de commande sour Linux
ou Mac :
mkdir Build ← Crée un dossier Build, le build directory
cd Build ← Va dans ce dossier
cmake .. ← Indique que le source directory est le parent (..)
make ← Il s’agit maintenant du make classique
Le lancement de cmake va créer un fichier CMakeCache.txt dans le build direc-
tory qui contient une liste de variables de CMake. Entre autres, on a CMAKE_BUILD_TYPE
qui indique le degré d’optimisation à appliquer. Le degré maximum est le mode Release,
tandis que le mode Debug est approprié pour un programme qu’on veut suivre ligne
à ligne. On peut parfaitement modifier une de ces variables 7 et relancer cmake. Le
lancement de cette commande génère le fichier Makefile pour nous, qui sert alors de
configuration au make classique.
À noter que la modification ultérieure du CMakeLists.txt, par exemple pour
ajouter des fichiers source dans la ligne add_executable, ne nécessite pas de relancer
explicitement cmake, car le Makefile généré retient que CMakeCache.txt est une
dépendance de CMakeLists.txt. Donc le fait de lancer make directement suffit à
relancer cmake avant d’effectuer le build normal.
Enfin, la force de CMake est qu’il connaît tous les IDE classiques (Visual Studio,
Eclipse, Code::Blocks, etc) et est capable de générer des projets pour ceux-ci (cf Fi-
gure C.2). Absents de cette liste sont Kdevelop (Linux) et surtout Qt Creator (toutes
plates-formes). En effet, ceux-ci gèrent eux-mêmes des projets CMake, donc on peut
lancer cmake depuis leur interface.
L’intérêt de tout ceci est que l’utilisateur peut choisir l’IDE qui lui convient le mieux
(ou aucun d’entre eux s’il préfère générer simplement des Makefiles) et qu’un seul
fichier de configuration CMakeLists.txt assûrera la portabilité du build sur toutes
les plates-formes. Ensuite, c’est au programmeur de coder en respectant la norme du
C++ pour un maximum de portabilité.
235
C.4. Utilisation d’un IDE C. Compilateur, CMake, Linker, Qt. . . Au secours !
F IGURE C.2 – Création d’un projet avec choix de l’IDE et interaction avec les variables
de CMake stockées dans CMakeCache.txt grâce à cmake-gui. Notons que CMake
gère les différentes versions de Visual Studio, en 32 et 64 bits. Le compilateur MinGW
étant installé (il vient avec l’installeur de Qt), on peut aussi créer des Makefiles comme
sous Linux et Mac, sauf que le GNU make qui les interprète s’appelle mingw32-make
et non make.
cutable cmake, ainsi que le compilateur, débugger, etc. Enfin, il faut faire attention de
garder CMake comme système de build, et décliner, quand on crée un nouveau fichier
source, la proposition de l’IDE de l’intégrer au projet. Cela doit se faire au niveau de
CMake, donc modifier le CMakeLists.txt pour cela.
Pour Imagine++, il faut que la commande find_package(Imagine REQUIRED)
puisse trouver le fichier ImagineConfig.cmake. Pour cela, il faut l’aider en donnant
le dossier d’installation dans la variable Imagine_DIR.
Lorsque plus rien ne semble marcher, il est sage de créer un nouveau dossier de
build vierge et de travailler dans celui-ci. Cela peut s’avérer nécessaire après des chan-
gements majeurs dans un projet.
Enfin, notons que Qt n’est pas un IDE mais un ensemble de bibliothèques C++ multi-
plates-formes, utilisées par Imagine++. Il se trouve que l’IDE QtCreator utilise ces bi-
bliothèques Qt, et s’il a été installé en même temps que les bibliothèques Qt, il sait
directement où trouver ces dernières, ce qui aide pour un projet Imagine++.
236
D. Fiche de référence finale
D. Fiche de référence finale
Annexe D
239
D. Fiche de référence finale
241
D. Fiche de référence finale
242