Note Cpp
Note Cpp
Note Cpp
Licensed under the Creative Commons Attribution-NonCommercial 3.0 Unported License (the
“License”). You may not use this file except in compliance with the License. You may obtain a
copy of the License at http://creativecommons.org/licenses/by-nc/3.0. Unless required
by applicable law or agreed to in writing, software distributed under the License is distributed on an
“AS IS ” BASIS , WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and limitations under the License.
1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.1 Il était une fois... 8
1.2 Utilisations actuelles 9
1.2.1 Popularité et utilisation des langages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.2.2 Relation avec les performances des processeurs . . . . . . . . . . . . . . . . . . . . . . . 10
1.3 Le C et le C++ 12
1.3.1 Histoires compilées . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.3.2 Utilisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2 Développement en C/C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.1 Rappels sur la compilation 17
2.1.1 Pré-processeur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.1.2 Compilateur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.1.3 Éditeur de liens . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.2 Exemple simple 18
3 Environnement de développement . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
3.1 Environnement QtCreator 21
3.2 Installation 22
3.2.1 Téléchargement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
3.2.2 Installation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
3.3 Création d’un projet 24
3.4 Compilation 26
3.5 Exécutions des programmes 27
3.6 Interactions utilisateur lors de l’exécution en mode console 28
3.6.1 Execution dans un terminal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
3.6.2 Passage de paramètres lors de l’exécution . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
3.6.3 Terminal interne ou externe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
3.7 Débogage 29
3.8 Paramétrage du compilateur 30
II Partie B: Le langage
4 Introduction à la programmation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
7 Flux et Fichiers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
7.1 Fichiers 67
7.2 Mode texte 67
7.3 Mode binaire 68
7.4 Et en C? 69
Annexes
Bibliographie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
1. Introduction
Apprendre, connaitre et savoir utiliser le C et le C++, oui mais pourquoi? Matlab, GNU Octave, R,
Scilab sont très adaptés pour mes calculs numériques et toutes les fonctions ’compliquées’ sont
connues ou existent! Puis pour le web, Php et javascript suffisent, non? Et si par hasard je veux faire
une application Android, et ben : java! Pour le reste Python est très souple et permet de quasiment
tout faire: du calcul numérique (scipy, numpy) à l’intelligence artificielle (Tensorflow/Keras,
PyTorch), en passant par des interfaces graphiques, l’accès aux bases de données, des pages pour
les serveurs web, et du traitement d’image et à la radio numérique1 (gnuradio et ses scripts python)!
Il n’y a que si j’ai besoin de développer un driver pour linux, ou d’écrire un programme bas niveau
pour un système embarqué que j’ai besoin du C?
C’est pas faux: il faut utiliser les langages les plus adaptés à son problème. Ceci sous entend de
bien évaluer le problème et l’environnement du problème. Mais il y a au moins deux pièges.
D’abord, quand le concept "qui peut le plus peut le moins" n’est pas adapté. Effectivement, ne
jurer que par Matlab (par exemple!), c’est comme croire qu’un automate s’impose toujours pour
piloter un système... n’importe quel système. Même pour faire clignoter une LED lorsqu’un bouton
est enfoncé. C’est aussi se restreindre à penser que les AOP permettent de tout amplifier, rendant
inutiles les transistors; que seules les machines synchrones sont pertinentes pour la mise en rotation,
etc.
Non, il faut bien sur considérer les ressources disponibles, de coûts, etc. Ainsi les contextes de
chaque problème imposent que la solution doit être réfléchie et adaptée.
En informatique, c’est évidemment pareil. Il y a des cas ou écrire un programme en assembleur est
trop laborieux, non portable et non réalisable en temps raisonnable (c’est d’ailleurs l’origine du
C), et des cas ou utiliser des langages très haut niveau interprétés n’est pas en adéquation avec le
matériel ou la consommation de ressources (qu’elles soient financières, énergétiques, calculatoires,
mémoires, encombrements... et souvent: toutes à la fois!). Dans certains de ces cas le C/C++ est une
alternative à considérer, surtout en terme de formation car ces deux langages, exigeants, permettent
de couvrir la totalité des enjeux précédents et offrent aussi une bonne base de connaissances pour
passer à d’autres langages (qui souvent utilisent C/C++).
1 Plus exactement SDR : Software Defined Radio.
8 Chapter 1. Introduction
Le second piège est la méconnaissance du système sur lequel on execute son programme.
• Quelles sont les limites des ressources de ce système? Que va t’il se passer si elles sont
dépassées?
• Pourquoi d’ailleurs mon programme plante de temps en temps, ou rend le système très lent?
• Quels sont les impacts des défaillances de l’exécution de mon programme? Existe il une
solution minimisant ces défaillances ou garantissant leur "bonne prise en compte"?
On peut tenter le parallèle avec un manageur qui budgétiserait, planifierait et exécuterait ses projets
sans connaitre son équipe opérationnelle. Parfois il faudra aller voir qui exécute le programme
et vérifier que celui-ci est bien adapté (sans nécessairement être l’optimal). Or, dans le cas des
langages et plus largement de la programmation informatique, cette vérification n’est pas forcément
aisée et pourra déboucher sur une réponse "on ne peut pas faire mieux avec ce langage" en cas de
connaissance trop superficiel du système.
Il faudra aussi se pencher sur l’algorithme lui-même: l’efficacité d’un algorithme (coût temporel et
mémoire) et son exactitude (donne t’il toujours le bon résultat?) sont des concepts à maitriser et
capitaux pour optimiser l’utilisation des ressources (voir le livre [Cor+09]). Il faudra aussi analyser
comment le programme est exécuté sur tel ou tel processeur et comment il interagit avec les autres
programmes.
Cette introduction continue avec un petit historique des langages, avant de faire un tour des
utilisations actuelles des langages puis de se focaliser sur le C et le C++.
Un langage interprété a une exécution interprétée (souvent ligne à ligne à partir du code source)
par un interpréteur qui est chargé de transformer dynamiquement le code en langage machine (au
travers d’une machine virtuelle). Les langages suivants sont généralement interprétés : BASIC,
JavaScript, Maple, PHP, Python, ...
Figure 1.2: Évolutions de la popularité des 20 langages les plus utilisés. Ces 20 langages correspon-
dent à 70% de la popularité totale. Source TIOBE, Septembre 2021.
le salaire proposé.
L’analyse de ces indicateurs montrent que python, java, javascript, C et C++ sont les langages
de programmation les plus utilisés et les plus présents. Les progressions de Kotlin et Rust sont à
considérer pour les développements Android et sécurisés, respectivement.
Figure 1.3: Popularité des langages par la recherche de tuto sur Google. Source PYPL, Avril 2024.
Figure 1.4: Corrélation des discussions Stack Overflow et projets GitHub. Source RedMonk, Avril
2024.
Figure 1.5: Compétences cherchées (à gauche) et salaire (à droite) sur des profils de postes dans 14
pays (sans la France). Source Trendy Skills, Juillet 2015.
beaucoup progressé et ne semblent pas être la cause du manque de performances des processus
calculatoires. Plusieurs auteurs concluent qu’en fait, la performance vient du logiciel [HP11]. Ils
pointent du doigt directement l’utilisation non optimale que les programmeurs font des ressources en
ne maitrisant que trop faiblement l’impact de leur lignes de code sur les performances des systèmes
et aussi les stratégies de programmation utilisées (dont la négligence de l’algorithmie). Tout ceci
est lié: de nombreux langages poussent et mettent en avant leur simplicité d’utilisation pour obtenir
très rapidement et sans trop de connaissances ni d’efforts des développements intéressants: ils
complaisent très souvent les développeurs dans une superficialité de maitrise et ne poussent pas
à comprendre plus en profondeur les systèmes. Ainsi, ils peuvent rapidement faire prendre de
12 Chapter 1. Introduction
mauvaises habitudes de programmation aux développeurs peu expérimentés et peu avertis qui
deviennent alors complètement dépendant du langage-outils.
Souvent, il y a des enjeux commerciaux ou financiers et de compétitivité à courts termes : développer
vite avec le moins de ressource possible et le moins qualifiée possible.
L’enquête "Embedded Market Study" présentée en 2013 au Design West montre que le C et C++
sont les deux langages privilégiés pour les systèmes embarqués, et qu’à priori cela ne changerait
pas tout de suite comme le montre les figures 1.6. Ces deux langages sont proches du matériel
et permettent une utilisation très juste des ressources. Encore faut-il les maitriser (langages et
ressources). Effectivement, ces deux langages demandes des efforts de compréhension et de rigueur
pour développer des applications fiables et sures.
Figure 1.6: Langages pour les systèmes embarqués qui sont (à gauche) et seront (à droite) utilisés par
les développeurs pour leurs prochains projets en embarqué. Source 2013 EMBEDDED MARKET
STUDY Design WEST.
1.3 Le C et le C++
1.3.1 Histoires compilées
Denis Ritchie travaillant aux laboratoires Bell a créé le langage C pendant l’année 1972. Le langage
C s’est très vite imposé pour principalement trois raisons.
• Ce langage a été créé pour le re-développement du système Unix qui était devenu une
référence de système d’exploitation multi-tâches et multi-utilisateurs mais impossible à
maintenir car initialement écrit en assembleur.
• La diffusion du compilateur C au milieu universitaire eut la même stratégie que pour la
diffusion d’Unix: une licence à coût très modeste (∼300$) à acquérir par les universités et
valable pour tous leurs étudiants.
• Sa puissance. C’est le premier langage de haut niveau qui grâce aux pointeurs permet d’être
au plus proche du processeur et d’interagir avec tous les éléments du systèmes.
Une extension Orientée Objet au langage C a été conçue par Bjarne Stroustrup (au Bell Labs
d’AT&T) dans les années 1980. Il s’agit du C++. Cette extension s’inspire d’autres langages4 dont
4 C, Ada 83 (dont le nom est un hommage au premier programmeur informatique (Ada Lovelace), ce langage
se voulait proche du matériel pour pouvoir être embarqué sur des systèmes temps-réels), Algol 68 (l’utilisateur peut
définir de nouveaux types de donnée, de nouveaux opérateurs ou surcharger des existants), CLU (pour CLUster, dont
l’élément novateur essentiel était cluster et qui correspondait au concept de classe, mais où intentionnellement de
nombreux paradigmes objets avaient été omis), ML (Meta Language, qui est un langage fonctionnel impur permettant
des adaptations et détermination de type)
1.3 Le C et le C++ 13
Simula67 (version sortie en 1967 du langage Simula) qui est le premier langage à classes c’est à
dire le premier langage orienté objet.
Le C comme le C++ sont normalisés par l’ISO et continuent d’évoluer. La prochaine révision du
langage C++ est celle de 2017.
1.3.2 Utilisation
Chaque langage a ses spécificités et des prédispositions à exprimer certaines solutions. En clair,
pour un type d’application donné, tous les langages ne se valent pas... et les développeurs l’ont
bien compris5 . Ainsi, Matlab n’est pas vraiment adapté à l’écriture d’un site web marchand ou à
l’écriture d’un driver pour une carte graphique.
Il existe cependant des langages dont l’usage s’est révélé général, voir à tout faire, ou presque.
L’un des plus général actuellement est le Java dont les domaines d’utilisation sont: l’écriture
d’applications, le développement sur plateformes embarquées, Web, l’écriture d’applications coté
serveur et coté client, la programmation générale,...
Le C est aussi utilisé pour la programmation générale, la programmation système, les opérations
de bas niveau et l’écriture d’applications. Ce dernier point est de moins en moins représenté. Le
C++ est lui plus utilisé pour la programmation d’applications et la programmation des systèmes.
De nombreuses bibliothèques de calcul sont écrites en C++ (Eigen, ITK, VTK, PCL, Boost, GMP,
OpenGL, ImageMagick, ...) de même pour les IHM (Qt, wxWidgets, GTKmm...).
Notons qu’il ne s’agit pas ici des aptitudes des langages mais bien de leurs utilisations les plus
courantes.
5 Ils sont même à l’origine de l’écriture d’un nouveau langage ou des évolutions d’un langage afin de répondre à une
nouvelle demande.
I
Partie A: How to "Développer"
2 Développement en C/C++ . . . . . . . . . . . 17
2.1 Rappels sur la compilation
2.2 Exemple simple
3 Environnement de développement . . . 21
3.1 Environnement QtCreator
3.2 Installation
3.3 Création d’un projet
3.4 Compilation
3.5 Exécutions des programmes
3.6 Interactions utilisateur lors de l’exécution en mode
console
3.7 Débogage
3.8 Paramétrage du compilateur
2. Développement en C/C++
De façon simplifiée, trois étapes sont réalisées lors de la création d’un exécutable.
18 Chapter 2. Développement en C/C++
2.1.1 Pré-processeur
Le pré-processeur ou pré-compilateur est un utilitaire qui traite le fichier source avant le compilateur.
C’est un manipulateur de chaînes de caractères. Il retire les commentaires, prend en compte les
lignes du texte source ayant un # en première colonne, etc, afin de créer le texte que le compilateur
analysera. Ses possibilités sont de trois ordres:
• inclusion de fichiers (mot clé #include). Lors de cette étape, le pré-compilateur remplace
les lignes #include par le fichier indiqué;
• définition d’alias et de macro-expression (mots clés #define, macro);
• sélection de parties de texte (mot clé inline).
A la fin de cette première étape, le fichier .cpp (ou .c) est complet et contient tous les prototypes
des fonctions utilisées.
2.1.2 Compilateur
L’étape de compilation consiste à transformer les fichiers sources en code binaire compréhensible
par un processeur. Le compilateur compile chaque fichier .cpp (ou .c) un à un. Les fichiers d’entête
.h (ou header) ne sont pas compilés. Le compilateur génère un fichier objet .o (ou .obj, selon
le compilateur) par fichier .cpp. Ce sont des fichiers binaires temporaires. Après la création du
binaire final (l’exécutable), ces fichiers pourront être supprimés ou gardés selon les besoins de
re-compilation. Enfin, il est à noter que ces fichiers peuvent contenir des références insatisfaites à
des fonctions. Elles devront être résolues par l’éditeur de liens.
Figure 2.2: Création d’un code C++ sous vim (à gauche) et les commandes linux permettant
l’édition du fichier main.cpp, la compilation avec g++ puis l’exécution du programme (à droite).
3. Environnement de développement
Dans ce qui suit, nous décrivons les étapes clés afin d’être rapidement autonome sur cet IDE.
Notons que la démarche et les termes présentés ci-après sont les mêmes pour les autres IDE.
3.2 Installation
L’environnement de développement QtCreator est compatible Linux, Mac et Windows.
Cet environnement peut s’installer sans la librairie Qt. Cependant, ceci n’est pas recommandé pour
un utilisateur débutant. Une telle installation nécessiterait de disposer :
• une chaine de compilation (compilateur, éditeur de lien, bibliothèques, ...),
• un débogueur (optionnel),
• un générateur de Makefile: CMake.
Il est donc conseillé de télécharger et d’installer Qt qui permettra de disposer de tous les
éléments nécessaires.
3.2.1 Téléchargement
La librairie Qt a subi de nombreux rebondissements de licence. Elle est maintenant disponible sous
forme commerciale et communautaire (LGPL v2/v3 2 ).
Le téléchargement de la version communautaire de Qt et de QtCreator se fait à partir du site
www.qt.io/download-qt-installer-oss. Il est possible de télécharger les sources de Qt puis de les
compiler soi-même. Cet exercice est très intéressant, mais peut se révéler particulièrement complexe
à réaliser.
L’utilisation d’un version compilée (ou binaires) est donc plus rapide à mettre en oeuvre sur
son système. De plus, les versions compilées (ou binaires) de Qt intègrent QtCreator et parfois
1 Prononcer ’quioute’... de l’anglais cute, ou "Q.T." à la française ...
2 Licence publique générale limitée GNU
3.2 Installation 23
3.2.2 Installation
Linux
Pour le système d’exploitation Linux, l’installation par le gestionnaire de paquets de votre dis-
tribution est plus simple que le téléchargement du binaire proposé sur le site web. Solution à
privilégier sauf si vous avez besoin de la toute dernière version et que vous ne pouvez pas attendre
la disponibilité du paquetage pour votre distribution. Les différents éléments de Qt sont à installer
individuellement par le gestionnaire de paquets de votre distribution.
Mac OS X
Pour Mac, l’installation du paquet qt-opensource-mac-x64-clang-X.X.X.dmg (les X représentent la
version) sur le site devrait suffire. Il faudra disposer de clang. Le plus simple est d’installer XCode
et de le lancer une fois, afin d’accepter les licences, avant d’installer qt-opensource-mac-x64-clang-
X.X.X.dmg.
Windows
Pour Windows, les versions actuelles de Qt intègrent la totalité des outils permettant le développe-
ment d’application Qt: la librairie elle même, QtCreator, une chaine de compilation complète
(mingw) ainsi que le débogueur. Lors de l’installation, on peut choisir les éléments à installer. Pour
limiter la taille sur le disque dur à moins de 4Go, choisir "Qt 6.X for desktop development" ou X
représente la dernière version, ou celle demandée pour votre projet de développement. Un exemple
est donné sur la figure 3.2.
Figure 3.2: Elements pour réduire l’empreinte de l’installation de Qt. Le numéro de version peut
varier!
24 Chapter 3. IDE
Figure 3.3: Fenêtre de dialogue pour la sélection du type de projet sous QtCreator
La deuxième étape consiste à donner un emplacement et un nom à votre projet (figure 3.4).
La fenêtre qui suit doit être similaire à celle de la la figure 3.5. Elle présente les ’kit’ disponibles et
utilisés pour le projet en cours de création. Dans l’exemple de la figure, tout est opérationnel et on
peut laisser la sélection effectuée.
Cependant, il est possible qu’aucun kit n’apparaisse. Dans ce cas, votre environnent de développe-
ment n’est pas correctement configuré ou installé et il faut se reporter au paragraphe 3.8.
Ensuite vient la sélection du système gestionnaire de projet. La figure 3.6 illustre le choix de qmake
qui est souvent recommandé 3 .
Figure 3.6: Fenêtre de création de projet, choix du système de gestion de projet. Ici qmake.
L’étape suivante est le résumé de la création du projet et le choix d’un gestionnaire de version 4 . Si
l’option ”Terminer” est disponible vous pouvez la choisir (figure 3.7).
3 L’alternative CMake est très intéressante, dans les nouvelles version de Qt, qmake génère un CMakelist.txt
automatiquement
4 Un système de gestion de version permet d’archiver les modifications successives faites à vos fichiers par un ou
plusieurs utilisateurs. Un des outils indispensables pour le travail collaboratif! Les plus connus sont CVS, SVN , GIT,
26 Chapter 3. IDE
Figure 3.7: Fenêtre de résumé de création de projet et choix d’un outils de gestion de version
(aucun).
3.4 Compilation
Il existe différentes échelles de compilation par rapport à votre projet:
• compilation d’un fichier. Ceci permet de corriger les erreurs de syntaxe éventuelles. Cette
étape n’existe pas dans QtCreator. Les erreurs de syntaxe sont soulignées en rouge pendant
l’écriture du code (en vert : les warnings). Cette étape est néanmoins présente dans de
nombreux autres IDE, en général accessible de la façon suivante: à partir du menu Build →
Compile;
• compilation de l’ensemble du projet. Un projet est généralement constitué d’un ensemble de
fichiers (.h, .cpp). Cette compilation permet de tester la syntaxe de l’ensemble des fichiers
ainsi que leurs interactions. De plus, avant la compilation, tous les fichiers sont enregistrés et
les modifications récentes sont prises en compte par le compilateur. Cette étape s’effectue de
la façon suivante: à partir du menu faire: Build → Build Project xxx, ou via le raccourci
ctrl+B;
• compilation de l’ensemble des projets. Il est possible qu’une application soit basée sur
plusieurs projets. La compilation de l’ensemble des projets se fera par Build→Build all,
raccourci ctrl+Maj+B, ou via l’icône en bas à gauche de l’application (marteau) (figure 3.8);
Figure 3.8: Boutons de exécution rapide. De haut en bas : choix du projet en cours, exécution en
mode release, exécution en mode debug et tout compiler
Lors de la compilation, les erreurs de syntaxe détectées sont signalées dans l’onglet Building Errors
Bazaar et Mercurial.
3.5 Exécutions des programmes 27
(figure 3.9). Il est important de noter que l’erreur est systématiquement décrite et qu’un double clic
sur cette description envoie le curseur à la ligne correspondant à l’erreur détectée dans le fichier
concerné. Ceci facilite grandement la résolution d’erreur de syntaxe dans les fichiers contenant le
code. La figure 3.9 donne un exemple de résultat de compilation avec un message d’erreur.
Figure 3.9: Exemple d’erreurs de compilation. remarquer le souligné rouge. Ici il ne manque qu’un
; à la fin de la ligne 7 comme indiqué par le message d’erreur.
Une fois l’ensemble du projet compilé (aucune erreur de compilation, ni de liage), il est possible
d’exécuter le programme.
R Pendant l’édition, une analyse de syntaxe est faite. Cela est très pratique pour corriger les
petites erreurs avant la compilation. Cependant, il est possible que des erreurs "fantômes"
apparaissent (symbolisées par un cercle rouge dans la marge, notamment sur les cin, cout,
etc). Elles viennent du fait que l’analyseur de syntaxe à la volée n’est pas C++ mais le Clang.
Pour désactiver le Clang : aller dans Aide → "A propos des plug-ins..." et désactiver dans
C++ le ClangCodeModel comme montré sur la figure 3.10. Il faudra redémarrer QtCreator
pour que la modification soit prise en compte.
R Pour vos projets en mode console, il est recommandé de toujours cocher la case "Exécuter
dans un terminal".
Cependant, quand un terminal existe sur le système, il peut être plus réaliste d’exécuter le pro-
gramme comme il le sera après les développements : sans l’environnement QtCreator. Pour utiliser
un terminal système au lieu de celui "interne" proposé par QtCreator, il faut se rendre dans les
préférences de QtCreator Édition → Préférences puis, dans "Terminal", décocher la case "Utiliser
le terminal interne" (voir la figure 3.12). Ainsi, le terminal par défaut du système d’exploitation
sera utilisé pour les exécutions de programmes.
Figure 3.12: Choix du terminal, ici interne, pour les exécutions de programmes dans les préférences
de QtCreator.
3.7 Débogage
Les deux principaux modes de compilation sont release et debug. Sous QtCreator, seuls ces deux
modes sont disponibles. Pour chaque projet, et à cahque compilation, il est possible de choisir
le mode de compilation. Contrairement au mode release, le mode debug permet d’effectuer le
débogage d’un programme. Le débogage d’un projet permet d’interagir avec le programme pendant
son exécution. Il est ainsi possible de surveiller les valeurs des variables, contrôler les passages
dans les tests, l’exécution de boucles, l’allocation des objets, modifier les valeurs de variables ...
Le débogage d’un programme s’effectue de la façon suivante:
• positionner dans le code un ou plusieurs points d’arrêt breakpoint. Cette étape s’effectue
en cliquant (bouton gauche) juste avant le numéro de la ligne où l’on souhaite insérer un
breakpoint, ou en pressant F9 sur la ligne voulue;
• exécuter le programme en mode debug: à partir du menu faire Debug → Start Debugging
30 Chapter 3. IDE
R Si vous avez des problèmes de création de projet ou de compilateur non trouvé, et ce, dès
votre première utilisation de QtCreator, vérifier :
• que vous disposez bien d’un compilateur et que celui-ci est reconnu et vu dans QtCre-
ator (vous pouvez spécifier le chemin des compilateurs C++ et C au besoin). Très
fréquemment, une absence de compilateur vient que vous installé QtCreator et non Qt
comme recommandé dans le paragraphe 3.2 et suivants.
• qu’une version de Qt soit présente. Si aucune n’est disponible, aller dans l’onglet
Version de Qt pour en trouver une. De même, s’il n’y en a pas, ce problème vient
souvent du fait que vous avez juste installé QtCreator et non Qt avec les éléments
recommandés.
• que l’architecture de votre PC corresponde à celle du compilateur. Ceci peut arriver
quand on utilise différents compilateurs et plateforme. Il faut dans ce cas bien vérifier
ceux utilisés et au besoin ajouter s’il en manque.
Sans ceci, vous ne pourrez pas compiler, voire ne même pas pouvoir créer de projet.
Il peut être recommandé d’ajouter le chemin vers votre chaine de compilation dans la variable
d’environnement PATH si elle n’est pas présente (souvent nécessaire sous windows avec cmake).
3.8 Paramétrage du compilateur 31
Figure 3.14: Fenêtre de configuration des outils de compilation sous QtCreator. Il s’agit ici d’un
environnement Windows. Pour le développement C++, il faudra veiller à avoir un environnement
avec Qt et un compilateur (ici MingGW). Ces éléments sont normalement installés et reconnus
automatiquement par QtCreator.
Sans suppression de votre part de fichiers, une fois un environnement correctement configuré et
fonctionnel est pérenne.
II
Partie B: Le langage
4 Introduction à la programmation . . . . 35
7 Flux et Fichiers . . . . . . . . . . . . . . . . . . . . . . 67
7.1 Fichiers
7.2 Mode texte
7.3 Mode binaire
7.4 Et en C?
4. Introduction à la programmation
Un grand nombre de langages de haut-niveaux comme le C++ sont des langages impératifs : ils
décrivent une suite d’instructions qui doivent être exécutées consécutivement par le processeur.
C’est un paradigme auquel nous sommes très familier: une recette de cuisine, un entrainement
sportif, une partition de musique, un trajet en voiture, ... s’apparentent à une suite de ’choses à
faire’ avant de passer à la suivante et dans le but d’atteindre un objectif.
Pour revenir à la programmation, les instructions exécutées interagissent et modifient le contenu
mémoire, les registres, les entrées/sorties, etc du système. On dit que l’état du système, à un instant
donné, est défini par le contenu de la mémoire, des registres et autres entrées/sorties à cet instant et
ainsi que le comportement de chaque instruction va dépendre de l’état du système au moment de
l’exécution de l’instruction. Cette exécution va elle même faire évoluer l’état du système.
La base impérative dispose de 4 types d’instructions:
• la séquence d’instructions ou bloc d’instructions §5.1.1 qui décrit successivement une
instruction, puis une autre, et ainsi de suite :
SortirPoelle; PrendreBeurre; FaireChaufferBeurreDansPoelle;
• les instructions d’assignation ou d’affectation §5.5 qui permettent de manipuler des
contenus mémoires et d’enregistrer un résultat en vue d’une utilisation ultérieure. Par
exemple la séquence d’instructions a ← 2; x ← 2 + a; permet d’affecter 2 dans la variable
nommée a, et ensuite de mettre 4 dans x. Les opérateurs arithmétiques et ceux du langage (et
leurs combinaisons) peuvent être utilisés pour l’affectation.
• les instructions conditionnelles §5.7 qui permettent d’exécuter un bloc d’instructions que si
une condition préalable est remplie:
si PoelleChaude alors MettreOeufsDansPoelle;
• les instructions de boucle §5.8 qui vont permettre de répéter un bloc d’instructions un
nombre déterminé de fois ou tant qu’une condition est vraie:
tant que OeufsPasCuits alors LaisserCuireUnPeuPlus;
Un langage de haut niveau va permettre au programmeur de disposer: de plusieurs instructions de
chaque type, de très nombreux opérateurs permettant de décrire des expressions très complexes, des
outils pour développer de grosses applications et pour communiquer avec d’autres outils existants,
36 Chapter 4. Introduction Programmation
...
Le mémento qui suit essaie de présenter tout ce que le C++ offre au programmeur. Il restera au
programmeur le soin de faire sa propre recette.
5. Mémento de la syntaxe C++
5.1.1 Instructions
R Toutes les instructions en C++ se terminent par ;. Le symbole ; ne changera jamais de rôle
ou de sens.
Les instructions sont des suites de symboles (+, -, =, ( )) ou de lettres pouvant avoir un sens (for,
MaFonction(), double, ...), aussi appelés opérateurs, permettant de manipuler des nombres ou des
sin(πx)
variables. Voici un exemple pour le calcul de s = sincπ (x) =
πx
1 x = 1.1234 ;
2 pi = 3.141592 ;
3 s = sin( x * pi) / ( x * pi );
De manière générale, le saut de ligne, les espaces ainsi que la tabulation, peuvent être ajoutés
n’importe où, sauf pour:
• l’écriture d’un chiffre (100000 et non 100 000);
• le nom des fonctions, opérateurs à deux symboles (== par exemple) et variables;
• les mots clés.
Un bloc d’instructions permet de grouper plusieurs instructions. La définition d’un bloc d’instructions
(ou scope) se fait avec des accolades:
38 Chapter 5. Syntaxe C++
1 {
2 instruction1;
3 instruction2;
4 ...;
5 }
La durée de vie (création et destruction) ainsi que la portée (possibilité d’utilisation / d’accès ...)
de tout ce qui est défini à l’intérieur d’un bloc d’instructions est limitée à ce bloc. Ces blocs sont
particulièrement utiles pour définir les instructions des fonctions ou des structures de boucles ou de
tests.
R Les espaces et tabulations, on parle d’indentation du code, ne permettent pas de créer un bloc
d’instructions. Ils sont cependant bienvenus pour faciliter la lecture.
5.1.2 Commentaires
Au même titre que l’indentation, il est important de commenter son code, c’est à dire d’ajouter des
informations non compilées dans le code source à destination des programmeurs ou utilisateurs
afin de mieux faire comprendre et se souvenir de ce qui a été programmé.
Deux styles de commentaire sont disponibles en C++:
• // permet de mettre en commentaire tout ce qui est après jusqu’au prochain saut de ligne;
• /* ... */ permettent de délimiter plusieurs lignes de commentaires (un bloc de commen-
taires).
Voici un exemple d’utilisation de commentaires en C++:
1 int b; // variable utilisee pour compter des notes
2 nb = 10; /* par defaut on connait
3 le nombre de notes qui est :
4 10... et toutes ces lignes sont en commentaires */
5.1.3 Casse
Le compilateur prend en compte la casse (majuscule ou minuscule) pour
• les noms des fichiers,
• les noms des variables et des fonctions,
• les mots clés du langage C++ et les directives du préprocesseur.
5.1.4 Symboles
La diversité des instructions nécessaires en informatique et le faible nombre de symboles communs
font que les symboles sont utilisés pour plusieurs instructions différentes. Ainsi, le rôle et le sens
des symboles changent en fonction du contexte de leur utilisation! Cependant, il ne doit toujours
exister qu’une unique manière de comprendre l’instruction et l’utilisation du symbole.
Les accents, cédilles et autres subtilités ne sont pas acceptées par le compilateur. Ils sont donc
interdits dans les noms de fonctions, variables, classes, ... On peut cependant les utiliser dans les
commentaires et les chaines de caractères. Cependant, attention à leur affichage à l’écran: il pourra
être erroné si le caractère n’est pas pris en charge par le périphérique d’affichage.
Le C, C++ et d’autres langages utilisent abondamment les accolades, crochets, dièse. Noter que
les caractères { et } peuvent être remplacés par les digraph <% et %>. Les caractères [ et ] par
respectivement <: et :>. Puis le caractère # par le digraph %:.
5.2 Mots clés et mots réservés 39
Enfin, le symbole \ ne peut être utilisé que dans les chaines de caractères et les commentaires. Il
a un rôle particulier de caractère d’échappement, notamment pour représenter des caractères non
affichables (entrée, tabulation, ...). Ainsi, sous Windows, il est recommandé de ne pas l’utiliser
pour donner le chemin d’accès à un fichier via une chaine de caractères (préférer /).
5.2.2 Directives
Les directives du préprocesseur ne sont pas des instructions du langage. Elles permettent des
adaptations du code ou de la compréhension du code par le compilateur. Ces directives commencent
par le symbole #. Le tableau 5.2 liste les directives du préprocesseur.
Il est évident que les mots clés et les directives du préprocesseur ne peuvent pas être utilisés par le
programmeur pour nommer ses propres variables, ses objets et ses fonctions1 .
Voici un exemple d’utilisation de mots clés et de directives du préprocesseur:
1 Il pourra cependant en surcharger quelques uns...
40 Chapter 5. Syntaxe C++
#define
#elif #else #endif #error
#if #ifdef #ifndef #include
#line
#pragma
#undef
La taille des variables de type short, int et long étant sujette à l’architecture du processeur, de
nouveaux types plus précis sont proposés dans la bibliothèque cstdint (ou stdint.h pour le C)
afin de garantir la taille des entiers. Ceci est très précieux pour la programmation des systèmes
embarqués. La table 5.4 donne ces types spécifiques.
ou entre des variables de types différents (dans ce cas, attention aux pertes lors de la conversion):
1 float a=3.1415;
2 char b;
3 double c;
4 b = a; // b vaut 3, un warning est genere a la compilation
5 c = a; // c vaut 3.1415, pas de warning ici car la precisison
6 // du type double est superieure a la precision du
7 // type float (pas de perte de precision)
5.5 Opérateurs
Nous abordons dans cette partie les définitions et les différentes propriétés des opérateurs du C++.
Comme dit précédemment, un symbole (par exemple *) peut avoir différentes significations en
fonction de son contexte d’utilisation.
Voici une utilisation des opérateurs logiques permettant de reproduire la première loi De Morgan
A + B ⇔ A.B:
1 !(A || B) est equivalent !A && !B
A noter que tous les opérateurs de tests et le opérateurs logiques du tableau 5.5.3 retournent une
valeur booléenne: 0 pour false et 1 pour true. Ainsi A == 0 retourne vrai si A est égale à zéro.
5.7.1 if / else
La syntaxe d’une condition if est la suivante:
1 if ( condition ) // "condition" est une variable booleenne (true ou false)
2 {
3 ... // instructions effectuees si test est vrai
4 }
La structure if peut être complétée d’un else permettant de traiter les cas ou la condition sera
fausse:
1 if ( condition ) // "condition" est une variable booleenne (true ou false)
2 {
3 ... // instructions effectuees si test est vrai
4 }
5 else // bloc else: n'est pas obligatoire
6 {
7 ... // instructions effectuees si test est faux
8 }
La variable ou valeur condition est forcément de type booléen (bool). Après l’instruction if,
les accolades sont nécessaires si plusieurs instructions sont à exécuter. Le bloc else n’est pas
obligatoire. Il est à noter que la syntaxe if(a) est équivalent à if( a!=0 ).
1 bool a,b;
2 if( a==true ) // il s'agit bien de 2 signes "=="
3 {
4 a = false;
5 // std::cout permet d'afficher a l'ecran un message
6 std::cout << "Initialisation de la variable a false";
7 }
Les structures des tests peuvent s’imbriquer au tant que nécessaire. Bien penser aux accolades
quand elles sont nécessaires (ou bien dans le doute!) et à l’indentation pour avoir un code lisible.
Voici un exemple d’utilisation de structures if imbriquées en C++:
1 double moyenne;
2 cin >> moyenne; // demande a l'utilisateur sa moyenne
3 if ( moyenne >= 16.0 )
4 std::cout << "Mention tres bien";
5 else
5.7 Structures conditionnelles 47
R Les crochets signalent des blocs optionels, ne pas écrire les crochets en C++!
1 switch ( variable )
2 {
3 case constante_1: // faire attention au ":"
4 instructions executees si variable == constante_1
5 [ break ; ] // le break n'est pas obligatoire... mais surement
necessaire !
6 case constante_2:
7 instructions executees si variable == constante_2
8 [ break ; ]
9 ...
10 [ default: // l'etiquette defaut et ses instructions sont optionnelles
11 instructions executees si aucun des
48 Chapter 5. Syntaxe C++
Les blocs d’instructions n’ont pas besoin d’accolades. Généralement chaque bloc se termine par
l’instruction break; qui saute à la fin de la structure switch (l’accolade fermante). Si un bloc case
ne se termine pas par une instruction break;, à l’exécution le programme passera au bloc case
suivant. Voici un exemple d’utilisation de structure switch case en C++:
1 char choix;
2 // cout permet d'afficher a l'ecran un message
3 cout << "Etes vous d'accord ? O/N";
4 // cin permet de recuperer une valeur saisie au clavier
5 cin >> choix;
6 switch( choix )
7 {
8 case 'o':
9 case 'O':
10 cout << "Vous etes d'accord";
11 break;
12 case 'n':
13 case 'N':
14 cout << "Vous n'etes pas d'accord";
15 break;
16 default:
17 cout << "Repondre par o/n ou O/N";
18 }
Dans cet exemple, l’utilisateur va rentrer ’O’ ou ’N’, mais s’il tape ’o’ les instructions du cas ’O’
(ligne 10 et 11) seront exécutées (respectivement pour ’n’ et les lignes 14 et 15).
5.8.1 for
La boucle for est à privilégier quand on connait la valeur initiale, la valeur finale et l’incrément à
utiliser (notamment pour les tableaux). La syntaxe d’une boucle for en C++ est la suivante:
1 for ( initialisations; condition; post-instructions )
2 {
3 // Instructions de boucle r\'ep\'et\'ees tant que la condition est vraie
4 }
Il n’y a pas de ; à la fin de la ligne du for. On rappelle que les crochets ne sont pas à écrire et
permettent dans ce document de désigner les blocs optionnels. On remarque que tous les blocs de la
boucle for sont optionnels! Voici le détail de l’exécution et la signification de chacun de ces blocs.
L’exécution d’une boucle for se déroule en 3 étapes.
1. Le bloc [initialisations] est toujours exécuté et une seule et unique fois,
2. Le bloc [condition] est évalué à chaque itération,
3. Si la condition est vraie, les instructions de boucle puis le bloc [post-instructions] sont
exécutés puis l’étape 2 est répétée. Si la condition est fausse, ni les instructions de boucle ni
le bloc [post-instructions] ne sont exécutés et la boucle se termine (on sort de la boucle et les
instructions qui suivent sont exécutées).
5.8 Structures de boucles 49
5.8.2 while
La syntaxe d’une boucle while est la suivante:
1 while ( condition )
2 {
3 // Instructions de boucle repetees tant que la condition est vraie
4 }
Il n’y a pas de ; à la fin de la ligne du while.
Il est à noter que le test du bloc [condition] se fait en début de boucle. Voici deux exemples
d’utilisation d’une boucle while en C++:
1 int i=0, Taille=3, j;
2 // Resultat identique a la boucle \verb$for$ ci-dessus
3 while ( i < Taille )
4 {
5 cout << i << endl; // Afficher i a l'ecran
6 i++;
7 }
8 //--------------------------------
9 // Multi conditions
10 while ( ( i< Taille ) && ( j != 0 ) )
11 {
12 cout << "Ok" << endl;
13 j = 2 * i - i * ( Taille - i );
14 i++;
15 }
5.8.3 do / while
Contrairement aux boucles for et while, la boucle do while effectue les instructions de boucle
avant d’évaluer l’expression d’arrêt (le bloc [condition]). La syntaxe d’une boucle do while est la
suivante:
50 Chapter 5. Syntaxe C++
1 do
2 {
3 // Instructions de boucle repetees tant que la condition est vrai
4 }
5 while ( condition ); // le ; apres le while est obligatoire!
Le type de variable pointée peut être aussi bien un type primaire (tel que int, char...), qu’un type
élaboré (tel qu’une struct ou une class). La taille des pointeurs, quel que soit le type pointé, est
toujours la même. Elle est de 4 octets avec un OS3 32 bits (et 8 en 64 bits).
3 Système d’Exploitation
5.9 Pointeurs et Références 51
Un pointeur est typé. Cependant, il est toutefois possible de définir un pointeur sur void, c’est-à-dire
sur quelque chose qui n’a pas de type prédéfini (void * toto). Ce genre de pointeur sert générale-
ment de pointeur de transition, dans une fonction générique, avant un transtypage qui permettra
d’accéder aux données pointées. Le polymorphisme (cf. cours) proposé en programmation orientée
objet est souvent une alternative au pointeur void *.
Pour initialiser un pointeur, il faut utiliser l’opérateur d’affectation = suivi de l’opérateur d’adresse
& auquel est accolé un nom de variable (voir la figure 5.9.1):
1 double *pt;
2 double y=3.14;
3 pt = &y; // initialisation du pointeur!
Après (et seulement après) avoir déclaré et initialisé un pointeur, il est possible d’accéder au contenu
de l’adresse mémoire pointée par le pointeur grâce à l’opérateur *. La syntaxe est la suivante:
1 double *pt;
2 double y=3.14;
3 cout << *pt; // Affiche le contenu pointe par pt, c'est a dire 3.14
4 *pt=4; // Affecte au contenu pointe par pt la valeur 4
5 // A la fin des ces instructions, y=4;
5.9.2 Références
Par rapport au C, le C++ introduit un nouveau concept: les références. Une référence permet de
faire référence à des variables. Le concept de référence a été introduit en C++ pour faciliter le
passage de paramètres à une fonction, on parle alors de passage par référence. La déclaration d’une
référence se fait simplement en intercalant le caractère &, entre le type de la variable et son nom:
1 type & Nom_de_la_variable = valeur;
1 int A = 2;
2 int &refA = A; //initialisation obligatoire a la declaration!
3 refA++; // maintenant A=3
4
5 // dereference impossible ==> modification de la valeur contenue:
6 int B = 5;
7 refA = B; // signifie A = 5 !!!!
8 refA++;
9 // on a : A = 6, B = 5, et refA = 6
Les références permettent d’alléger la syntaxe du langage C++ vis à vis des pointeurs. Voici une
comparaison de 2 codes équivalents, utilisant soit des pointeurs soit des références.
1
1 2 //Code utilisant une reference
2 /* Code utilisant un pointeur */ 3 int main()
3 int main() 4 {
4 { 5 int age = 21;
5 int age = 21; 6 int &refAge = age; /* l'
6 int *ptAge = &age; affectation a la
7 cout << *ptAge; declaration */
8 *ptAge = 40; 7 cout << refAge;
9 cout << *ptAge; 8 refAge = 40;
10 } 9 cout << refAge;
10 }
Bien que la syntaxe soit allégée, les références ne peuvent pas remplacer les pointeurs. En effet,
contrairement aux pointeurs, les références ne peuvent pas faire référence à un tableau d’éléments
et ne peuvent pas changer de référence pour une nouvelle variable. Ainsi, en fonction des situations,
il sera plus avantageux d’utiliser soit des pointeurs soit des références.
R L’usage le plus commun des références est le passage de paramètres aux fonctions.
Conseil : n’utiliser les références que pour le passage de paramètres aux fonctions ( 5.11.4 )
5.10 Tableaux
Un tableau est une variable composée de données de même type, stockées de manière contiguë
en mémoire (les unes à la suite des autres, 5.1). Un tableau est donc une suite de cases (espaces
mémoires) de même taille. La taille de chacune des cases est conditionnée par le type de donnée
que le tableau contient. Les éléments du tableau peuvent être :
• des données de type simple : int, char, float, long, double...;
• des pointeurs, des tableaux, des structures et des classes
En C/C++, les éléments d’un tableau sont indicés à partir de 0. La lecture ou l’écriture du ième
élément d’un tableau tableau se fait de la façon suivante:
1 A = tableau[i]; // lecture du ieme element
2 tableau[i] = A; // ecriture du ieme element
Connaitre la taille d’un tableau est primordial pour savoir jusqu’à où il est possible de lire ou écrire
en mémoire. Si un tableau contient N éléments, le dernier élément est accessible par tableau[N-1].
Le nombre d’éléments d’un tableau (sa taille) est capital car conditionne directement l’utilisation
de la mémoire.
Il existe deux familles de tableaux différencier par la manière de gérer l’utilisation (ou l’allocation)
mémoire:
5.10 Tableaux 53
1. les tableaux alloués statiquement où la taille réservée en mémoire est connue (fixée et
constante) à la compilation,
2. les tableaux alloués dynamiquement qui permettent d’allouer et de libérer l’espace au gré
du programme. Ils permettent d’optimiser l’occupation de la mémoire mais demande au
programmeur de gérer lui même les réservations et libérations de mémoires.
R Remarque: La norme ISO 1999 du C++ interdit l’usage des variable-size array ou variable-
length array mais de nombreux compilateurs la tolère (via des extensions propres au compila-
teur). Ce qui n’arrange pas les choses est que de nombreux sites web utilisent ces VLA.
dynamique. Pour faire un tableau dynamique, il faut d’abord créer un pointeur, puis allouer l’espace
mémoire souhaité par la commande new. A la fin de l’utilisation du tableau, il ne faut pas oublier de
libérer l’espace alloué par la commande delete[]. Voici un récapitulatif des instructions à utiliser
en C++ pour la gestion dynamique de tableaux:
• Déclaration d’un pointeur
1 type *variable_tableau;
se simplifie:
1 Type *variable_unique = new type;
L’intérêt de l’écriture Type *variable_unique = new type; est de pouvoir initialiser la vari-
able avec des valeurs particulières5 , par exemple:
1 float *tab = new float(1.3); // un seul objet de type float
2 // est alloue en memoire
3 // et sa valeur est initialisee a 1.3
L’allocation dynamique permet au programmeur de gérer la mémoire. Ceci est utile lorsque le
programme est gourmand en ressource mémoire (empreinte mémoire importante) ou lors de partage
de ressources entre processus par exemple.
5 newtype exécute le constructeur par défaut, newtype(arg1, arg2, ...) exécute un constructeur utilisateur, cf section
8.3.1
5.10 Tableaux 55
5.11 Fonctions
Une fonction est un sous-programme qui permet d’effectuer un ensemble d’instructions par simple
appel à cette fonction. Les fonctions permettent d’exécuter dans plusieurs parties du programme
une série d’instructions, ce qui permet de simplifier le code et d’obtenir une taille de code minimale.
Il est possible de passer des variables aux fonctions. Une fonction peut aussi retourner une valeur
(au contraire des procédures, terminologie oblige...). Une fonction peut faire appel à une autre
fonction, qui fait elle même appel à une autre, etc. Une fonction peut aussi faire appel à elle-même,
on parle alors de fonction récursive (il ne faut pas oublier de mettre une condition de sortie au
risque de ne pas pouvoir arrêter le programme).
• Déclaration de fonction:
1 type_retour NomFonction( type *Arg1, type *Arg2,... );
Définition de fonction:
1 type_retour NomFonction( type *Arg1, type *Arg2,... )
2 {
3 // instructions de la fonction
4 // les variables passees par pointeur s'utilisent
5 // comme les autres variables
6 return variable_de_type_retour;
7 }
Voici un exemple d’utilisation:
1
2 void CopieTab(double *origine,
3 double *copie,
1
4 int taille)
2 // suite du programme
5 {
3 int main()
6 for (int i=0; i<taille, i++)
4 {
7 copie[i] = origine[i];
5 int dim=20;
8 }
6 double tab1[20];
9
7 double tab2[20];
10 void ModifTailleTab(double **tab,
8 double *p = NULL;
11 int taille)
9 // Initialisation
12 {
10 // de tab2
13 // suppression du tableau
11 CopieTab(tab1,tab2,dim);
14 if ( *(tab) != NULL )
12 // Allocation
15 delete[] *(tab);
13 // dynamique de p
16 *(tab) = new double[taille];
14 ModifTailleTab(&p,dim);
17 // il faut definir **tab pour
15 delete[] p;
18 // que l'allocation dynamique
16 return 0;
19 // soit prise en compte au
17 }
20 // niveau de la fonction
21 // appelante
22 }
5.11.5 Cas des paramètres avec allocation dynamique dans une fonction
Il peut être commode de créer des fonctions qui vont allouer (et respectivement dé-allouer) des
espaces mémoires. Pour les fonctions d’allocation, il y a deux approches :
• soit le nouvel espace est retourné par le paramètre de retour de la fonction, cas simple.
• soit le nouvel espace est un argument qui doit être modifié, cas plus complexe. Ce cas peut
se présenter quand on doit modifier un espace déjà existant ou quand on souhaite retourner
une autre valeur par retour de fonction (par exemple un booléen qui précise si l’allocation a
pu se faire correctement).
Dans les deux cas, il faudra passer par des pointeurs. Pour les fonctions de libération de l’espace
mémoire, seul le cas par argument est possible.
Retour de fonction d’un espace mémoire dynamique
Cette approche est relativement simple et ne comporte pas de piège particulier: on retourne un
pointeur alloué dynamiquement. L’exemple suivant est donné pour une allocation d’un simple
tableau de double, mais il pourra s’agir d’allocations dynamiques plus complexes comme des
matrices ou des objets plus spécifiques.
1 double * AllouerVecteur(int n)
2 {
3 double *r = new double[n];
4 return r;
5 }
Son utilisation dans un main sera :
1 int main()
2 {
3 double *t = 0;
4 t = AllouerVecteur(100);
5 // ... utilisation de t
60 Chapter 5. Syntaxe C++
6 delete[] t;
7 return 0;
8 }
Parfois il n’est pas possible de retourner un pointeur sur l’espace mémoire. Dans ce cas il faut
considérer l’approche présentée dans le paragraphe suivant.
Allocation dynamique dans une fonction par un argument
Voici la même fonction mais avec la variable à allouer passée en argument. Il y a deux difficultés
pour cette fonction. D’une part il s’agit de modifier l’adresse stockée par le pointeur et non le
contenu pointé. Le type de cette variable est un pointeur de pointeur soit type ** s’il s’agit
d’allouer un espace pour un tableau mono-dimensionnel (type *** pour une matrice) .
D’autre part, les règles de priorité des opérateurs * et [] imposent une rigueur d’écriture et une
bonne connaissance du langage notamment si on souhaite modifier les valeurs du tableau.
1 void AllouerVecteur(double **t, int n)
2 {
3 *t = new double[n];
4 for(int i=0; i<n; i++) // exemple de mise a 0 des valeurs
5 (*t)[i] = 0; // noter les parentheses obligatoires!
6 }
L’initialisation des valeurs n’est pas forcément utile mais permet d’illustrer l’impact de la priorité
des opérateurs * et [] (voir paragraphe 5.5.2). Cependant, dans le cas des matrices, il faudra
nécessairement utiliser cette syntaxe :
1 void AllouerMatrice(int l, int c, double ***res)
2 {
3 *res = new double*[l]; // allocation des lignes
4 for(int i=0; i<l;i++)
5 (*res)[i] = new double[c]; // allocation des colonnes
6 }
L’utilisation de la fonction AllouerVecteur dans un main sera :
1 int main()
2 {
3 double *t = 0;
4 AllouerVecteur(&t, 100);
5 // ... utilisation de t
6 delete[] t;
7 return 0;
8 }
Dans l’exemple précédent, on suppose que l’argument ne pointe vers aucun espace mémoire
préalablement alloué. Dans un cadre plus général, il faudrait d’abord vérifier si cela est vrai et au
besoin libérer l’espace mémoire utilisé avant d’en allouer un autre. Sans cela, le premier espace
mémoire sera perdu si aucune autre variable pointe vers cet espace. La fonction du paragraphe
suivant pourra être appelée au début de la fonction AllouerVecteur.
Fonction de libération d’espace mémoire
La libération d’une variable allouée par une fonction nécessite le passage de la variable par
un argument. Cette fonction commence par vérifier que l’adresse pointée ne soit pas zéro qui
correspond à une zone mémoire qui ne peut pas être allouée.
1 void LiberationVecteur(double **t)
2 {
3 if( *t != 0 )
5.11 Fonctions 61
4 delete[] *t;
5 *t = 0;
6 }
Et son utilisation:
1 int main()
2 {
3 double *t = 0;
4 t = AllouerVecteur(100);
5 // ... utilisation de t
6 LiberationVecteur(&t);
7 return 0;
8 }
R Pour savoir si une surcharge est légitime ou non, il faut se mettre à la place du compilateur
et vérifier que l’appel de fonction n’est pas ambiguë c’est à dire qu’il n’y a aucun doute sur
quelle fonction utiliser. A noter que le type de retour d’une fonction n’est pas discriminant.
6. Flux et interactions utilisateurs
Pour insérer une pause (attente que l’on appuie sur la touche Entree), il est conseillé de d’abord
purger la mémoire tampon lue par cin qui contient souvent le dernier caractère de la saisie
précédente (le deuxième symbole de la touche Entree).
1 cin.sync(); // tous les caracteres de la memoire tampon non lus sont
ignores
2 cin.get(); // attente de la touche Entree
24 }
Si on souhaite demander à l’utilisateur d’entrer une chaine comportant des espaces, l’utilisation du
code suivant est nécessaire (même recommandé dans de nombreux cas pour éviter les failles de
type buffer-overflow).
1 char ch[50];
2 cin.get(ch,50);
66 Chapter 6. FluxES
Avec un object string ceci est possible grâce au code suivant (utilisation de la fonction globale
getline: ajouter #include <string> ):
1 string str;
2 getline( cin, str);
7. Flux et Fichiers
7.1 Fichiers
En C++, les flux génériques permettent d’accéder et de manipuler de nombreux contenus. Les
fichiers en font partis. Il y a cependant des limites à la généricité des flux dues aux différents
types d’accès (lecture seule, écriture seule, ...) et au mode des fichiers (binaire ou texte).
L’implémentation en C d’accès en lecture/écriture est tout aussi dépendante de ces différences.
Celle ci est donnée dans le paragraphe 7.4.
3 if( fileT.fail() )
4 {
5 cout << "Text File cannot be opened/created !" << endl;
6 return -1; // fin avec un retour d'erreur
7 }
Écriture de quelques informations:
1 fileT << "Hello_SansEspace" << " " << 3.141592654 << " " << 2.718281 <<
endl;
2 fileT << 6.022141 << " " << 1.380650 << endl;
Puis la lecture de quelques informations. D’abord un retour au début du fichier:
1 //read Something
2 fileT.clear(); // mise a 0 des flags
3 fileT.seekg(0, fstream::beg); // retour au debut du fichier
Et enfin la lecture des informations. A noter le respect des types et de l’ordre des informations!
1 string stT;
2 double piT, eT, aT, bT;
3
4 fileT >> stT >> piT >> eT >> aT >> bT; // lecture
5
6 cout << stT << endl;
7 cout << "PI : "<< piT << " E : " << eT << endl;
8 cout << "Avogadro : " << aT << " Boltzmann : " << bT << endl;
9 fileT.close(); // close file
10 return 0;
11 }
6 {
7 fstream fileB;
8 fileB.open ("test.bin", fstream::binary | fstream::in | fstream::out |
fstream::trunc );
9
10 if( fileB.fail() )
11 {
12 cout << "Binary File cannot be opened/created !" << endl;
13 return -1; // end
14 }
15
16 // write something
17 double pi = 3.1415192;
18 double e = 2.718281;
19 int c = 299999;
20 fileB.write(reinterpret_cast<char *>(&pi),sizeof(double) );
21 fileB.write(reinterpret_cast<char *>(&e),sizeof(double) );
22 fileB.write(reinterpret_cast<char *>(&c),sizeof(int) );
23
24 //read Something
25 fileB.clear();
26 fileB.seekg(0, fstream::beg);
27 double piB, eB;
28 int cB;
29
7.4 Et en C?
Il pourrait arriver que vous ayez besoin de manipuler fichiers sans disposer du C++ mais uniquement
du C... Or, en C, les flux ne sont pas disponibles. Pour accéder aux fichiers, il faut utiliser le type
FILE * et les fonctions:
• fopen pour ouvrir un fichier (mode binaire ou texte, lecture/écriture)
• fclose pour fermer un fichier ouvert
• fseek pour se déplacer dans un fichier
• fread pour lire une quantité de donnée dans le fichier (unité : l’octet ou le char)
• fwrite pour écrire dans un fichier (unité : l’octet ou le char)
Voici une fonction en C qui permet la lecture générique de tous les fichiers (mode binaire). Il
faudra changer le type (cast) de message pour obtenir le type de données voulu 1 . Pour les formats
complexes de données, il est conseillé d’utiliser des structures. Le message est un tableau de donnée.
Ce tableau est alloué dynamiquement (en C) par la fonction LireFichier.
1 le type de message de la fonction pourrait être void *
70 Chapter 7. FluxFichier
1 #include <stdio.h>
2 #include <stdlib.h>
3 int LireFichier( const char fichier[], unsigned char **message, int *taille,
int offset)
4 {
5 int nb_lu;
6
7 FILE* fich = fopen(fichier,"rb");
8 if(!fich) return 1;
9
10 // Lecture de la taille du fichier
11 fseek( fich, 0, SEEK_END); // Positionnement du pointeur a la fin du
fichier
12 *taille = ftell(fich) - offset; // Lecture de la position du pointeur (=
taille)
13 fseek( fich, offset, SEEK_SET); // rePositionnement du pointeur au debut
du fichier (offset)
14
15 *message = (char*) malloc (*taille);
16 if( *message == NULL ) return 3; // PB allocation
17
18 nb_lu = fread( *message, sizeof(char), *taille, fich); // Lecture
19 if ( nb_lu != *taille ) return 1;
20
21 fclose(fich);
22 return 0; // pas de probleme
23 }
La fonction générique pour écrire dans un fichier en mode binaire est la suivante. Si le message est
la version chaine de caractère de données (fprintf), on créera un fichier lisible en mode texte.
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 int EcrireFichier( const char fichier[], const unsigned char message[], int
taille)
5 {
6 FILE *f_dest;
7 f_dest = fopen(fichier,"wb");
8 if(!f_dest) return 1; //probleme d'acces en ecriture
9 fwrite( message, sizeof(char), taille, f_dest);
10
11 fclose(f_dest);
12 return 0; //pas de probleme
13 }
III
Partie C: Orienté Objet
Le langage C est un langage procédural, c’est-à-dire un langage permettant de définir des données
grâce à des variables, et des traitements grâce aux fonctions. L’apport principal du langage C++ par
rapport au langage C est l’intégration des concepts "objet", afin d’en faire un langage orienté objet.
Les approches orientées objets (programmation, conception, ...) sont nombreuses et s’appliquent à
de nombreux domaines autre que l’informatique (par exemple en gestion de stock, l’électronique,
l’automatisme, ...). De nombreux outils sont disponibles pour représenter et concevoir en orienté
objet. L’UML est particulièrement bien adapté à cette tâche, notamment grâce aux diagrammes de
classes et d’objets pour décrire les constituants du système puis grâce aux diagrammes d’interactions
et de machines d’états pour décrire les comportements d’un système.
Ce qui caractérise les méthodes "orientées objet" est l’utilisation d’un ou plusieurs paradigmes
objets dont les plus importants sont:
• Encapsulation: regrouper données et opérations
• Héritage : généralisation de classes
• Polymorphisme: découle de l’héritage, permet aux classes les plus générales d’utiliser les
spécifications des classes plus spécifiques (nécessite la surcharge de fonction membre)
• Généricité (extension de l’encapsulation) : Modèle d’encapsulation, quelque soit le type des
données.
Après avoir présenté rapidement l’UML, ces 4 paradigmes sont développés.
8.1 UML
L’UML (Unified Modeling Language, que l’on peut traduire par langage de modélisation unifié) est
une notation permettant de modéliser une application ou un système sous forme de concepts abstraits
(les classes) et d’interactions entre les instances de ces concepts (les objets). Cette modélisation
consiste à créer une représentation des éléments du monde réel auxquels on s’intéresse, sans se
préoccuper de leur réalisation (ou implémentation en informatique). Ainsi, pour l’informatique,
cette modélisation est indépendante du langage de programmation.
L’UML est depuis 1997 un standard industriel (Object Management Group) initié par Grady Booch,
James Rumbaugh et Ivar Jacobson. L’UML est un langage formel, ce qui se traduit par une
74 Chapter 8. POO/C++
concision des descriptions mais exige une bonne précision (rigueur) pour la représentation des des
systèmes. Il se veut aussi intuitif et graphique: la totalité des représentations d’un système se fait
par des diagrammes.
L’UML 2 dispose de 13 diagrammes différents permettant de modéliser l’ensemble d’un système
(encore une fois, pas seulement informatique!) et de décrire totalement un système et son dé-
ploiement, à tel point que l’UML 2.0 est compilable et permet de générer des exécutables pour les
applications informatiques. Ces diagrammes sont classiquement regroupés par vues (il y a 5 vues)
pour faciliter la modélisation des systèmes dont la représentation est donnée sur la figure 8.3.
Par la suite, nous nous focaliserons sur le diagramme de classe - contenu dans la Vue Logique - qui
permet de décrire le contenu et l’interface de chaque classe et représenter les liens entre les classes
d’un système.
Modéliser un système n’est pas évident d’autant plus quand il s’agit d’un système à concevoir. La
première difficulté est de déterminer les objets (et les concepts) présents (une sorte de regroupement
pertinent de données et fonctions du système) puis les interactions qui existent entre les différents
concepts. Ensuite il faut passer à la description précise de chaque concept: décrire les données et
les fonctions qu’il utilise pour fonctionner correctement jusqu’au déploiement de l’application.
Figure 8.2: Diagramme de classe pour des nombres complexes, avec les terminologies usuelles.
membres avec leurs arguments et le type de retour de ces dites fonctions. Pour résumer cela, la
figure 8.3 donne un exemple générique d’une classe avec les syntaxes pour chaque partie.
Figure 8.3: Syntaxes de chaque partie constituant la description d’une classe par son diagramme.
Un point important est la visibilité qui permet de préciser la restriction aux attributs et aux méthodes
par les autres éléments du système (les autres classes). Cette visibilité est indiquée au début de
chaque ligne de la classe par les symboles "+", "-", "#" ou "sim". La visibilité est détaillée dans le
paragraphe 8.2.2.
Point
Point
- X:double
- X:double
- Y:double
- Y:double
- this: Point
+ GetX():double
+ GetX():double
+ GetY():double
+ GetY():double
+ SetX(x:double):void
+ SetX(x:double):void
+ SetY(y:double):void
+ SetY(y:double):void
+ Add(pt: Point):Point
+ Add(pt: Point):Point
+ Afficher():void
+ Afficher():void
<<create>> + Point(pt:Point)
<<create>> + Point()
+ operator=(pt:Point):Point
<<destroy>> + Point()
Table 8.1: Diagramme de classe UML (à gauche) et diagramme complet de l’implémentation C++
(à droite). Les trois dernières méthodes du deuxième diagramme sont automatiquement créées en
implémentation C++.
La déclaration en C++ de la classe Point du diagramme UML (Table 8.1) est la suivante:
1 class Point
2 {
3 private:
4 double X;
5 double Y;
6 public:
7 Point Add(const Point &pt);
8 void Afficher();
9 double GetX();
10 void SetX(const double &x);
11 double GetY();
12 void SetY(const double &y);
13 };
8.2.2 Visibilité
L’utilisateur d’une classe n’a pas forcément besoin de pouvoir accéder directement aux données
d’un objet ou à certaines méthodes. En fait, un utilisateur n’est pas obligé de connaître tous les
détails de l’implémentation des méthodes et doit être considéré comme potentiellement maladroit:
les méthodes de la classe doivent garantir le bon fonctionnement du système. En interdisant
l’utilisateur de modifier directement les données, il est obligé d’utiliser les fonctions définies pour
les modifier. Si ces fonctions sont bien faites, il est alors possible de garantir l’intégrité des données
et un fonctionnement cohérent de la classe et ainsi du système. Différents niveaux de visibilité
permettent de restreindre l’accès à chaque donnée (ou fonction) suivant que l’on accède à cette
donnée par une méthode de la classe elle-même, par une classe fille (ou dérivée), ou bien d’une
classe ou fonction quelconque.
En UML, il existe 4 niveaux de visibilité des éléments de la classe:
• ’+’ correspondant à public
• ’#’ correspondant à protected
• ’∼’ correspondant à paquet (pas de correspondance dans les classes du C++)
• ’-’ correspondant à private.
La figure 8.4 liste les différents types de visibilité en UML et C++.
En C++, il existe 3 niveaux de visibilité des éléments de la classe:
1. public Aucune restriction. Il s’agit du plus bas niveau de restriction d’accès, toutes les
fonctions membres (de n’importe quelle classe) peuvent accéder aux champs ou aux méthodes
public,
1 Contrairement aux structures : struct
78 Chapter 8. POO/C++
2. protected : l’accès aux variables et fonctions est limité aux fonctions membres de la classe
et aux fonctions des classes filles,
3. private Restriction d’accès le plus élevé. L’accès aux données et fonctions membres est
limité aux méthodes de la classe elle-même.
Pour les définir en UML, elles doivent être précédées de leur type et de leur visibilité (public,
protected ou private) permettant de préciser quelles autres éléments du système peuvent accéder
à ces valeurs (portée des champs). Il est recommandé d’utiliser des champs private ou protected
pour les champs ayant un impact sur le bon fonctionnement des objets et du système. Ainsi l’accès
à ces données ne peut se faire qu’au moyen de fonctions membres (ou méthodes) qui s’assurent
de la bonne utilisation de ces variables. La visibilité de type public facilite la programmation de
la classe (moins de méthodes à écrire) mais doit être réservée pour des champs dont le rôle n’est
pas critique et n’affecte pas le fonctionnement de l’objet(la partie réelle et imaginaire d’une classe
complexe par exemple).
Voici un exemple de déclaration d’une classe Point avec des attributs ayant différentes visibilités:
1 class Exemple
2 {
3 double X; // par defaut dans une classe, la visibilite est private
4 double Y;
5 protected:
6 char *Txt;
7 public:
8 double Val;
9 private:
10 Exemple *A; // champs prive, pointeur sur objet de type Exemple
11 };
Il est à noter que toute classe peut être utilisée comme type d’attribut. Cependant il est impossible
de créer un objet du même type que la classe en construction (dans ce cas il faut passer par un
pointeur ou une référence) ou un objet d’un type de la hiérarchie avale (car les objets fils n’existent
pas encore).
fait partie. On utilise pour cela l’opérateur de résolution de portée, noté ::. A gauche de l’opérateur
de portée figure le nom de la classe, à sa droite le nom de la fonction. Cet opérateur sert à lever
toute ambiguïté par exemple si deux classes ont des fonctions membres portant le même nom. Voici
un exemple de déclaration d’une classe Valeur avec attribut et méthodes:
1 // fichier Valeur.cpp
1 //fichier Valeur.h
2 // Definition des methodes a l'
2 //Declaration des methodes a l'
exterieur de la classe
interieur de la classe
3 void Valeur::SetX(double a)
3 class Valeur
4 {
4 {
5 X = a;
5 private:
6 }
6 double X;
7
7 public:
8 double Valeur::GetX()
8 void SetX(double);
9 {
9 double GetX();
10 return X;
10 };
11 }
8.2.5 Objets
En C++, il existe deux façons de créer des objets, c’est-à-dire d’instancier une classe:
• de façon statique,
• de façon dynamique.
Création statique
La création statique d’objets consiste à créer un objet en lui affectant un nom, de la même façon
qu’une variable:
1 NomClasse NomObjet;
Ainsi l’objet est accessible à partir de son nom. L’accès à partir d’un objet à un attribut ou une
méthode de la classe instanciée se fait grâce à l’opérateur . :
1 // Fichier Valeur.h 1 #include "Valeur.h"
2 class Valeur 2 #include <iostream>
3 { 3 using namespace std;
4 private: 4
5 double X; 5 int main()
6 public: 6 {
7 void SetX(double a) 7 Valeur a;
8 { X = a; } 8 a.SetX(3);
9 double GetX() 9 cout << "a.X=" << a.GetX();
10 { return X; } 10 return 0;
11 }; 11 }
Création dynamique
La création d’objet dynamique se fait de la façon suivante:
• définition d’un pointeur du type de la classe à pointer,
• création de l’objet dynamique grâce au mot clé new, renvoyant l’adresse de l’objet nouvelle-
ment créé,
• affecter cette adresse au pointeur,
• après utilisation: libération de l’espace mémoire réservé: delete.
Voici la syntaxe à utiliser pour la création d’un objet dynamique en C++:
1 NomClasse *NomObjet;
2 NomObjet = new NomClasse;
8.2 Concept de classe pour l’encapsulation 81
ou directement
1 NomClasse *NomObjet = new NomClasse;
Tout objet créé dynamiquement devra impérativement être détruit à la fin de son utilisation grâce
au mot clé delete. Dans le cas contraire, une partie de la mémoire ne sera pas libérée à la fin de
l’exécution du programme. L’accès à partir d’un objet créé dynamiquement à un attribut ou une
méthode de la classe instanciée se fait grâce à l’opérateur -> (il remplace (*pointeur).Methode()):
1 // Fichier Valeur.h
2 class Valeur 1 #include "Valeur.h"
3 { 2
4 private: 3 int main()
5 double X; 4 {
6 public: 5 Valeur *a = new Valeur;
7 void SetX(double a) 6 a->SetX(3); // ou (*a).SetX(3);
8 { X = a; } 7 delete a;
9 double GetX() 8 return 0;
10 { return X; } 9 }
11 };
La définition de cette fonction membre spéciale n’est pas obligatoire, notamment si vous ne
souhaitez pas initialiser les données membres par exemple, et dans la mesure où un constructeur
par défaut (appelé parfois constructeur sans argument) est défini par le compilateur C++ si la classe
n’en possède pas. Voici un autre exemple d’utilisation d’un constructeur pour initialiser les attributs
d’une classe:
1 class Point
2 {
3 private:
4 double X;
5 double Y;
6 public:
7 Point(double a=0,double b=0); // valeurs par defaut {0,0}
8 };
9
10 Point::Point(double a, double b)
11 {
12 X = a;
13 Y = b;
14 }
Comme pour n’importe quelle fonction membre, il est possible de surcharger les constructeurs,
c’est-à-dire définir plusieurs constructeurs avec un nombre/type d’arguments différents. Ainsi, il
sera possible d’avoir des comportements d’initialisation différents en fonction de la méthode de
construction utilisée.
On distingue 3 formes de constructeurs:
• le constructeur par défaut, qui ne prend aucun paramètre: Point p1, p2();. Les champs
sont initialisés à une valeur par défaut.
8.3 Méthodes particulières 83
• les constructeurs utilisateurs, qui prennent différents arguments et permettant d’initialiser les
champs en fonction de ces paramètres: Point p3(1), p4(1,2);
• le constructeur de copie, qui permet de créer un nouvel objet à partir d’un objet existant:
Point p5(p2);. Ce type de constructeur est détaillé dans le paragraphe 8.3.2.
Il faut aussi noter qu’un seul constructeur est appelé et uniquement lors de la création en mémoire
de l’objet. Il n’y a pas d’appel de constructeur pour les pointeurs et références. Ainsi Point *
pt; n’appelle pas de constructeur (il sera appelé lors d’un pt = new Point(1,2);). De même,
Point rp=&p1; n’appelle pas de constructeur pour la création de la référence rp.
Un constructeur peut être rendu particulièrement efficace en terme de cout mémoire et de temps pour
l’initialisation des champs de la classe. En effet il est possible de passer des valeurs d’initialisation
aux champs lors de leur création en mémoire. Ceci est fait ainsi sur l’exemple précédent de la
classe Point:
1 Point::Point(double a, double b):X(a),Y(b)
2 { } // plus rien a mettre! X vaudra "a", et Y sera egale a "b"
8.3.3 Destructeurs
Le destructeur est une fonction membre qui s’exécute automatiquement lors de la destruction d’un
objet (pas besoin d’un appel explicite à cette fonction pour détruire un objet). Cette méthode
permet d’exécuter des instructions nécessaires à la destruction de l’objet. Cette méthode possède
les propriétés suivantes:
• un destructeur porte le même nom que la classe dans laquelle il est défini et est précédé d’un
tilde ~;
• un destructeur n’a pas de type de retour (même pas void);
• un destructeur n’a pas d’argument;
• la définition d’un destructeur n’est pas obligatoire lorsque celui-ci n’est pas nécessaire.
84 Chapter 8. POO/C++
Le destructeur, comme dans le cas du constructeur, est appelé différemment selon que l’objet auquel
il appartient a été créé de façon statique ou dynamique.
• le destructeur d’un objet créé de façon statique est appelé de façon implicite dès que le
programme quitte la portée dans lequel l’objet existe;
• le destructeur d’un objet créé de façon dynamique sera appelée via le mot clé delete.
Il est à noter qu’un destructeur ne peut être surchargé, ni avoir de valeur par défaut pour ses
arguments, puisqu’il n’en a tout simplement pas. Voici un exemple de destructeur:
1 class Point
2 {
3 private:
4 double X;
5 double Y;
6 double *tab; // champs dynamique !!!
7 public:
8 Point(double a=0,double b=0); // constructeur
9 ~Point(); // destructeur
10 };
11
12 Point::Point(double a, double b)
13 {
14 X = a;
15 Y = b;
16 tab = new double[2];
17 }
18
19 Point::~Point()
20 {
21 delete[] tab;
22 }
8.3.4 Opérateurs
Par convention, un opérateur op reliant deux opérandes a et b est vu comme une fonction prenant en
argument un objet de type a et un objet de type b. Ainsi, l’écriture a op b est traduite en langage
C++ par un appel op(a,b). Par convention également, un opérateur renvoie un objet du type de
ses opérandes. Cela permet pour de nombreux opérateurs de pouvoir intervenir dans une chaîne
d’opérations (par ex. a=b+c+d).
La fonction C++ correspondant à un opérateur est représentée par un nom composé du mot operator
suivi de l’opérateur (ex: + s’appelle en réalité operator+()).
Un opérateur peut être définit en langage C++ comme une fonction membre, ou comme une fonction
amie indépendante. Ici, nous nous limiterons au cas des fonctions membres. Tableau (8.2) donne
une liste de l’ensemble des opérateurs qui peuvent être surchargés en C++:
+ - * / % ^ &
| ~ ! = < > +=
-= *= /= %= ^= &= |=
<< >> >>= <<= == != <=
>= && || ++ -- ->*, ,
-> [] () new delete type()
Table 8.2: Liste des opérateurs que l’on peut surcharger en C++
1 class Valeur
2 {
3 private: 1 #include "Valeur.h"
4 double X; 2
5 public: 3 int main()
6 Valeur(double a=0) { X=a; } 4 {
7 //attention exemple PAS optimal! 5 Valeur a(2);
8 Valeur operator+(Valeur pt) 6 Valeur b(3);
9 { 7 // surcharge de l'operateur
10 Valeur result; 8 Valeur c = a + b;
11 result.X = X + pt.X; 9 return 0;
12 return result; 10 }
13 }
14 };
Dans le paragraphe précédent, nous avons présenté comment effectuer une surcharge d’opérateur
avec une transmission d’argument et de valeur de retour par copie. Dans le cas de manipulation
d’objet de grande taille, il est préférable d’utiliser une transmission d’arguments (voir de valeur
de retour) par référence (gain en temps et économie de mémoire). Le prototype de surcharge
d’opérateur avec une fonction membre, avec une transmission par référence, est le suivant (les
crochets signalent des blocs optionnels):
1 class Valeur
2 {
1 #include "Valeur.h"
3 private:
2
4 double X;
3 int main()
5 public:
4 {
6 Valeur(double a=0) { X=a; }
5 Valeur a(2);
7 Valeur operator+(
6 Valeur b(3);
8 const Valeur &pt)
7 // surcharge de l'operateur
9 {
8 Valeur c;
10 Valeur result;
9 c = a + b;
11 result.X = X + pt.X;
10 return 0;
12 return result;
11 }
13 }
14 };
Comme toutes fonctions, les opérateurs peuvent être surchargés. On peut donc définir un autre
opérateur + entre un objet Valeur et un double :
1 class Valeur
2 {
3 private:
4 double X;
5 public:
6 Valeur(double a=0) { X=a; }
7 Valeur operator+( const Valeur &pt)
8 {
9 Valeur result;
10 result.X = X + pt.X;
11 return result;
12 }
13 Valeur operator+( const double &a)
14 {
15 Valeur result;
16 result.X = X + a;
17 return result;
18 }
19 };
Ainsi on pourra écrire:
1 #include "Valeur.h"
2
3 int main()
4 {
5 Valeur a(2);
6 // surcharge de l'operateur
7 Valeur c;
8 c = a + 3.14;
9 return 0;
10 }
c = 3.14 + a; 3.14 (donc le type double) qui appelle son opérateur sur a (classe Valeur). Le
problème serait comment expliquer à "double" qu’il existe une opération "+" avec un objet de type
Valeur?
Il faut pour cela passer par l’autre forme des opérateurs : la forme dyadique, c’est à dire qui prend
en paramètres les 2 arguments. L’ordre des paramètres étant important, il y a deux fonctions:
1 Valeur operator+( const double &a, const Valeur &pt); // permettra c = 3.13
+ a; OK !
2 Valeur operator+( const Valeur &pt, const double &a); // permettra c = a
+3.14 ; PB !
3 // en redondance avec "Valeur operator+( const
double &a)"
Pour être appelée à partir de n’importe quel type d’objet, ces fonctions ne doivent pas appartenir
à une classe: il s’agira de fonctions classiques (pas de fonction membre d’une classe: pas de
Valeur:: dans l’exemple). Ainsi pour l’écriture des instructions d’opérateur dyadique il ne
sera pas possible d’accéder aux champs privés ou protégés directement. Dans notre exemple
(pas de fonctions d’accès au champ X qui est privé) il est impossible d’écrire l’opérateur dyadique
+.
Pour résoudre ce problème, afin d’accéder directement aux champs privés ou protégés de la classe,
l’utilisation du mot clé friend devra être utilisé. On déclare ainsi une fonction amie à la classe,
sous entendue que cette fonction pourra accéder aux champs des objets.
1 class Valeur
2 {
3 private:
4 double X;
5 public:
6 Valeur(double a=0) { X=a; }
7 // focntion amie, operateur dyadique : double + Valeur
8 friend Valeur operator+( const double &a, const Valeur &pt);
9
10 // operateur classique Valeur + double
11 Valeur operator+( const double &a)
12 {
13 Valeur result;
14 result.X = X + a;
15 return result;
16 }
17 };
18
1 class Valeur
2 {
3 private:
4 double X;
5 public:
6 Valeur(double a=0) { X=a; }
7 friend std::ostream & operator<<(std::ostream &os, const Valeur &pt)
8 {
9 return os << pt.X;
10 }
11
12 // ... suite de la classe
13 };
Ces deux fonctions amies permettent d’écrire:
1 #include <iostream>
2 #include "Valeur.h"
3
4 using namespace std;
5
6 int main()
7 {
8 Valeur a(2);
9 Valeur c;
10 c = 3.14 + a ; // appel de : Valeur operator+( const double &a,
const Valeur &pt)
11 cout << " c = " << c << endl; // appel de : std::ostream & operator<<( ... )
12 return 0;
13 }
Le mot clé Friend est aussi détaillé au paragraphe 9.2.
8.4 Héritage 89
8.4 Héritage
L’héritage est un principe propre à la programmation orientée objet, permettant de créer une
nouvelle classe à partir d’une classe existante. Le nom d’héritage (pouvant parfois être appelé
dérivation de classe) provient du fait que la classe dérivée (la classe nouvellement créée, ou classe
fille) contient les attributs et les méthodes de sa classe mère (la classe dont elle dérive). L’intérêt
majeur de l’héritage est de pouvoir définir de nouveaux attributs et de nouvelles méthodes pour la
classe dérivée, qui viennent s’ajouter à ceux et celles hérités.
Par ce moyen, une hiérarchie de classes de plus en plus spécialisées est créée. Cela a comme
avantage majeur de ne pas avoir à repartir de zéro lorsque l’on veut spécialiser une classe existante.
De cette manière il est possible de récupérer des librairies de classes, qui constituent une base,
pouvant être spécialisées à loisir.
Un particularité de l’héritage est qu’un objet d’un classe dérivée est aussi du type de la classe
mère...
Héritage public
L’héritage public se fait en C++ à partir du mot clé public. C’est la forme la plus courante de
dérivation (et la seule décrite en UML). Les principales propriétés liées à ce type de dérivation sont
les suivantes:
• les membres (attributs et méthodes) publics de la classe de base sont accessibles par tout le
monde, c’est à dire à la fois aux fonctions membres de la classe dérivée et aux utilisateurs de
la classe dérivée;
• les membres protégés de la classe de base sont accessibles aux fonctions membres de la
classe dérivée, mais pas aux utilisateurs de la classe dérivée;
• les membres privés de la classe de base sont inaccessibles à la fois aux fonctions membres de
la classe dérivée et aux utilisateurs de la classe dérivée.
De plus, tous les membres de la classe de base conservent dans la classe dérivée le statut qu’ils
avaient dans la classe de base. Cette remarque n’intervient qu’en cas de dérivation d’une nouvelle
classe à partir de la classe dérivée.
Héritage privé
L’héritage privé se fait en C++ à partir du mot clé private. Les principales propriétés liées à ce
type de dérivation sont les suivantes:
• les membres (attributs et méthodes) publics de la classe de base sont inaccessibles à la fois
aux fonctions membres de la classe dérivée et aux utilisateurs de cette classe dérivée;
• les membres protégés de la classe de base restent accessibles aux fonctions membres de
la classe dérivée mais pas aux utilisateurs de cette classe dérivée. Cependant, ils seront
considéré comme privés lors de dérivation ultérieures.
90 Chapter 8. POO/C++
Héritage protégé
L’héritage protégé se fait en C++ à partir du mot clé protected. Cette dérivation est intermédiaire
entre la dérivation publique et la dérivation privée. Ce type de dérivation possède la propriété
suivante: les membres (attributs et méthodes) publics de la classe de base seront considérés comme
protégés lors de dérivation ultérieures.
1 #include "PointColor.h"
2 #include <iostream>
3
16 return 0;
17 }
8.4 Héritage 91
Si on construit l’objet B ob2(3); les processus seront les mêmes sauf que le paramètre 3 lance
l’appel du constructeur utilisateur B::B(double a) qui fait un appel explicite au constructeur
utilisateur A::A(double a) ge). Ainsi X est initialisé à 3 et on obtient l’affichage suivant :
1 A:User
2 B:User
Figure 8.9:
Figure 8.10:
8.5 Polymorphisme
Parfois appelé polymorphisme d’héritage. L’intérêt du polymorphisme est d’exécuter la méthode la
plus appropriée à l’objet quand on manipule celui-ci indirectement (pointeur ou référence sur une
classe parente). Il s’agit de changer le comportement d’un objet en fonction de sa spécialisation
(héritage). L’intérêt principal est pour les objets conteneurs (qui contiennent des objets) comme les
tableaux, les listes, ... quelque soit le type de l’objet stocké (pour peut qu’il appartienne à la même
hiérarchie de classe) le comportement adapté à l’objet stocké.
La Programmation Orienté Objets autorise, sous certaines règles, les objets à changer de forme
ou plutôt de type. Rappelons que le C++ est un langage fortement typé: le type des objets est
obligatoirement connu à la compilation et sert à identifier les opérations à réaliser. Les règles qui
vont permettre aux objets du C++ de changer de formes sont très strictes.
Voici les trois ingrédients indispensables à la mise en place du polymorphisme:
• une hiérarchie de classes (au moins une classe héritant d’une autre)
• une surcharge de fonction dans cette hiérarchie de classes avec de la virtualisation (virtual)
• un pointeur ou une référence de type amont et initialisé sur un objet de type aval de la
hiérarchie
8.5.1 Définition
Le polymorphisme signifie ici "peut prendre plusieurs formes". D’abord, ce concept est relatif à la
surcharge des méthodes (ou opérations) des classes d’une même hiérarchie. Ceci implique qu’une
fonction soit présente dans la classe fille et dans la classe mère sous la même forme (nombre et
type d’arguments similaires) comme illustré dans la figure 8.10
Ensuite, il permet d’étendre les possibilités du mécanisme d’héritage qui induit qu’un objet d’une
classe fille est du même type que sa classe mère. En utilisant des pointeurs ou des référence, on
va pouvoir accéder aux différents types de l’objet. Ainsi, les codes suivants sont exécutables:
1 A a1; // ok
2 B b1; // ok
3 A *pt_a = &b1; // ok!
4 A &ref_a = b1; // ok!
5
6 a1.afficher(); // appel de la fonction A::Afficher()
7 b1.afficher(); // appel de la fonction B::Afficher()
8 pt_a->Afficher(); // appel de la fonction A::Afficher()
9 ref_a.Afficher(); // appel de la fonction A::Afficher()
Cependant, les deux derniers affichages ne conduisent pas à l’appel de B::Afficher() pourtant ils
travaillent avec un objet de type B: ils ne sont pas adaptés.
94 Chapter 8. POO/C++
C’est le dernier élément de la mise en oeuvre du polymorphisme qu’il faut ajouter : le mot clé
virtual qui explique que la méthode A::Afficher() peut être surchargée dans une classe fille et
qu’en fonction du type de l’objet appelant, il faudra faire appel à la fonction la plus adaptée. La
figure 8.11 illustre son utilisation pour la classe A précédente.
1 class A
2 {
3 protected:
4 double Aa;
5 public:
6 A(double a=1):Aa(a) {}
7 virtual void Afficher()
8 { cout << Aa; }
9 };
8.6 Généricité 95
8.6 Généricité
La généricité, parfois appelée polymorphisme paramétrique, n’a rien à voir avec le polymorphisme
du C++ (le polymorphisme dit d’héritage). Le but de la généricité est de rendre générique les
traitements aux types des variables. Ces variables concernées peuvent être des champs d’une classe
ou les paramètres d’une fonction.
Le rappel précédent sur le langage fortement typé vaut aussi ici: le type d’une variable est d’une
importance capitale. En C++, il va être possible de décrire des traitements sur des types de données
quelconques (c’est ce qui est détaillé ici), il n’est cependant pas possible de les compiler. Ceci a
deux conséquences:
• les méthodes génériques ne peuvent être écrites que dans des entêtes (.h par exemple) car ces
fichiers de sont pas compilés,
• lors de la production d’un exécutable, les types doivent être connus et fixés. Dès lors, les
types génériques sont remplacés par le type fixé et les fonctions ou classes génériques peuvent
alors être compilées.
IV
Part C: Thèmes choisis
permettra d’adapter l’addition complexe à n’importe quel type d’objet... même les non-numériques
(considérer std::enable_if).
A noter que la qualification friend ne se propage pas lors de l’héritage: une fonction amie d’une
classe mère n’est pas amie pour les classes filles.
volatile
Exemple
inline
Pour les fonctions, permet de copier le code de la fonction à l’endroit de l’appel, à la place de faire
un appel à la fonction. Ceci permet d’optimiser le temps d’exécution,
• dans le cas où la fonction inline est très petite (le cout de l’appel est aussi long que le code
de la fonction)
• dans le cas où la préparation des paramètres pour l’appel de la fonction est trop couteux en
temps (copie en mémoire).
9.2 Mot clés 101
static
La seule différence avec une variable normale, est qu’une variable static ne va être créée qu’une
seule fois. Il peut s’agir d’une variable à l’intérieur d’une fonction ou bien d’un champ d’une
classe (ou encore d’une variable globale). Cette variable n’est accessible que dans son contexte de
définition: le code (scope de définition) dans une fonction ou sa visibilité dans une classe. Il s’agit
d’un liage interne. Voici un exemple de variable statique dans une fonction.
1 #include <iostream>
2 using namespace std;
3
4 int countme()
5 {
6 static int j=0;
7 j++;
8 return j;
9 }
10
11 int main()
12 {
13 cout << countme() << endl; //affiche 1
14 cout << countme() << endl; //affiche 2
15 return 0;
16 }
On peut ainsi voir les variables static comme des variables globales (une seule instance, initialis-
ables, jamais détruites) visibles et accessibles comme des variables locales. Il s’agit donc d’une
alternative à l’utilisation de variable globale.
extern
Une variable extern est assez similaire à une variable static sauf qu’il s’agit d’utiliser une variable
déclarée dans un autre contexte (autre scope). C’est à dire soit une autre fonction ou un autre fichier.
Une fonction peut aussi être extern. Il s’agit d’un liage externe : la variable ou la fonction est dans
un autre fichier objet... possiblement venant d’un autre langage de programmation!
Voici le premier fichier ex_extern_def.cpp qui ne contient pour cet exemple que la déclaration et
initialisation de la variable j:
1 int j = 0;
Le second fichier ex_extern.cpp implémente un compteur et un affichage:
1 #include <iostream>
2 using namespace std;
3
4 int countme()
5 {
6 extern int j;
7 j++;
8 return j;
9 }
9.2 Mot clés 103
10
11 int main()
12 {
13 int j=10;
14 cout << countme() << endl; // affiche 1
15 cout << countme() << endl; // affiche 2
16 return 0;
17 }
Il faut compiler ces fichiers puis les lier ensembles. Ceci peut être fait simplement par g++ -g
ex_extern.cpp ex_extern_def.cpp -o ex_extern.o.
L’affichage sera bien 1 puis 2 car la variable j utilisée par la fonction countme est celle qui est
globale (définie dans ex_extern_def.cpp). On rappelle que la variable j déclarée dans le main n’est
pas visible par la fonction countme. Enfin, il sera déconseillé d’utiliser des noms commun pour les
variables globales afin d’éviter les ambiguïtés... j est ici un très mauvais exemple.
explicit
Ce mot clé est très utilisé pour les constructeurs et, depuis le C++11, pour les opérateurs de
conversions. Par défaut, toutes les fonctions sont implicites: le compilateur est autorisé à convertir
implicitement le type des paramètres et à exécuter les initialisations par copie. En ajoutant devant
un constructeur, ou une fonction de conversion de type, le mot clé explicit ces mécanismes
deviennent interdits (erreur de compilation).
1 class A
2 {
3 private:
4 int m_A;
5 public:
6 A(int a): m_A(a) {} // constructeur avec un seul parametre int
7 int GetA() {return m_A;}
8 };
9
10 void DoSomething(A a)
11 { int i = a.GetA (); }
12
13 int main()
14 {
15 DoSomething(42); // l'int '42' est converti implicitement en ojet de type
A
9.2 Mot clés 105
16 // DoSomething( A(42) );
17 }
15 int main()
16 {
17 // A n'a pas de constructeur explicit ou de conversion
18 // Tout est ok
19 A a1 = 1;
20 A a2 ( 2 );
21 A a3 { 3 };
22 int na1 = a1;
23 int na2 = static_cast<int>( a1 );
24
25 B b1 = 1; // Erreur: conversion implicite de int vers B
26 B b2 ( 2 ); // OK: appel explicite au constructor
27 B b3 { 3 }; // OK: appel explicite au constructor
28 int nb1 = b2; // Erreur: conversion implicite de B vers int
29 int nb2 = static_cast<int>( b2 ); // OK: explicit cast
30 }
Il est utile d’interdire les conversions implicites quand l’utilisation devient ambiguë. Par exemple,
avec un constructeur MyString(int : size) qui construit une chaine de caractères de taille size
et une fonction d’affichage print(MyString: &), un appel à print(3) ne produira pas l’affichage
"3" mais celui d’une chaine vide de taille 3...
using
Le mot using a vocation de créer des raccourcis pour les noms de fonctions, de fonctions dans un
namespace ou dans une classe mère, ou de type (nouveau avec le C++11).
Le mot clé using s’utilise de 3 manières différentes:
• comme directive pour l’utilisation de namespace (exemple : using namespace std;) ou de
membre de namespace (using-declaration),
• la déclaration d’utilisation de membres de classe (using-declaration),
• déclaration d’alias de type (C++11): il remplace typedef.
Détaillons rapidement les deux modes using-declaration. Tout d’abord pour un namespace:
1 #include <iostream>
2 #include <string>
3 #include <vector>
4
5 // using namespace std; // exemple familier
106 Chapter 9. C++ avancé
8 int main()
9 {
10 std::vector<double> a; // designation complete
11 string s = "Hello"; // designation raccourcie grace au using std::string
12
13 // a la volee:
14 using std::cout; // ici le cout
15 cout << s << std::endl; // mais pas le endl
16
17 return 0;
18 }
Ensuite dans une classe:
1 class A
2 {
3 protected:
4 int Aa;
5 public:
6 A(int a);
7 // A dispose des constructeurs suivants:
8 // 1- A(int)
9 // 2- A(const A&)
10 // 3- A(A&&) C++11
11 };
12
13 class B: public A
14 {
15 public:
16 using A::Aa; // le champs Aa est maintenant public a partir de B!
17 using A::A; // B herite des constructeurs de A (disposant des memes et
bons arguments)
18 // B dispose des constructeurs:
19 // 1- B() : constructeur par defaut
20 // 2- B(const B&)
21 // 3- B(B&&) C++11
22 // 4- B(int) : herite de A!
23 };
auto
Le mot clé auto a subi un lifting avec la version C++11. Avant il permettait de spécifier la durée
de vie d’une variable déclarée dans un bloc d’instructions ou dans une liste de paramètres d’une
fonction. Il indiquait qu’une telle variable avait un comportement par défaut (normal...) : allouée
au début du bloc et supprimée (dé-allocation) à la fin du bloc.
Depuis le C++11, auto permet de déclarer une variable (ou fonction) en spécifiant que le type de
la variable sera automatiquement déduit à partir de son initialisation. Attention, le C++ étant un
langage fortement typé, ceci implique que l’initialisation soit connue à la compilation. A noter que
ce mot clé pousse à déclarer les variables à la volée dans le code.
1 #include <iostream>
2 #include <typeinfo>
3
4 int main()
5 {
9.2 Mot clés 107
6 auto a = 1 + 2; // int
7 std::cout << " type de a : " << typeid(a).name() << std::endl;
8 // --> type de a : int
9
10 auto l = { 1, 2, 3};// une liste d'entier, et non un tableau
11 std::cout << " type de l : " << typeid(l).name() << std::endl;
12 // --> type de l : std::initializer_list<int>
13
14 return 0;
15 }
Terminons!
9.3 Messages d’aide au débogue 109
R A noter que le C++11 et le C++17 ajoutent le mot clé static_assert au C++. L’objectif
n’est pas le même que pour assert: static_assert doit avoir un test constant, c’est à dire
connu au moment de la compilation. static_assert permettra de tester des compatibilités
de types, de gestion d’exception, de fonctionnalités sur les types, etc... voir #include <
type_traits> pour ce qui est testable.
Annexes
Bibliographie
[Cor+09] Thomas H. Cormen et al. Introduction to Algorithms, Third Edition. 3rd. The MIT
Press, 2009. ISBN: 0262033844, 9780262033848 (cited on page 8).
[HP11] John L. Hennessy and David A. Patterson. Computer Architecture, Fifth Edition: A
Quantitative Approach. 5th. San Francisco, CA, USA: Morgan Kaufmann Publishers
Inc., 2011. ISBN: 012383872X, 9780123838728 (cited on page 11).
Index