Programmer en C++
Programmer en C++
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 . . . . . . . . . . . . . . . . . . . . . . . . . 45
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 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
TABLE DES MATIÈRES TABLE DES MATIÈRES
4 Les tableaux 53
4.1 Premiers tableaux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
4.2 Initialisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
4.3 Spécificités des tableaux . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
4.3.1 Tableaux et fonctions . . . . . . . . . . . . . . . . . . . . . . . . . . 56
4.3.2 Affectation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
4.4 Récréations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
4.4.1 Multi-balles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
4.4.2 Avec des chocs ! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
4.4.3 Mélanger les lettres . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
4.5 TP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
4.6 Fiche de référence . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
5 Les structures 69
5.1 Révisions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
5.1.1 Erreurs classiques . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
5.1.2 Erreurs originales . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
5.1.3 Conseils . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
5.2 Les structures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
5.2.1 Définition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
5.2.2 Utilisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
5.3 Récréation : TP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
5.4 Fiche de référence . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
6 Plusieurs fichiers ! 77
6.1 Fichiers séparés . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
6.1.1 Principe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
6.1.2 Avantages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
6.1.3 Utilisation dans un autre projet . . . . . . . . . . . . . . . . . . . . 80
6.1.4 Fichiers d’en-têtes . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
6.1.5 A ne pas faire... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
6.1.6 Implémentation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
6.1.7 Inclusions mutuelles . . . . . . . . . . . . . . . . . . . . . . . . . . 83
6.1.8 Chemin d’inclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
6.2 Opérateurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
6.3 Récréation : TP suite et fin . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
6.4 Fiche de référence . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
7 La mémoire 89
7.1 L’appel d’une fonction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
7.1.1 Exemple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
7.1.2 Pile des appels et débuggeur . . . . . . . . . . . . . . . . . . . . . 92
7.2 Variables Locales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
7.2.1 Paramètres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
7.2.2 La pile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
7.3 Fonctions récursives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
7.3.1 Pourquoi ça marche ? . . . . . . . . . . . . . . . . . . . . . . . . . . 94
7.3.2 Efficacité . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
7.4 Le tas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
2
TABLE DES MATIÈRES TABLE DES MATIÈRES
7.4.1 Limites . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
7.4.2 Tableaux de taille variable . . . . . . . . . . . . . . . . . . . . . . . 97
7.4.3 Essai d’explication . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
7.5 L’optimiseur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
7.6 Assertions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
7.7 Examens sur machine . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
7.8 Fiche de référence . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
3
TABLE DES MATIÈRES TABLE DES MATIÈRES
4
TABLE DES MATIÈRES TABLE DES MATIÈRES
B Imagine++ 221
B.1 Common . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 221
B.2 Graphics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 222
B.3 Images . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 223
B.4 LinAlg . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 223
B.5 Installation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 224
5
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 jour-
née à se tenir au courant des nouveaux logiciels 11 et en oublie qu’il pourrait lui aussi
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
12. Nous ne réduisons évidemment pas Java à un sous ensemble de C++. Il lui est supérieur sur
certains aspects mais il est d’expressivité plus réduite.
13. Java force un cadre de programmation objet, déroutant pour le débutant.
14. Ne pas comprendre ce que la machine doit faire pour exécuter un programme, conduit à des pro-
grammes inconsidérément gourmands en temps ou mémoire.
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 .
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()
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 ?)
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...
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
Comment ça marche ?
Le problème avec le programme précédent est qu’il est très loin de ce qu’un ordi-
nateur sait faire naturellement. En fait, un ordinateur ne sait pas faire de C++. Il ne
sait que calculer 10 , transformer des nombres en autres nombres. Bien que peu compré-
hensible pour le débutant, un programme en C++ se veut le plus proche possible de
l’Homme, tout en restant évidemment accessible 11 à la machine. Le C++ est un lan-
gage très complet, peut-être même trop. Il peut être relativement proche de la machine
si nécessaire et au contraire de "haut niveau" quand il le faut. La largeur de son spectre
est une des raisons de son succès. C’est aussi ce qui fait que son apprentissage complet
demande un long travail et nous ne verrons ici qu’un partie restreinte du C++ !
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 mov eax,dword ptr [a] // mettre dans le registre eax
// le contenu de l’adresse où
// est mémorisée la variable a
00415A64 imul eax,eax,3 // effectuer eax=eax*3
00415A67 mov ecx,dword ptr [b] // idem mais b dans ecx
00415A6A lea edx,[eax+ecx*2] // effectuer edx=eax+ecx*2
00415A6D mov dword ptr [c],edx // mettre le contenu du registre edx
// à l’adresse où est mémorisée la
// variable c
Ce programme est désigné comme du Code Machine. Le nombre au début de chaque
ligne est une adresse. Nous allons en reparler. A part lui, le reste est relativement li-
sible pour l’Homme (attention, c’est moi qui ai ajouté les remarques sur le coté droit !).
Ceci parce qu’il s’agit d’un programme en langage assembleur, c’est-à-dire un langage
où chaque instruction est vraiment une instruction du micro-processeur, mais où le
nom de ces instructions ainsi que leurs arguments sont explicites. En réalité, le micro-
processeur ne comprend pas l’assembleur. Comprendre "mov eax,dword ptr [a]"
lui demanderait non seulement de décoder cette suite de symboles, mais aussi de sa-
voir où est rangée la variable a. Le vrai langage du micro-processeur est le langage
machine, dans lequel les instructions sont des nombres. Voici ce que ça donne pour
notre "c=3∗a+2∗b;" :
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 :
Une fois traduit en langage machine pour un micro-processeur donné, un
programme C++ n’a de sens que pour ce micro-processeur.
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 :
00415A61 8B 45 F8 mov eax,dword ptr [a]
00415A64 6B C0 03 imul eax,eax,3
00415A67 8B 4D EC mov ecx,dword ptr [b]
00415A6A 6B C9 03 imul ecx,ecx,3
00415A6D 03 C1 add eax,ecx
00415A6F 89 45 E0 mov dword ptr [c],eax
15. Nombres un peu bizarres, certes, puisqu’il contiennent des lettres. Patience, jeune Padawan ! Nous
en reparlons aussi tout de suite !
16. Et pourtant, les informaticiens programmaient comme cela il n’y a pas si longtemps. C’était déjà
très bien par rapport à l’époque antérieure où il fallait programmer en base 2... et beaucoup moins bien
que lorsqu’on a pu enfin programmer en assembleur !
18
2. Bonjour, Monde ! 2.1. L’ordinateur
2.1.2 La mémoire
19
2.1. L’ordinateur 2. Bonjour, Monde !
où les octets a1 , ..., a4 combinés donnent l’entier a sur 32 bits. Certains processeurs (dits
big-endian) 26 décident a = a1 a2 a3 a4 , d’autres (little-endian) 27 que a = a4 a3 a2 a1 . Cela
signifie que :
23. Les variables ayant plus de 256 valeurs possibles sont forcément stockées sur plusieurs octets.
Ainsi, avec 4 octets on peut compter en binaire sur 4 × 8 = 32 bits, soit 232 valeurs possibles (plus de 4
milliards).
24. Ce qui était le plus pénible n’était pas de décider où il fallait ranger les variables en mémoire, mais
d’ajuster les instructions en conséquence. Si on se trompait, on risquait d’écrire au mauvais endroit de
la mémoire. Au mieux, cela effaçait une autre variable — ce comportement est encore possible de nos
jours — au pire, cela effaçait des instructions et le programme pouvait faire de "grosses bêtises" — ceci
est aujourd’hui impossible sous Windows ou Linux, et ne concerne plus que certains systèmes.
25. Nous faisons ici un horrible mensonge à des fins simplificatrices. Dans notre cas, les variables
étaient des variables locales à la fonction main() donc stockées dans la pile. Elles ne sont pas à une
adresse mémoire définie à l’avance de manière absolue mais à une adresse relative à l’emplacement
où la fonction rangera ses variables locales en fonction de ce que le programme aura fait avant. Cela
explique la simplicité de l’instruction mov eax,dword ptr [a] dans notre cas. Nous verrons tout
cela plus tard.
26. Comme les PowerPC des vieux Macs
27. Comme les processeurs Intel et AMD
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.
28. Moins que les registres, ou même que le cache mémoire du processeur, dont nous ne parlerons pas
ici.
29. Il est pénible qu’une ROM ne puisse être modifiée. Alors, à une époque, on utilisait des mémoires
modifiables malgré tout, mais avec du matériel spécialisé (EPROMS). Maintenant, on a souvent recours
à de la mémoire pouvant se modifier de façon logicielle (mémoire "flashable") ou, pour de très petites
quantités de données, à une mémoire consommant peu (CMOS) et complétée par une petite pile. Dans
un PC, la mémoire qui sert à démarrer s’appelle le BIOS. Il est flashable et ses paramètres de règlage
sont en CMOS. Attention à l’usure de la pile !
30. A chaque fois qu’on allumait l’ordinateur mais aussi à chaque fois que le programme plantait et
s’effaçait lui-même, c’est-à-dire la plupart du temps !
31. Très lent et très peu fiable, mais le quotidien des ordinateurs personnels.
32. Le luxe. Un lecteur de 40Ko coûtait 5000F !
33. Les premiers étaient de véritables moteurs de voiture, réservés aux importants centres de calcul.
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).
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().
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. Ca 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écute 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
Starting /home/pascal/TEMP/Build/EssaiQtCreator...
/home/pascal/TEMP/Build/EssaiQtCreator exited with code 0
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). 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.
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
déjà rencontré les int qui sont le plus souvent aujourd’hui stockés sur quatre octets,
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
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 :
3. Nous avons aussi vu que cette simple idée donne déjà lieu à deux façons d’utiliser les 4 octets :
big-endian ou little-endian.
4. Coin des collégiens : c’est à dire 0, 1, 2, ... mais aussi −1, −2, −3, ...
5. à un près !
6. En fait, les int s’adaptent au processeur et un programme compilé sur un processeur 64 bits aura
des int sur 64 bits ! Si l’on a besoin de savoir dans quel cas on est, le C++ fournit les constantes INT_MIN
et INT_MAX qui sont les valeurs minimales et maximales prises par les int.
7. Là, tout le monde fait pareil ! On compte en binaire à partir de 0, et arrivé à 2147483647,
le suivant est -2147483648, puis -2147483647 et ainsi de suite jusqu’à -1. On a par exemple :
0 = 000...000, 1 = 000...001, 2147483647 = 011...111, −2147483648 = 100...000, −2147483647 =
100..001, −2 = 111...110, −1 = 111...111
32
3. Premiers programmes 3.1. Tout dans le main() !
— 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() !
les 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 ;
25 break ;
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() !
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 attend
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 : " ;
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
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
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 :
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.
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() !
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
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
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
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 }
On peut aussi compléter le programme "supplémentaire" du TP de l’annexe A.1. Il
s’agissait d’une balle rebondissant dans un carré. (Voir l’annexe B pour les instructions
graphiques...)
1 # i n c l u d e <Imagine/Graphics . h>
2 using namespace Imagine ;
3
4 i n t main ( )
5 {
6 i n t w=300 , h = 2 1 0 ;
7 openWindow (w, h ) ; / / F e n ê t r e g r a p h i q u e
8 i n t i =0 , j = 0 ; // Position
9 i n t di =2 , d j = 3 ; // Vitesse
10 while ( t r u e ) {
11 f i l l R e c t ( i , j , 4 , 4 ,RED ) ; / / D e s s i n d e l a b a l l e
12 m i l l i S l e e p ( 1 0 ) ; / / On a t t e n d un peu . . .
13 i f ( i +di >w || i +di <0) {
14 di=−di ; / / Rebond h o r i z o n t a l s i on s o r t
15 }
16 i n t n i = i +di ; / / N o u v e l l e p o s i t i o n
17 i f ( j +dj >h || j +dj <0) {
18 d j=−d j ; / / Rebond v e r t i c a l s i on s o r t
19 }
20 int nj= j +dj ;
21 f i l l R e c t ( i , j , 4 , 4 ,WHITE ) ; / / E f f a c e m e n t
22 i = n i ; / / On c h a n g e d e p o s i t i o n
23 j =nj ;
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
40
3. Premiers programmes 3.2. Fonctions
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 ;
41
3.2. Fonctions 3. Premiers programmes
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 !
i n t main ( )
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
{
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 :
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 " ) ;
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
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 ( ) ;
}
}
}
}
}
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 ) ;
24. Contrairement à certains vieux langages, comme le Pascal
44
3. Premiers programmes 3.2. Fonctions
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.
1. Fonction sans paramètre : x=hop(); et non x=hop;.
2. Division entière :
— double x=1.0/3; et non double x=1/3;
— double x=double(i)/j; et non double x=i/j;, ni même
double x=double(i/j); a
a. Cette conversion en double arrive trop tard !
45
3.2. Fonctions 3. Premiers programmes
void t r i p l e ( i n t x ) {
x=x ∗ 3 ;
}
...
i n t a =2;
triple (a );
cout << a << endl ;
Il affiche 2 et non 6. En fait, le paramètre x de la fonction triple vaut bien 2, puis 6.
Mais son passage à 6 ne modifie pas a. Nous verrons plus loin que x est mémorisé
à un endroit différent de a, ce qui explique tout ! C’est la valeur de a qui est passée
à la fonction triple () et non pas la variable a ! On parle de passage par valeur. On
peut toutefois faire en sorte que la fonction puisse vraiment modifier son paramètre.
On s’agit alors d’un passage par référence (ou par variable). Il suffit de rajouter un &
derrière le type du paramètre :
void t r i p l e ( i n t& x ) {
x=x ∗ 3 ;
}
Généralement, on choisit l’exemple suivant pour justifier le besoin des références :
void echanger1 ( i n t x , i n t y ) {
i n t t =x ;
x=y ;
y= t ;
}
void echanger2 ( i n t& x , i n t& y ) {
i n t t =x ;
x=y ;
y= t ;
}
...
i n t a =2 , b = 3 ;
echanger1 ( a , b ) ;
cout << a << " " << b << " " ;
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,
et ce, de la façon suivante :
46
3. Premiers programmes 3.2. Fonctions
47
3.2. Fonctions 3. Premiers programmes
40 }
Avec le conseil suivant :
penser à utiliser directement le résultat d’une fonction et ne pas le mémori-
ser dans une variable lorsque c’est inutile.
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.
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 :
48
3. Premiers programmes 3.2. Fonctions
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 ();
}
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 ...
49
3.2. Fonctions 3. Premiers programmes
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 :
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 :
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 ( ) {
50
3. Premiers programmes 3.3. TP
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
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.
54
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 !
55
4.3. Spécificités des tableaux 4. Les tableaux
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;
16 affecte1 (a , 0 ) ; / / a ne s e r a p a s mis à 0
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.
56
4. Les tableaux 4.3. Spécificités des tableaux
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 ;
10 }
11 ...
57
4.4. Récréations 4. Les tableaux
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
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
16 ...
58
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 ( )
15 void InitRandom ( )
59
4.4. Récréations 4. Les tableaux
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 ( ) ;
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 ] ) ;
60
4. Les tableaux 4.4. Récréations
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 }
61
4.4. Récréations 4. Les tableaux
62
4. Les tableaux 4.4. Récréations
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 }
63
4.4. Récréations 4. Les tableaux
L’avez-vous comprise ? Peu importe ! C’est le listing que vous devez comprendre :
1 # i n c l u d e <iostream >
2 # include <string >
3 # include <cstdlib >
4 # i n c l u d e <ctime >
5 using namespace s t d ;
6
7 // / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
8 / / Generateur a l e a t o i r e
9 / / A n ’ a p p e l e r qu ’ une f o i s , a v a n t Random ( )
10 void InitRandom ( )
11 {
12 srand ( ( unsigned i n t ) time ( 0 ) ) ;
13 }
14 / / Entre a e t b
15 i n t Random ( i n t a , i n t b )
16 {
17 r e t u r n a +( rand ( ) % ( b−a + 1 ) ) ;
18 }
19
20 // / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
21 / / Permuter l e s l e t t r e s i n t e r i e u r e s de s n f o i s
22 s t r i n g Melanger ( s t r i n g s , i n t n )
23 {
24 int l=int ( s . size ( ) ) ;
25 i f ( l <=3)
26 return s ;
27 s t r i n g t =s ;
28 f o r ( i n t i = 0 ; i <n ; i ++) {
29 i n t a=Random ( 1 , l − 2 ) ;
30 int b ;
31 do
32 b=Random ( 1 , l − 2 ) ;
33 while ( a==b ) ;
34 char c= t [ a ] ;
35 t [ a ]= t [ b ] ; t [ b ]= c ;
36 }
37 return t ;
38 }
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 ( ) ;
64
4. Les tableaux 4.5. TP
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 !).
65
4.6. Fiche de référence 4. Les tableaux
66
4. Les tableaux 4.6. Fiche de référence
67
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 !
70
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.
71
5.2. Les structures 5. Les structures
};
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 ;
P [ i ] . y= f ( 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 !
72
5. Les structures 5.3. Récréation : TP
...
/ / 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 !
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 (figure
5.1) !
4. La situation s’améliorera avec les objets.
5. Coin des collégiens : il y a dans ce TP des mathématiques et de la physique pour étudiant de
l’enseignement supérieur... mais on peut très bien faire les programmes en ignorant tout ça !
73
5.4. Fiche de référence 5. Les structures
74
5. Les structures 5.4. Fiche de référence
75
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 !
78
6. Plusieurs fichiers ! 6.1. Fichiers séparés
// 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 section
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.
79
6.1. Fichiers séparés 6. Plusieurs fichiers !
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 ) {
...
}
int g () {
...
80
6. Plusieurs fichiers ! 6.1. Fichiers séparés
}
/ / 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 () {
...
}
/ / Autres f o n c t i o n s
...
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.
81
6.1. Fichiers séparés 6. Plusieurs fichiers !
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
...
82
6. Plusieurs fichiers ! 6.1. Fichiers séparés
6.1.6 Implémentation
Finalement, la philosophie de ce système est que
83
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
find_package(Imagine)
ImagineUseModules(Mastermind Graphics)
<Imagine_DIR>/include
11. Notez que les commandes du préprocesseur ne se terminent pas par un point-virgule, mais avec
la fin de ligne.
84
6. Plusieurs fichiers ! 6.2. Opérateurs
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 .
/ / 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 ) {
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.
85
6.3. Récréation : TP suite et fin 6. Plusieurs fichiers !
86
6. Plusieurs fichiers ! 6.4. Fiche de référence
87
6.4. Fiche de référence 6. Plusieurs fichiers !
88
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 {
20 cout << " E n t r e z deux e n t i e r s p o s i t i f s : " ;
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
des appels. Pour mieux comprendre cette pile, nous allons utiliser le debuggeur. Avant
cela, précisons ce qu’un informaticien entend par 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.
90
7. La mémoire 7.1. L’appel d’une fonction
(a)
(b)
(c)
(d)
(e)
(f)
(g)
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.
91
7.2. Variables Locales 7. La mémoire
7.2.1 Paramètres
Pour les paramètres, c’est simple :
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...", ( )
92
7. La mémoire 7.3. Fonctions récursives
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).
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.
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.
93
7.3. Fonctions récursives 7. La mémoire
sive de la programmer :
5 int fact1 ( int n)
6 {
7 i f ( n==1)
8 return 1;
9 r e t u r n n∗ f a c t 1 ( n − 1 ) ;
10 }
On remarque évidemment que les fonctions récursives contiennent (en général au dé-
but, et en tout cas avant l’appel récursif !) une condition d’arrêt : ici si n vaut 1, la fonction
retourne directement 1 sans s’appeler elle-même 8 .
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 à :
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.
8. Le fait de pouvoir mettre des return au milieu des fonctions est ici bien commode !
94
7. La mémoire 7.3. Fonctions récursives
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
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 ) {
95
7.4. Le tas 7. La mémoire
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 }
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 !
12. 500000x4 soit 2Mo seulement !
96
7. La mémoire 7.4. 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 :
97
7.5. L’optimiseur 7. La mémoire
26
27 void v a r i a b l e ( )
28 {
29 int n;
30 cout << "Un e n t i e r SVP : " ;
31 c i n >> n ;
32 i n t ∗ t =new i n t [ n ] ; / / A l l o c a t i o n
33 remplit ( t , n ) ;
34 i n t s=somme( t , n ) ;
35 cout << s << " d e v r a i t v a l o i r " << n ∗ ( n+1)/2 << endl ;
36 delete [ ] t ; / / D e s a l l o c a t i o n : ne p a s o u b l i e r !
37 }
38
39 i n t main ( )
40 {
41 fixe ( ) ;
42 variable ( ) ;
43 return 0;
44 }
7.5 L’optimiseur
Mentionnons ici un point important qui était négligé jusqu’ici, mais que nous allons
utiliser en TP.
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.
98
7. La mémoire 7.6. Assertions
— Rester en mode Debug le plus longtemps possible pour bien mettre au point le
programme.
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.
99
7.8. Fiche de référence 7. La mémoire
100
7. La mémoire 7.8. Fiche de référence
101
8. Allocation dynamique
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 :
On est obligé de préciser les dimensions d’un tableau 2D paramètre de fonc-
tion.
Impossible donc de programmer des fonctions qui peuvent travailler sur des tableaux
de différentes tailles comme dans le cas 1D (cf 4.3.1). C’est très restrictif et explique
que les tableaux 2D ne sont pas toujours utilisés. On peut donc avoir le programme
suivant :
1 / / Passage de paramètre
2 double t r a c e ( double A [ 2 ] [ 2 ] ) {
3 double t = 0 ;
4 f o r ( i n t i = 0 ; i < 2 ; i ++)
5 t +=A[ i ] [ i ] ;
6 return t ;
7 }
8
9 / / Le p a s s a g e e s t t o u j o u r s p a r r é f é r e n c e . . .
10 void s e t ( double A [ 2 ] [ 3 ] ) {
11 f o r ( i n t i = 0 ; i < 2 ; i ++)
12 f o r ( i n t j = 0 ; j < 3 ; j ++)
13 A[ i ] [ j ]= i + j ;
14 }
15
16 ...
17 double D[ 2 ] [ 2 ] = { { 1 , 2 } , { 3 , 4 } } ;
18 double t = t r a c e (D ) ;
19 double E [ 2 ] [ 3 ] ;
20 set (E ) ;
21 ...
mais il est impossible de programmer une fonction trace () ou set () qui marche pour
différentes tailles de tableaux 2D comme on l’aurait fait en 1D :
1 / / OK
2 void s e t ( double A[ ] , i n t n , double x ) {
3 f o r ( i n t i = 0 ; i <n ; i ++)
4 A[ i ]= x ;
5 }
6 / / NON ! ! ! ! ! ! ! ! ! ! ! ! ! ! !
7 / / double A[ ] [ ] est refusé
8 void s e t ( double A [ ] [ ] , double m, double n , double x ) {
9 f o r ( i n t i = 0 ; i <m; i ++)
10 f o r ( i n t j = 0 ; j <n ; j ++)
11 A[ i ] [ j ]= x ;
12 }
104
8. Allocation dynamique 8.1. Tableaux bidimensionnels
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 ...
14 // P=... x=...
15 p r o d u i t ( P , 2 , 3 , x , y ) ; / / y=Px
105
8.2. Allocation dynamique 8. Allocation dynamique
106
8. Allocation dynamique 8.2. Allocation dynamique
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 !
107
8.2. Allocation dynamique 8. Allocation dynamique
/ / 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 ) {
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) ;
108
8. Allocation dynamique 8.3. Structures et allocation dynamique
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.
109
8.3. Structures et allocation dynamique 8. Allocation dynamique
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 ;
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 ++)
110
8. Allocation dynamique 8.3. Structures et allocation dynamique
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 ++) {
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
111
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...
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.
for ( . . . ) {
...
i f (A)
continue ;
...
i f (B)
continue ;
...
}
for ( . . . ) {
...
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).
112
8. Allocation dynamique 8.5. TP
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.6 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).
113
8.6. Fiche de référence 8. Allocation dynamique
114
8. Allocation dynamique 8.6. Fiche de référence
115
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és. 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 :
obj a ;
i n t i =a . f ( ) ; / / a p p e l à l a méthode f ( ) de a
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
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.
118
9. Premiers objets 9.2. Exemple simple
i n t j =a . g ( 2 ) ;
...
s t r u c t obj1 {
int x ; / / champ x
int f ( ) ; / / méthode f ( ) ( d é c l a r a t i o n )
int g( int y ) ; / / méthode g ( ) ( d é c l a r a t i o n )
};
s t r u c t obj2 {
double x ; / / champ x
double f ( ) ; / / méthode f ( ) ( d é c l a r a t i o n )
};
...
i n t obj1 : : f ( ) { / / méthode f ( ) de o b j 1 ( d é f i n i t i o n )
...
return . . .
}
i n t obj1 : : g ( i n t y ) { / / méthode g ( ) de o b j 1 ( d é f i n i t i o n )
...
return . . .
}
double o b j 2 : : f ( ) { / / méthode f ( ) de o b j 2 ( d é f i n i t i o n )
...
return . . .
}
...
i n t main ( ) {
obj1 a ;
obj2 b ;
a . x =3; / / l e champ x d e a e s t i n t
b . x = 3 . 5 ; / / c e l u i de b e s t d o u b l e
i n t i =a . f ( ) ; / / m é t h o d e f ( ) d e a ( donc o b j 1 : : f ( ) )
i n t j =a . g ( 2 ) ; / / m é t h o d e g ( ) d e a ( donc o b j 1 : : g ( ) )
double y=b . f ( ) ; / / m é t h o d e f ( ) d e b ( donc o b j 2 : : f ( ) )
...
119
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 à certains moment en
C++ et que les programmeurs venant de Java mettent partout en se trompant d’ailleurs sur
son type. Vous n’en n’aurez en général pas 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)
/ / ==================================================
/ / f o n c t i o n s sur l e s m a t r i c e s
120
9. Premiers objets 9.4. Exemple des matrices
/ / 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 ;
}
}
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);
}
121
9.5. Cas des opérateurs 9. Premiers objets
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;
}
En clair, le programme :
s t r u c t objA {
...
122
9. Premiers objets 9.5. Cas des opérateurs
};
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 ) ;
};
...
/ / 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 ) ;
123
9.6. Interface 9. Premiers objets
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...
9.6 Interface
Si on regarde bien le main() de notre exemple de matrice, on s’aperçoit qu’il n’utilise
plus les champs des Matrice mais seulement leurs méthodes. En fait, seule la partie
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 ) ;
124
9. Premiers objets 9.7. Protection
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
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é des les utiliser !
Rien ne l’empêche en effet des 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
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.
125
9.7. Protection 9. Premiers objets
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
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!
126
9. Premiers objets 9.7. Protection
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 :
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] !
6. sans compter qu’ils les déclarent souvent comme en C avec d’inutiles typedef. Mais bon, ceci ne
devrait pas vous concerner !
127
9.8. TP 9. Premiers objets
9.8 TP
Vous devriez maintenant pouvoir faire le TP en A.7 qui dessine quelques courbes
fractales (figure 9.1) en illustrant le concept d’objet..
128
9. Premiers objets 9.9. Fiche de référence
130
9. Premiers objets 9.9. Fiche de référence
131
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éé a .
a. Ce qui explique qu’il n’est pas besoin de lui préciser un type de retour.
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!
1. En réalité, il y a une autre raison, plus profonde et trop difficile à expliquer ici, qui fait qu’en
général, dès qu’on programme des objets, cette façon d’initialiser devient impossible.
134
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 ( ) ;
135
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 :
point ( i n t X, i n t Y ) ;
136
10. Constructeurs et Destructeurs 10.3. Cas général
};
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 ( )
137
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 !
138
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).
139
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 :
140
10. Constructeurs et Destructeurs 10.6. Références Constantes
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?
}
...
i n t a =1;
f (a );
La fonction f () ne modifie pas son paramètre y et tout va bien. Imaginons une deuxième
version de g() :
void g ( i n t& x ) {
x ++;
}
Alors y serait modifiée dans f () à cause de l’appel à g(). Le programme ne se compi-
lerait évidemment pas... En réalité, la première version de g() serait refusée elle aussi
car
pour savoir si une sous-fonction modifie ou non un des paramètres d’une
fonction, le compilateur ne se base que sur la déclaration de cette sous-
fonction et non sur sa définition complète a .
a. Le C++ n’essaie pas de deviner lui-même si une fonction modifie ses paramètres
puisque la logique est que le programmeur indique lui-même avec const ce qu’il veut faire,
et que le compilateur vérifie que le programme est bien cohérent.
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 :
c l a s s obj {
...
void g ( ) c o n s t ;
141
10.7. Destructeur 10. Constructeurs et Destructeurs
...
};
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é :
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.)
142
10. Constructeurs et Destructeurs 10.7. Destructeur
# i n c l u d e <iostream >
using namespace s t d ;
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 :
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...
143
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 " ;
}
144
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 :
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 )
145
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;
}
146
10. Constructeurs et Destructeurs 10.11. Objets avec allocation dynamique
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;
}
vect : : ~ vect ( ) {
147
10.11. Objets avec allocation dynamique 10. Constructeurs et Destructeurs
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. C’est le cas en mode Debug sous Visual, ce qui aide à repérer les bugs !
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.
148
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
49 vect : : ~ vect ( ) {
149
10.11. Objets avec allocation dynamique 10. Constructeurs et Destructeurs
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 }
150
10. Constructeurs et Destructeurs 10.12. Fiche de référence
151
10.12. Fiche de référence 10. Constructeurs et Destructeurs
152
10. Constructeurs et Destructeurs 10.12. Fiche de référence
153
10.12. Fiche de référence 10. Constructeurs et Destructeurs
154
11. Chaînes de caractères, fichiers
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).
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.
156
11. Chaînes de caractères, fichiers 11.2. Fichiers
Vous trouverez d’autres fonctions dans l’aide en ligne de votre environnement de déve-
loppement, ou tout simplement proposées par celui-ci quand vous utiliserez les string.
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 ( ) ;
2. Il est bon de vérifier que l’ouverture s’est bien passée. Une erreur fréquente est
de préciser un mauvais nom de fichier : le fichier n’est alors pas ouvert.
i f s t r e a m g ( " . . / data/hop . t x t " ) ;
i f ( ! g . is_open ( ) ) {
cout << " help ! " << endl ;
return 1;
}
(Attention, toujours utiliser le slash /, portable, et non le backslash \ même sous
Windows). On peut aussi avoir besoin de savoir si on est arrivé au bout du fi-
chier :
do {
...
} while ( ! ( g . e o f ( ) ) ;
3. Une fonction (en fait macro du préprocesseur) utile de Imagine++ (dans Com-
mon) est srcPath, qui remplace un chemin relatif en chemin absolu en faisant
précéder le chemin par l’emplacement du dossier contenant le fichier source.
Ainsi, le fichier sera trouvé quel que soit le dossier courant dans lequel est lancé
le programme. Ainsi si notre dossier source est /home/pascal/Test/,
157
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 " ;
stringstream f ;
158
11. Chaînes de caractères, fichiers 11.3. Valeurs par défaut
int i ;
/ / Chaîne vers entier
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 . clear ( ) ; / / 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 " )
159
11.3. Valeurs par défaut 11. Chaînes de caractères, fichiers
void f ( i n t a , i n t b =0 , i n t c =0) {
// ...
}
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.
160
11. Chaînes de caractères, fichiers 11.4. Accesseurs
void f ( i n t a , i n t b =3 , i n t c ) { / / NON! L e s d e r n i e r s p a r a m è t r e s
/ / Pas c e u x du m i l i e u !
}
11.4 Accesseurs
Voici, en cinq étapes, les points utiles à connaître pour faire des accesseurs pratiques
et efficaces.
161
11.4. Accesseurs 11. Chaînes de caractères, fichiers
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 ] ;
}
...
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 {
162
11. Chaînes de caractères, fichiers 11.4. Accesseurs
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 ;
163
11.4. Accesseurs 11. Chaînes de caractères, fichiers
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
— Une fonction inline est recompilée à chaque ligne qui l’appelle, ce qui ralentit la
compilation et augmente la taille du programme !
— inline est donc réservé aux fonctions courtes pour lesquelles l’appel est péna-
lisant par rapport au corps de la fonction !
— Si la fonction était déclarée dans un .h et définie dans un .cpp, il faut maintenant
la mettre entièrement dans le .h car l’utilisateur de la fonction a besoin de la
définition pour remplacer l’appel de la fonction par son corps !
— Pour pouvoir exécuter les fonctions pas à pas sous debuggeur, les fonctions inline
sont compilées comme des fonctions normales en mode Debug. Seul le mode Re-
lease profitera donc de l’accélération.
Dans le cas d’une méthode, il faut bien penser à la mettre dans le ficher .h si la
classe était définie en plusieurs fichiers. C’est le moment de révéler ce que nous gar-
dions caché :
4. Les programmeurs C pourraient aussi être tentés de programmer des "macros" (ie. des raccourcis
avec des #define, ce que nous n’avons pas appris à faire). Celles-ci sont moins puissantes que les
inline car elles ne vérifient pas les types, ne permettent pas d’accéder aux champs privés, etc. Le
programmeur C++ les utilisera avec parcimonie !
164
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++...
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 ) {
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 {
r e t u r n x [ i +M∗ j ] ;
}
};
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 :
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 ] ;
}
};
165
11.6. Types énumérés 11. Chaînes de caractères, fichiers
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...
166
11. Chaînes de caractères, fichiers 11.7. Fiche de référence
167
11.7. Fiche de référence 11. Chaînes de caractères, fichiers
168
11. Chaînes de caractères, fichiers 11.7. Fiche de référence
169
11.7. Fiche de référence 11. Chaînes de caractères, fichiers
170
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 ;
tmp=a ;
12.1. template 12. Fonctions et classes paramétrées (templates)
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 :
/ / Maximum d e deux v a r i a b l e s ( a c o n d i t i o n que o p e r a t o r > ( ) e x i s t e
/ / pour l e t y p e T)
t e m p l a t e <typename T>
T maxi ( T a , T b ) {
r e t u r n ( a>b ) ? a : b ;
}
La déclaration typename T précise le type générique. On peut en préciser plusieurs :
/ / 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 ;
...
172
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 ) ;
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 ) ;
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é !
173
12.1. template 12. Fonctions et classes paramétrées (templates)
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 ; }
};
...
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 ;
...
174
12. Fonctions et classes paramétrées (templates) 12.1. template
175
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 cer-
tain nombre de fonctions et de classes utilisant les template. Cet ensemble est commu-
nément désigné sous le nom de STL (Standard Template Library). Vous en trouverez
la documentation complète sous Visual ou à défaut sur Internet. Nous exposons ci-
dessous quelques exemples qui devraient pouvoir servir de point de départ et faciliter
la compréhension de la documentation.
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 =[]
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 :
2. Nous n’avons pas vu comment définir de nouveaux types cachés dans des classes ! C’est ce qui est
fait ici...
176
12. Fonctions et classes paramétrées (templates) 12.1. template
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 ) ;
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.
177
12.2. Opérateurs binaires 12. Fonctions et classes paramétrées (templates)
178
12. Fonctions et classes paramétrées (templates) 12.3. Valeur conditionnelle
179
12.5. Variables statiques 12. Fonctions et classes paramétrées (templates)
180
12. Fonctions et classes paramétrées (templates) 12.6. const et tableaux
first=false ;
srand ( ( unsigned i n t ) time ( 0 ) ) ;
}
r e t u r n double ( rand ( ) ) /RAND_MAX;
}
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++.
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 ] ) {
...
}
181
12.7. Fiche de référence 12. Fonctions et classes paramétrées (templates)
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 [ ]
...
182
12. Fonctions et classes paramétrées (templates) 12.7. Fiche de référence
183
12.7. Fiche de référence 12. Fonctions et classes paramétrées (templates)
184
12. Fonctions et classes paramétrées (templates) 12.7. Fiche de référence
185
12.7. Fiche de référence 12. Fonctions et classes paramétrées (templates)
186
12. Fonctions et classes paramétrées (templates) 12.7. Fiche de référence
187
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 :
(a) Dans la fenêtre "Solution explorer" de Visual Studio, rechercher et afficher le
fichier Tp1.cpp.
(b) "Build/Build solution", ou "F7" ou bouton correspondant.
(c) Vérifier l’existence d’un fichier Tp1 (Tp1.exe sous Windows) dans Build/Tp1
(Build/Tp1/Debug sous Windows).
5. Exécution :
(a) Sous Kdevelop, il faut commencer par aller dans le menu "Configure launches",
ajouter avec le bouton "+" et sélectionner l’exécutable. On peut alors le lancer
avec le bouton "Execute".
(b) Lancer le programme (sous Visual) avec "Debug/Start Without Debugging"
(ou "Ctrl+F5" ou bouton correspondant). Il faut d’abord lui préciser quel
programme lancer (clic droit sur le projet Tp1 dans l’explorateur de solu-
tion, "Sélectionner comme projet de démarrage") Une fenêtre à fonds noir
"console" s’ouvre, dans laquelle le programme s’exécute, et la fenêtre console
reste ouverte jusqu’à l’appui d’une touche du clavier.
(c) Vérifier qu’on a en fait créé un programme indépendant qu’on peut lancer
dans une fenêtre de commande :
— Essayer de le lancer depuis le gestionnaire de fichiers standard : le pro-
gramme se referme tout de suite !
— Dans les menus Windows : "Démarrer/Exécuter"
— "Ouvrir: cmd"
— Taper "D:", 1 puis
"cd \Documents and Settings\login\Bureau\Tp1\Tp1\Debug"
(apprenez à profiter de la complétion automatique avec la touche TAB).
— Vérifier la présence de Tp1.exe avec la commande "dir".
— Taper "Tp1".
6. Fichiers :
On a déjà suivi la création des fichiers principaux au fur et à mesure. Constater la
présence de Tp1.obj qui est la compilation de Tp1.cpp (que le linker a ensuite
utilisé pour créer Tp1.exe). Voir aussi la présence de nombreux fichiers de travail.
Quelle est la taille du répertoire de Build de Tp1 (clic droit + propriétés) ?
7. Nettoyage :
Supprimer les fichiers de travail et les résultats de la génération avec "Build /
Clean solution" puis fermer Visual Studio. Quelle est la nouvelle taille du
répertoire ?
8. Compression :
Sous Windows, en cliquant à droite sur le répertoire Tp1, fabriquer une archive
comprimée Tp1.zip (ou Tp1.7z suivant la machine). Attention il faut quitter
Visual Studio avant de comprimer. Il peut sinon y avoir une erreur ou certains
fichiers trop importants peuvent subsister.
1. Sur certaines machines, il faut en fait aller sur C :, vérifiez en regardant où est installé votre projet
190
A. Travaux Pratiques A.1. L’environnement de programmation
9. Envoi :
Envoyer le fichier par mail à son responsable de PC en indiquant bien le nom de
son binôme et en mettant [ENPCInfo1A] dans le sujet.
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 recompilera 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ésitez 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 comprendre un peu comment tous ces outils
se coordonnent, on peut se reporter à l’annexe C du polycopié.
(a) Tester une nouvelle génération/exécution. Vérifier que Visual Studio sauve
le fichier automatiquement avant de générer.
(b) Modifier à nouveau le programme. Tester directement une exécution. Visual
Studio demande automatiquement une génération !
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 :
191
A.1. L’environnement de programmation A. Travaux Pratiques
3. Erreur de linker
Il est un peu tôt pour réussir à mettre le linker en erreur. Il est pourtant indis-
pensable de savoir différencier ses messages de ceux du compilateur. En général,
le linker indiquera une erreur s’il ne trouve pas une fonction ou des variables
parce qu’il manque un fichier objet ou une bibliothèque. C’est aussi une erreur
s’il trouve deux fois la même fonction...
(a) Rajouter une ligne f(2); avant le return et faire Ctrl+F7. C’est pour l’ins-
tant une erreur de compilation.
(b) Corriger l’erreur de compilation en rajoutant une ligne (pour l’instant "ma-
gique")
void f ( int i ); avant la ligne avec main. Compiler sans linker : il n’y a plus
d’erreur. 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.
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.
5. Warnings du compilateur
En modifiant le main(), provoquer les warnings suivants : 2
(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) int i , j ;
j =i ;
Exécuter (répondre "abandonner" !)
(d) Provoquer le warning inverse : variable déclarée mais non utilisée.
(e) 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); . Il y a maintenant un autre warning. Pourquoi ? (La fonc-
tion exit () quitte le programme en urgence !)
2. Certains de ces warnings ne se manifestent qu’en niveau d’exigence le plus élevé : pour le mettre
en place, clic droit sur le projet dans la fenêtre de gauche, menu “Propriétés”, onglet C++, sélectionner
le “warning level” 4 (3, moins exigent, est le défaut).
192
A. Travaux Pratiques A.1. L’environnement de programmation
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.
-DCMAKE_BUILD_TYPE=Debug
193
A.1. L’environnement de programmation A. Travaux Pratiques
9. Enfin, pour voir à quoi ressemble du code machine, exécuter jusqu’à un point
d’arrêt puis faire
Debug/Windows/Disassembly. On peut aussi dans ce même menu voir les
registres du micro-processeur. Il arrive qu’on se retrouve dans la fenêtre "code
machine" sans l’avoir demandé quand on debugge un programme pour lequel
on n’a plus le fichier source. Cet affichage est en fait très utile pour vérifier ce que
fait le compilateur et voir s’il optimise bien.
10. Pour faire l’étape précédente sous QtCreator, on peut sélectionner l’option “Ope-
rate by Instruction” du menu Build . Les registres sont visibles en allant dans le
menu Window puis Views. Voir Figure A.1.
F5 = = Debug
Touches utiles : F10 = = Step over
F11 = = Step inside
194
A. Travaux Pratiques A.2. Variables, boucles, conditions, fonctions
195
A.2. Variables, boucles, conditions, fonctions A. Travaux Pratiques
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 }
196
A. Travaux Pratiques A.2. Variables, boucles, conditions, fonctions
197
A.2. Variables, boucles, conditions, fonctions A. Travaux Pratiques
sur DessineBalle. Placer les appels de ces fonctions aux endroits appropriés dans
la boucle principale.
2. Gestion du clavier :
La gestion du clavier est réalisée pour vous par la fonction Clavier dont nous
ignorerons le contenu pour l’instant. Cette fonction nous permet de savoir direc-
tement si une des touches qui nous intéressent (q et s pour le déplacement de la
première raquette, k et l pour la deuxième) sont enfoncées ou non. Cette fonc-
tion, Clavier(int& sens1, int& sens2), retourne dans sens1 et sens2, les valeurs 0,
-1 ou 1 (0 : pas de déplacement, -1 : vers la gauche, 1 : vers la droite).
3. Déplacement des raquettes :
Coder le déplacement d’une raquette dans une fonction
void BougeRaquette(int &x, int sens)
puis appeler cette fonction dans la boucle principale pour chacune des deux ra-
quettes. Evidemment, faire en sorte que les raquettes ne puissent sortir de la fe-
nêtre.
4. Rebonds sur les raquettes :
S’inspirer de la gestion des rebonds de la balle. Ici il faut non seulement vérifier si
la balle va atteindre le bas ou le haut de l’écran mais aussi si elle est assez proche
en abscisse de la raquette correspondante.
5. Comptage et affichage du score :
Modifier la fonction BougeBalle afin de comptabiliser le score des deux joueurs
et l’afficher dans la console.
6. Pour ceux qui ont fini :
Lors d’un rebond sur la raquette, modifier l’inclinaison de la trajectoire de la balle
en fonction de la vitesse de la raquette ou de l’endroit de frappe.
Vous devriez avoir obtenu un programme ressemblant à celui de la figure A.2.
198
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.
199
A.3. Tableaux A. Travaux Pratiques
Pour que la séquence de nombres générée ne soit pas la même d’une fois sur
l’autre, il est nécessaire d’initialiser le générateur avec une graine variable. La
manière la plus simple de procéder consiste à utiliser l’heure courante. La fonc-
tion time() déclarée dans le fichier ctime permet de l’obtenir.
En fin de compte, la fonction suivante nous permet donc de générer une combi-
naison :
# include <cstdlib >
# i n c l u d e <ctime >
using namespace s t d ;
200
A. Travaux Pratiques A.3. Tableaux
201
A.3. Tableaux A. Travaux Pratiques
Les fonctions graphiques sont déjà définies. Elles fonctionnent selon un principe
de division de la fenêtre graphique en lignes. La fonction :
void a f f i c h e C o m b i n a i s o n ( i n t combi [ nbcases ] , i n t n ) ;
permet d’afficher la combinaison combi sur la ligne n. Au début du programme,
on laisse en haut de la fenêtre graphique autant de lignes libres que le joueur a
d’essais pour y afficher le déroulement du jeu. On affiche en bas de la fenêtre
graphique un mini mode d’emploi qui résume les correspondances entre touches
et couleurs.
2. Mastermind graphique :
Réinsérer dans ce projet les fonctions de génération aléatoire d’une combinaison
et de comparaison de deux comparaisons écrites précédemment. Puis reprogram-
mer la boucle principale du jeu en utilisant l’affichage graphique.
3. Ultime amélioration :
On souhaite pouvoir effacer une couleur après l’avoir tapée, au cas où l’on se
serait trompé. Etudier les fonctions
int Clavier ( ) ;
et
void getCombinaison ( i n t [ ] , i n t ) ;
La première prend déjà en compte la touche Retour arrière, mais pas la seconde
qui la considère comme une erreur de frappe. Modifier cette dernière en consé-
quence.
202
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.zip sur la page habituelle, le décompresser et lancer
votre environnement de développement. Parcourir le projet, en s’attardant sur
les variables globales et la fonction main (inutile de regarder le contenu des fonc-
tions déjà définies mais non utilisées). Le programme fait évoluer un point (x, y)
selon un mouvement 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
d’accélérer 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
203
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.5 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++.
13. À vous de jouer !
Transformer le projet Duel, à l’aide des fonctions qui y sont déjà présentes, en un
jeu de tir, à deux joueurs. Chacun des deux joueurs a une position fixée, et divers
soleils sont placés aléatoirement dans l’écran. Chaque joueur, à tour de rôle, peut
204
A. Travaux Pratiques A.4. Structures
lancer une Balle avec la vitesse initiale de son choix, la balle subissant les effets
de gravitation des divers soleils, et disparaissant au bout de 250 pas de temps
d’affichage. Le gagnant est le premier qui réussit à atteindre l’autre... Conseils
pratiques : positionner symétriquement les joueurs par rapport au centre, de pré-
férence à mi-hauteur en laissant une marge d’un huitième de la largeur sur le
côté ; utiliser la fonction GetMouse pour connaître la position de la souris ; en dé-
duire la vitesse désirée par le joueur en retranchant à ces coordonnées celles du
centre de la boule à lancer, et en multipliant par un facteur 0.00025.
14. Améliorations :
Faire en sorte qu’il y ait systématiquement un gros soleil au centre de l’écran (de
masse non nécessairement conséquente) afin d’empêcher les tirs directs.
15. Initialisation correcte :
Modifier la fonction de placement des soleils de façon à ce que les soleils ne s’in-
tersectent pas initialement, et qu’ils soient à une distance minimale de 100 pixels
des emplacements des joueurs.
Chocs élastiques
(figure A.5 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 :
void InitRandom ( ) ;
est à exécuter une fois avant le premier appel à Random.
double Random ( double a , double b ) ;
205
A.4. Structures A. Travaux Pratiques
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 :
→
−
1. la quantité de mouvement P = m → −
Av +m →
A
−
v B B
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.
206
A. Travaux Pratiques A.4. Structures
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) = − →
− 2
∆ v (t)
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.
207
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
double norme2 ( Vector a ) ; / / Norme e u c l i d i e n n e
208
A. Travaux Pratiques A.5. Fichiers séparés
209
A.5. Fichiers séparés A. Travaux Pratiques
Jeu de tir
Chocs élastiques
210
A. Travaux Pratiques A.5. Fichiers séparés
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.
211
A.6. Images A. Travaux Pratiques
A.6 Images
F IGURE A.7 – 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.7).
A.6.1 Allocation
1. Récupérer le projet :
Télécharger le fichier Tp7_Initial.zip sur la page habituelle, le décompresser
et lancer Visual C++.
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 et re-
garder la taille du process grandir. (Utiliser Ctrl+Shift+Echap pour accéder
au gestionnaire de tâches). Compiler en mode Release pour utiliser la "vraie" ges-
tion du tas (Le mode Debug utilise une gestion spécifique qui aide à trouver les
bugs et se comporte différemment...)
212
A. Travaux Pratiques A.6. Images
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) ;
9. Fichiers :
Créer un image.cpp et un image.h en conséquence...
213
A.6. Images A. Travaux Pratiques
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 − 1, j))/2 et la dérivée verticale dy , puis la norme du gradient
|∇I| = d2x + d2y 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.
214
A. Travaux Pratiques A.7. Premiers objets et dessins de fractales
215
A.7. Premiers objets et dessins de fractales A. Travaux Pratiques
trait. Les trois sous-triangles seront dessinés avec un trait plus fin. Ne pas oublier
la condition d’arrêt de la récursion !
Utiliser cette fonction dans le main en lui fournissant un triangle initial d’épais-
seur 6.
4. Classe vecteur :
Transformer la structure Vector en une classe. Y incorporer toutes les fonctions
et les opérateurs. Passer en public le strict nécessaire. Faire les modifications né-
cessaires dans main.cpp.
5. Accesseurs pour les membres :
Rajouter des accesseurs en lecture et en écriture pour les membres, et les utiliser
systématiquement dans le programme principal. L’idée est de cacher aux utilisa-
teurs de la classe Vector les détails de son implémentation.
6. Dessin récursif d’un arbre :
Nous allons maintenant dessiner un arbre. Pour cela il faut partir d’un tronc et
remplacer la deuxième moitié de chaque branche par deux branches de même
longueur formant un angle de 20 degrés avec la branche mère. La figure ci-
dessous illustre le résultat obtenu pour différentes profondeurs de récursion.
Écrire une fonction récursive pour dessiner une telle courbe. Vous aurez besoin
7. Deuxième implémentation :
Modifier l’implémentation de la classe Vector en remplaçant les membres double x,y;
par un tableau double coord[2];. Quelles sont les modifications à apporter dans
main.cpp ?
8. Vecteurs de dimension supérieure :
L’avantage de cette dernière implémentation est qu’elle se généralise aisément à
des vecteurs de dimension supérieure. Placer une constante globale DIM égale à
2 au début de Vector.h et rendre la classe Vector indépendante de la dimen-
sion.
NB : la fonction Rotate et les accesseurs que nous avons écrits ne se généra-
lisent pas directement aux dimensions supérieures. Les laisser tels quels pour
l’instant...
216
A. Travaux Pratiques A.7. Premiers objets et dessins de fractales
217
A.8. Tron A. Travaux Pratiques
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 vous donne une idée du résultat 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éliminaire
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.
218
A. Travaux Pratiques A.8. Tron
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.
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.
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.
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 :
219
A.8. Tron A. Travaux Pratiques
/ / 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 avec la méthode :
void putNativeBitmap ( i n t x , i n t y , NativeBitmap nb )
220
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.
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 :
B.2. Graphics B. Imagine++
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.
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 ) :
222
B. Imagine++ B.3. Images
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;
}
B.4 LinAlg
Le module LinAlg propose l’algèbre linéaire avec des classes matrice et vecteur.
223
B.5. Installation B. Imagine++
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)
/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
/usr/share/Imagine++/lib
224
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.
226
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.
227
C.3. Make et CMake C. Compilateur, CMake, Linker, Qt... Au secours !
6. pour créer une bibliothèque (dynamique par défaut) à la place, il suffit de remplacer par
add_library
228
C. Compilateur, CMake, Linker, Qt... Au secours ! C.3. Make et CMake
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.
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.
7. directement avec un éditeur de texte, ou alors avec les outils ccmake ou cmake-gui qui offrent
une interface graphique
229
C.4. Utilisation d’un IDE C. Compilateur, CMake, Linker, Qt... Au secours !
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é.
230
D. Fiche de référence finale
D. Fiche de référence finale
Annexe D
232
D. Fiche de référence finale
233
D. Fiche de référence finale
234
D. Fiche de référence finale
235
D. Fiche de référence finale
236