Source: https://cpp.developpez.com/cours/cpp-objet/
Timestamp: 2017-03-27 08:31:17+00:00

Document:
UNIVERSITÉ NANCY 2 - INSTITUT UNIVERSITAIRE DE TECHNOLOGIE
I. La conception objet
I-B. La modularité
I-B-1. Deux méthodes de conception de modules
I-B-2. Quelques critères de qualités
I-B-3. Les principes de définition
I-C. La réutilisabilité
I-C-1. Les principes de la réutilisabilité
I-C-2. De nouvelles techniques
I-D. Principes de conception objet
I-D-2. La conception par objets
I-D-3. Détermination des objets
I-D-4. Conclusion
I-E. Résumé et terminologie
II. Introduction au langage C++
II-B. Implantation de modules en C++
II-C. Types de base
II-E. Structure d'un programme C++
II-F. Les fonctions
II-G. Définition des variables
II-H. Les références
II-I. Les constantes
II-J. Allocation dynamique de mémoire
II-K. La surcharge
II-L. Les entrées-sorties en C++
II-M. Les fonctions template
III-C. Les méthodes inline
III-D. Les constructeurs
III-E. Le destructeur
III-F. Méthodes constantes
III-G. Le mot-clé this
III-H. La surcharge
III-I. Les classes template
III-J. Les amis
III-K. Membres statiques
IV-B. La dérivation
IV-C. Redéfinition de méthodes
IV-D. Le polymorphisme
IV-E. La liaison dynamique
IV-F. Les classes abstraites
V. La bibliothèque STL
V-B. Les containers
V-C. Les itérateurs
V-D. Les algorithmes
VI. Format des entrées-sorties
VI-A. A.1 La classe ios
VI-B. A.2 La classe ostream
VI-C. A.3 La classe istream
VI-D. A.4 Les fichiers
VII. Carte de référence STL
VII-A. B.1 Les containers
VII-B. B.2 Les itérateurs
VII-C. B.3 Les algorithmes
VIII-A. C.1 Les namespaces
VIII-B. Les nouveaux casts
VIII-C. C 3 Où trouver un compilateur C++?
Le but de ce premier chapitre est d'introduire la notion même de conception objet, d'un point de vue
théorique. Ce chapitre n'a pas la prétention d'expliquer en détail ce type de conception, mais se propose
d'en rappeler les idées maîtresses en préambule à l'étude du langage C++.
Publié le 1er janvier 2001 - Mis à jour le 20 novembre 2008 Version PDF
I. La conception objet▲
d'en rappeler les idées maîtresses en préambule à l'étude du langage C++. Le chapitre est très largement
inspiré de [Mey 91] et [Gau 96] auquel le lecteur pourra se reporter pour plus de précisions. La philosophie
du langage C++, comme celle des autres langages à objets, est en effet directement inspirée de ce type de
La conception par objet trouve ses fondements dans une réflexion menée autour de la vie du logiciel. D'une part, le développement de logiciels de plus en plus importants nécessite l'utilisation de règles
permettant d'assurer une certaine qualité de réalisation. D'autre part, la réalisation même de logiciel est
composée de plusieurs phases, dont le développement ne constitue que la première partie. Elle est suivie
dans la plupart des cas d'une phase dite de maintenance qui consiste à corriger le logiciel et à le faire
évoluer. On estime que cette dernière phase représente 70 % du coût total d'un logiciel [Mey 91], ce qui
exige plus encore que la phase de développement de produire du logiciel de qualité.
La conception objet est issue des réflexions effectuées autour de cette qualité. Celle-ci peut-être atteinte
à travers certains critères :
la correction ou la validité : c'est-à-dire le fait qu'un logiciel effectue exactement les tâches pour lesquelles il a été conçu ;
l'extensibilité : c'est-à-dire la capacité à intégrer facilement de nouvelles spécifications, qu'elles soient demandées par les utilisateurs ou imposées par un événement extérieur ;
la réutilisabilité : les logiciels écrits doivent pouvoir être réutilisables, complètement ou en partie. Ceci impose lors de la conception une attention particulière à l'organisation du logiciel et à la définition de ses composantes ;
la robustesse : c'est-à-dire l'aptitude d'un logiciel à fonctionner même dans des conditions anormales.
Bien que ce critère soit plus difficile à respecter, les conditions anormales étant par définition
non spécifiées lors de la conception d'un logiciel, il peut être atteint si le logiciel est capable de
détecter qu'il se trouve dans une situation anormale.
Nous détaillons dans un premier temps les critères utilisés pour concevoir du logiciel de qualité, tels
que la modularité [§ 1.2] ou encore la réutilisabilité [§ 1.3]. Nous définissons ensuite les bases générales
de la programmation par objets [§ 1.4] avant d'introduire la terminologie associée à cette approche [§ 1.5].
I-B. La modularité▲
Les critères énoncés au paragraphe précédent influent sur la façon de concevoir un logiciel, et en
particulier sur l'architecture logicielle. En effet, beaucoup de ces critères ne sont pas respectés lorsque
l'architecture d'un logicielle est obscure, monolithique. Dans ces conditions, le moindre changement de
spécification peut avoir des répercutions très importantes sur le logiciel, imposant une lourde charge de
travail pour effectuer les mises à jour.
On adopte généralement une architecture assez flexible pour parer ce genre de problèmes, basée sur
les modules. Ceux-ci sont des entités indépendantes intégrées dans une architecture pour produire un logiciel.
L'ensemble des modules utilisés, ainsi que les relations qu'ils entretiennent entre eux, sont dénominés
système. L'intérêt de ce type de conception est de concentrer les connaissances liées à une entité logique
à l'intérieur d'un module qui est seul habilité à exploiter ces connaissances. L'une des conséquences immédiates
est que lorsqu'une maintenance est à effectuer sur une entité logique, celle-ci ne doit concerner
qu'un seul module, ce qui confine la maintenance.
I-B-1. Deux méthodes de conception de modules▲
Si la définition de modules est une approche communément admise, il faut également une méthode
de construction de systèmes qui permette de déduire quels sont les bons modules. Il existe deux grandes
familles de méthodes modulaires :
Les méthodes descendantes qui procèdent par décomposition de problème. Un problème est ainsi divisé en un certain nombre de sous-problèmes, chacun de complexité moindre. Cette division est ensuite appliquée aux sous-problèmes générés, et ainsi de suite, jusqu'à ce que chacun des sousproblèmes soit trivial.
Les méthodes ascendantes qui procèdent par composition de briques logicielles simples, pour obtenir des systèmes complets. C'est en particulier le cas des bibliothèques de sous-programmes disponibles avec tous les systèmes, langages, environnements...
Les deux méthodes ne sont pas automatiquement à opposer, et sont souvent utilisées en même temps
lors de la conception d'un logiciel. On peut cependant noter que l'approche descendante ne favorise pas
toujours la réutilisabilité des modules produits.	I-B-2. Quelques critères de qualités▲
En dehors de la démarche même menant aux modules, il est bon de préciser quelques critères de qualité
à respecter lors de la définition des modules :
Compréhensibilité modulaire : les modules doivent être clairs et organisés de manière compréhensible dans le système. Ceci implique que les modules doivent communiquer avec peu de modules, ce qui permet de les « situer » plus facilement. De même, l'enchaînement des différents modules doit être logique, et on ne doit pas avoir par exemple à utiliser plusieurs fois de suite un module pour produire une action atomique.
Continuité modulaire : ce critère est respecté si une petite modification des spécifications n'entraîne qu'un nombre limité de modifications au sein d'un petit nombre de modules, sans remettre en cause les relations qui les lient.
Protection modulaire : ce critère signifie que toute action lancée au niveau d'un module doit être confiné à ce module, et éventuellement à un nombre restreint de modules. Ce critère ne permet pas de corriger les erreurs introduites, mais de confiner autant que possible les erreurs dans les modules où elles sont apparues.
Ces notions, qui sont assez intuitives, et qui découlent des réflexions menées autour de la vie du logiciel,
doivent être considérées lors de la définition et de la maintenance des modules, même si elle ne sont
pas accompagnées d'une méthodologie précise permettant d'y arriver. C'est à travers elles que la qualité
globale d'un logiciel peut être atteinte.
I-B-3. Les principes de définition▲
À partir des critères exposés ci-dessus, quelques principes de conception ont été retenus pour la réalisation
Interface limitée : le but n'est pas de borner les actions associées à un module, mais de se restreindre à un nombre limité d'actions bien définies, ce qui supprime une part des erreurs liées à l'utilisation de modules.
Communications limitées : les communications entre modules, réalisées via leur interface, doivent être limitées de façon quantitative. Ceci est une conséquence du principe de modularité, qui est d'autant mieux respecté que les modules jouent leur rôle. Si les échanges sont trop importants, la notion même de module devient floue, limitant l'intérêt de cette technique.
Interface explicites : les communications entre modules doivent ressortir explicitement.
Masquage de l'information : toutes les informations contenues dans un module doivent être privées au module, à l'exception de celles explicitement définies publiques. Les communications autorisées sont ainsi celles explicitement définies dans l'interface du module, via les services qu'il propose.
Les modules définis lors de la conception doivent correspondre à des unités modulaires syntaxiques liées au langage de programmation. En clair, le module spécifié ne doit pas s'adapter au langage de programmation, mais au contraire le langage de programmation doit proposer une structure permettant d'implanter le module tel qu'il a été spécifié. Par exemple, si le langage de programmation ne permet pas d'effectuer le masquage de l'information (comme le langage C), il n'est pas adéquat pour implanter les modules de manière satisfaisante selon les critères de la conception objet.
Ce genre de critères proscrit ainsi des comportements tels que l'utilisation de variables globales par
exemple, qui va à l'encontre des principes énoncés. En effet, les variables globales peuvent être utilisées
et modifiées par n'importe quelle composante d'un programme, ce qui complique d'autant la maintenance
autour de ce genre de variables.
I-C. La réutilisabilité▲
La réutilisabilité n'est pas un concept nouveau en informatique et a été utilisée dès les balbutiements.
En effet, les types de données à stocker sont toujours construits autour des mêmes bases (tables, listes, ensembles)
et la plupart des traitements comportent des actions atomiques telles que l'insertion, la recherche,
le tri, ... qui sont des problèmes résolus en informatique. Il existe une bibliographie assez abondante décrivant
des solutions optimales à chacun de ces problèmes. La résolution des problèmes actuels passe par la
composition des solutions de chacun de ces problèmes basiques.
Les bibliothèques (systèmes, mathématiques, etc.) sont des bons exemples de réutilisabilité et sont
couramment utilisées par les programmeurs. Elles montrent cependant parfois leurs limites. En effet, les	fonctions qu'elles comportent ne sont pas capables de s'adapter aux changements de types ou d'implantation.
La solution dans ce cas est de fournir une multitude de fonctions, chacune adaptée à un cas particulier,
ou d'écrire une fonction prenant tous les cas en considération. Dans un cas comme dans l'autre, ce n'est
que peu satisfaisant. C'est pourquoi la conception objet se propose de formaliser un peu plus cette notion
de réutilisabilité et de proposer de nouvelles techniques pour l'atteindre pleinement.
I-C-1. Les principes de la réutilisabilité▲
Le paragraphe précédent a introduit le notion de module, en insistant sur les avantages de la conception
modulaire, mais n'a pas donné de détails sur la conception même d'un module. On conviendra ici
qu'un « bon » module est un module réutilisable, c'est-à-dire conçu dans l'optique d'être placé dans une
bibliothèque à des fins de réutilisation. Afin de marier modularité et réutilisabilité, quelques conditions
nécessaires à la conception de bons modules ont été définies :
un module doit pouvoir manipuler plusieurs types différents. Un module de listes par exemple doit pouvoir manipuler aussi bien des entiers que des types composites ;
de même, un module doit pouvoir s'adapter aux différentes structures de données manipulées dotées de méthodes spécifiques. Il devra ainsi par exemple pouvoir rechercher de la même manière une information contenue dans un tableau, une liste, un fichier ;
un module doit pouvoir offrir des opérations aux clients qui l'utilisent sans que ceux-ci connaissent l'implantation de l'opération. Ceci est une conséquence directe du masquage de l'information préconisé [§ 1.2.3]. C'est une condition essentielle pour développer de grands systèmes : les clients d'un module sont ainsi protégés de tout changement de spécifications relatif à un module ;
les opérations communes à un groupe de modules doivent pouvoir être factorisées dans un même module. Ainsi par exemple, les modules effectuant du stockage de données, tels que les listes, les tables, etc. doivent être dotés d'opérations de même nom permettant d'accéder à des éléments, d'effectuer un parcours, de tester la présence d'éléments. Ceci peut permettre entre autres de définir des algorithmes communs, tels que la recherche, quelle que soit la structure de données utilisée pour stocker les données.
I-C-2. De nouvelles techniques▲
L'idée, afin de faire cohabiter les principes inhérents à la modularité et à la réutilisabilité, est d'utiliser
la notion de paquetage, introduite par des langages tels que Ada ou Modula-2. Un paquetage correspond
à un regroupement, au sein d'un même module, d'une structure de données et des opérations qui lui sont
propres. Ceci satisfait en particulier les critères de modularité, en isolant chaque entité d'un système, ce
qui la rend plus facile à maintenir et à utiliser. En ce qui concerne les critères de réutilisabilité, il est
possible d'aller encore un peu plus loin. Nous introduisons ici de nouvelles notions qui apparaissent avec
les paquetages et vont permettre de franchir ce pas :
La surcharge : cette notion prévoit que des opérations appartenant à des modules différents peuvent être associées au même nom. Les opérations ne sont donc plus indépendantes, elle prennent leur signification contextuellement en fonction du cadre dans lequel elles sont utilisées. Parmi ces opérations, on trouve les fonctions, mais également les opérateurs. Cela peut permettre par exemple de définir une fonction insérer dans chaque module de stockage, permettant d'écrire de manière uniforme : insérer(elt, container) quelque soit le type de container (liste, tableau, fichier...) ;
La généricité : cette notion permet de définir des modules paramétrés par le type qu'ils manipulent. Un module générique n'est alors pas directement utilisable : c'est plutôt un patron de module qui sera « instancié » par les types paramètres qu'il accepte. Cette notion est très intéressante, car elle va permettre la définition de méthodes (façon de travailler) plus que de fonctions (plus formelles).
Ces définitions et ces nouveaux outils vont nous permettre de définir de nouvelles manières de concevoir
des systèmes informatiques.	I-D. Principes de conception objet▲
Après avoir énuméré les qualités souhaitables nécessaires à l'élaboration d'un système de qualité, il
nous reste maintenant à déterminer les règles de construction de tels systèmes. D'un point de vue général,
la construction d'un système informatique se résume par la formule :
Le concepteur d'un système informatique a donc deux grandes options pour l'architecture d'un système :
Dans les méthodes de conception par traitements, qui constituent l'approche traditionnelle, la base de la
réflexion est effectuée autour des traitements. Le concepteur considère ainsi les tâches que doit accomplir
le programme, en décomposant celui-ci en une série de tâches simples (approche descendante) ou en le
construisant par composition de traitements (fonctions) disponibles (approche ascendante). On peut faire
les remarques suivantes sur ce type d'approche :
Les modules trouvés par cette approche se trouvent souvent être des modules ad hoc, adaptés au type de problème posé au départ et ne permettant que peu d'extensibilité du système obtenu et que peu de réutilisabilité.
Les traitements définis ne prennent pas assez en considération les structures de données sous-jacentes, qui se retrouvent partagées entre plusieurs modules. Il devient difficile dans ces conditions de maintenir et de protéger ces structures de données.
Il n'est pas toujours évident d'identifier les traitements, ou leur enchaînement, impliqués dans un système informatique. Sur des cas simples, cela reste aisé, mais sur des cas plus compliqués (définition d'un système d'exploitation par exemple) cela ne permet pas de déduire une architecture naturelle.
Les traitements sont généralement beaucoup moins stables que les données. En effet, un programme une fois terminé se verra souvent étendre par de nouvelles fonctionnalités, parfois en concordance avec les objectifs premiers du programme, mais pas toujours. Par exemple, n'importe quel programme devant initialement calculer des fiches de paie ou archiver des données devra par la suite effectuer des statistiques ou être capable de répondre interactivement alors qu'il était prévu initialement pour fonctionner chaque nuit ou chaque mois. Les données en revanche sont beaucoup plus stables, et si des modifications interviennent dans la représentation des données (ajout de la CSG pour les fiches de paie, ajout d'un champ supplémentaire dans le classement des données archivées), elles ne changent pas radicalement la représentation des données.
Bien sûr, les approches basées sur les traitements ont quand même quelques avantages, dont celui d'être
relativement intuitives et facilement applicables. Elles peuvent à ce titre être utilisées pour la réalisation	d'applications de taille raisonnable. Elles se révèlent cependant rapidement inadaptées lors de la réalisation
de systèmes plus conséquents ou sensibles aux changements de spécification.	I-D-2. La conception par objets▲
Afin d'établir de façon stable et robuste l'architecture d'un système, il semble maintenant plus logique
de s'organiser autour des données manipulées, les objets. En effet, les données étant de par leur nature
plus stables que les traitements, la conception en est simplifiée. De plus il apparaît que la conception par
traitement ne favorise pas l'utilisation des principes de qualité mis en évidence, tels que la modularité ou la
réutilisabilité. Il reste maintenant à éclaircir les fondements mêmes de la conception par objets. Dans cette
optique, voici une première définition de la conception par objets, énoncée par B. Meyer [Mey 91] :
« La conception par objet est la méthode qui conduit à des architectures logicielles fondées
sur les OBJETS que tout système ou sous-système manipule »
« Ne commencez pas par demander ce que fait le système, demandez À QUOI il le fait ! »
La spécification d'un système va donc maintenant s'axer principalement sur la détermination des objets
manipulés. Une fois cette étape réalisée, le concepteur n'aura plus qu'à réaliser les fonctions de haut-niveau
qui s'appuient sur les objets et les familles d'objets définis. C'est cette approche, préconisant de considérer
d'abord les objets (c'est-à-dire en première approche les données) avant l'objectif premier du système à
réaliser, qui permet d'appliquer les principes de réutilisabilité et d'extensibilité. Reste maintenant à décrire
comment trouver, décrire et exploiter les objets !	I-D-3. Détermination des objets▲
Pour la détermination même des objets, il faut simplement se rattacher aux objets physiques ou abstraits
qui nous entourent. Typiquement, dans un programme de fiches de paie, le bulletin de salaire, l'employé,
l'employeur, la date d'émission, etc. sont des objets. Les objets vont couvrir tous les types d'entités, des
plus simples aux plus complexes, et sont à ce titre partiellement similaires aux structures utilisées dans des
langages tels que C ou Pascal. Mais ils sont également dotés de nombreuses propriétés supplémentaires
très intéressante, comme nous le découvrirons au fil de ce cours.
Pour représenter ou décrire de tels objets, nous allons nous intéresser non pas aux objets directement
mais aux classes qui les représentent. Les classes vont constituer le modèle dont seront issus chacun des
objets, appelés aussi instances de classes. Et pour décrire ces classes, nous n'allons pas faire intervenir des
champs (ou attributs - des données) comme cela pouvait être le cas auparavant, nous allons les spécifier
formellement en fonction des services (ou fonctions) qu'elles offrent. Ceci apporte l'avantage de se détacher
de la représentation physique des données et de raisonner uniquement sur les services qu'elles doivent
offrir. À titre d'exemple, la figure [FIG. 1.1] présente la description d'une classe Pile.
La partie Type indique le type de données décrit, paramétré ici par le type T. Ceci permet de définir un type générique de pile, qui sera ensuite instancié lors de son utilisation par le type d'objets que l'on souhaite empiler : des entiers, des caractères, d'autres types...
La partie Fonctions décrit les services disponibles sur les piles. Ces fonctions sont exprimées sous une forme mathématique, décrivant les paramètres nécessaires à l'utilisation de la fonction et les résultats fournis	La partie Préconditions précise les conditions d'utilisation des fonctions définies à l'aide d'une flèche barrée (9), qui ne sont pas définies pour toutes les valeurs de paramètres possibles.
D'autres parties peuvent être ajoutées :
Postconditions qui s'utilise comme la partie Préconditions, et qui précise les propriétés des résultats qui seront fournis par les différentes fonctions de la classe ;une partie Axiomes qui précise les propriétés que chacun des objets de type Pile doit respecter à tout moment.
FIG. 1.1 - Description simplifiée d'une pile d'après [Gau 96].
Cette méthode est appliquée sur chacun des types d'objets recensés nécessaires à l'élaboration du
système. L'approche est conforme à la philosophie énoncée car le module est bien défini en fonction
des services qu'il offre, c'est-à-dire à partir de son interface. Les principes de masquage d'information
sont notamment bien respectés : l'utilisateur du module ne sait pas quelle est la représentation physique
des données. Il connaît juste les services offerts par la classe. Cette démarche s'avère nécessaire pour le
développement d'un système conséquent et respecte les règles de qualités établies auparavant.
Un des intérêts de ce type de démarche est par ailleurs de permettre d'améliorer les principes de réutilisabilité.
Cette étape de spécification de type devance celle de l'implantation de la classe correspondant au
type. Et toute classe définie pourra être potentiellement étendue par une nouvelle classe qui la spécialisera.
Ce mécanisme, appelé héritage, est étudié en détails dans le chapitre 4 et permet l'utilisation de nouvelles
techniques telles que la liaison dynamique et le polymorphisme.
I-D-4. Conclusion▲
Pour conclure, faisons le point sur les objectifs souhaités pour obtenir du logiciel de qualité et les
techniques introduites pour atteindre ce but :
modularité : cette technique permet de découper un système complet en un ensemble de modules qui sont indépendant ;
réutilisabilité : les classes produites peuvent être regroupées en bibliothèques et être réutilisés. L'héritage permet également de réutiliser des classes en les spécialisant ;
abstraction de données et masquage de l'information: les classes n'indiquent pas la représentation physique des données qu'elles utilisent, mais se contentent de présenter les services qu'elles offrent. Le concept de généricité permet encore d'accroître cette abstraction, en proposant des classes qui sont paramétrées par des types de données ;
extensibilité : les classes sont définies en terme de services. Dès lors, un changement de représentation interne de données ou une modification de celles-ci n'altère pas la façon dont les autres classes les utilisent.
lisibilité : l'interface (documentée) permet d'avoir un mode d'emploi clair et précis de l'utilisation d'une classe, qui est d'autant plus clair que l'implantation des classes est cachée.
Pour des détails plus précis sur la conception objet, se reporter à des ouvrages détaillant ce type d'approche,
tels que [Mey 91] ou [Gau 96].	I-E. Résumé et terminologie▲
Ce paragraphe se propose de faire le point sur toutes les notions abordées et d'introduire le vocabulaire
spécifique à la conception objet.
De part la prédominance de la maintenance dans la conception d'un logiciel, il est essentiel d'utiliser des règles permettant de construire du logiciel de qualité.
Dans ce contexte, certains critères tels que la réutilisabilité et l'extensibilité ont été définis, répondant en partie à cette exigence.
La modularité est une approche qui permet de parvenir à ces objectifs. Ce concept s'applique à l'implantation, mais surtout à la conception.
La programmation est une tâche qui s'avère assez répétitive et qui nécessite de se doter de techniques favorisant la réutilisabilité. Deux de ces techniques ont été présentées : la généricité et la surcharge.
L'architecture d'un système peut se concevoir autour des traitements ou autour des données. Si la première approche est assez intuitive et facile d'utilisation, elle ne convient que pour des systèmes de taille limitée. En revanche, la deuxième approche permet de construire des systèmes plus stables et répond mieux aux objectifs de qualités définis.
La conception par objets se propose de décrire un système à travers les classes d'objets qui sont manipulées. Ces classes sont regroupées en familles de classes qui peuvent être implantées à travers l'héritage.
Une classe est une structure regroupant des données, appelées attributs ou variables d'instance et des fonctions disponibles, appelées méthodes.
Une instance de classe est appelée objet.
La classe dont est issu un objet est appelée type de l'objet.
Une classe A est appelée client d'une classe B si A contient un attribut de type B. La classe B est alors appelée fournisseur de A.	II. Introduction au langage C++▲
Le langage C++ est un langage complexe que ce seul polycopié ne peut pas exhaustivement couvrir. Le
lecteur pourra se reporter à des ouvrages tels que [Str 97] et [Lip 98] (ou sa traduction française [Lip 92])
pour de plus amples informations. Un excellent ouvrage est aussi disponible gratuitement sur le Web
à l'adresse http://www.EckelObjects.com/ThinkingInCPP2e.html. C++ peut être considéré
comme une extension de C, si bien que tout programme C valide est un programme C++ valide.
La différence par rapport à C est essentiellement faite au niveau des fonctionnalités objets ajoutées, et de
quelques conventions différentes étudiées dans ce polycopié.
Tout comme le langage C, le langage C++ a été conçu dans les laboratoires AT&T Bell. Son concepteur,
Bjarne Stroustrup, désirait étendre les fonctionnalités du langage C afin d'y intégrer les concepts de
la programmation par objet. Il désirait en effet conserver les habitudes des programmeurs C, leur permettre
de récupérer une bonne partie des programmes qu'ils avaient développés, tout en intégrant les avantages
de la programmation objet, en s'inspirant de langages tels que simula. Ceci a donné naissance en 1979 au
langage C+, qui intégrait quelques fonctionnalités objet, et en 1983 au langage C++, un peu plus complet,
incluant notamment la généricité, la liaison dynamique... Le langage a continué ensuite à évoluer, jusqu'à
l'adoption d'une norme qui est assez récente, puisqu'elle date de la fin de 1997. Ce polycopié ne s'attarde
pas sur les dernières fonctionnalités offertes par la norme définitive du langage, que peu de compilateurs
supportent dans leur globalité, mais plutôt sur les bases mêmes offertes par ce langage. De même, les fonctionnalités
communes aux langages C et C++ ne sont pas reprises ; seules les différences sont soulignées.
Le lecteur doit donc avoir une bonne connaissance du langage C pour aborder cette introduction.	II-B. Implantation de modules en C++▲
Tout comme en C, un module C++ est généralement implanté par deux fichiers : un fichier d'interface
qui regroupe les services offerts par le module (définition de types, constantes, déclaration de fonctions)
et un fichier d'implantation qui permet de définir les fonctions déclarées dans l'interface. Les extensions
conventionnellement utilisées en C++ sont :
.hh, .H pour les fichiers d'interface C++ (les headers). Il n'est pas rare cependant de voir des fichiers d'extension .h contenir des interfaces C++. Remarque : la norme finale du langage C++ prévoit que les fichiers d'interface perdent leur extension.
C'est la raison pour laquelle les interfaces spécifiques au C++ sont désormais désignées sans leur
extension (comme iostream par exemple). Il en est de même pour les headers déjà disponibles en
C. Ceux-ci sont de plus préfixés par le caractère « c ». Ainsi, stdio.h est maintenant dénominé
par cstdio.
.cc, .cpp, .C pour les fichiers d'implantation C++. Ici par contre les extensions sont bien différenciées et sont souvent utilisées directement par les compilateurs pour déterminer le type de compilation (C, C++) à effectuer.
II-C. Types de base▲
Les types de bases disponibles en C, tels que char, int, float, double, void sont également
disponibles en C++. Un type supplémentaire a été introduit pour manipuler de manière plus rigoureuse
et plus explicite les booléens, c'est le type bool [PROG. 2.1]. Les variables de type bool peuvent
avoir deux valeurs différentes : true ou false. Cependant, afin de rester compatible avec les variables
booléennes utilisées en C, il est toujours possible de stocker et de manipuler des booléens à partir de
variables de type int.
PROG. 2.1 - Exemple d'utilisation d'un booléen.Sélectionnez
1 bool stop = false;
3 while (!stop) {
Il existe deux moyens de définir des commentaires en C++. Le premier moyen correspond à des commentaires
longs et est déjà disponible sous C. Il correspond aux délimiteurs « /* */ » : tout ce qui se
trouve entre ces deux délimiteurs est considéré comme commentaire, même s'il y a des changements de
lignes. Le deuxième moyen permet de définir des commentaires courts, tenant sur une seule ligne. Il correspond
au délimiteur « // » : tout ce qui se trouve à droite de ce délimiteur sur la même ligne est considéré
comme commentaire [PROG. 2.2].
PROG. 2.2 - Deux types de commentaires disponiblesSélectionnez
2 	Ceci est un commentaire de
3 	plusieurs lignes
6 bool stop; // Commentaire court
II-E. Structure d'un programme C++▲
La structure minimale d'un programme C++ est similaire à celle d'un programme C. Elle peut ensuite
être étendue par des éléments (fonctions, instructions, structures de contrôle, etc...) abordés lors de l'étude
du langage C, et par les éléments propres au C++ présentés dans ce polycopié. La fonction principale,
appelée lors du début de l'exécution d'un programme, est la fonction main qui peut être définie de deux
manières différentes [PROG. 2.3].
PROG. 2.3 - Structure minimale d'un programme C++.Sélectionnez
1 int main() { ... } // Programme sans paramètre
3 /* Programme avec paramètres :
4 	 argc : nombre de paramètres (y compris le nom du programme)
5 	 argv : tableau de paramètres, argc entrées
8 int main(int argc, char *argv[]) { ... }
II-F. Les fonctions▲
Les fonctions C++ se déclarent et se définissent comme en C. Plusieurs caractéristiques ont cependant été ajoutées ou modifiées.
Vérification stricte des types. Contrairement à C qui est un langage faiblement typé, et qui autorise ainsi l'utilisation d'une fonction avant sa définition ou sa déclaration 1, C++ est un langage fortement typé. L'utilisation d'une fonction sans une déclaration ou une définition préalable conduit à une erreur de compilation.
Possibilité de définir des valeurs par défaut pour certains paramètres de fonctions. Certaines fonctions sont appelées avec des paramètres qui changent rarement. Considérons par exemple une fonction EcranInit qui est chargée d'initialiser un écran d'ordinateur (en mode caractères). Dans 90 % des cas, l'écran a les dimensions 24 lignes × 80 caractères et doit être initialisé dans 99 % des cas avec le caractère ' ', qui provoque l'effacement de l'écran. Plutôt que de contraindre le programmeur à énumérer des paramètres qui sont généralement invariants, C++ offre la possibilité de donner des valeurs par défaut à certains paramètres lors de la déclaration de la fonction [PROG. 2.4]. Quelques remarques sur cette fonctionnalité :
Une fonction peut définir des valeurs par défaut pour tous ses paramètres ou seulement pour une partie. Les paramètres acceptant des valeurs par défaut doivent se trouver après les paramètres sans valeur par défaut dans la liste des paramètres acceptés par une fonction.
Les valeurs par défaut de chaque paramètre ne peuvent être mentionnées qu'une seule fois parmi les définitions / déclarations d'une fonction. Ainsi, par convention, ces valeurs sont généralement mentionnées dans la déclaration de la fonction et pas dans sa définition (donc dans le .H et pas dans le .C).
L'ordre de déclaration des paramètres est important : dans l'exemple [PROG. 2.4] il est en effet impossible de donner une valeur à col sans en donner une auparavant à lig. D'une façon générale, il faut donc positionner parmi les paramètres ayant des valeurs par défaut en premier ceux qui ont le plus de chances d'être modifiés.	1. La fonction en question est alors supposée par défaut renvoyer un entier (type int).
PROG. 2.4 - Exemple d'initialisations par défaut.Sélectionnez
1 	void EcranInit(Ecran ecran, int lig = 24, int col = 80, char fond = ' ');
3 	void EcranInit(Ecran ecran, int lig, int col, char fond)
8 	int main()
10 		Ecran ec;
12		EcranInit(ec); 			// Éq. à : EcranInit(ecran, 24, 80, ' ');
13 		EcranInit(ec, 26); 		// Éq. à : EcranInit(ecran, 26, 80, ' ');
14 		EcranInit(ec, 26, 92); 	// Éq. à : EcranInit(ecran, 26, 92, ' ');
15 		EcranInit(ec, 26, 92, '+');
Définition de fonctions inline. Les fonctions offrent beaucoup d'avantages, mais ont un inconvénient majeur lorsque le nombre d'instructions qu'elles comportent est réduit : elles sont coûteuses en temps d'exécution (exemple : fonction int min(int a, int b);). En effet, à chaque appel de fonction, le système met à jour un certain nombre de valeurs (essentiellement avec la pile) et effectue d'autres mises à jour lors de la terminaison de la fonction. Le coût de ces mises à jour est d'autant plus élevé (relativement) que le nombre d'instructions contenues dans la fonction est réduit, ce qui peut être critique dans des traitements exigeant de hautes performances. On peut cependant vouloir garder la structure offerte par les fonctions pour des raisons de lisibilité, d'uniformisation, de réutilisation. La solution est de définir ce genre de fonctions comme inline [PROG. 2.5].
PROG. 2.5 - Exemple de fonction inline.Sélectionnez
1 						// Définition dans une interface (.H)
3 inline int min(int a, int b)
5 	return ((a &lt; b)? a : b);
8 						// Utilisation dans un programme
11 int m = min (i, j); // Remplacé par : m = ((i &lt; j)? i : j);
12 ... 					// (à la compilation)
En rajoutant le mot-clé inline lors de la définition de la fonction, la fonction est considérée comme
inline : chaque appel fait à la fonction sera remplacé par le code source correspondant à la fonction
[PROG. 2.5]. On dit que la fonction est expansée à son point d'appel. Quelques remarques :
Pour bénéficier de cette fonctionnalité, la fonction doit être définie (et non pas seulement déclarée) dans une interface, c'est-à-dire dans un fichier .H.
La spécification inline n'est qu'une recommandation faite au compilateur, qui peut décider ou non d'effectivement laisser cette fonction inline. En particulier, si le corps de la fonction est trop important, il y a peu de chances que la fonction reste inline ; il y a peu de gain aussi de toute façon, cette technique s'avérant rentable uniquement sur les fonctions comportant peu d'instructions.
II-G. Définition des variables▲
Contrairement au C qui impose de définir toutes les variables d'un bloc au début de ce bloc, C++ offre
la possibilité de définir les différentes variables utilisées au fur et à mesure des besoins. Seule condition
requise : une variable doit être soit déclarée, soit définie, avant de pouvoir être utilisée. Voir [PROG. 2.6]
(et notamment les lignes 10 et 12) pour des exemples de définition de variables. Toutefois, dans un souci
de lisibilité, il est conseillé de définir autant que possible les variables en début de bloc.
PROG. 2.6 - Définition de variables en C++. Sélectionnez
3 		int a = 5;
4 		int b = 4;
5 		int c;
7 		c = a * b;
8 		b += c;
10 		int j = 0;
12 		for (int i = 0; i &lt; c; i++)
13 			j+= i;
II-H. Les références▲
Les références permettent de bénéficier d'un mécanisme d'alias sur les variables. Le langage C avait
déjà introduit la notion de pointeur sur variable, qui permet d'accéder à la valeur contenue dans une variable
grâce à l'adresse de cette variable. Une référence est un nouveau nom donné à une variable existante, ce
qui lui confère ce rôle d'alias. Une référence est également représentée par une adresse (un pointeur), mais
s'utilise comme une variable : les opérations effectuées sur la référence sont en fait réalisées sur l'objet
référencé [PROG. 2.7]. Pour définir une référence : Type &nomRéf = variableÀRéférencer.
PROG. 2.7 - Exemple d'utilisation des références.Sélectionnez
2 		int val = 10;
3 		int i;
4 		int *pval = &amp;val; // Pointeur sur val
5 		int &amp;refVal = val; // Référence sur val
6 		int &amp;refAutre; // Erreur !
8 		*pval = 12;
9 		i = refval; // 'i' vaut 12, valeur de 'val'
Il existe une différence entre les pointeurs et les références : les références ne peuvent être vides. C'est
pour cela qu'elles sont toujours initialisées lors de leur déclaration, et qu'on préférera leur usage aux
pointeurs dans les situations où on ne souhaite pas avoir de valeur inconsistante, comme cela peut être
le cas avec un pointeur NULL. Voici ci-dessous un exemple de fonction réalisée avec des pointeurs, et la
même version réalisée avec des références [PROG. 2.8].
PROG. 2.8 - Comparaison pointeur / référence.Sélectionnez
1 	void swapC(int *i, int *j)
3 		int tmp = *i;
4 		*i = *j;
5 		*j = tmp;
8 	void swapCpp(int &amp;i, int &amp;j)
10 		int tmp = i;
11 		i = j;
12 		j = tmp;
15 	int main() {
16 		int m = 1, n = 2;
18 		swapC(&amp;m, &amp;n); // D'où : m=2 et n=1
19 		swapCpp(m, n); // D'où : m=1 et n=2
II-I. Les constantes▲
C++ offre la possibilité de définir des entités constantes (variables, fonctions). La valeur de ces entités
ne peut alors plus être modifiée lors de l'exécution du programme, ce qui suppose, comme pour les
références, qu'elles doivent être initialisées à la déclaration. Des exemples typiques d'utilisation de cette
fonctionnalité pour des variables sont [PROG. 2.9] :
la représentation des bornes de boucles. C'est le cas de la variable taille qui ne doit pas être modifiée une fois qu'elle est fixée ;
le passage de paramètres dans des fonctions qui ne sont pas sensées modifier la valeur de ces paramètres.
C'est le cas de la fonction contient qui effectue une recherche et qui ne doit pas modifier
la valeur des paramètres qui lui sont fournis.
Toute tentative de modification du contenu d'une variable déclarée const génère une erreur à la
compilation : seules les opérations de lecture sont autorisées. On est donc assuré à la déclaration d'une telle
variable qu'aucune modification de valeur ne sera permise. Cette restriction entraîne qu'il n'est pas possible
de définir un pointeur « normal » sur une variable constante : le contenu de la variable pourrait alors être
modifié via le pointeur. On est alors obligé de définir un pointeur sur variable constante [PROG. 2.10]. De
même, si on souhaite que la valeur du pointeur ne soit pas modifiée au cours du programme, il faut définir
un pointeur constant sur variable constante [PROG. 2.10].
Il est toujours possible de considérer temporairement (le temps d'une fonction par exemple) comme
constante une variable non constante. L'inverse n'est pas possible : cela génère automatiquement une erreur
à la compilation. L'utilisation de fonctions constantes est étudié [§ 3.6].	II-J. Allocation dynamique de mémoire▲
Tout comme C, C++ permet de manipuler dynamiquement la mémoire. En C, ces manipulations étaient
implantées grâce aux fonctions malloc et free disponibles dans les bibliothèques standards C. En C++,
PROG. 2.9 - Utilisation des constantes.Sélectionnez
1 	// Un tableau 'tab' de 'taille' éléments contient-il 'elt' ?
3 	bool contient(const int tab[], const int taille, const int elt)
7 		for (i = 0; (i &lt; taille) &amp;&amp; (tab[i] != elt); i++)
8 			;
10 		return (i != taille);
13 	int main()
15 		const int j; // Erreur à la compilation !
16 		int taille = 1000;
17 		int tab[1000];
18 		bool appartient;
19 		int i;
21 		...
22 		appartient = contient(tab, taille, i);
23 		...
PROG. 2.10 - Pointeurs et constantes.Sélectionnez
1 const int taille = 1000;
2 int compteur;
3 int *ptaille1 = &amp;taille; 				// Erreur à la compilation !
4 const int *ptaille2 = &amp;taille; 		// Ok
5 const int *const ptaille3 = &amp;taille; 	// Ok
7 *ptaille2 = 1010; 					// Erreur à la compilation !
8 ptaille2 = &amp;compteur; 				// Ok. 'compteur' devient constant via ptaille2
9 *ptaille2 = 10; 						// Erreur à la compilation !
10 compteur = 10; 						// Ok
11 ptaille3 = &amp;compteur; 				// Erreur à la compilation !
ces manipulations sont implantées via les opérateurs new et delete et il n'est plus nécessaire d'utiliser
l'opérateur sizeof conjointement avec la fonction malloc. Le principe d'utilisation reste globalement
inchangé, le programmeur doit être vigilant : à chaque allocation doit correspondre une désallocation de
Lorsque les allocations portent sur des tableaux plutôt que des objets simples, les opérateurs new
et delete sont remplacés respectivement par new [] et delete []. Voir [PROG. 2.11] pour des
exemples d'utilisation de l'allocation dynamique de mémoire en C++ et la comparaison par rapport aux
mêmes exemples en C.
PROG. 2.11 - Allocation dynamique de mémoire en C++.Sélectionnez
1 	// Allocation d'une variable et d'un tableau en C
3 	#include &lt;stdlib.h&gt;
5	 main()
7 		int *pi = malloc(sizeof(int));
8 		int *tab = malloc(sizeof(int) * 10);
10 		if ((pi != NULL) &amp;&amp; (tab != NULL)) {
11 			...
12 			free(pi);
13 			free(tab);
17 	// Allocation d'une variable et d'un tableau en C++
19 	main()
21 		int *pi = new int;
22 		int *tab = new int[10];
24 		if ((pi != NULL) &amp;&amp; (tab != NULL)) {
25 			...
26 			delete pi;
27 			delete [] tab;
Attention : les mécanismes d'allocation dynamique de mémoire en C ou en C++ font intervenir des
structures internes gérées par le langage. Ces structures sont différentes selon que l'on utilise la paire
malloc/free ou la paire new/delete. Il est ainsi d'usage de n'utiliser que la première en C et que la
deuxième en C++
II-K. La surcharge▲
La surcharge est une des nouvelles techniques présentées [§ 1.3.2] permettant d'améliorer la réutilisabilité
en conception objet. Elle permet d'attribuer le même nom à plusieurs opérateurs ou à plusieurs fonctions.
L'ambiguïté sur la fonction appelée est alors levée après examen du contexte, c'est-à-dire du nombre
et/ou du type des paramètres. Cette technique est disponible en C++, qui offre ainsi la possibilité de définir
plusieurs fonctions portant le même nom, à la condition que ces fonctions aient des profils différents. Cela	permet par exemple de déclarer dans un même programme les fonctions présentées [PROG. 2.12].
PROG. 2.12 - Exemples de surcharge de fonction.Sélectionnez
1 int max(int a, int b);
2 int max(int *tab, int taille);
3 int max(int a, int b, int c);
Une définition séparée de la fonction max est nécessaire pour chaque prototype de la fonction. Les
fonctions ont cependant toutes le même but : extraire la valeur maximale en exploitant les paramètres
qui leurs sont fournis. Ceci facilite donc la tâche du programmeur, qui peut associer un nom unique à
une action déterminée, quels que soient les arguments fournis. La fonction correspondant aux paramètres
fournis est alors exécutée. Si les types des paramètres fournis à une fonction surchargée ne concorde pas
totalement avec l'une des définitions, un certain nombre de règles sont utilisées pour choisir (ou ne pas
choisir) l'implantation de la fonction qui est utilisée. Ces règles ne sont pas toujours triviales, consulter un
ouvrage de référence pour plus d'informations.
Il est également possible de surcharger des opérateurs. Une application est présentée [§ 2.12], et le
paragraphe [§ 3.8] présente la surcharge dans le contexte particulier des classes.	II-L. Les entrées-sorties en C++▲
Les entrées-sorties ont été remaniées en C++, notamment pour profiter des avantages offerts par la surcharge.
Comme en C, elle ne font pas partie intégrante du langage, mais sont définies dans les bibliothèques
standards fournies avec le langage. Pour pouvoir les utiliser, il faut inclure l'en-tête <iostream>. Les
entrées-sorties sont ensuite réalisées par l'intermédiaire de trois flots :
cin : flot d'entrée standard (par défaut le clavier) ;
cout : flot de sortie standard (par défaut l'écran) ;
cerr : flot de sortie des messages d'erreur (par défaut l'écran).
Les opérateurs << et >> permettent ensuite de réaliser respectivement les écritures et les lectures. Ils
sont surchargés pour supporter tous les types de base disponibles en C++ [PROG. 2.13]. L'opération endl
permet de passer à la ligne et de vider le tampon 2 correspondant au flot. Elle a donc le même rôle que le
caractère '\n' en C, qui peut être utilisé en C++ aussi, mais ne permet alors que de passer une ligne.
Il y a plusieurs avantages à utiliser les entrées-sorties C++ par rapport aux entrées-sorties C:
La vitesse d'exécution est plus rapide. En effet, à chaque appel de la fonction printf, les paramètres sont analysés (en particulier la chaîne de formatage). Avec les flots, la traduction est effectuée au moment de la compilation.
Il n'y a plus de problème de type. Les erreurs typiques en C (associer un spécificateur %d avec un réel par exemple) ne peuvent plus se produire.
Les instructions générées sont plus réduites : il n'y a plus que le code correspondant effectivement à la valeur manipulée, alors que tout le code correspondant à printf est inclus sinon.
L'utilisateur a la possibilité de surcharger les opérateurs << et >> pour manipuler les types qu'il définit, ce qui permet d'homogénéiser les entrées-sorties.
PROG. 2.13 - Exemples d'entrées-sorties en C++.Sélectionnez
1 	#include &lt;iostream&gt;
3 	main()
5 		int age;
6 		int annee;
8 		cout &lt;&lt; "Entrez votre age : ";
9 		cin &gt;&gt; age;
10 		cout &lt;&lt; "Entrez l'année courante : ";
11 		cin &gt;&gt; annee;
12 		cout &lt;&lt; "Vous êtes né en " &lt;&lt; (annee - age) &lt;&lt; endl;
De toute façon, tout comme pour l'allocation dynamique de mémoire, les entrées-sorties font intervenir des structures internes gérées par le langage (les tampons). Les fonctions du type printf ou scanf ne sont donc plus du tout utilisées en C au profit des opérateurs manipulant les flots, sous risque d'interférences entre ces deux mécanismes.
Le format des entrées-sorties est décrit en annexe A. La gestion des fichiers en C++ est présentée
brièvement [§ A.4]
II-M. Les fonctions template▲
Parmi les techniques présentées [§ 1.3.2] pour améliorer la réutilisabilité, nous avons présenté la notion
de généricité, qui permet de paramétrer les fonctions (et les classes, voir pour cela [§ 3.9]) par un type de
données. Quelle peut en être l'utilité? L'utilité se mesure dans l'abstraction offerte par cette fonctionnalité.
Prenons un exemple concret. Supposons que l'on souhaite écrire une fonction min qui accepte deux
paramètres et qui renvoie la plus petite des deux valeurs qui lui est fournie. On désire bénéficier de cette
fonction pour certains types simples disponibles en C++ (int, char, float, double). La première
solution pour atteindre ce but est d'utiliser la surcharge [§ 2.11] et de définir 4 fonctions min, une
pour chacun des types considérés [PROG. 2.14].
PROG. 2.14 - Définition des fonctions min grâce à la surcharge.Sélectionnez
1 	int min (int a, int b) {
2 		return ((a &lt; b)? a : b);
5 	float min (float a, float b) {
6 		return ((a &lt; b)? a : b);
9 	double min (double a, double b) {
10 		return ((a &lt; b)? a : b);
13 	char min (char a, char b) {
14 		return ((a &lt; b)? a : b);
Lors d'un appel à la fonction min, le type des paramètres est alors considéré et l'implantation correspondante
est finalement appelée. Ceci présente cependant quelques inconvénients :
La définition des 4 fonctions mène à des instructions identiques, qui ne sont différenciées que par le type des variables qu'elles manipulent. On s'aperçoit ici que plus qu'une fonction, on souhaiterait exprimer une méthode, valable pour n'importe quel type manipulé : la fonction min est la fonction qui renvoie le plus petit des paramètres qui lui est fourni. Cet élément est déterminé grâce à l'opérateur < qui établit une relation d'ordre sur le type d'élément considéré.
Si on souhaite étendre la définition de cette fonction à de nouveaux types, il faut définir une nouvelle implantation de la fonction min par type considéré.
Une autre solution est de définir une fonction template, c'est-à-dire générique [PROG. 2.15]. Cette
définition définit en fait un patron de fonction, qui est instancié par un type de données (ici le type T) pour
produire une fonction par type manipulé.
PROG. 2.15 - Définition de la fonction min générique.Sélectionnez
1 	template &lt;class T&gt;
2 	T min (T a, T b)
4		 return ((a &lt; b)? a : b);
7 	main()
9 		int a = min(10, 20); // int min(int, int)
10 		float b = min(10.0, 25.0); // float min(float, float)
11 		char c = min('a', 'W'); // char min(char, char)
Il n'est donc plus nécessaire de définir une implantation par type de données. De plus, la fonction min
est valide avec tous les types de données dotés de l'opérateur <. On définit donc bien plus qu'une fonction,
on définit une méthode permettant d'obtenir une certaine abstraction en s'affranchissant des problèmes de
type. Quelques remarques :
Il est possible de définir des fonctions template acceptant plusieurs types de données en paramètre.Chaque paramètre désignant une classe est alors précédé du mot-clé class, comme dans l'exemple : template <class T, class U> ....
Chaque type de données paramètre d'une fonction template doit être utilisé dans la définition de cette fonction.
Pour que cette fonctionnalité soit disponible, les fonctions génériques doivent être définies dans des fichiers d'interface (fichiers .H) 3. Les fonctions template sont en effet expansées elles aussi. Ainsi, chaque appel fait à ce genre de fonctions est remplacé, à la précompilation, par le code source correspondant à la fonction.
3. Les fonctions inline et template sont ainsi les seules fonctions à être définies dans les interfaces. Toutes les autres sont
définies dans les fichiers d'implantation (.C) et sont seulement déclarées dans les interfaces.
Les classes C++ permettent d'implanter la notion de paquetage introduite [§ 1.3.2]. Une classe C++
peut se rapprocher de la notion de module définie en C, et se trouve implantée dans deux fichiers : l'interface
et l'implantation. On trouve à l'intérieur d'une classe C++ des attributs, correspondants aux variables
définies dans la classe, et des méthodes correspondant aux services offerts par la classe [PROG. 3.1, 3.2].
PROG. 3.1 - Interface de la classe Article (Article.H).Sélectionnez
1 	#ifndef __ARTICLE_H_INCLUDED__
2 	#define __ARTICLE_H_INCLUDED__
4 	class Article {
5 	public:
6 		// Nom de l'article
7 		const char* nom();
9 		// Prix brut de l'article
10 		float prixBrut();
12 		// Quantité en stock
13 		int quantite();
15 		// Ajout d'articles dans le stock
16 		void ajout(int quantite);
18 		// Suppression d'articles dans le stock
19 		void suppression(int quantite);
22 	char* _nom; // Nom de l'article
23 	float _prixBrut; // Prix brut de l'article
24 	int _quantite; // Quantité en stock
27 #endif /* __ARTICLE_H_INCLUDED__ */
Pour déclarer une classe C++, il faut utiliser le mot-clé class. Les différents attributs et méthodes
sont ensuite énumérés de la même manière que des champs sont définis à l'intérieur d'une structure C. Il y
a cependant quelques différences :
PROG. 3.2 - Implantation de la classe Article (Article.C).Sélectionnez
1 #include "Article.H"
3 // **********************************************************************
5 const char*
6 Article::nom()
8 		return _nom;
11 // **********************************************************************
14 Article::prixBrut()
16 		return _prixBrut;
19 // **********************************************************************
22 Article::quantite()
24 		return _quantite;
27 // **********************************************************************
30 Article::ajout(int quantite)
32 		_quantite += quantite;
35 // **********************************************************************
38 Article::suppression(int quantite)
40 		_quantite -= quantite;
Les structures C ne permettent que de regrouper des variables, alors que les classes C++ permettent également de regrouper des fonctions (les méthodes). Ces fonctions sont également appelées fonctions membres pour les différencier des fonctions qui sont définies en dehors de toute classe.
Une classe C++ peut comporter plusieurs sections, chacune d'elles étiquetée par l'un des mots-clés suivants : public, protected, private. Ces mots-clés permettent de définir le niveau de masquage de l'information tel qu'il a été défini [§ 1.2.3] :
une section public permet de déclarer les attributs ou les méthodes qui sont accessibles par n'importe quel client de la classe, c'est-à-dire à n'importe quel autre endroit du programme, que ce soit à l'intérieur de cette classe, d'une autre classe, ou en dehors du contexte de toute classe ;
une section protected ou private permet de déclarer les attributs et méthodes qui sont inaccessibles aux clients extérieurs à la classe, mais pas à la classe elle-même. Toute tentative d'accès à ces informations par un client extérieur se soldera alors par une erreur de compilation. La distinction qui existe entre les sections protected et private ne concerne pas les clients d'une classe, mais est relative à l'héritage ; elle est expliquée [§ 4.2].
Remarque : afin de bien différencier les identificateurs (attributs, méthodes) publics de ceux qui ne
le sont pas, on utilise généralement une convention d'écriture, qui correspond ici au préfixe '_'utilisé.
Dans ce contexte, les attributs de classe sont conventionnellement protégés, en les déclarant dans une section autre que public. La conséquence de ce masquage de l'information est que les attributs d'une classe ne sont plus directement accessibles. On définit alors des méthodes permettant d'accéder à leur valeur. Ces méthodes sont désignées sous l'appellation fonctions d'accès. C'est ce qu'on appelle de l'encapsulation : la structure de données réelle est masquée et peut donc évoluer. Du coup, on ne montre que ce que l'on veut, et sous la forme que l'on veut.	La définition des méthodes d'une classe se fait dans le fichier d'implantation de la classe (sauf les
méthodes inline [§ 3.3] et template [§ 3.9]). Le prototype des méthodes est repris et est préfixé par le nom
de la classe à laquelle appartiennent les méthodes et par l'opérateur de résolution de portée '::'.	4. C'est-à-dire l'identificateur correspondant à la méthode, le type de ses paramètres et le type de son résultat.
Un exemple d'utilisation de cette classe se trouve [PROG. 3.3]. Une classe définit un type, qui peut
être utilisé comme tout autre type disponible. On note au passage la notation pointée ('.') utilisée pour
appliquer une méthode sur un objet ou sur une référence sur objet. Cette notation se transforme en notation
fléchée ('->') si on dispose d'un pointeur sur objet. La ligne 11 par exemple présente l'application de la
méthode supprimer sur l'objet unArt de type Article.
Syntaxiquement, c'est une des grandes différences entre C et C++. En C [PROG. 3.4], la conception est
axée autour des traitements : les appels fonctionnels sont donc réalisés en appelant des fonctions auxquelles
sont passés en paramètres les données à traiter. En C++ [PROG. 3.5], la conception est axée autour des
données : les appels fonctionnels sont donc réalisés en appliquant des fonctions (les méthodes) sur les
données (les objets). Du coup, la notation est affectée, même si, intuitivement, cela ne représente que peu
de changement : il suffit de considérer qu'en C++, l'objet sur lequel on applique une méthode serait, en C,
le premier paramètre de la fonction correspondant à la méthode.
PROG. 3.3 - Exemple d'utilisation de la classe Article.Sélectionnez
5 	Article unArt;
6 	Article *unAutre;
7 	Article &amp;refArt = unArt;
9 	cout &lt;&lt; "Quantité disponible de " &lt;&lt; unArt.nom()
10 		 &lt;&lt; " : " &lt;&lt; unArt.quantite() &lt;&lt; endl;
11 	unArt.suppression(3);
12 	cout &lt;&lt; "Nouvelle quantité de " &lt;&lt; unArt.nom()
13 	 	 &lt;&lt; " : " &lt;&lt; unArt.quantite() &lt;&lt; endl;
15 	cout &lt;&lt; "Nom de l'autre : " &lt;&lt; unArt-&gt;nom() &lt;&lt; endl;
16 	cout &lt;&lt; "Nom du référencé : " &lt;&lt; refArt.nom() &lt;&lt; endl;
PROG. 3.4 - Appel fonctionnel réalisé en C. Sélectionnez
2 	Extrait de l'interface d'un module C 'Article'
7 } Article;
9 void ajoutArticle(Article art, int quantite);
12 	Exemple d'utilisation
17 	Article a;
19 	...
21 	ajoutArticle(a, 10);
PROG. 3.5 - Appel fonctionnel réalisé en C++.Sélectionnez
2 	Extrait de l'interface d'une classe C++ 'Article'
5 class Article {
8 	void ajout(int quantite);
13 	Exemple d'utilisation
18 	Article a;
20 	...
22 	a.ajout(10);
Au niveau mémoire, chaque définition d'un nouvel article entraîne la réservation d'un espace mémoire
permettant de représenter tous les attributs de la classe Article (le nom, le prix, la quantité). Chaque
article possède donc sa propre copie d'attributs, ce qui est identique au comportement que le langage
C propose avec les structures. La différence, encore une fois, n'est que syntaxique. Ainsi, en reprenant
l'exemple d'appel à la méthode supprimer (ligne 11 du [PROG. 3.3]), la diminution de stock effectuée
ligne 40 [PROG. 3.2] va donc affecter les attributs associés à l'objet unArt (lignes 5 et 11 du [PROG. 3.3]).
III-C. Les méthodes inline▲
Nous avons vu que C++ permettait de définir des fonctions inline [§ 2.6]. Cette fonctionnalité est
également disponible avec les méthodes. Il est possible de déclarer de telles méthodes de deux façons
différentes [PROG. 3.6] :
Soit en définissant directement une méthode à l'intérieur d'une classe plutôt que d'effectuer une simple déclaration (méthode nom).
Soit en utilisant le mot-clé inline lors de la déclaration, ce qui permet de définir la méthode en dehors de la partie déclarative de la classe. La définition doit cependant dans ce cas être présente dans le fichier d'interface de la classe (méthode prixBrut).
Ainsi, dans tous les cas, une méthode inline se trouve définie dans le fichier d'interface d'une classe.
La raison est la même que dans le cas des fonctions inline : la méthode est expansée lors de l'étape de
précompilation, ce qui implique que le précompilateur doit avoir à sa disposition le corps de la méthode en
PROG. 3.6 - Exemples de méthodes inline.Sélectionnez
1	 #ifndef __ARTICLE_H_INCLUDED__
7 		const char* nom() {return _nom}
10 		inline float prixBrut();
12 	protected:
13 		char* _nom; 		// Nom de l'article
14 		float _prixBrut; 	// Prix brut de l'article
17 	inline float
18 	Article::prixBrut()
20 		return _prixBrut;
23 	#endif /* __ARTICLE_H_INCLUDED__ */
III-D. Les constructeurs▲
Il est souvent nécessaire d'initialiser les objets au moment de leur création. Dans le cas de la classe
Article en particulier, on souhaite pouvoir attribuer un nom, un prix et une quantité à tout nouvel article
créé. Dans d'autres cas, ce peut être pour initialiser certains attributs spéciaux (comme des compteurs),
pour effectuer une allocation mémoire, etc. Une solution pourrait être de définir pour toutes ces classes
une méthode init qui réaliserait les initialisations souhaitées. Mais cela est problématique : pour toute
création d'un nouvel objet, deux actions vont être nécessaires (déclaration + appel de la méthode init)
alors que la création d'un objet est a priori une action atomique. Et que faire si un client oublie d'appliquer
la méthode init?
Pour résoudre ce problème, C++ possède un mécanisme d'initialisation automatique d'objets de classe.
Une ou plusieurs méthodes particulières, appelées constructeurs, sont appliquées implicitement dès qu'un
objet est défini. Ces constructeurs, généralement publics, portant le même nom que la classe à laquelle ils
appartiennent [PROG. 3.7, 3.8]. Quelques commentaires généraux et relatifs à l'exemple présenté :
Les constructeurs n'ont aucun type de retour (même pas void) et ne sont jamais appelés explicitement par le programmeur. C'est le compilateur qui se charge de le faire à chaque création d'objet, après avoir choisit le constructeur à utiliser en fonction des paramètres d'initialisation fournis (principe de la surcharge). Voir [PROG. 3.9] pour des exemples relatifs à chacun des constructeurs définis.
Le premier constructeur de la classe Article est un constructeur particulier, dit constructeur par défaut, qui est utilisé lorsqu'un objet est instancié sans paramètre. L'action réalisée ici est d'initialiser le pointeur correspondant au nom de l'article à la valeur nulle pour indiquer que l'article est incomplet.
Le second constructeur est également un constructeur particulier, dit constructeur de copie, utilisé lorsqu'un objet est créé à partir d'un autre objet de même type. On peut noter la syntaxe à utiliser	PROG. 3.7 - Constructeurs et destructeur de la classe Article (interface).Sélectionnez
1 	#ifndef __ARTICLE__
2 	#define __ARTICLE__
4 	#include &lt;string.h&gt;
6 	class Article {
7 	public:
8 		// Constructeur par défaut
9 		Article();
11 		// Constructeur de copie
12 		Article(const Article&amp; unArt);
14 		// Constructeur normal
15 		Article(const char* nom, float prixBrut, int quantite = 0);
17 		// Destructeur
18 		virtual ~Article();
20 	protected:
21 		char* _nom; // Nom de l'article
22 		float _prixBrut; // Prix brut de l'article
23 		int _quantite; // Quantité en stock
24 	};
26 	#endif
pour spécifier à C++ que l'on souhaite définir un constructeur de copie : Type (const Type&
unObjet) ; cette syntaxe est obligatoire pour définir ce type de constructeur. Remarques sur ce constructeur :
les attributs de l'objet passé en paramètre sont recopiés dans les attributs de l'objet à créer, en prenant garde d'allouer un nouvel espace mémoire pour stocker le nom de l'article ;
les attributs ne sont pas recopiés si l'objet passé en paramètre est incomplet. Le nouvel objet devient alors lui-même un objet incomplet ;
on peut ici accéder directement aux attributs de l'objet passé en paramètre, sans avoir à utiliser les fonctions d'accès définies. Ceci n'est bien entendu possible que parce que l'objet à créer appartient à la même classe que l'objet passé en paramètre. Il n'est donc pas un « client ordinaire ».
Le troisième constructeur se charge de l'initialisation d'un objet à partir de toutes les données nécessaires pour cela. Si la quantité est omise, elle prend alors la valeur 0 par défaut. Ici encore, on alloue un nouvel espace mémoire pour stocker le nom de l'article. Ce nom se trouve ainsi dans un espace propre à l'objet et ne peut donc pas être affecté par un événement extérieur, comme cela aurait pu être le cas si on avait simplement effectué une copie de pointeurs.
Un constructeur marche en deux temps : il effectue tout d'abord l'initialisation des attributs de la classe en utilisant leur constructeur par défaut, et exécute seulement ensuite le corps du constructeur.	Il n'est cependant pas toujours souhaitable d'utiliser le constructeur par défaut des attributs
d'une classe. Il faut dans ce cas placer l'appel aux constructeurs des attributs entre le prototype du
constructeur de l'objet et son corps, en les séparant du symbole ':' [PROG. 3.10]. Plusieurs appels
situés à ce niveau devront être séparés par des virgules.
PROG. 3.8 - Constructeurs et destructeur de la classe Article (implantation).Sélectionnez
1 	#include "Article.H"
4 	Article::Article()
6 		_nom = 0;
9 	// **********************************************************************
11 	Article::Article(const Article&amp; unArt)
13 	if (unArt._nom) {
14 		_nom = new char[srlen(unArt._nom) + 1];
15 		strcpy(_nom, unArt._nom);
16 		_prixBrut = unArt._prixBrut;
17 		_quantite = unArt._quantite;
19 	else
20 		_nom = 0;
23 	// **********************************************************************
25 	Article::Article(const char* nom, float prixBrut, int quantite)
27 		_nom = new char[strlen(nom) + 1];
28 		strcpy(_nom, nom);
29 		_prixBrut = prixBrut;
30 		_quantite = quantite;
33 	// **********************************************************************
35 	Article::~Article()
37 		if (_nom)
38 			delete [] _nom;
PROG. 3.9 - Appels des constructeurs et du destructeur de la classe Article.Sélectionnez
5 	Article a; 						// Constructeur par défaut appelé
6 	Article b("livre", 100, 10); 	// 3ème constructeur appelé
7 	Article c(b); 					// Constructeur par copie appelé
8 	Article *d;
10 d = new Article(a); 				// Constructeur par copie appelé
12 delete d; 						// Destructeur de 'd' explicitement appelé
13 } 								// Destructeurs de 'a', 'b' et 'c'
14 									// implicitement appelés
PROG. 3.10 - Exemple d'initialisation sans appel au constructeur par défaut.Sélectionnez
3 class LotArticles {
5 	LotArticle(const char* nom, float prixBrut, int quantite, int nbLot)
6 		: _art(nom, prixBrut, quantite), _nbLot(nbLot)
11 	Article _art; // Article du lot
12 	int _nbLot; // Nombre d'articles par lot
III-E. Le destructeur▲
Le mécanisme automatique d'initialisation proposé par les constructeurs est très pratique. Mais d'autres
problèmes surviennent : à quel moment libérer l'espace mémoire alloué pour stocker le nom d'un article en
particulier? C++ apporte également une solution à ce problème grâce à un second mécanisme automatique,
permettant d'appliquer une méthode particulière, appelée destructeur, au moment de la destruction de
l'objet. Quelques remarques sur cette méthode :
il n'y a qu'un seul destructeur possible par classe ;
l'identificateur désignant le destructeur est composé du nom de la classe préfixé par le symbole '˜' (ce qui donne ainsi le nom ˜Article pour désigner le destructeur de la classe Article) ;
les destructeurs n'acceptent aucun paramètre et ne renvoient aucun résultat ;
dans l'exemple présenté [PROG. 3.8], le destructeur de la classe n'a qu'un seule rôle : libérer l'espace mémoire alloué dynamiquement pour stocker le nom de l'article ;
il est préférable de préfixer les destructeurs du mot-clé virtual. Une explication de cette convention est fournie [§ 4.5] ;
les destructeurs, comme les constructeurs, ne sont jamais explicitement appelés dans un programme. Ils sont utilisés automatiquement et implicitement par le compilateur au moment de la création et de la destruction des objets.
III-F. Méthodes constantes▲
Après avoir étudié les variables constantes [§ 2.9], nous étudions dans ce paragraphe les méthodes
constantes. Nous avons vu à propos des variables constantes que toute tentative de modification qui leur
est relative se solde par une erreur de compilation. Dans le contexte particulier des instances de classe,
cela n'est pas toujours approprié car les objets sont rarement directement modifiés, vu que leurs attributs
sont généralement cachés. En fait, ce sont généralement des méthodes publiques qui sont invoquées pour
réaliser ces modifications. Et c'est pour assurer la constance des objets que les méthodes constantes ont été
introduites. Le concepteur d'une classe peut ainsi indiquer les méthodes qui peuvent être utilisées en toute
sécurité, sans risquer de modifier les objets de cette classe.
Pour rendre une méthode constante, il suffit de placer le spécificateur const après le prototype de la
méthode. Les méthodes constantes sont ensuite les seules à pouvoir être appliquées sur des objets constants.
Elles ne peuvent ainsi pas comporter d'instruction permettant de changer la valeur de l'objet sous peine
d'erreur de compilation. Un exemple de définition et d'utilisation est présenté [PROG. 3.11].
PROG. 3.11 - Définition et utilisation des méthodes constantes.Sélectionnez
3 	class Article {
4 	public:
5 		// Nom de l'article
6 		const char* nom() {return _nom;}
8 		// Nom de l'article (const)
9 		const char* nomCste() const {return _nom;}
11 		// Change le nom de l'article (const)
12 		// -&gt; ERREUR À LA COMPILATION
13 		void changeNom(char *nom) const {_nom = nom};
15 	protected:
16 		char* _nom; // Nom de l'article
19 	void testAffichage(Article a, const Article b)
21	 	cout &lt;&lt; a.nom() &lt;&lt; endl; // Ok
22	 	cout &lt;&lt; a.nomCste() &lt;&lt; endl; // Ok
24 		cout &lt;&lt; b.nom() &lt;&lt; endl; // Erreur compilation
25 		cout &lt;&lt; b.nomCste() &lt;&lt; endl; // Ok
III-G. Le mot-clé this▲
On a parfois besoin de désigner à l'intérieur d'une fonction membre l'objet qui est manipulé par la méthode.
Comment le désigner cependant alors qu'il n'existe aucune variable le représentant dans la fonction
membre? Les fonctions membres travaillent en effet directement sur les attributs de classes : ceux qui sont
atteints correspondent alors à ceux de l'objet courant. C++ apporte une solution à ce problème en introduisant
le mot-clé this qui permet à tout moment dans une fonction membre d'accéder à un pointeur sur
l'objet manipulé. Un exemple d'application se trouve [PROG. 3.12].	III-H. La surcharge▲
Il est possible d'utiliser la surcharge à l'intérieur des classes, de la même manière que cela est possible
sur les fonctions classiques [§ 2.11]. Les constructeurs de classe en sont d'ailleurs un très bon exemple ! La
surcharge est ainsi beaucoup utilisée dans le contexte des classes, que ce soit pour surcharger des méthodes
ou des opérateurs. Imaginons en effet que nous souhaitions disposer de l'opérateur d'affectation '=' pour
nous permettre de réaliser une affectation d'articles. Un opérateur d'affectation par défaut existe, mais il
n'effectue qu'une copie champ-à-champ des attributs de la classe. Ce comportement est ennuyeux dans le
cas de la classe Article car il ne permet pas de dupliquer le nom des articles, mais juste de réaliser une
copie des pointeurs.
PROG. 3.12 - Utilisation du mot-clé this.Sélectionnez
2 	#include "Article.H"
4 	// Fonction présente pour les besoins du test
6 	void testAffichage(Article *unArt)
8 		cout &lt;&lt; "Article : " &lt;&lt; unArt-&gt;nom() &lt;&lt; endl;
11 	// Méthode de la classe 'Article'
13 	void
14 	Article::methodeQuelconque()
16 		// Comment appeler la fonction 'testAffichage' ?
17 		// Avec le mot-clé 'this' !
19 	testAffichage(this);
PROG. 3.13 - Surcharge des opérateurs de la classe Article.Sélectionnez
1 	Article&amp; Article::operator=(const Article &amp;unArt)
3 	// Si l'article courant a un nom, on le libère
5 	if (_nom)
6 		delete [] _nom;
8 	// On recopie l'article passé en paramètre
10 	if (unArt._nom) {
11 		_nom = new char[srlen(unArt._nom) + 1];
12 		strcpy(_nom, unArt._nom);
13 		_prixBrut = unArt._prixBrut;
14 		_quantite = unArt._quantite;
16 	else
17 		_nom = 0;
Pour que l'opérateur d'affectation effectue les actions que nous attendons de lui, nous pouvons le surcharger
[PROG. 3.13]. Un grand nombre d'opérateurs peuvent être surchargés [TAB. 3.14], même s'il n'est
pas toujours souhaitable d'abuser de cette pratique, qui peut entraver la clarté du programme et être à l'origine
d'erreurs difficiles à localiser. En effet, ces opérateurs sont dans la plupart des cas définis par défaut.
Un programmeur qui n'a pas regardé attentivement la documentation d'une classe peut s'attendre à un
comportement donné en considérant un opérateur appliqué à un objet, alors que le comportement peut être
tout autre si l'opérateur est surchargé 5. Ce genre de désagrément n'arrive pas avec les méthodes, aucune
méthode n'étant définie par défaut pour une classe donnée. Cependant, il est courant de surcharger des opérateurs tels que l'affectation (=), l'inférieur (<), la différence (!=) ou l'égalité (==). C'est en particulier le
cas lors de l'utilisation de la bibliothèque STL livrée en standard avec C++ et détaillée chapitre 5.
5. On peut par exemple s'amuser à surcharger new pour qu'il effectue ce qui effectué par delete, et vice-versa ! Mais même
sans atteindre ce type de situations contre nature, la surcharge peut réserver quelques surprises... Cette fonctionnalité a d'ailleurs
été supprimée dans le langage Java, plus récent et souhaitant éliminer les causes d'erreurs fréquentes en C++.
TAB. 3.14 - Opérateurs pouvant être surchargés (liste non exhaustive).
III-I. Les classes template▲
Il est possible, comme pour les fonctions [§ 2.13], de définir des classes template, c'est-à-dire paramétrées
par un type de données. Cette technique évite ainsi de définir plusieurs classes similaires pour
décrire un même concept appliqué à plusieurs type de données différents. Elle est largement utilisée pour
définir tous les types de containers (comme les listes, les tables, les piles, etc.), mais aussi des algorithmes
génériques par exemple. La bibliothèque STL (chapitre 5) en particulier propose une implantation d'un
bon nombre de types abstraits et d'algorithmes génériques.
La syntaxe permettant de définir une classe template est similaire à celle qui permet de définir des
fonctions template. Voir [PROG. 3.15, 3.16] pour un exemple de classe template, portant sur des points
dont la précision de représentation (à partir d'entiers, de réels, etc.) est le type paramètre de la classe.
Comme dans le cas des fonctions template, tout le code source correspondant à des classes template (y compris la définition de leurs méthodes) doit se trouver dans l'interface de la classe correspondante.
Une classe template permet de définir des attributs, des paramètres ou des valeurs de retour de méthodes template. De façon réciproque, pour pouvoir définir des entités template à l'intérieur d'une classe, la classe doit elle-même être template.
Attention à la syntaxe des méthodes template définies en dehors du corps de la classe. La définition se fait : template <class T> typeRetour nomClasse<T>::nomMéthode(params).
III-J. Les amis▲
Il arrive parfois que l'on souhaite accorder des accès plus fins que les accès public, protected ou
private proposés par défaut. En particulier, il peut être intéressant d'accorder des accès aux attributs ou
aux méthodes à certaines classes clientes, tout en protégeant ce même accès vis-à-vis des autres classes.
Un des exemples les plus fréquents est la surcharge de l'opérateur << permettant d'afficher un objet. En
effet, afin de concaténer les opérateurs successivement (p.ex. cout << a << b << c << endl;),
le profil de la fonction est défini comme :
ostream&amp; operator&lt;&lt;(ostream&amp;, Classe&amp;);
où Classe est la classe qui est manipulée. Dans ce contexte, l'opérateur << n'est alors pas défini comme
membre de chaque classe, mais comme opérateur à portée globale, devant être surchargé pour chaque
type à manipuler. Problème : comment autoriser cet opérateur à accéder à des attributs de classe, tout en
protégeant ce même accès vis-à-vis des autres classes ? C++ introduit pour cela la notion d'amitié, dont
une définition intuitive est : « Un ami est quelqu'un qui peut toucher vos parties privées. » En clair, un ami
peut accéder aux attributs et aux méthodes protégées ou privées d'une classe. Cela permet de surcharger
<< comme présenté [PROG. 3.17].
PROG. 3.15 - Définition d'une classe template.Sélectionnez
2 	class Point {
3 	public :
4 		// Constructeur par défaut
5		 Point() : _x(0), _y(0) {}
7 		// Constructeur
8 		Point(T x, T y) : _x(x), _y(y) {}
10	 	// Accès à x
11 		const T x() const {return _x;}
13	 	// Accès à y
14	 	const T y() const {return _y;}
16	 	// Translation
17 		void translation(T x, T y);
19 	protected:
20 		T _x; // Abcisse
21 		T _y; // Ordonnée
22 	};
24 	template &lt;class T&gt; void
25	Point&lt;T&gt;::translation(T x, T y)
26	 {
27 		_x += x;
28		 _y += y;
PROG. 3.16 - Utilisation d'une classe template.Sélectionnez
1 	#include "Point.H"
3 	int main()
5 		Point&lt;int&gt; pointEntier(2, 3);
6 		Point&lt;float&gt; pointReel(3.14, 2.27);
8 		pointReel.translation(pointEntier.x(), pointEntier.y());
PROG. 3.17 - Amitié et Surcharge de « pour la classe Article.Sélectionnez
4	 public:
5		 // Un exemple de fonction amie
6 		friend ostream&amp; operator&lt;&lt;(ostream&amp;, Article&amp;);
8 		// Un exemple de classe amie
9 		friend class uneClasse;
11		 // Un exemple de méthode amie
12		 friend void autreClasse::methode();
14 	protected:
15 		char* _nom; // Nom de l'article
16 		float _prixBrut; // Prix brut de l'article
17 		int _quantite; // Quantité en stock
18 	};
20 	ostream&amp; operator&lt;&lt;(ostream&amp; os, Article&amp; art)
22 		os &lt;&lt; "Article " &lt;&lt; art._nom
23 			&lt;&lt; ", prix : " &lt;&lt; art._prixBrut
24 			&lt;&lt; ", quantité : " &lt;&lt; art._quantite
25 			&lt;&lt; endl;
27		 return os;
C++ permet de définir de cette manière des classes et des fonctions amies, suivant les accès que l'on
souhaite autoriser. La notion d'amitié constitue une infraction aux règles d'encapsulation, mais s'avère
utile dans certaines situations ; elle est généralement utilisée avec parcimonie.	III-K. Membres statiques▲
Jusqu'à présent, les attributs et méthodes tels que nous les avons définis ont toujours été liés aux objets.
En effet, dans le cas d'un attribut, une valeur différente de cet attribut peut être associée à chaque objet.
De même, les méthodes sont toujours invoquées par rapport à un objet, et ne peuvent pas être appelées en
dehors de tout contexte.
Dans certains cas, ce comportement n'est cependant pas toujours souhaité. On peut en effet avoir
envie de rattacher un attribut non pas à chaque instance d'une classe, mais à la classe elle-même. De la
même manière, il peut parfois être utile de pouvoir invoquer une méthode dans le contexte d'une classe, et
non dans celui d'un objet. Cette fonctionnalité est possible en C++ grâce aux membres statiques, appelés
également variables et méthodes de classe (par opposition aux variables et méthodes d'instance que nous
avons étudiés jusqu'à présent).
Par exemple, si nous souhaitons attribuer automatiquement un numéro d'article à chaque nouvel article
que nous créons, il est possible de définir un compteur statique au niveau de la classe article. Ce compteur
n'aura plus alors qu'à être incrémenté à chaque nouvel création d'article. La modification s'opère donc
au niveau de l'interface de la classe (pour la définition de ce nouvel attribut), ainsi que dans le ou les
constructeurs de cette classe [PROG. 3.18, 3.19]. Dans cet exemple, l'attribut _prochainNumero est
PROG. 3.18 - Utilisation d'un membre statique pour la création d'un compteur (extrait de l'interface).Sélectionnez
1 class Article {
3 	// Constructeur.
4 	Article();
6 	// Accès au numéro de l'article.
7 	int numero() const {return _numero;}
10 	int _numero; // Numéro de l'article
11 	static int _prochainNumero; // Variable de classe
PROG. 3.19 - Utilisation d'un membre statique pour la création d'un compteur (extrait de l'implémentation).Sélectionnez
3 int Article::_prochainNumero = 0;
5 Article::Article()
7 		_numero = _prochainNumero++;
donc commun à toutes les instances de la classe Article. En effet, étant donné que cette variable est
une variable de classe, elle est partagée par tous les objets issus de la classe Article. Attention à la
syntaxe de l'initialisation de telles variables : l'initialisation est impérativement unique, et se trouve dans
le fichier d'implémentation de la classe, en rappelant tous les éléments du contexte (type de la variable et
classe d'appartenance).
Cette variable possède des propriétés supplémentaires : il est possible (mais pas nécessaire) d'y accéder
sans passer par un objet. En effet, cette variable existant indépendamment des objets qui sont créés, il est
également possible d'y accéder ou de la modifier, même si aucune instance de la classe Article n'est
disponible. Pour cela, il suffit de définir des méthodes de classes, c'est-à-dire des méthodes statiques, qui
peuvent être appelées en dehors du contexte d'un objet ; il est cependant alors nécessaire de préciser le
contexte de la classe [PROG. 3.20, 3.21].
Attention : une méthode statique pouvant être appelée en dehors du contexte d'un objet, il est impossible
d'y intégrer des accès aux membres (attributs ou méthodes) non-statiques de la classe. Il est également
impossible, pour les mêmes raisons, de faire référence au pointeur this à l'intérieur d'une méthode statique.
La définition de membres statiques est très utile pour définir et utiliser des variables dont le comportement
est proche de variables globales. Il existe cependant des avantages à définir des membres statiques
PROG. 3.20 - Définition d'une méthode statique (extrait de l'interface).Sélectionnez
3 		// Constructeur.
4 		Article();
6 		// Accès au numéro de l'article.
7 		int numero() const {return _numero;}
9 // Accès au prochain numéro d'article.
10 static int prochainNumero() const {return _prochainNumero;}
13 		int _numero; // Numéro de l'article
14 		static int _prochainNumero; // Variable de classe
PROG. 3.21 - Utilisation d'une méthode statique.Sélectionnez
3 		int pn;
5 		pn = Article::prochainNumero();
plutôt que de vraies variables globales :
Les règles de masquage de l'information sont respectées. En particulier, une variable statique peut être publique ou privées, alors qu'une variable globale ne peut être que publique.
Une variable membre statique est définie à l'intérieur d'une classe, et ne peut donc pas interférer avec les membres définis dans les autres classes (pas de conflit de noms).
IV. L'héritage▲
L'héritage, qui a été évoqué [§ 1.4.3], est une technique de plus permettant de servir la réutilisabilité.
Son objectif est de permettre la définition aisée de sous-types, correspondant à une spécialisation (à
l'extension) de types existants. Imaginons en effet que nous souhaitions implanter une classe Boisson.
Les boissons sont des sortes d'articles, et doivent comporter à ce titre les même attributs que tout article
(_nom, _prixBrut, _quantite), ainsi qu'un autre qui est spécifique aux boissons : _volume.
L'héritage permet de réutiliser la définition de la classe Article pour définir la classe boisson.
IV-B. La dérivation▲
La dérivation de classe est la technique qui consiste à faire hériter une classe d'une autre classe en C++.
Au niveau terminologique, on dit ainsi qu'une classe B est dérivée d'une classe A si elle en hérite. La classe
A est alors appelée superclasse ou classe de base de la classe B. Dans l'exemple évoqué au paragraphe
précédent, la classe Boisson est une classe dérivée de la classe Article puisqu'elle la spécialise. Pour
spécifier en C++ qu'une classe B hérite d'une classe A, on déclare la classe B de la manière suivante :
class B : &lt;mode_dérivation&gt; class A { ... };
où le mode de dérivation permet de fixer le statut des membres (attributs, fonctions) de la classe B en
fonction du statut des membres de la classe A (voir le tableau [TAB. 4.1] pour un récapitulatif des différents
cas possibles). Le mode de dérivation par défaut est le mode private, mais le mode de dérivation le plus
courant est le mode public : c'est celui qui permet de donner les même statuts aux membres dérivés que
ceux des membres de base. Une définition de la classe Boisson dans ce contexte est donnée [PROG. 4.2].
TAB. 4.1 Statut des membres de la classe dérivée en fonction du statut des membres de la classe de base
PROG. 4.2 - Rappel de la classe Article et définition de la classe Boisson.Sélectionnez
4 // Classe Article
7 class Article {
9 		// Constructeur
10 		Article(const char* nom, float prixBrut,
11 				int quantite = 0, float tva = 1.206)
12 			: _prixBrut(prixBrut), _quantite(quantite), _tva(tva)
14 			_nom = new char[strlen(nom) + 1];
15 			strcpy(_nom, nom);
18 		// Prix brut de l'article
19 		float prixBrut() const;
21 		// Prix net de l'article
22 		float prixNet() const {return _prixBrut * _tva;}
25 		char* _nom; // Nom de l'article
26 		float _prixBrut; // Prix brut de l'article
27 		int _quantite; // Quantité en stock
28 		float _tva; // TVA appliquée à l'article
32 // Classe Boisson
35 class Boisson : public Article {
37 		// Constructeur
38 		Boisson(const char* nom, float prixBrut, int volume, int quantite = 0)
39 			: Article(nom, prixBrut, quantite), _volume(volume) {}
41 		// Accès au volume
42 		int volume() const {return _volume;}
45 		int _volume; // Volume de la boisson (cl)
Note : un attribut correspondant à la TVA a également été ajouté au niveau de la classe Article.
La classe Boisson, qui est avant tout un article, hérite des attributs et des méthodes définis dans la
classe Article, en dehors des constructeurs, du destructeur, du constructeur de copie et de l'opérateur
d'affectation. Le statut de ces membres hérités est fixé par le mode de dérivation. La classe Boisson
peut aussi définir de nouveaux membres, comme l'attribut _volume et la nouvelle méthode d'accès à cet
Lors de la création d'une boisson (c'est-à-dire d'un objet de ce type), tous les constructeurs de la
hiérarchie d'héritage sont automatiquement appelés, du plus général (ici Article) au plus particulier (ici
Boisson). Le type d'appel fait au constructeur de la classe Article est implanté dans le constructeur
de la classe Boisson, en utilisant la même syntaxe que pour initialiser les attributs de classe. Si cet appel
n'est pas spécifié, c'est le constructeur par défaut de la classe Article qui est invoqué.
Lors de la destruction de cette même boisson, les différentes destructeurs en présence dans la hiérarchie
d'héritage sont successivement appelés, dans l'ordre inverse de celui des constructeurs, c'est-à-dire du plus
particulier (Boisson) au plus général (Article).
Note : C++ permet également d'utiliser l'héritage multiple. Cette partie n'est pas abordé dans ce polycopié.
IV-C. Redéfinition de méthodes▲
L'héritage permet de bénéficier d'autres fonctionnalités, telles que la redéfinition de méthodes. Supposons,
pour illustrer cette technique, que nous souhaitions implanter une classe BoissonAlcoolisee.
Cette classe hérite de la classe Boisson et introduit deux nouveaux attributs :
_degre qui correspond au degré d'alcool contenu dans la boisson,
_accise qui contient le montant des accises 6.
Cette classe hérite donc des attributs et méthodes définis successivement dans les classes Article
et Boisson. Cependant, le calcul effectué pour calculer le prix net de la boisson doit être modifié pour
prendre en compte les droits d'accise. Pas de problème, l'héritage offre la possibilité de redéfinir la méthode
prixNet dans la classe BoissonAlcoolisee [PROG. 4.3]. Pour cela, l'entête de la méthode redéfinie
doit être absolument identique à l'original. La méthode prixNet originale reste applicable sur un objet
la classe BoissonAlcoolisee, mais il faut pour cela la préfixer du nom de la classe où elle a été
définie [PROG. 4.4].
L'avantage de cette technique est qu'il est ainsi possible de disposer de deux méthodes prixNet
différentes sur des articles - en fonction du type exact de l'article -, effectuant leur calcul de deux
manières différentes, tout en conservant une homogénéité de nom.
Attention : lorsqu'une méthode surchargée est redéfinie dans une classe dérivée, la redéfinition masque
toutes les définitions de la méthode de base, et pas seulement celles redéfinies.
6. Taxe appliquée à certains alcools, proportionnelle au degré d'alcool et au volume.
IV-D. Le polymorphisme▲
Un certain nombre de conversions standards sont automatiquement définies entre une classe de base et
ses classes dérivées de façon publique. Ainsi, pour une classe A de base et une classe B dérivée de A, des	conversions implicites sont définies :
d'un objet de type B vers un objet de type A,
d'un pointeur sur un objet de type B vers un pointeur sur un objet de type A,
d'une référence sur un objet de type B vers une référence sur un objet de type A.
PROG. 4.3 - Définition de la classe BoissonAlcoolisee.Sélectionnez
1 class BoissonAlcoolisee : public Boisson {
3 		// Constructeur
4 		BoissonAlcoolisee(const char *nom, float prixBrut, int volume,
5 						  float degre, float accise, int quantite = 0)
6 			: Boisson(nom, prixBrut, volume, quantite),
7 			_degre(degre), _accise(accise) {}
9 		// Prix net de l'article
10 		float prixNet() const
12 		return ((_prixBrut + _accise * _degre * _volume) * _tva);
16 		float _degre; // Degrés de la boisson
17 		float _accise; // Droits d'accise par degré et litre
PROG. 4.4 - Utilisation de la classe BoissonAlcoolisee.Sélectionnez
1 #include "BoissonAlcoolisee.H"
5 		Article table("Table", 250, 1);
6 		BoissonAlcoolisee biere("Bière", 3.40, .25, 4.7, .062);
8 		cout &lt;&lt; "Prix de la table : " &lt;&lt; table.prixNet() &lt;&lt; endl;
9 		cout &lt;&lt; "Prix de la bière : " &lt;&lt; biere.prixNet() &lt;&lt; endl;
10 		cout &lt;&lt; "Prix de la bière si elle n'était pas alcoolisée : "
11 			 &lt;&lt; biere.Article::prixNet() &lt;&lt; endl;
Ce type de conversions n'est pas risqué puisqu'un objet de type B est avant tout un objet de type A.
Dans le premier cas (celui des objets), c'est une conversion d'objet qui est effectuée : l'objet de type A est
affecté à l'objet de type B. Dans ce cas, seul les attributs définis dans A sont pris en compte, les éventuels
attributs supplémentaires définis dans B ne sont pas pris en considération. Dans les deux autres cas (ceux
des pointeurs et références), c'est seulement une conversion de type qui est réalisée : l'objet en lui-même
n'est pas affecté. Ce qui implique qu'un pointeur sur type A peut pointer vers un objet de type B. C'est ce
qu'on appelle le polymorphisme 7. Voir [PROG. 4.5] pour des exemples pratiques de polymorphisme.
7. En biologie, c'est la caractéristique d'un organisme qui peut se présenter sous diverses formes sans changer de nature.
PROG. 4.5 - Exemples de polymorphisme.Sélectionnez
2 #include "Article.H"
3 #include "BoissonAlcoolisee.H"
7 		// Avec des objets
9 		Article a("Sucre", 4.55);
10 		BoissonAlcoolisee b("Biere", 3.4, .25, 4.7, .062);
12 		cout &lt;&lt; a.nom() &lt;&lt; endl; // Sucre
13 		cout &lt;&lt; b.nom() &lt;&lt; endl; // Biere
15 		a = b;
17 		cout &lt;&lt; a.nom() &lt;&lt; endl; // Biere
19 		// Avec des pointeurs
21 		Article *pa = new Article("Sucre", 4.55);
22 		BoissonAlcoolisee *pb = new BoissonAlcoolisee("Biere", 3.4, .25,
23 													4.7, .062);
25 	cout &lt;&lt; pa-&gt;prixNet() &lt;&lt; endl; // Article::prixNet()
26 	cout &lt;&lt; pb-&gt;prixNet() &lt;&lt; endl; // BoissonAlcoolisee::prixNet()
28 		pa = pb;
29 		cout &lt;&lt; pa-&gt;prixNet() &lt;&lt; endl; // Article::prixNet()
31 		pb = (BoissonAlcoolisee*) pa; // Juste car pa pointe vers un alcool
32 		cout &lt;&lt; pb-&gt;prixNet() &lt;&lt; endl; // BoissonAlcoolisee::prixNet()
On ne peut pas accéder aux membres spécifiques à B à travers un pointeur ou une référence de type A. Les seuls membres accessibles sont donc ceux qui sont définis au niveau de la classe A. Un exemple pratique de cette remarque se trouve ligne 29 : même si pa pointe vers un objet de type BoissonAlcoolisee, c'est bien la méthode Article::prixNet qui est appelée.
Si la conversion implicite marche très bien du type B vers le type A, ce n'est pas le cas de la conversion inverse (d'un objet de type A vers un objet de type B) qui nécessite une conversion de type explicite (un cast). Voir en particulier l'exemple de la ligne 31 qui illustre cette situation. D'autre part, pour réaliser cette conversion, le programmeur doit être sûr qu'elle a un sens, c'est à dire que pa pointe effectivement vers une boisson alcoolisée. Il existe plusieurs moyens d'avoir cette assurance (ce qui est utile sur des exemples plus complets que celui présenté) :
Définir dans la classe de base un attribut permettant de stocker un identificateur unique pour chaque classe de la hiérarchie. En interrogeant préalablement cet identificateur avant une conversion de type, on sait exactement quel est le type de l'objet en présence, et on peut donc appliquer la conversion ad hoc. Cette technique a longtemps été la seule disponible dans ce genre de situation.Utiliser les nouveaux opérateurs de conversion définis dans la norme finale C++, qui permettent de réagir dynamiquement face aux types des objets manipulés, sans avoir à implanter quelque chose de plus. Ces nouveaux opérateurs ne sont cependant pas encore supportés par tous les compilateurs C++ et il est toujours recommandé d'utiliser l'ancienne méthode si le programme C++ est destiné à être porté sur d'autres compilateurs que celui utilisé lors de la conception. Ces opérateurs sont décrits [§ C.2].
IV-E. La liaison dynamique▲
L'héritage et le polymorphisme sont des fonctionnalités très puissantes pour représenter et manipuler
non seulement des objets, mais également des familles d'objets. Il existe cependant une entrave à la bonne
maniabilité de ces familles d'objets : c'est la liaison statique des méthodes à la compilation. En effet,
dans les exemples que nous avons étudiés jusqu'à présent, le choix de la méthode à appliquer à un objet est
déterminé à la compilation. C'est pour cette raison d'ailleurs que l'appel de la ligne 29 présent [PROG. 4.5]
a provoqué l'appel de la fonction Article::prixNet. Le compilateur s'est basé sur le type du pointeur
pa pour déterminer la méthode concernée et a effectué une liaison statique.
Ce comportement est parfois assez contraignant. Imaginons en effet que nous ayons un tableau de
pointeurs sur des objets de type Article, et que ces pointeurs pointent vers des articles ou des objets
issus des classes dérivées de Article (comme des alcools par exemple), ceci grâce au polymorphisme.
Comment faire pour afficher le prix net de chacun des articles contenus dans ce tableau? Une des solutions
est de convertir chacun de ces pointeurs, afin d'accorder leur type avec celui de l'objet pointé, et de calculer
ensuite le prix net [PROG. 4.6]. Cette solution n'est pas très élégante, et le programme doit être modifié
à chaque ajout d'un article redéfinissant la méthode prixNet, ce qui pose des problèmes évidents de
Pour éviter ce genre de situations, C++ permet de bénéficier de la liaison dynamique. Pour cela, il
suffit de définir les méthodes qui sont concernées par ce type de liaison comme virtuelles. Cette définition
s'effectue en préfixant la méthode concernée du mot-clé virtual 8 lors de la première déclaration de la
méthode . Une méthode déclarée virtuelle dans une classe de base le reste en effet dans toutes
les classes dérivées de la hiérarchie. Dès lors, il n'est plus nécessaire de convertir au fur et à mesure les
différents articles. La méthode qui correspond au type d'objet manipulé est dynamiquement déterminée,
c'est-à-dire pendant l'exécution du programme et non pas lors de sa compilation [PROG. 4.8].
Remarque importante : afin de pouvoir détruire tous les objets comme indiqué ligne 11 [PROG. 4.8],
il est nécessaire de déclarer comme virtuel le destructeur de la classe de base (Article). Dans le cas
contraire, c'est uniquement le destructeur de la classe de base qui sera appelé sur chacun des articles,
quelque soit leur type réel... On déclare donc conventionnellement tous 9 les destructeurs de classe comme
virtuels, à moins d'avoir de bonnes raisons de ne pas le faire.
8. Attention : ce mot-clé n'est utilisé que lors de déclarations, il ne faut pas le reprendre lors de la définition des méthodes
9. Même sur des classes qui ne sont pas destinées à être classe de base d'une hiérarchie. Cela peut éviter des problèmes si ces
classes sont finalement dérivées.
PROG. 4.6 - Résolution statique de l'application d'une méthode à une famille d'objets.Sélectionnez
1 #define MAXELTS 1000
5 	Article *lesArticle[MAXELTS];
7 	// Initialisation du tableau
8 	// avec des articles hétérogènes
10 	for (int i = 0; i &lt; MAXELTS; i++)
11 		switch (lesArticle[i]-&gt;type()) {
12 		case ARTICLE:
13 		cout &lt;&lt; lesArticle[i]-&gt;prixNet() &lt;&lt; endl;
16 	case ALCOOL:
17 		BoissonAlcoolisee *ba;
19 		ba = (BoissonAlcoolisee*) lesArticle[i];
20 		cout &lt;&lt; ba-&gt;prixNet() &lt;&lt; endl;
21 		break;
23 		// Et ainsi de suite pour les autres articles...
PROG. 4.7 - Déclaration d'une méthode virtuelle dans la classe Article.Sélectionnez
4 		virtual float prixNet() const;
PROG. 4.8 - Résolution dynamique de l'application d'une méthode à une famille d'objets.Sélectionnez
9 	for (int i = 0; i &lt; MAXELTS; i++) {
10 		cout &lt;&lt; lesArticle[i]-&gt;prixNet() &lt;&lt; endl;
11 		delete lesArticle[i];
14 	// Plus de distinction à faire : la bonne méthode est
15 	// automatiquement appelée en fonction du type
16 	// dynamique de l'article courant
IV-F. Les classes abstraites▲
Nous avons vu dans le paragraphe précédent comment déclarer des fonctions virtuelles. C++ autorise
la déclaration de fonctions virtuelles pures, c'est-à-dire de fonctions virtuelles dont la définition n'est pas
donnée. Dans l'exemple présenté [PROG. 4.9], une classe FigureGeometrique est définie, destinée
à être la classe de base de toutes les figures géométriques. Suivant leurs caractéristiques, ces figures vont
être implantées dans diverses classes dérivées (on pourrait imaginer les figures en 2D, en 3D, etc.). Dans
ce contexte, la classe FigureGeometrique sert de cadre générique pour la définition des méthodes
dans les classes dérivées. Ainsi, chaque figure doit pouvoir être dessinée, et c'est ce qui est exprimé par
l'intermédiaire de la fonction virtuelle pure dessin.
PROG. 4.9 - Déclaration d'une fonction virtuelle pure.Sélectionnez
1 	class FigureGeometrique {
4 		FigureGeometrique();
6 		// Dessin (Fonction virtuelle pure)
7 		virtual void dessin() const = 0;
La fonction dessin, qui est une fonction virtuelle pure, n'a pas de définition. Cette définition devra
être donnée dans les classes dérivées. Lorsqu'une classe, comme la classe FigureGeometrique,
possède au moins une méthode virtuelle pure, elle ne peut pas être instanciée. Elle est alors qualifiée de
classe abstraite. Si les classes abstraites ne peuvent être instanciées, elles peuvent cependant être utilisées
dans le cadre du polymorphisme, ce qui est très pratique pour manipuler de façon homogène une famille
d'objets [PROG. 4.10].	PROG. 4.10 - Utilisation d'une classe abstraite dans le cadre du polymorphisme.Sélectionnez
1 #include "FigureGeometrique.H"
3 #define NBFIG 1000
7 		FigureGeometrique *lesFigures[NBFIG];
9 		// Dessin de toutes les figures géométriques
11 		for (int i = 0; i &lt; NBFIG; i++)
12 			lesFigures[i]-&gt;dessin();
13 		...
V. La bibliothèque STL▲
La bibliothèque STL (Standard Template Library 10) est certainement l'un des atouts de C++. Cette
bibliothèque fournit un ensemble de composants C++ bien structurés qui marchent de façon cohérente et
peuvent aussi être adaptés facilement. En effet, il est possible d'utiliser les structures de données proposées
par STL avec des algorithmes personnels, les algorithmes de la bibliothèque avec des structures de données
personnelles, ou d'utiliser toutes les composantes STL ! Lors de sa conception, l'accent a été mis sur
l'efficacité et sur l'optimisation des composants, ce qui en fait un outil très puissant.
Ce chapitre présente les généralités liées à STL. Pour en tirer pleinement partie, une bonne documentation
s'avère nécessaire. Un des meilleurs ouvrages de référence de cette bibliothèque est celui de Musser
& Saini [Mus 96]. Il est également possible de trouver des informations sur internet :
http://www.sgi.com/tech/stl/ : manuel d'utilisation de STL.
http://www.cs.rpi.edu/~musser/doc.ps : autre manuel d'utilisation.
STL contient cinq types de composants : des containers, des itérateurs, des algorithmes, des objetsfonctions
et des adaptateurs. Nous nous intéressons dans ce chapitre aux trois premiers composants.
10. Bibliothèque standard générique.
V-B. Les containers▲
Les containers sont des objets qui permettent de stocker d'autres objets. Ils sont décrits par des classes
génériques représentant les structures de données logiques les plus couramment utilisées : les listes, les
tableaux, les ensembles... Ces classes sont dotées de méthodes permettant de créer, de copier, de détruire
ces containers, d'y insérer, de rechercher ou de supprimer des éléments. La gestion de la mémoire, c'est-àdire
l'allocation et la libération de la mémoire, est contrôlée directement par les containers, ce qui facilite
leur utilisation. L'exemple [PROG. 5.1] présente une application où les valeurs entières saisies par un
utilisateur sont stockées dans une liste et dans un tableau ; un exemple d'exécution est présenté [PROG. 5.2].
PROG. 5.1 - Utilisation des listes et tableaux STL.Sélectionnez
2 	#include &lt;vector&gt;
3 	#include &lt;list&gt;
5 	int main()
7 		vector&lt;int&gt; tableauEntiers; // Crée un tableau d'entiers vide
8 		list&lt;int&gt; listeEntiers; // Crée une liste d'entiers vide
9 		int unEntier;
12 		// Saisie des entiers
14 		cout &lt;&lt; "Saisir le prochain entier (-1 pour finir) : ";
15 		cin &gt;&gt; unEntier;
17 		while (unEntier != -1) {
18 			tableauEntiers.push_back(unEntier);
19 			listeEntiers.push_back(unEntier);
21 		cout &lt;&lt; "Saisir le prochain entier (-1 pour finir) : ";
22 		cin &gt;&gt; unEntier;
25 	// Nombre d'éléments des containers
27 	cout &lt;&lt; "Il a y " &lt;&lt; tableauEntiers.size()
28 		&lt;&lt; " éléments dans le tableau" &lt;&lt; endl;
30	cout &lt;&lt; "Il a y " &lt;&lt; listeEntiers.size()
31 		 &lt;&lt; " éléments dans la liste" &lt;&lt; endl;
33 	// Accès à des éléments
35 	cout &lt;&lt; "Premier élément du tableau : "
36 		 &lt;&lt; tableauEntiers.front() &lt;&lt; endl;
38 	cout &lt;&lt; "Premier élément de la liste : "
39 		 &lt;&lt; listeEntiers.front() &lt;&lt; endl;
41 	int milieu = tableauEntiers.size() / 2;
43 	cout &lt;&lt; "Élément de milieu de tableau : "
44 		 &lt;&lt; tableauEntiers[milieu] &lt;&lt; endl;
PROG. 5.2 - Exemple d'utilisation de [PROG. 5.1].Sélectionnez
1 Saisir le prochain entier (-1 pour finir) : 4
2 Saisir le prochain entier (-1 pour finir) : 5
3 Saisir le prochain entier (-1 pour finir) : 3
4 Saisir le prochain entier (-1 pour finir) : 7
5 Saisir le prochain entier (-1 pour finir) : 6
6 Saisir le prochain entier (-1 pour finir) : 3
7 Saisir le prochain entier (-1 pour finir) : -1
8 Il a y 6 éléments dans le tableau
9 Il a y 6 éléments dans la liste
10 Premier élément du tableau : 4
11 Premier élément de la liste : 4
12 Élément de milieu de tableau : 7
Quelques remarques sur les containers et sur l'exemple présenté :
Un certain nombre de méthodes sont disponibles sur tous les types de containers, ce qui permet d'homogénéiser leur utilisation. C'est le cas par exemple de la méthode push_back (lignes 18 et 19) qui insère un nouvel élément à la fin d'un container.
D'autres méthodes ou opérateurs sont disponibles en fonction du type de container utilisé. L'opérateur [] est disponible sur les objets de type vector, mais pas sur ceux de type list : il permet d'accéder directement à un élément. Plus de précisions sur ces méthodes sont disponibles [§ B.1].
L'utilisateur n'a pas à se soucier de l'allocation ou de la libération de la mémoire. C'est vrai lors de l'insertion d'éléments (lignes 18 et 19) et aussi à la fin du programme : aucune instruction particulière n'est nécessaire pour restituer la mémoire occupée par les containers. À la sortie du bloc dans lequel ils sont définis, leur destructeur se charge de libérer toutes les ressources occupées.
Les containers peuvent manipuler n'importe quel type de données, à partir du moment où la classe correspondante est dotée d'un certain nombre de méthodes nécessaires à STL (pour une classe X) :
X() : un constructeur par défaut,X(const X&), un constructeur par copie,operator=(const X&) : l'opérateur d'affectation,operator==(const X&) : l'opérateur d'égalité,operator<(const X&) : l'opérateur inférieur (utile uniquement pour les tris). Les différentes sortes de containers disponibles sont :
vector : container implantant les tableaux, qui autorise les accès directs sur ses éléments. Les opérations de mise à jour (insertion, suppression) sont réalisées en un temps constant à la fin du container, et en un temps linéaire (dépendant du nombre d'éléments) aux autres endroits.
list : container implantant les listes doublement chaînées, dédié à la représentation séquentielle de données. Les opérations de mise à jour sont effectuées en un temps constant à n'importe quel endroit du container.
deque : container similaire au vector, effectuant de plus les opérations de mise à jour en début de container en un temps constant.
set : container implantant les ensembles où les éléments ne peuvent être présents au plus qu'en un seul exemplaire.
multiset : container implantant les ensembles où les éléments peuvent être présents en plusieurs exemplaires.
map : container implantant des ensembles où un type de données appelé clé est associé aux éléments à stocker. On ne peut associer qu'une seule valeur à une clé unique. On appelle aussi ce type de container tableau associatif.
multimap : container similaire au map supportant l'association de plusieurs valeurs à une clé unique.
stack : container implantant les piles, qui sont des listes spéciales, dites LIFO 11.
queue : container implantant les files, qui sont des listes spéciales, dites FIFO 12.
11. Last In, First Out : dernier entré, premier sorti.
12. First In, First Out : premier entré, premier sorti.
Une carte de référence sur les containers existants et sur les méthodes dont ils dont dotés est disponible
[§ B.1].
V-C. Les itérateurs▲
Les itérateurs sont une généralisation des pointeurs, ce qui permet au programmeur de travailler avec
des containers différents de façon uniforme. Ils permettent de spécifier une position à l'intérieur d'un
container, peuvent être incrémentés ou déréférencés (à la manière des pointeurs utilisés avec l'opérateur
de déréférencement '*'), et deux itérateurs peuvent être comparés. Tous les containers sont dotés
d'une méthode begin qui renvoie un itérateur sur le premier de leurs éléments, et d'une méthode end
qui renvoie un itérateur sur une place se trouvant juste après le dernier de leurs éléments. On ne peut
ainsi pas déréférencer l'itérateur renvoyé par la méthode end. Un exemple d'utilisation des itérateurs se
trouve [PROG. 5.3].
PROG. 5.3 - Utilisation typique des itérateurs pour un parcours de container.Sélectionnez
2 #include &lt;list&gt;
6 		list&lt;int&gt; lesEntiers;
8 		// Ici, des instructions pour initialiser la
9 		// liste des entiers
11 		...
13 		// Affichage des éléments contenus dans la liste
15 		list&lt;int&gt;::iterator it;
17 		for (it = lesEntiers.begin(); it != lesEntiers.end(); it++)
18 			cout &lt;&lt; *it &lt;&lt; endl;
Ces itérateurs sont dotées de méthodes permettant de les manipuler, décrites [§ B.2]. Il existe une
hiérarchie d'itérateurs (qui n'est pas liée à un quelconque héritage) :
Les itérateurs d'entrée (input iterators) : ils permettent d'accéder séquentiellement à des sources de données. Cette source peut-être un container, un flot.
Les itérateurs de sortie (output iterators): ils permettent de préciser la localisation d'une destination permettant de stocker des données. Cette source peut-être un container, un flot.
Les itérateurs à sens-unique (forward iterators) : ils sont dotés de toutes les méthodes des itérateurs d'entrée et de sortie. Ils sont utilisés pour parcourir séquentiellement une séquence de données dans un sens. Ils ne peuvent pas être utilisés pour effectuer des retours en arrière.
Les itérateurs à double-sens (bidirectional iterators) : ils sont dotés de toutes les méthodes des itérateurs à sens-unique. Ils sont également utilisés pour effectuer des parcours séquentiels de données, qu'ils peuvent effectuer dans les deux sens.
Les itérateurs à accès direct (random-access iterators) : ils sont dotés de toutes les méthodes des itérateurs à double-sens. Ils permettent d'accéder directement à des valeurs contenues dans un container, sans être obligé d'y accéder séquentiellement.	Tous les containers disponibles sous STL fournissent au moins des itérateurs à double-sens, et certains
fournissent des itérateurs à accès direct (voir [§ B.1] pour les itérateurs par défaut renvoyés par chaque type
de container).
V-D. Les algorithmes▲
Les algorithmes sont des fonctions C++ génériques qui permettent d'effectuer des opérations sur les
containers. Afin de pouvoir s'appliquer à plusieurs types de containers, les algorithmes ne prennent pas de
containers en arguments, mais des itérateurs qui permettent de désigner une partie ou tout un container.
De ce fait, il est même possible d'utiliser ces algorithmes sur des objets qui ne sont pas des containers.
On peut par exemple utiliser un istream_iterator (voir [§ B.2]) comme paramètre d'un algorithme,
qui va alors s'appliquer à l'entrée standard. Certains algorithmes ne nécessitent que des itérateurs de base
(d'entrée ou de sortie), et d'autres nécessitent des itérateurs plus évolués, comme la fonction sort 13
(effectuant un tri) qui nécessite un itérateur à accès direct.
Les algorithmes disponibles sont décrits en annexe [§ B.3]. Pour les utiliser, il suffit d'inclure l'en-tête
algorithm. Un exemple d'utilisation des algorithmes est présenté [PROG. 5.4], et un résultat produit à
partir de cet exemple est présenté [PROG. 5.5].
13. Du coup, cet algorithme n'est pas applicable sur les listes qui ne fournissent que des itérateurs à double sens. C'est pour
cette raison que la classe list est dotée d'une méthode sort.
PROG. 5.4 - Utilisation de l'algorithme de tri.Sélectionnez
8 		int unEntier;
10 		// Saisie des entiers
12 		cout &lt;&lt; "Saisir le prochain entier (-1 pour finir) : ";
13 		cin &gt;&gt; unEntier;
15 		while (unEntier != -1) {
16 			tableauEntiers.push_back(unEntier);
18 		cout &lt;&lt; "Saisir le prochain entier (-1 pour finir) : ";
19 		cin &gt;&gt; unEntier;
22 	// Tri du tableau
24 	sort(tableauEntiers.begin(), tableauEntiers.end());
26 	// Affichage des éléments triés
28 	vector&lt;int&gt;::iterator it;
30 	for (it = tableauEntiers.begin(); it != tableauEntiers.end(); it++)
31 		cout &lt;&lt; *it &lt;&lt; " ";
33 	cout &lt;&lt; endl;
PROG. 5.5 - Exemple d'utilisation de [PROG. 5.4].Sélectionnez
1 Saisir le prochain entier (-1 pour finir) : 5
2 Saisir le prochain entier (-1 pour finir) : 3
3 Saisir le prochain entier (-1 pour finir) : 8
4 Saisir le prochain entier (-1 pour finir) : 10
5 Saisir le prochain entier (-1 pour finir) : 3
6 Saisir le prochain entier (-1 pour finir) : 6
7 Saisir le prochain entier (-1 pour finir) : 9
8 Saisir le prochain entier (-1 pour finir) : -1
9 3 3 5 6 8 9 10
VI. Format des entrées-sorties▲
VI-A. A.1 La classe ios▲
La classe ios est la classe de base des classes représentant les flots. Toutes les classes de flot présentées
ci-dessous héritent donc de cette classe et des méthodes qu'elle définit (non exhaustif) :
int ios::good() : retourne une valeur nulle s'il y a eu un échec lors de la dernière opération d'entrée-sortie, et une valeur non nulle sinon.
int ios::fail() : retourne le contraire de la méthode précédente.
int ios::eof() : retourne une valeur non nulle si la fin de fichier est atteinte, et la valeur nulle sinon.
La classe ios définit aussi un certain nombre de méthodes relatives au format des informations manipulées
(non exhaustif) (voir [PROG. A.1] pour un exemple d'utilisation) :
PROG. A.1 - Exemples de formatage de texte.Sélectionnez
5 		cout &lt;&lt; "Largeur standard : "
6 			 &lt;&lt; cout.width() &lt;&lt; endl; // Affiche : 0
8 		cout.width(10);
9 		cout.fill('#');
11 		cout &lt;&lt; 654 &lt;&lt; endl; // Affiche : #######654
int ios::width(int n) : positionne la largeur du champ (le nombre de caractères) de sortie.
int ios::width() : retourne la largeur du champ de sortie.
char ios::fill(char c) : positionne le caractère de remplissage (utilisé pour remplir toute la largeur des champs).
char ios::fill() : retourne le caractère de remplissage.
int ios::precision(int p) : positionne la précision, c'est-à-dire le nombre de caractères (y compris le point) qu'occupe un réel.
int ios::precision() : retourne la précision.
Enfin, cette classe définit un certain nombre d'opérations qui peuvent être utilisées sans les préfixer du
flot sur lequel on travaille, et autorise ainsi l'écriture de lignes du type :
cout &lt;&lt; "La valeur octale de 154 est : " &lt;&lt; oct &lt;&lt; 154 &lt;&lt; endl;
Parmi ces opérations, on trouve :
endl : écrit un '\n' et vide le tampon du flot.
ends : écrit un '\0' et vide le tampon du flot.
flush : vide le tampon du flot.
dec : la prochaine opération d'entrée-sortie se fera en décimal.
hex : la prochaine opération d'entrée-sortie se fera en hexadécimal.
oct : la prochaine opération d'entrée-sortie se fera en octal.
ws : saute les espaces lors d'une lecture sur un flot d'entrée.
setfill(int c) : positionne le caractère de remplissage pour la prochaine opération d'entréesortie.
setprecision(int p) : positionne la précision de la prochaine opération d'entrée-sortie à p chiffres.
setw(int n) : positionne la largeur de la prochaine entrée-sortie à n caractères.	VI-B. A.2 La classe ostream▲
Cette classe est dédiée aux sorties formatées ou non. Un objet de type ostream est défini par défaut
dans tout programme C++ : c'est la variable cout. La classe ostream surcharge l'opérateur << pour
tous les types prédéfinis du C++. Il faut, si besoin est, le surcharger pour les nouveaux types introduits. Un
exemple de surcharge est présenté [§ 3.10]. En plus de l'opérateur <<, la classe ostream est dotée des
méthodes suivantes (non exhaustif) :
ostream &ostream::put(char c) : insère un caractère dans le flot.
Exemple : cout.put('\n').
ostream &ostream::write(const char *, int n) : insère n caractères dans le flot.
ostream &ostream::flush() : vide le tampon du flot.
VI-C. A.3 La classe istream▲
Cette classe est dédiée aux entrées formatées ou non. Un objet de type istream est défini par défaut
dans tout programme C++ : c'est la variable cin. La classe istream surcharge l'opérateur >> pour tous
les types prédéfinis du C++. Il faut, si besoin est, le surcharger pour les nouveaux types introduits. Un
exemple de surcharge est présenté [§ 3.10]. En plus de l'opérateur >>, la classe istream est dotée des
int istream::get() : retourne la valeur du caractère lu (EOF si la fin du flot est atteinte).
istream &istream::get(char &c) : extrait le premier caractère du flot (même si c'est un espace) et le place dans c.
int &istream::peek() : lit le prochain caractère du flot sans l'enlever du flot (renvoie EOF si la fin du flot est atteinte).
istream &istream::get(char *ch, int n, char delim = '\n') : extrait (n - 1) caractères du flot et les place à l'adresse ch (le tampon). La lecture s'arrête éventuellement après le délimiteur s'il est rencontré.
istream &istream::getline(char *ch, int n, char delim = '\n') : identique à la méthode précédente sans placer le délimiteur dans le tampon.
istream &istream::read(char *ch, int n) : extrait au plus n caractères du flot et les place à l'adresse ch. Le nombre de caractères effectivement lus peut être obtenu grâce à la méthode gcount.
int istream::gcount() : retourne le nombre de caractères extraits lors de la dernière lecture.
istream &istream::flush() : vide le tampon du flot.	VI-D. A.4 Les fichiers▲
Les entrées-sorties sur les fichiers sont également réalisées avec des flots en C++. Ce type d'opérations
nécessite l'inclusion de l'en-tête fstream en plus de l'en-tête iostream. Les deux grandes classes
permettant de réaliser ces opérations sont :
La classe ofstream : cette classe est dédiée aux écritures réalisées dans des fichiers. La classe ofstream est dérivée de la classe ostream et bénéficie donc de toutes les méthodes définies dans cette classe. Un exemple d'utilisation est présenté [PROG. A.2].
PROG. A.2 - Écritures dans un fichier.Sélectionnez
6 		// Deux modes d'ouverture sont possibles :
7 		// - ios::out -&gt; création, fichier écrasé si existant
8 		// - ios::app -&gt; ajout en fin de fichier
10 	ofstream fichierSortie("donnees.txt", ios::out);
12 		// Test d'ouverture du fichier
14 		if (!fichierSortie) {
15 			cerr &lt;&lt; "Problème d'ouverture de fichier" &lt;&lt; endl;
16 			exit(1);
19 	fichierSortie &lt;&lt; "J'écris des caractères dans le fichier "
20 				  &lt;&lt; "et des nombres : " &lt;&lt; 10 &lt;&lt; " " &lt;&lt; 20
21 				  &lt;&lt; endl;
23 		// Fermeture du fichier
25 fichierSortie.close();
La classe ifstream : cette classe est dédiée aux lectures réalisées dans des fichiers. La classe ifstream est dérivée de la classe istream et bénéficie donc de toutes le méthodes définies dans cette classe. Un exemple d'utilisation est présenté [PROG. A.3].
PROG. A.3 - Lectures dans un fichier.Sélectionnez
6 			// Ouverture du fichier
8 	ifstream fichierEntree("donnees.txt", ios::in);
10 			// Test d'ouverture du fichier
12 	if (!fichierEntree) {
13 		cerr &lt;&lt; "Problème d'ouverture de fichier" &lt;&lt; endl;
14 		exit(1);
17 	char buf[1024];
19 			// Tant qu'il a y des lignes dans le fichier, on les
20 			// lit et on les affiche à l'écran
22 	while (!fichierEntree.eof()) {
23 	fichierEntree.getline(buf, 1024);
24 		cout &lt;&lt; buf &lt;&lt; endl;
27 			// Fermeture du fichier
29 fichierEntree.close();
VII. Carte de référence STL▲
VII-A. B.1 Les containers▲
Tous les containers sont dotés des caractéristiques suivantes :
Certains types prédéfinis (typedefs) :
size_typepointerconst_pointerreferenceconst_reference
Des types prédéfinis pour la création d'itérateurs :
iteratorconst_iteratorreverse_iteratorconst_reverse_iterator
constructor() : pour la création de containers vides
copy-constructor()
size_type size() const : nombre d'éléments du container
size_type max_size() const : capacité (mémoire occupée) maximum du container
container-ref operator=(const-container-ref) : remplacement de tout le contenu
void swap(container-ref) : inverse tout le contenu
bool operator==(const-container-ref) : teste l'égalité sur tous les éléments
bool operator<(const-container-ref)
begin() et end() : méthodes pour accéder au contenu
Propriétés des containers
Iterateur par déf.
random-acc
front(), back(), op[],
front(), back(), op[]
push_front(),
pop_back(), flip(),
push_back(),
pop_front(), pop_back,
sort(), splice(), remove(),
unique(), merge()
pop_front(), pop_back()
upper_bound(),
count(), op[]
pop_back(), test(),
any(), none(), op&=,
op|=, opˆ=, op<<, op>>,
set(), reset(), to_ulong(),
to_string(), count(),
VII-B. B.2 Les itérateurs▲
Propriété des itérateurs
op++(), op++(int)
op*() write only
op*() read only
op=, =()
op*() read write
", operator=()
", op-(), op-(int)
", op+=, op+, op-=, op-, op[]
", op<()
C style pointer
Itérateurs spéciaux
lit un flot d'entrée C++
écrit dans un flot de sortie C++
insère à la fin d'un container
insère au début d'un container
insère dans un container à n'importe quelle position
itérateur sur mémoire allouée non initialisée
itérateur dans le sens inverse
VII-C. B.3 Les algorithmes▲
Dans le tableau suivant, les arguments des fonctions et des valeurs de retour sont codés de la manière
- r random access iterator
- p unary predicate
- b bidirectional iterator
- R reference
- p2 binary predicate
- f forward iterator
- P pair
- c compare function
- i input iterator
- B bool
- F unary function
- o output iterator
- F2 binary function
- [...] optional args
Algorithmes en STL
i,i[,p2]
find sequence of equal elements
f,f,V[,c]
find a value in a sorted range
i,i,V,R
count matching elements
i,i,p,R
count elements which satisfy p
i,i,V
locate an equal element
i,i,p
locate an element which satisfies p
f,f,f,f[,p2]
locate a subrange within a range
f,f,n,V[,p2]
find the last subrange which satisfies;
like search but from the end
returns the first possible insert location
into a sorted collection
returns the last possible insert location
returns the range of possible insert locations
i,i[,c]
apply a function to a range
i,i,o,F or i,i,i,o,F2
apply an operation against a range
replace all matching elements with a
f,f,p,V
i,i,o,V,V
replace during copy, all matching elements
i,i,o,p,V
fill with a value
fill with a single value
f,f,unary_op
fill with generated values
f,n,unary_op
i,i,p2,R
count the number of matches, using
i,i,i[,p2]
returns the first subrange than does not
true if the ranges match
i,i,o
copy one range to another
reverse copy one range to another
swap one range with another
move unwanted entries to the end of the
i,i,o,V
copy and remove unwanted entries
i,i,o,p
f,f[,p2]
collapse the range so that multiple copies
of equal elements are removed
i.i,o[,p2]
copy the range skipping multiple copies
of equal elements
reverse the order of a range
b,b,o
rotate a range, given first, middle and
f,f,f,o
rotate and copy, given first, middle and
r,r[,rand_gen]
shuffle the order of a range
swaps to make all the pred-successes
precede the pred-failures
precede the pred-failures; preserves relative
r,r[,c]
sorts the elements in the range
sorts the range; preserve relative order
on the "equal" ones
r,r,r[,c]
sorts the range into the subrange
i,i,r,r[,c]
sorts the range into the subrange at a
sorts the range so that one specific one
b,b[,c]
transforms range to next permutation
tranforms range to previous permutation
i,i,i,i,o[,c]
merges two input ranges
b,b,b[,c]
i,i,i,i[,c]
tests for the elementf of one range
builds the sorted union
Heap Support
adds the last element of a range to a
changes a heap into a smaller heap plus
a last element
changes a range into a heap
sorts a heap
VIII. Compléments▲
VIII-A. C.1 Les namespaces▲
Les namespaces (espaces de noms) ont été principalement introduits dans la norme définitive de C++
pour gérer les gros projets. Dans ce type de projet, il n'est pas rare d'utiliser plusieurs bibliothèques C++
qui peuvent parfois définir les mêmes identificateurs, ce qui génère des conflits. Avec les namespaces, il ne
doit plus y avoir de conflits de noms : les déclarations restent cachées dans un namespace jusqu'à ce qu'on
fasse explicitement appel à lui.
Pour définir un namespace, il faut utiliser le mot-clé namespace comme cela est présenté [PROG. C.1].
La déclaration d'un même namespace peut être réalisée dans plusieurs fichiers d'interface, le namespace
PROG. C.1 - Définition d'un namespaceSélectionnez
1 namespace MonNameSpace
3 // Toutes les déclarations sont regroupées
4 // dans ce bloc
8 // D'autres déclarations...
10 // Fin du namespace
complet résultant alors de l'union des déclarations. Dans l'exemple présenté, le nom complet de la fonction
f devient MonNameSpace::f, selon une syntaxe qui est similaire à celle des méthodes membres de
classe. Cependant, afin de ne pas être contraint de désigner la fonction f par rapport à son namespace lorsqu'il
n'y a pas de risque de conflit, des facilités d'utilisation sont disponibles grâce à la définition d'alias
ou à l'utilisation du mot-clé using [PROG. C.2]. L'utilisation de ces directives est cependant à bannir des
fichiers d'interface, pour éviter des conflits qui pourrait apparaître dans les modules incluant ces interfaces.
Du coup, la plupart des déclarations de la bibliothèque standard C++ ont été regroupées dans un namespace,
appelé std.	PROG. C.2 - Utilisation d'un namespace.Sélectionnez
1 	// Si on souhaite définir un alias sur le nom d'un
2 	// namespace
4 	namespace mon = MonNameSpace;
6 	mon::f(); // Appelle MonNameSpace::f()
8 	// Si la fonction f est la seule à être présente,
9 	// on peut déclarer
11 	using MonNameSpace::f;
13 	f(); 	// Appelle MonNameSpace::f()
15 	// 	Si on souhaite bénéficier de toutes les déclarations
16 	// 	d'un namespace sans avoir à les préfixer du nom
17 	// 	du namespace
19 using namespace MonNameSpace;
VIII-B. Les nouveaux casts▲
Dans les nouveautés introduites dans la norme finale du langage, on trouve également de nouveaux
opérateurs de conversion (casting). Ils sont particulièrement utiles dans des contextes tels que le polymorphisme,
afin de convertir un objet d'une classe de base vers une classe dérivée [§ 4.4]. En effet, jusqu'à
l'introduction de ces opérateurs, ce type de conversion délicate reposait entièrement sur les épaules du
programmeur, qui avait la charge de vérifier la validité de la conversion avant de la réaliser. Du coup, cela
pouvait engendrer quelques problèmes de sécurités, que ces nouveaux opérateurs sont sensés résoudre.
Certains nouveaux casts se basent sur une fonctionnalité ajoutée il y a peu au langage C++ : la RunTime Type Identification (RTTI) 14. L'objectif 15 de ces nouveaux opérateurs est de disposer d'une syntaxe
améliorée, plus claire, d'une sémantique moins ambiguë et de réaliser des conversions de types en toute
14. Identification des types au cours de l'exécution.
15. Ça ne reste qu'un objectif pour certains d'entre eux (NdA).
Ces nouveaux opérateurs sont au nombre de 4 :
1. L'opérateur static_cast<T> (expr) : cet opérateur est utilisé pour effectuer des conversions
qui sont résolues à la compilation. Il peut être utilisé pour convertir un pointeur (ou une référence) sur
une classe de base vers un pointeur (ou une référence) sur une classe dérivée. L'opérateur n'effectue
aucune vérification au cours de l'exécution (comme son nom l'indique) et doit donc être utilisé pour
des conversions non-ambiguës. Mal utilisé, il renvoie un résultat indéfini. Il doit surtout être utilisé
pour effectuer des conversions arithmétiques. Il est assez proche de la conversion traditionnelle, mais
permet de supprimer des trous de sécurité qui existaient.
2. L'opérateur const_cast<T> (expr) : cet opérateur permet de supprimer la constance d'un
objet. Ce n'est pas très naturel, mais utile dans certaines situations ; il doit être ainsi utilisé avec parcimonie.
Un exemple d'utilisation se trouve [PROG. C.3]. De même, il peut être utilisé à l'intérieur
d'une méthode de classe constante : en l'appliquant sur le pointeur this, on peut modifier par la
suite l'objet courant (!).
3. L'opérateur dynamic_cast<T> (expr) : c'est certainement l'un des nouveaux opérateurs les
plus intéressants. Il peut uniquement être utilisé sur des pointeurs ou des références pour naviguer
PROG. C.3 - Utilisation de l'opérateur const_cast.Sélectionnez
1 void f(Article &amp;i)
5 void g(const Article &amp;j)
7 	f(j); // Erreur : j est constant et f n'attend pas un const
8 	f(const_cast&lt;Article&amp;&gt; (j)); // Ok
dans une hiérarchie de classes. Il peut être utilisé pour convertir un objet d'une classe dérivée vers
un objet d'une classe de base ou inversement. Dans le premier cas, c'est une classique conversion
statique qui est effectuée tandis que dans le second cas c'est une conversion dynamique qui est
réalisée, en se basant sur le système RTTI. Dans ce cas, si la conversion est possible, l'opérateur
de conversion renvoie un pointeur valide, ou un pointeur nul sinon. Cette fonctionnalité est très
puissante comme le montre l'exemple [PROG. C.4].
PROG. C.4 - Utilisation de l'opérateur dynamic_cast.Sélectionnez
7 	// Initialisation du tableau avec des articles hétérogènes
8 	// Deux cas différenciés : Alcools ou autres articles
10 	for (int i = 0; i &lt; MAXELTS; i++) {
11 		BoissonAlcoolisee *ba;
13 		ba = dynamic_cast&lt;BoissonAlcoolisee*&gt; (lesArticle[i]);
15 		// Si l'article est un alcool, affichage du nom et du
16 		// degré d'alcool. Affiche du nom uniquement sinon.
18 		if (ba)
19 			cout &lt;&lt; ba-&gt;nom() &lt;&lt; " (" &lt;&lt; ba-&gt;degre() &lt;&lt; ")" &lt;&lt; endl;
20 		else
21 			cout &lt;&lt; lesArticle[i]-&gt;nom() &lt;&lt; endl;
4. L'opérateur reinterpret_cast<T> (expr) : cet opérateur peut être utilisé pour convertir des objets dont les types ne sont pas en relation. Le résultat de la conversion est dépendante de l'implantation, et n'est ainsi pas portable. Il peut être utilisé dans certains contextes particuliers de conversion entre types de pointeurs de fonctions.
Conclusion : parmi les nouveaux opérateurs introduits, seuls le static_cast et le dynamic_cast
sont relativement conformes à l'approche objet. Ils sont à utiliser en priorité.
VIII-C. C 3 Où trouver un compilateur C++?▲
DJGPP (http://www.delorie.com/djgpp/). C'est un système complet de développement C/C++ tournant sous DOS, donc en mode texte. Il comprend un éditeur multi-fichiers, un compilateur, un gestionnaire de projet et un débogueur. Si l'environnement est plus austère que celui de CodeWarrior, le compilateur en revanche est plus récent (il s'agit en fait de GCC 2.95). Il peut être téléchargé librement à partir de http://www.telecharger.com/ par exemple. Nous recommandons de sélectionner les packages suivants : bnu281b, djdev202, gcc2951b, gpp2951b, lgp2951b, rhide14b et de se référer au fichier lisezmoi.1er (répertoire v2) pour la procédure d'installation.DEV-C++ (http://www.bloodshed.net/devcpp.html). Là aussi, c'est un système complet de développement C/C++, tournant cette fois sous Windows directement. Comme DJGPP, il comprend un éditeur multi-fichiers, un compilateur, un gestionnaire de projet et un débogueur. Le compilateur de base est d'ailleurs le même : GCC 2.95. Il peut être téléchargé librement à partir de http://www.telecharger.com/.PRODUITS BORLAND (http://www.borland.fr/download/compilateurs/). Borland met gratuitement à la disposition de la communauté des développeurs ses compilateurs, dont les célèbres Turbo C++ et Borland C++. Le premier comprend un environnement complet en mode semi-graphique, et le deuxième correspond à un compilateur utilisable en ligne de commande, fourni avec quelques outils annexes.
Sous Unix : GCC. Les sources sont téléchargeables à partir de ftp://ftp.gnu.org/pub/%20gnu/gcc/, mais l'installation est plus délicate. Il est livré en standard avec certains systèmes Unix (comme Linux), ce qui permet d'éviter l'étape d'installation. Les débogueurs du monde Unix livrés en standard ne sont généralement pas très conviviaux, mais il est possible de récupérer un débogueur graphique (DDD) à l'adresse http://www.gnu.org/software/ddd/. Les personnes travaillant sous Unix utiliseront généralement leur éditeur de texte favori (comme Emacs ou XEmacs) pour compléter cet environnement. Il existe cependant une version sous Unix similaire à celle proposée pour Windows (donc en mode texte) : c'est RHIDE, qui est disponible à http://www.rhide.com/.
[Gau 96] M. Gautier, G. Masini et K. Proch. Cours de programmation par objets - Principes et applications avec Eiffel et C++. Masson, Paris, 1996.
[Lip 92] S.B. Lippman. L'essentiel du C++, 2ème édition. Addison-Wesley, France, 1992.
[Lip 98] S.B. Lippman et J. Lajoie. C++ Primer, 3rd Edition. Addison-Wesley, Reading, MA, USA, 1998.
[Mey 91] B. Meyer. Conception et programmation par objets, Seconde édition. Informatique Intelligence Artificielle. InterÉditions, Paris, 1991.
[Mus 96] D. R. Musser et A. Saini. STL Tutorial and Reference Guide: C++ Programming with the Standard Template Library. Addison-Wesley, Reading, MA, 1996.
[Str 97] B. Stroustrup. The C++ Programming Language, 3rd Edition. Addison-Wesley, Reading, MA, USA, 1997.
IX-A. Remerciements▲
Je tiens à remercier tout particulièrement Gérald Masini et Karl Tombre qui m'ont aidé dans la rédaction
de ce polycopié en me fournissant des documents sur le langage C++, utilisés en partie comme base

References: l'article
7
 l'article
10
 l'article
23
 l'article
24
 l'article
14
 l'article
17
 l'article
22
 l'article
23
 l'article
6
 l'article
19
 l'article
16
 l'article
17
 l'article
11
 l'article
14
 l'article
19
 l'article
22
 l'article
26
 l'article
27
 l'article
32
 l'article
10