Table des Matières
Notes au lecteur
7 LES BASES DU C++
1) Les objets et les classes Notions de base L'idée Mise en œuvre C + + ' L'intérêt par rapport au C Résumé Pour bien démarrer en C + + Compléments Fonctions inline Le mot-clé this Éléments static dans une classe Déclaration de classe anticipée
13 13 13 14 19 20 20 21 21 23 24 25
2) Les c o n s t r u c t e u r s et les destructeurs Les notions de base L'idée Mise en œuvre C + + constructeurs Mise en œuvre C + + destructeurs Compléments Constructeur copie Constructeurs de conversion
27 27 27 28 30 32 32 36
3) L'héritage simple Notions de base L'idée Mise en œuvre C + + Redéfinition d'une fonction-membre Conversion de Dérivée* vers Base* Résumé
37 37 37 38 41 42 43
4
Pont entre C et C + +
L'intérêt par rapport au C Compléments Héritage et constructeurs Héritage et destructeurs Exemple complet Listes d'initialisations
43 44 44 44 45 47
4) La surcharge Notions de base L'idée Mise en œuvre C + + L'intérêt par rapport au C Compléments Surcharge d'opérateurs Surcharge de l'opérateur = Opérateurs de conversion de types
51 51 51 52 53 54 54 57 61
5) Les petits + du C++ Les commentaires Ruse de sioux Paramètres par défaut Les constantes Déclarations
63 63 64 65 67 73
RÉSUMÉ Encapsulation Constructeurs & destructeurs Héritage Surcharge
75 77 77 78 79
ENCORE PLUS DE C++ 6) La fin du malloc : new et delete Notions de base L'idée Mise en œuvre C + + L'intérêt par rapport au C
83 83 83 84 86
7) La f i n du printf : cout, cin et cerr Notions de base L'idée Mise en œuvre C + + L'intérêt par rapport au C Compléments Surcharger « et »
89 90 90 90 91 93 93
Table des matières
Formater les sorties Tableau récapitulatif
5
95 100
8) Le p o l y m o r p h i s m e et la virtualité Notions de base L'idée Mise en œuvre C + + Résumé L'intérêt par rapport au C Compléments Destructeurs virtuels Classes abstraites et fonctions virtuelles pures
102 102 102 104 105 105 107 107 108
9) Les références Notions de base L'idée Mise en œuvre C + + L'intérêt par rapport au C Compléments Références et objets provisoires Fonction retournant une référence
114 114 114 115 115 116 116 117
10) Les templates ou la mise en œuvre de la généricité Notions de base L'idée Mise en œuvre C + + Templates de fonctions Templates de classes Compléments Attention à l'ambiguïté
118 119 119 119 119 121 124 124
11) Les classes et f o n c t i o n s amies Notions de base L'idée Mise en œuvre C + + Attention !
125 126 126 126 127
12) L'héritage m u l t i p l e Notions de base L'idée Mise en œuvre C + + L'intérêt par rapport au C Compléments Duplication de données d'une classe de base Masquage du polymorphisme
130 130 130 131 133 134 134 136
6
Pont entre C et C++
Héritage virtuel
138
13) Les e x c e p t i o n s Notions de base L'idée Mise en œuvre C + + L'intérêt par rapport au C Résumé Exemple complet Compléments Spécifier des exceptions Exception non interceptée Exceptions en cascade
140 140 140 141 145 145 145 147 147 148 150
14) La c o m p i l a t i o n séparée Notions de base L'idée Mise en œuvre Quoi mettre, et dans quels fichiers ? Comment lancer la compilation ? Compléments Comment éviter les redéclarations ? Static, inline et portée Travailler avec d'autres langages
153 153 153 154 155 159 161 161 162 164
GUIDE DE SURVIE 15) Conseils Les étapes d'un projet Conception Trouver les classes Architecturer les classes Détailler les classes Évaluer l'ensemble Codage C + + Quels outils ? Mise en œuvre de la réutilisabilité Implantation des classes
169 169 170 170 171 173 174 174 174 175 177
16) Questions-Réponses
179
Index
185
16
Pont entre C et C++
d o n n é e s d ' u n objet, données-membres. P a r opposition, les autres d o n n é e s et fonctions sont qualifiées de hors-classe. R e p r e n o n s notre e x e m p l e . Voici c o m m e n t n o u s aurions p u définir la classe L i v r e : class Livre { private : char char char public : void void void
N o t e z la présence du point-virgule après l'accolade fermante.
Le C + + n'aime pas forcer la main des programmeurs. Il est possible de définir des données public, c'està-dire accessibles
titre[20]; auteur[20]; éditeur[20];
Saisir(); Afficher(); Imprimer();
} ;
C o m m e n t définir une f o n c t i o n - m e m b r e C o m m e v o u s p o u v e z le constater, n o u s n ' a v o n s fait q u e déclarer les fonctions S a i s i r , A f f i c h e r e t I m p r i m e r . I l faut m a i n t e n a n t les définir c o m p l è t e m e n t . La s y n t a x e est identiq u e au l a n g a g e C, à ceci près : il faut indiquer au compilateur à quelle classe est rattachée u n e fonction. Le format de définition d'une fonction-membre (c'est-à-dire inclue dans u n e classe) est le suivant :
directement de l'extérieur de l'objet C'est une pratique à déconseiller, car elle viole l'idée m ê m e d'encapsulation.
type_retourné NomDeVotreClasse: : f o n c t i o n { p a r a m è t r e s ) { // corps de la fonction }
Ce qui d o n n e d a n s n o t r e e x e m p l e : void
Livre::Saisir()
{
puts("Entrez le titre du livre : " ) ; gets(titre); puts("Entrez le nom de l'auteur : " ) ; gets(auteur); puts("Entrez l'éditeur :"); gets(éditeur); }
void Livre::Afficher() { printf("Titre : %s par %s (%s)", titre, auteur, éditeur);
Les objets et les classes
17
)
void
Livre::Imprimer()
{
fprintf(stdprn, "Titre : %s par %s (%s)", titre, auteur, éditeur); }
L e s fonctions d ' u n e classe p e u v e n t l i b r e m e n t a c c é d e r à toutes ses d o n n é e s - m e m b r e s (ici : t i t r e , a u t e u r e t é d i t e u r ) . N ' i m p o r t e quelle fonction d ' u n e classe p e u t é g a l e m e n t a p p e ler u n e autre fonction de la m ê m e classe. Voilà ! N o t r e prem i è r e classe est écrite ! V o y o n s m a i n t e n a n t de quelle m a n i è r e n o u s p o u v o n s l'utiliser. C r é e r un objet P o u r utiliser u n e classe, il faut d ' a b o r d créer un objet qui a u r a c o m m e type cette classe. L a s y n t a x e est l a m ê m e q u e p o u r déclarer une variable en C : Nom_Classe
nom_objet;
D a n s l ' e x e m p l e du livre, cela d o n n e : Livre
mon_livre;
B i e n e n t e n d u , toutes les formes de déclaration du C s o n t acceptées : tableaux, pointeurs, tableaux de p o i n t e u r s , etc. Par exemple : Livre Livre
ma_bibliotheque[20]; // tableau de 20 objets-livres *sur_ma_table_de_chevet; // pointeur sur un objet-livre
A c c é d e r à la partie p u b l i q u e d'un objet M a i n t e n a n t q u e n o u s s a v o n s c o m m e n t déclarer un objet, il reste à découvrir c o m m e n t l'utiliser. En clair : c o m m e n t saisir, afficher ou i m p r i m e r un objet « livre ». V o u s ne serez p a s d é p a y s é : l'accès a u x f o n c t i o n s - m e m b r e s p u b l i q u e s d ' u n o b jet se fait c o m m e si v o u s accédiez à u n e d o n n é e d ' u n e structure classique du l a n g a g e C :
18
Pont entre C et C++
objet.fonction_publigue(paramètres); pointeur_sur_objet -> fonction_publique(paramètres);
Il est t e m p s d'écrire un petit p r o g r a m m e qui va saisir puis afficher dix livres : void
main()
{
Livre int
bouquin[10]; i;
for (i = 0; i < 10; i++) bouquin[i].Saisir(); for (i = 0; i < 10; i++) bouquin[i].Afficher(); }
C ' e s t aussi s i m p l e que cela. V o u s s a v e z m a i n t e n a n t c o m m e n t écrire des classes simples, et c o m m e n t utiliser des objets. Précision i m p o r t a n t e sur l'accès a u x d o n n é e s private V o u s v o u s r a p p e l e z q u e les d o n n é e s o u fonctions p r i v a t e d ' u n e classe ne sont p a s visibles à partir d e s fonctions d'un autre objet. M a i s u n e petite précision s ' i m p o s e : u n e fonction m e m b r e d ' u n e classe A peut accéder directement à toutes les d o n n é e s (y c o m p r i s p r i v a t e ) d'autres objets de classe A. C e c i n ' e s t p a s f o r c é m e n t évident p u i s q u ' e l l e m a n i p u l e dans ce cas les d o n n é e s d ' u n autre objet. Elle p e u t p o u r t a n t accéd e r a u x d o n n é e s e t fonctions p r i v a t e d e cet autre objet car il est de la m ê m e classe. E x e m p l e : class A { private : int a; public : //
...
void fontion_a(); } ;
class B { private : int b; public : // };
...
L e s objets et les classes
void
19
A::fonction_a()
{
A B
autre_a; objet_b;
a = 1; autre_a.a = 1 ; objet_b.b = 1;
// // // //
OK : on reste dans cet objet OK : autre_a est de classe A NON : b est private dans une autre classe
}
L'intérêt par rapport au C
V o u s l ' a v e z v u , u n objet r e g r o u p e des d o n n é e s e t d e s fonctions qui o p è r e n t sur ces d o n n é e s . Q u e l est l'intérêt de rais o n n e r e n objets plutôt q u ' e n fonctions o u e n structures, c o m m e c'est le cas en C ? L e c o d e g a g n e e n sécurité. V o u s s a v e z q u e les d o n n é e s d ' u n objet ne sont m a n i p u l é e s q u e p a r ses p r o p r e s fonctions, les f o n c t i o n s - m e m b r e s . V o u s p o u v e z d o n c contrôler l'intégrité des d o n n é e s et cibler v o s r e c h e r c h e s en c a s de bogue. L e s p r o g r a m m e s g a g n e n t e n clarté. S i l'architecture d e s objets est b i e n conçue*, v o u s c o m p r e n e z r a p i d e m e n t le rôle de tel ou tel objet. U n e fonction ne se p r o m è n e p a s d a n s le vide, elle est rattachée à un objet, d o n c à l ' e n s e m b l e des d o n n é e s q u ' e l l e m a n i p u l e . La clarté des p r o g r a m m e s et leur c l o i s o n n e m e n t en objets p e r m e t t e n t u n e m a i n t e n a n c e et u n e évolutivité p l u s facile. G r â c e a u x trois a v a n t a g e s p r é c é d e n t s , des é q u i p e s d e dév e l o p p e u r s p e u v e n t p l u s facilement e n v i s a g e r l'écriture de très gros p r o g r a m m e s .
* Les pointeurs de fonction ne rendent jamais un programme plus clair...
C e r t a i n s p o u r r a i e n t p r é t e n d r e q u e l'on p e u t s i m u l e r le principe de classe en C. Il suffirait, après tout, de c r é e r u n e structure c o n t e n a n t des pointeurs de fonction, p o u r s t o c k e r les fonctions m e m b r e s . C e r t e s , m a i s s'agissant d ' u n e g y m n a s t i q u e périlleuse, v o u s p e r d e z le bénéfice de la clarté d e s p r o g r a m m e s * . D e plus, rien n e v o u s interdit d ' a c c é d e r d i r e c t e m e n t a u x d o n n é e s de votre structure, ce qui tord le c o u à l'une d e s règles f o n d a m e n t a l e s de l ' e n c a p s u l a t i o n : cac h e r les d o n n é e s . Par ailleurs, v o u s serez d a n s l'incapacité de s i m u l e r les autres caractéristiques m a j e u r e s d u C + + , q u e n o u s d é c o u v r i r o n s d a n s les chapitres suivants.
20
Pont entre C et C++
Résumé
N o u s v e n o n s de p r é s e n t e r la n o t i o n la p l u s i m p o r t a n t e du C + + et des l a n g a g e s orientés-objets : l'encapsulation. L e C + + p e r m e t d e créer e t m a n i p u l e r d e s objets. U n objet s e c o m p o s e de données et de fonctions de traitement qui lui sont p r o p r e s . C e r t a i n e s de ces d o n n é e s et de ces fonctions sont c a c h é e s , c'est-à-dire q u ' e l l e s ne sont accessibles q u e de l'intérieur de l'objet, à partir de ses fonctions p r o p r e s . P o u r créer u n objet e n C + + , i l faut d ' a b o r d définir u n m o u l e , a p p e l é classe e n C + + . L e s objets sont e n s u i t e créés c o m m e des variables classiques, à partir de leur classe. Les d o n n é e s et fonctions définies d a n s u n e classe s o n t a p p e lées respectivement données membres et fonctions membres, par o p p o s i t i o n a u x d o n n é e s et fonctions hors-classe. Par e x e m p l e , n ' i m p o r t e quelle fonction de la librairie s t a n d a r d du C ( c o m m e p r i n t f ) est u n e fonction hors-classe.
Pour bien démarrer en C++
En tant q u e p r o g r a m m e u r en l a n g a g e C, la p h i l o s o p h i e objets v o u s déroutera peut-être a u début. V o u s n e d e v e z p l u s p e n ser en t e r m e de fonctions, m a i s en t e r m e d'objets, r e g r o u p a n t leurs p r o p r e s d o n n é e s e t leurs p r o p r e s fonctions. Par e x e m ple, si v o u s v o u l e z afficher le p l a t e a u d ' u n j e u , ne p e n s e z p a s à quelque chose c o m m e a f f i c h e r _ p l a t e a u (plateau) m a i s plutôt à p l a t e a u . a f f i c h e r ( ) , o ù p l a t e a u est u n objet c o n t e n a n t , par e x e m p l e , la p o s i t i o n des p i o n s du j e u . Bref, v o u s partez d ' u n objet et lui e n v o y e z un m e s s a g e (sous forme de fonction). R a s s u r e z - v o u s , c'est un réflexe qui viendra r a p i d e m e n t e t q u e v o u s trouverez d e p l u s e n p l u s agréable. B i e n q u e la p h i l o s o p h i e objet n é c e s s i t e u n e c o n c e p tion p l u s s o i g n é e q u ' a u p a r a v a n t , elle p r o c u r e un réel plaisir et b e a u c o u p d ' a v a n t a g e s .
Les objets et les classes
21
Compléments R a p p e l o n s q u e v o u s p o u v e z sauter cette section d a n s u n p r e m i e r t e m p s , et p a s s e r d i r e c t e m e n t au chapitre suivant. S i , toutefois, v o u s désirez e n connaître u n p e u p l u s sur les classes, c o n t i n u e z votre lecture.
Fonctions inline
L e s fonctions inline sont une facilité d u C + + p e r m e t t a n t d'optimiser la vitesse d'exécution des programmes. L ' e x é c u t i o n d ' u n e fonction inline est en effet p l u s rapide q u e celle d ' u n e fonction définie n o r m a l e m e n t : le c o m p i l a t e u r r e m p l a c e les appels à de telles fonctions p a r le c o d e de c e s fonctions. Cette r e m a r q u e en e n g e n d r e u n e autre : il v a u t m i e u x q u e les fonctions inline soient très courtes (une ligne ou d e u x ) p o u r éviter d e cloner i n u t i l e m e n t d e n o m b r e u s e s lignes d e c o d e . M i s e en œuvre C + + E x a m i n o n s l ' e x e m p l e suivant : class UnNombre { private : int public : int void void
nbr; get_nbr (); set_nbr (int); Afficher();
} ;
C o m m e v o u s p o u v e z le voir, cette classe déclare trois fonctions p u b l i q u e s , qui ne sont e n c o r e définies nulle part. En théorie, il faudrait définir c h a c u n e de ces fonctions en d e h o r s de la classe, de cette m a n i è r e : int UnNombre: :get_nbr() { return nbr; }
Le C + + m e t à notre disposition les fonctions inline, p o u r définir c o m p l è t e m e n t u n e fonction à l'intérieur de la définition de sa classe. Ainsi, au lieu de définir à part d e s fonctions très
22
Pont entre C et C++
c o u r t e s , v o u s p o u v e z les inclure d i r e c t e m e n t d a n s la classe. C ' e s t c e q u e n o u s allons faire p o u r les fonctions s e t _ n b r e t get_nbr: class UnNombre { private : int public : int void void
nbr;
get_nbr() set_nbr(int n2) Afficher();
{ return nbr; } { nbr = n2; }
} ;
C o m m e v o u s p o u v e z l e constater, n o u s a v o n s défini complèt e m e n t les d e u x p r e m i è r e s fonctions p u b l i q u e s de cette classe (en gras d a n s l ' e x e m p l e ) . C e s fonctions sont a p p e l é e s inline. L a troisième, A f f i c h e r , n ' e s t q u e déclarée — n o t e z l e pointv i r g u l e après la déclaration—. Il faudra la définir en dehors de la classe, c o m m e n o u s a v i o n s l'habitude de le faire. Q u a n d on définit u n e fonction inline, il est inutile d'accoler le n o m de la classe et les caractères « : : » a v a n t le n o m de la fonction, p u i s q u ' e l l e est définie d a n s le c o r p s de sa classe.
Ce système est efficace si vous utilisez la compilation séparée.
Le mot-clé inline Il existe un autre m o y e n de bénéficier du m é c a n i s m e inline, sans définir les fonctions d i r e c t e m e n t d a n s la classe : il suffit d'utiliser le mot-clé inline en tête de la définition de la fonction. Q u a n d v o u s spécifiez q u ' u n e fonction est inline, v o u s d o n n e z s i m p l e m e n t u n e indication a u c o m p i l a t e u r , qui est libre de la suivre ou p a s . D'ailleurs certains compilateurs refusent de rendre inline d e s fonctions qui c o n t i e n n e n t des mots-clés c o m m e for ou while. Si, par e x e m p l e , n o u s avions v o u l u q u e Afficher soit une fonction inline, sans p o u r autant inclure s o n c o d e d a n s la déclaration de la classe UnNombre, il aurait fallu écrire :
N o u s détaillerons cela au chapitre 14.
inline
void
UnNombre:: Afficher()
{
printf("Le nombre est : %d", nbr); }
Les objets et les classes
23
La déclaration de classe restant identique, m a i s il faut cette fois-ci indiquer au c o m p i l a t e u r le n o m de la classe à laquelle appartient la fonction inline (c'est le rôle de « U n N o m b r e : : »), p u i s q u e cette fonction n ' e s t p a s déclarée d i r e c t e m e n t d a n s la classe.
Remarque : des fonctions hors-classes peuvent elles-aussi êtres définies inline.
Le mot-clé this
Résumé L e C + + p e r m e t a u p r o g r a m m e u r d e définir d e s fonctionsm e m b r e inline. C e s fonctions se c o m p o r t e n t c o m m e toutes les autres fonctions d ' u n e classe, à la seule e x c e p t i o n q u e lors d e l a compilation, c h a q u e appel à u n e fonction i n l i n A e s t r e m p l a c é p a r le corps de cette fonction. Il en résulte un g a i n de t e m p s à l'exécution. On réserve c e p e n d a n t cet a v a n t a g e a u x fonctions très courtes, afin q u e la taille finale de l ' e x é c u t a b l e ne soit p a s trop é n o r m e .
C h a q u e classe p o s s è d e a u t o m a t i q u e m e n t u n e d o n n é e c a c h é e u n p e u spéciale : l e pointeur t h i s . Utilisé d a n s u n e fonction d e classe, t h i s est u n pointeur sur l'objet c o u r a n t à partir d u q u e l on a appelé la fonction m e m b r e . E x e m p l e : class A { private: int i ; public : void f ( ) ; ) ;
void A : :f() { this -> i = 1;
// équivaut à i = 1;
}
t h i s est de type « p o i n t e u r sur l'objet c o u r a n t qui a a p p e l é l a fonction » . D a n s l ' e x e m p l e , t h i s est d e t y p e p o i n t e u r d e A . V o u s e n déduirez q u e ( * t h i s ) représente l'objet c o u rant, et v o u s a u r e z raison. L'utilité d e t h i s est patente p o u r certains opérateurs, o u p o u r tester l'égalité de d e u x objets (voir p a g e 6 1 ) .
24
Pont entre C et C++
Éléments static dans une classe
On ne peut pas utiliser le mot-clé this dans une fonction static, puisque celle-ci ne dépend pas d'un objet précis.
V o u s p o u v e z déclarer des objets, variables ou fonctions s t a t i c d a n s u n e classe. C e s é l é m e n t s seront alors c o m m u n s à tous les objets de cette classe. Ainsi, u n e variable s t a t i c aura la m ê m e valeur p o u r tous les objets de cette classe. S i l'un des objets c h a n g e cette variable s t a t i c , elle sera m o d i f i é e p o u r tous les autres objets. On distingue ainsi les variables et fonctions d'instance (celles qui sont spécifiques à un objet), et les variables et fonctions de classe ( c o m m u n e s à tous les objets de cette classe). C e s dernières doivent être déclarées s t a t i c . I l faut initialiser c h a q u e variable s t a t i c e n d e h o r s d e l a déclaration d e classe, sans répéter l e mot-clé s t a t i c (voir l ' e x e m p l e ci-dessous). L e s f o n c t i o n s - m e m b r e s déclarées s t a t i c d o i v e n t être a p p e lées à partir du n o m de la classe, c o m m e ceci : N o m C l a s s e : : f ( ), sans faire référence à un objet précis. Elles p e u v e n t d o n c être a p p e l é e s s a n s q u ' a u c u n objet de la classe n'existe. #include
Pour c o m p r e n d r e cet exemple, vous devez d'abord lire le chapitre 2 sur les constructeurs.
class Flam { protected: static int nb_objets; int donnée; public : Flam() : donnée(0) { nb_objets++; } static affiche_nb_objets() { cout << nb_objets << endl; } };
int
Flam::nb_objets = 0;
// initialisation
void main() { // appel de la fonction static à partir de la classe Flam::affiche_nb_objets(); Flam
a, b, c; // déclarons 3 objets
// appel de la fonction static à partir d'un objet a.affiche_nb_objets(); }
// affichage // 0 // 3
Les objets et les classes
25
D a n s cet e x e m p l e , le constructeur de Flam ajoute 1 à la v a riable de classe nb_objets (déclarée static au s e n s C + + ) . En créant trois objets a, b et c, n o u s a p p e l o n s trois fois ce constructeur. On p e u t appeler u n e fonction static en la faisant p r é c é d e r du n o m de la classe suivi de d e u x d o u b l e p o i n t s ou à partir d ' u n objet. Il n ' y a p a s de différence entre les d e u x . D i f f é r e n c e e n t r e le m o t - c l é static du C et du C + + A t t e n t i o n : d a n s ce q u e n o u s v e n o n s d ' e x p l i q u e r , le m o t - c l é static sert à déclarer d e s variables o u fonctions de classe. En C standard, r a p p e l o n s q u e static signifie q u e l ' é l é m e n t n ' e s t accessible que dans s o n fichier s o u r c e .
Déclaration de classe anticipée
T o u t c o m m e v o u s p o u v e z déclarer u n e structure à un endroit et la définir c o m p l è t e m e n t ailleurs, v o u s a v e z la p o s sibilité de le faire p o u r des classes. La s y n t a x e est s i m p l e : class nom_de_classe;. I m a g i n o n s q u ' u n e classe A c o n tienne un objet de classe B et r é c i p r o q u e m e n t . Il faut recourir à u n e déclaration p o u r q u e A ait c o n n a i s s a n c e de B a v a n t q u e B ne soit définie : class B;
// déclaration anticipée
class A { protected: B objet_B; public : I I . . . };
class B { protected: A objet_A; public : // ... };
Les constructeurs et les destructeurs
La vie et la mort d'un objet C++ sont régies et les destructeurs. Les fonctions constructeurs création d'un objet, les fonctions destructeurs qu'il sort de sa portée de visibilité, c'est-à-dire
par les constructeurs sont appelées à la sont invoquées dès dès qu'il meurt.
Les notions de base L'idée
E n C o u e n C + + , q u a n d v o u s déclarez u n e variable s a n s l'initialiser, son c o n t e n u est i n d é t e r m i n é . E n C + + , q u a n d v o u s déclarez u n objet d ' u n e certaine classe, s o n c o n s t r u c teur sera a p p e l é automatiquement. Un c o n s t r u c t e u r est u n e f o n c t i o n - m e m b r e qui p o r t e toujours le n o m de la classe d a n s laquelle elle est définie, et qui ne renvoit rien (pas m ê m e un v o i d ) . S i v o u s n e définissez p a s e x p l i c i t e m e n t u n c o n s t r u c teur p o u r u n e classe, l e C + + e n utilise u n d'office, qui v a s e contenter de réserver de la m é m o i r e p o u r toutes les variables de v o t r e classe. Attention : q u a n d v o u s définissez un p o i n t e u r sur un objet, a u c u n constructeur n ' e s t appelé. Si v o u s désirez e n s u i t e allouer de la m é m o i r e et appeler un constructeur, utilisez n e w (voir chapitre 6, p a g e 8 3 ) .
28
Pont entre C et C++
P a r a l l è l e m e n t a u x constructeurs, l e C + + n o u s p r o p o s e les destructeurs. Q u a n d l'objet sort de la p o r t é e du bloc d a n s lequel il a été déclaré, n o t a m m e n t q u a n d l ' e x é c u t i o n du prog r a m m e atteint la fin d ' u n bloc où l'objet est défini, son destructeur est appelé automatiquement.
Mise en œuvre C++ constructeurs
C o m m e n t définir u n e fonction c o n s t r u c t e u r ? N o u s savons q u ' e l l e doit porter l e m ê m e n o m q u e s a c l a s s e . P r e n o n s l ' e x e m p l e d ' u n e classe Equipage : class Equipage { private : int
personnel;
public : Equipage(int personnel_initial); } ;
Equipage::Equipage(int personnel_initial) { personnel = personnel_initial; }
N o t r e constructeur de la classe Equipage ne r e t o u r n e auc u n e valeur, p a s m ê m e u n v o i d , p u i s q u e c'est u n constructeur. Il accepte un p a r a m è t r e , le n o m b r e de p e r s o n n e s dans l ' é q u i p a g e . C e l a signifie q u e l o r s q u e v o u s v o u d r e z déclarer un objet de classe Equipage, il faudra faire suivre l'identifiant de l'objet p a r les p a r a m è t r e s effectifs du constructeur, entre p a r e n t h è s e s . À l ' e x é c u t i o n du p r o g r a m m e suivant, le c o n s t r u c t e u r Equipage : : Equipage est appelé a u t o m a t i q u e m e n t , avec c o m m e p a r a m è t r e l'entier 311 : Ici, le constructeur est appelé, ce qui initialise la d o n n é e - m e m b r e
void
main()
{
Equipage base_alpha(311) ; // déclaration de l'objet et appel du constructeur
personnel à 311 )
Constructeur sans paramètre Si v o u s définissez un constructeur s a n s p a r a m è t r e , par e x e m p l e c o m m e ceci :
Les constructeurs et les destructeurs
29
Equipage : : Equipage() {
nombre = 0 ; )
il faut faire attention à la m a n i è r e de l'appeler. L ' e x e m p l e suivant v o u s m o n t r e ce qu'il faut et ne faut pas faire : void
main()
{
Equipage Equipage
mon_equipage(); mon_autre_equipage;
// (1) incorrect // (2) correct
}
Si v o u s ajoutez des p a r e n t h è s e s , c o m m e d a n s le c a s 1, le c o m p i l a t e u r c o m p r e n d q u e v o u s déclarez u n e fonction ! Donc, pour appeler correctement un constructeur sans par a m è t r e , il faut o m e t t r e les p a r e n t h è s e s , c o m m e d a n s le cas 2. C ' e s t s e u l e m e n t dans ce cas que le c o n s t r u c t e u r sans p a r a m è tre est appelé. C o n s t r u c t e u r s surchargés Il est tout à fait p o s s i b l e de définir p l u s i e u r s c o n s t r u c t e u r s h o m o n y m e s , qui s e d i s t i n g u e r o n t alors p a r l e t y p e e t / o u l a quantité d e p a r a m è t r e s . R e p r e n o n s l a classe E q u i p a g e e t définissons trois c o n s t r u c t e u r s : class Equipage { private : int
personnel;
public : Equipage(int personnel_initial); Equipage(); Equipage(int nbr_femmes, int nbr_hommes); } ;
// constructeur 1 Equipage:: Equipage(int personnel_initial) { personnel = personnel_initial; }
// constructeur 2 Equipage : : Equipage() { personnel = 0;
// 1 // 2 // 3
30
Pont entre C et C++
} // constructeur 3 Equipage::Equipage(int nbr_femmes, int nbr_hommes) { personnel = nbr_femmes + nbr_hommes; }
V o u s d i s p o s e z m a i n t e n a n t de trois m a n i è r e s d'initialiser un objet d e classe E q u i p a g e . L ' e x e m p l e qui suit illustre c e concept : void
main()
{
Equipage base_alpha(311); Equipage mixte(20, 1 ) ; Equipage inconnu;
// appel au constructeur 1 // appel au constructeur 3 // appel au constructeur 2
}
Mise en œuvre C++ destructeurs
R a p p e l o n s q u e le destructeur est a p p e l é automatiquement q u a n d un objet sort de la p o r t é e du b l o c d a n s lequel il est déclaré — c'est-à-dire q u a n d il n ' e s t p l u s accessible au prog r a m m e u r . U n e f o n c t i o n - m e m b r e destructeur n e retourne p a s de t y p e (pas m ê m e un v o i d ) , et n ' a c c e p t e aucun paramètre. C o m m e p o u r les c o n s t r u c t e u r s , le c o m p i l a t e u r g é n è r e un destructeur p a r défaut p o u r toutes les classes qui n ' e n sont p a s p o u r v u e s . Ce destructeur p a r défaut se b o r n e à libérer la m é m o i r e des d o n n é e s de la classe. M a i s si v o u s désirez faire u n e o p é r a t i o n particulière p o u r détruire un objet, et notamm e n t si v o u s a v e z d e s p o i n t e u r s d o n t il faut libérer l'espace m é m o i r e , v o u s p o u v e z définir v o t r e p r o p r e destructeur. Attention ! C o n t r a i r e m e n t a u x c o n s t r u c t e u r s , il ne p e u t exister qu'un seul destructeur par classe ! Le n o m de la fonction destructeur est de la forme « -NomDeClasse ». Le p r e m i e r s i g n e est un « tilde ». Exemple I m a g i n o n s u n e classe qui c o n t i e n n e u n e d o n n é e - m e m b r e p o i n t a n t v e r s u n e c h a î n e de caractères : #include <stdio.h> #include <string.h> class Capitaine
Les constructeurs et les destructeurs
31
{
private : char *nom; public : // constructeur Capitaine) char *nom_initial ); // destructeur -Capitaine(); } ;
// constructeur Capitaine::Capitaine( char *nom_initial ) { nom = (char*) malloc(strlen(nom_initial) + 1 ) ; strcpy(nom, nom_initial); }
// destructeur Capitaine::-Capitaine() { free( nom ); }
Compléments Constructeur copie
Lorsqu'il faut initialiser un objet avec un autre objet de m ê m e classe, le constructeur copie est appelé a u t o m a t i q u e m e n t . Voici u n petit e x e m p l e d e c o d e a p p e l a n t l e c o n s t r u c t e u r c o pie de la classe B e t a : class
Beta
// le contenu ne nous intéresse pas ici
void
fonction_interessante(Beta beta)
// traitement sur beta
void
main()
32
Pont entre C et C++
Beta
premier, deuxième (premier), troisième = premier;
fonction_interessante(premier);
// (1) // (2) // (3)
}
* voir le paragraphe sur les listes d'initialisations, page 47,
* Dans ce cas, l'opérateur = n'est pas appelé puisque ce n'est pas une affectation.
La ligne (1) signifie q u e l'objet d e u x i è m e doit être initialisé a v e c l'objet p r e m i e r * . La ligne (2) initialise elle aussi l'objet t r o i s i è m e avec l'objet p r e m i e r . C e n ' e s t p a s u n e affectation, c'est une initialisation !* Enfin, la ligne (3) fait appel à u n e fonction qui va recopier le p a r a m è t r e effectif, p r e m i e r , d a n s s o n p a r a m è t r e formel, b e t a . Bref, ces trois lignes font implicitement a p p e l au constructeur c o p i e . C o m m e p o u r les c o n s t r u c t e u r s o r d i n a i r e s ou les destructeurs, l e C + + e n g é n è r e u n p a r défaut s i v o u s n e l'avez p a s fait v o u s - m ê m e . Ce c o n s t r u c t e u r c o p i e p a r défaut effectue u n e c o p i e « b ê t e », m e m b r e à m e m b r e , d e s d o n n é e s de la classe. D a n s les cas où v o s classes c o n t i e n n e n t d e s pointeurs, cela p o s e un p r o b l è m e : en ne c o p i a n t q u e le pointeur, l'objet c o p i é et l'objet c o p i a n t pointeraient sur la m ê m e zone mém o i r e . Et ce serait fâcheux ! C e l a signifierait q u ' e n modifiant l ' u n d e s d e u x objets, l'autre serait é g a l e m e n t modifié de m a n i è r e « invisible » ! L e s c h é m a suivant illustre c e p r o b l è m e .
Schéma illustrant le problème d'une copie membre à membre
Les constructeurs et les destructeurs
33
P o u r r é s o u d r e c e p r o b l è m e , v o u s d e v e z d o n c écrire votre propre constructeur copie, qui effectuera u n e c o p i e p r o p r e — avec, par e x e m p l e , u n e nouvelle allocation p o u r c h a q u e pointeur d e l'objet copié. N o u s o b t e n o n s l e s c h é m a suivant, après u n e c o p i e « p r o p r e ».
Schéma illustrant le résultat d'une copie propre.
C o m m e n t définir son p r o p r e c o n s t r u c t e u r copie ? La f o r m e d ' u n c o n s t r u c t e u r copie est toujours la m ê m e . Si votre classe s'appelle Gogol, s o n c o n s t r u c t e u r c o p i e
s'appellera Gogol: : Gogol (const Gogol&). Le p a r a m è * les références sont traitées au chapitre 9, page I 13.
*cout est un mot clé C++ utilisé pour l'affichage de caractères. Voir
tre est u n e référence* à un objet de classe Gogol. Ici const n ' e s t p a s n é c e s s a i r e m a i s fortement r e c o m m a n d é , p u i s q u e v o u s ne modifierez pas l'objet à copier. É c r i v o n s la classe Gogol, p o s s é d a n t un p o i n t e u r de char, et efforçons-nous d'écrire son c o n s t r u c t e u r de c o p i e : #include <string.h> #include
// pour cout*
class Gogol { private: char
chaptre 7, page 89.
*pt;
public : // constructeur normal Gogol(char * c ) ; // constructeur de copie Gogol(const Gogol &a_copier);
34
Pont entre C et C++
// accès aux données membre void set_pt(char * c ) ; char *get_pt(); // autres fonctions void Afficher();: >;
// constructeur normal Gogol::Gogol(char *c) { pt = new char [strlen(c) + 1 ] ; strcpy(pt, c ) ; }
// constructeur de copie Gogol:: Gogol(const Gogol &a_copier) {
if (this != &a_copier) // voir dans la marge
Il faut vérifier que
{
l'objet source et
// effaçons l'ancien pt delete pt;
l'objet destination sont différents, car
// allouons de la place pour le nouveau pt pt = new char [strlen(a_copier.pt) + 1 ] ;
s'ils étaient identiques, delete pt effacerait à la fois les pointeurs
// donnons une valeur au nouveau pt strcpy(pt, a_copier.pt);
source et destination ! } }
void Gogol::set_pt(char *c) { delete pt; pt = new char [strlen(c) + 1 ] ; strcpy(pt, c ) ; }
char *Gogol: :get_pt() { return pt; }
void Gogol::Afficher() {
cout << pt << endl; }
void {
maint) Gogol
gog("Zut"), bis = gog; // appel du constructeur copie
gog.Afficher(); bis.Afficher();
// Zut // Zut
Les constructeurs et les destructeurs
35
// on modifie la chaine de gog gog.set_pt("Mince alors"); gog.Afficher(); bis.Afficher();
// Mince alors // Zut
}
C o n c l u s i o n : les objets gog et bis p o s s è d e n t b i e n d e u x p o i n teurs d é s i g n a n t d e u x z o n e s m é m o i r e s différentes. C ' e s t parfait. M a i s si n o u s n ' a v i o n s p a s écrit notre p r o p r e constructeur copie, celui g é n é r é p a r défaut aurait c o p i é les d o n n é e s m e m b r e à m e m b r e . A i n s i , d a n s gog et bis, pt aurait été le m ê m e . D o n c , le fait de détruire le p o i n t e u r de gog aurait é g a l e m e n t détruit celui de bis p a r effet de b o r d . Inutile de préciser les c o n s é q u e n c e s d é s a s t r e u s e s d ' u n tel é v é nement.
Constructeurs de conversion
Si vous avez besoin de l'opération réciproque (convertir un objet de votre classe vers une variable de type T), allez voir page 61 où l'on vous dira que c'est possible.
Un c o n s t r u c t e u r n ' a y a n t q u ' u n seul a r g u m e n t de t y p e T spécifie u n e c o n v e r s i o n d ' u n e variable de t y p e T v e r s un o b jet de la classe du constructeur. Ce c o n s t r u c t e u r sera a p p e l é a u t o m a t i q u e m e n t c h a q u e fois q u ' u n objet de t y p e T sera rencontré là où il faut un objet de la classe de constructeur. P a r e x e m p l e , i m a g i n o n s q u ' o n veuille u n e c o n v e r s i o n autom a t i q u e d ' u n entier vers un objet de classe Prisonnier, où l'entier r e p r é s e n t e s o n n u m é r o matricule. A i n s i , q u a n d l e c o m p i l a t e u r attend un objet Prisonnier et qu'il n ' a q u ' u n entier à la p l a c e , il effectue la c o n v e r s i o n a u t o m a t i q u e m e n t en créant l'objet Prisonnier à l'aide du c o n s t r u c t e u r de conversion. Exemple : #include class Prisonnier { protected: int numéro; char nom [50] ; public : //
...
Prisonnier(int n) : numéro(n) { cout << "Conversion de " << n << endl; nom[0] = 0 ; } } ;
void {
fonction(Prisonnier p) //
. . .
36
Pont entre C et C++
}
void {
main()
Prisonnier
p = 2;
// donne p = Prisonnier(2)
fonction(6); // donne fonction(Prisonnier(6)) } // affiche : // Conversion de 2 // Conversion de 6
L'héritage simple
L'héritage vous permet de modéliser le monde réel avec souplesse, d'organiser vos classes et de réutiliser celles qui existent déjà. C'est un concept fondamental en programmation orientée-objets. Nous parlons ici d'héritage simple, par opposition à l'héritage multiple qui sera développé dans la deuxième partie du livre.
Notions de base L'idée
L e s informaticiens se feraient b e a u c o u p m o i n s de soucis s'ils p o u v a i e n t r é g u l i è r e m e n t réutiliser des p o r t i o n s d e c o d e qu'ils ont déjà écrit p o u r des projets antérieurs. G r â c e à l'héritage, u n e part du r ê v e devient réalité : v o u s p o u v e z profiter des classes déjà existantes p o u r en créer d'autres. M a i s l à o ù l'héritage est intéressant, c'est q u e v o u s p o u v e z ne garder q u ' u n e partie de la classe initiale, modifier certaines de ses fonctions ou en ajouter d'autres. L'héritage s'avère très a d a p t é a u x cas o ù v o u s d e v e z m a n i puler des objets qui ont des p o i n t s c o m m u n s , m a i s qui diffèrent l é g è r e m e n t .
38
Pont entre C et C++
* N o u s aurions pu parler de la classifica-
Le principe Le p r i n c i p e initial de l'héritage est p r o c h e de celui de la classification en catégories : on part du c o n c e p t le p l u s général p o u r se spécialiser d a n s les cas particulier. A d m e t t o n s que n o u s v o u l i o n s parler de véhicules*, et e s s a y o n s de tisser des liens d'héritage entre q u e l q u e s v é h i c u l e s b i e n c o n n u s :
t i o n des cornichons, des jeunes filles ou des flippers, mais un exemple classique est sans doute mieux adapté à un large public.
Le sens des flèches d'héritage pourrait être : « p o s s è d e les caractéristiques de » ou « est un », au s e n s large du terme. Ainsi, voiture est un véhicule. De m ê m e p o u r le bateau. Le camion, lui, p o s s è d e les caractéritiques de voiture et, indirectem e n t , de véhicule. O n dit q u e v o i t u r e hérite de v é h i c u l e . D e m ê m e , bateau hérite de v é h i c u l e , et c a m i o n de voiture. D ' u n cas général, r e p r é s e n t é par véhicule, n o u s a v o n s tiré d e u x cas particuliers : voiture et bateau. Camion est lui-même un « cas particulier » de voiture, car il en p o s s è d e grosso modo toutes les caractéristiques, et en ajoute d'autres q u i lui sont propres.
Mise en œuvre C++
D a n s l ' e x e m p l e ci-dessus, c h a q u e é l é m e n t du s c h é m a est rep r é s e n t é p a r u n e classe. U n e r e m a r q u e de vocabulaire : q u a n d u n e classe A hérite d ' u n e classe B, on dit q u e A est la classe de base et B la classe dérivée. P o u r y voir p l u s clair, écriv o n s les classes v é h i c u l e et voiture, et e x a m i n o n s ensuite ce q u e cela signifie a u n i v e a u d u C + + :
Le m o t clé protected est expliqué dans les pages qui suivent.
class Véhicule { protected : int // ...
nombre_de_places;
L'héritage simple
39
public : // constructeurs Véhicule(); // ... // fonctions de traitement void Afficher)); // . . . } ;
Remarque : nous n'avons pas détaillé
class Voiture : public Véhicule {
protected : int chevaux_fiscaux; // . . . public : // constructeurs Voiture(); // fonctions de traitement void CalculVignette();
ces classes afin de nous concentrer sur le mécanisme d'héritage. Vous retrouverez les classes complètes en fin de chapitre. } ;
R i e n ne d i s t i n g u e ces classes d'autres classes ordinaires, si ce
n ' e s t la c h a î n e « : public Véhicule » à la suite du n o m de la classe Voiture. C ' e s t bel et b i e n cette c h a î n e qui indiq u e au compilateur q u e la classe Voiture hérite de la classe
Véhicule. Le mot-clé public qualifie la spécification d'accès d e l'héritage. N o u s v e r r o n s ci-dessous q u e l e t y p e d ' h é r i t a g e d é t e r m i n e quelles d o n n é e s de la classe de b a s e sont accessib l e s d a n s la classe dérivée. V o i l à , n o u s a v o n s déclaré q u e la classe Voiture héritait de la classe Véhicule. Q u e cela signifie-t-il c o n c r è t e m e n t ? Q u e d a n s d e s objets de classe dérivée (Voiture), v o u s p o u v e z a c c é d e r à des d o n n é e s ou fonctions m e m b r e s d'objets de la classe de b a s e (Véhicule).
* Rappelons que la spécification d'accès est déterminée par le mot-clé qui suit les deux-points («:») après la ligne class
NomClasseDérivée.
Accès a u x m e m b r e s de la classe de b a s e P o u r savoir p r é c i s é m e n t quelles d o n n é e s s o n t accessibles d a n s la classe dérivée, il faut considérer la spécification d'accès* ainsi q u e le t y p e des d o n n é e s (public, protected ou private) de la classe de b a s e . Le tableau s u i v a n t d o n n e les c o m b i n a i s o n s possibles :
40
Pont entre C et C++
A u t r e m e n t dit, le mot-clé protected est identique à private si vous n'utilisez pas l'héritage.
L e c o n t e n u d u tableau i n d i q u e l e type d u m e m b r e ( p u b l i c , p r o t e c t e d , p r i v a t e o u inaccessible) d a n s l a classe dérivée. V o u s d é c o u v r e z d a n s c e tableau u n n o u v e a u spécificateur d ' a c c è s : p r o t e c t e d . C e spécificateur est i d e n t i q u e à p r i v a t e à l'intérieur de la classe. Il diffère u n i q u e m e n t en cas d'héritage, p o u r d é t e r m i n e r s i les é l é m e n t s p r o t e c t e d sont accessibles ou n o n d a n s les classes dérivées. P o u r m i e u x c o m p r e n d r e ce tableau, voici trois petits exemples de dérivation : class Base { private : int protected : int public : int
detective_prive; acces_protege; domaine_publicp±e;
} ;
class Deriveel : public Base { // detective_prive est inaccessible ici // acces_protege est considéré comme « protected » // domaine_public est considéré comme « public » } ;
class Derivee2 : protected Base { // detective_prive est inaccessible ici // acces_protege est considéré comme « protected » // domaine_public est considéré comme « protected » >;
class { // // // };
Derivee2 : private Base detective_prive est inaccessible ici acces_protege est considéré comme « private » domaine_public est considéré comme « private »
L'héritage simple
41
Ces exemples amènent plusieurs remarques : U n e d o n n é e o u fonction m e m b r e p r i v a t e est toujours inaccessible d a n s ses classes dérivées L a spécification d ' a c c è s d ' u n e d o n n é e ( p u b l i c , p r o t e c t e d o u p r i v a t e ) d a n s u n e classe dérivée est e n fait l a spécification d ' a c c è s la p l u s forte. Par e x e m p l e , si u n e f o n c t i o n - m e m b r e est p r o t e c t e d d a n s l a classe d e b a s e , e t q u e l'héritage est p u b l i c , elle devient p r o t e c t e d d a n s la classe dérivée. E n règle générale, o n utilise l'héritage p u b l i c p o u r m o déliser des relations du m o n d e « réel ». V o i r le chapitre 15 sur les conseils.
Redéfinition d'une fonctionmembre
V o u s p o u v e z définir des fonctions a u x entêtes i d e n t i q u e s d a n s différentes classes a p p a r e n t é e s p a r u n e relation d'héritage. La fonction a p p e l é e d é p e n d r a de la classe de l'objet qui l'appelle, ce qui est d é t e r m i n é s t a t i q u e m e n t à la compilation. R e p r e n o n s : u n e f o n c t i o n - m e m b r e d ' u n e classe dérivée p e u t d o n c avoir le m ê m e n o m (et les mêmes p a r a m è tres) q u e celle d ' u n e classe de b a s e . P r e n o n s l ' e x e m p l e d ' u n e gestion de textes qui d i s t i n g u e les textes b r u t s des textes m i s en forme, et définissons d e u x fonctions a u x entêtes identiques (mais au c o r p s différent) : #include <stdio.h> class TexteBrut public : void void
Imprimer(int nb) ;
TexteBrut::Imprimer(int nb)
printf("Imprimer de la classe TexteBrutXn");
class TexteMisEnForme : public TexteBrut public : void void
// héritage
Imprimer(int n b ) ;
TexteMisEnForme::Imprimer(int nb)
printf("Imprimer de la classe TexteMisEnFormeXn");
42
Pont entre C et C++
void main() { TexteBrut TexteMisEnForme
txt; joli_txt;
txt.Imprimer( 1 ) ; joli_txt.Imprimer(1); }
// affichage : Imprimer de la classe TexteBrut Imprimer de la classe TexteMisEnForme
Conversion de Dérivée* vers Base*
D a n s un cas où u n e classe Dérivée hérite p u b l i q u e m e n t d ' u n e classe Base, les c o n v e r s i o n s de p o i n t e u r s de Dérivée v e r s des p o i n t e u r s de Base est faite a u t o m a t i q u e m e n t aux endroits où le c o m p i l a t e u r attend un p o i n t e u r sur Base : class Base { >; class Dérivée : public Base { }; void main() { Base *base; Dérivée *derivee; base = dérivée;
// OK
}
Attention : cette c o n v e r s i o n n ' e s t p a s autorisée si l'héritage
est protected ou private ! Si v o u s r e m p l a c e z public p a r protected d a n s le listing ci-dessus, la d e r n i è r e ligne du main p r o v o q u e r a u n e erreur de c o m p i l a t i o n . P o u r q u o i ? P a r c e q u e p o u r q u ' u n p o i n t e u r sur u n e classe dérivée puisse être converti sur u n e classe de b a s e , il faut q u e cette dernière soit accessible, c'est-à-dire q u ' e l l e p o s s è d e des m e m b r e s public accessibles. Or ce n ' e s t p a s le cas d ' u n h é r i t a g e pro-
tected ou private. Résumé
E n C + + , u n e classe p e u t hériter d ' u n e autre. O n dit alors que la classe qui hérite est u n e classe dérivée, l'autre classe étant a p p e l é e classe de base. Q u a n d u n e classe A hérite d'une classe B, elle p e u t a c c é d e r à certaines d o n n é e s ou fonctions
L'héritage simple
43
m e m b r e s d e l a classe B . C ' e s t u n petit p e u c o m m e s i u n e partie de la classe B avait été recopiée d a n s le c o r p s de la classe A. Pour savoir ce qui est accessible d a n s A, il faut c o n s i d é r e r la spécification d ' a c c è s ( p u b l i c , p r o t e c t e d o u p r i v a t e ) , l e type d ' a c c è s des d o n n é e s d e B ( p u b l i c , p r o t e c t e d o u p r i v a t e ) e t consulter l e tableau d e l a p a g e 4 0 .
L'intérêt par rapport au C
Là, le C est c o m p l è t e m e n t battu. G r â c e à l'héritage, v o u s allez enfin p o u v o i r réutiliser facilement le c o d e q u e v o u s a v e z déjà écrit. V o u s p o u r r e z enfin « factoriser » les p o i n t s c o m m u n s d ' u n e structure d'objets. Il n ' e x i s t e rien, d a n s le l a n g a g e C , qui p e r m e t t e d e mettre e n œ u v r e facilement u n m é c a n i s m e similaire. P r e n o n s un e x e m p l e : v o u s d e v e z gérer les e m p l o y é s d ' u n e centrale nucléaire. À partir d ' u n e classe de b a s e E m p l o y é , v o u s faites hériter d'autres classes c o r r e s p o n d a n t a u x catégories de p e r s o n n e l : Ouvrier, C a d r e , A g e n t S é c u r i t é , etc. Si à l'avenir d'autres p o s t e s sont créés (par e x e m p l e P o r t e P a r o l e ou A v o c a t ) , v o u s p o u r r e z créer la classe c o r r e s p o n d a n t e et la faire hériter de E m p l o y é ou de classes spécifiques. D a n s n o tre e x e m p l e , un P o r t e P a r o l e pourrait hériter de la classe C a dre. L ' a v a n t a g e : v o u s n ' a v e z p a s à ré-écrire tout ce q u e les portes-paroles et les c a d r e s ont en c o m m u n s .
Compléments Héritage et constructeurs
L e s c o n s t r u c t e u r s n e sont j a m a i s hérités. Q u a n d v o u s déclarez un objet d ' u n e classe dérivée, les c o n s t r u c t e u r s p a r d é faut d e s classes de b a s e s v o n t être a p p e l é s a u t o m a t i q u e m e n t avant q u e le c o n s t r u c t e u r spécifique de la classe dérivée ne le soit. Voici un e x e m p l e : class Base { public : Base();
44
Pont entre C et C++
} ;
class Deriveel : public Base { public : Deriveel(); } ;
class Derivee2 : public Deriveel { public : Derivee2(); };
void
main()
{
Base b; // appel à Base() Deriveel dl; // appels à Base() puis à Deriveel() Derivee2 d2 ; // appels à Base() , Deriveel().. . // puis Derivee2() // corps de la fonction }
P o u r ne p a s a p p e l e r les c o n s t r u c t e u r s p a r défauts m a i s des c o n s t r u c t e u r s avec des p a r a m è t r e s , v o u s d e v e z recourir aux listes d'initialisations, e x p l i q u é e s p l u s loin d a n s ce chapitre.
Héritage et destructeurs
A l o r s q u e les constructeurs sont a p p e l é s d a n s l'ordre desc e n d a n t (de la classe de b a s e à la classe d é r i v é e ) , c'est l'inverse p o u r les destructeurs : celui de la classe d é r i v é e est a p p e l é en p r e m i e r , p u i s celui de la classe de b a s e immédiat e m e n t supérieure, et ainsi de suite j u s q u ' à la classe de base la plus haute. C o m p l é t o n s l ' e x e m p l e p r é c é d e n t : void
main()
{
Base b; // appel à Base() Deriveel dl ; // appels à Base{) puis à DeriveelO Derivee2 d2 ; // appels à Base(), Deriveel().. . // ...puis Derivee2() // corps de la fonction }
// mort de b : appel à -Base() // trépas de dl : appels à -Deriveel() puis -Base() // éradication de d2 : appels à ~Derivee2(), // -Deriveel() puis -Base()
L'héritage simple
Exemple complet
45
R e p r e n o n s notre p r o b l è m e d e véhicules. Voici l e c o d e c o m plet des classes Véhicule et Voiture, et d e s e x e m p l e s d'accès a u x d o n n é e s de la classe de b a s e , à savoir Véhiculé. class Véhicule { private : char buffer_interne[128]; // uniquement utilisé par les fonctions de // la classe Véhicule. Inaccessible aux // classes dérivées. protected : char nom_vehicule[20]; int nombre_de_places; // données protégées, transmises aux classes // dérivées mais toujours inaccessibles / / à l'extérieur de l'objet public : // fonctions qui resteront publiques // dans les classes dérivées (à part les // constructeurs) // constructeur Véhicule(char *nom, int places); // fonctions d'accès aux données membre char *get_nom_vehicule(); int get_nombre_de_places(); void set_nom_vehicule(char *nom); int set_nombre_de_place(int p ) ; // fonctions de traitement void Afficher();; };
// définition des fonctions Véhicule::Véhicule(char *nom, int places) { sprintf(nom_vehicule, "%20s", n o m ) ; // on écrit dans la chaine nom_vehicule les // premiers caractères de la chaine nom. nombre_de_places = places; }
char
*Vehicule::get_nom_vehicule()
{
return nom_vehicule; }
int
Véhicule::qet nombre de places!)
{
return nombre_de_places;
20
46
Pont entre C et C++
}
void Véhicule::set nom„vehicule(char *nom) { sprintf(nom_vehicule, "%20s", nom); // on écrit dans la chaine nom_vehicule les 20 // premiers caractères de la chaine nom. }
int {
Véhicule::set_nombre_de_place(int
p)
nombre_de_places = p; } //
Voici maintenant la classe dérivée
class Voiture : public Véhicule {
private : float SavantCalcul (); protected : int chevaux_fiscaux; public : // constructeurs Voiture(char *nom, int places, int chevaux); // fonctions d'accès int get_chevaux_fiscaux(); void set_chevaux_fiscaux(int c h ) ; // fonctions de traitement float CalculVignette(); } ;
Voiture::Voiture(char *nom, int places, int chevaux) {
sprintf(nom_vehicule, "%20s", n o m ) ; nombre_de_places = places; chevaux_fiscaux = chevaux; } int Voiture : :get_chevaux_fiscaux() {
return chevaux_fiscaux; }
void Voiture::set_chevaux_fiscaux(int ch) { chevaux_fiscaux = ch; }
float Voiture::SavantCalcul(int code) { float savant_calcul; // on fait un savant calcul avec "code" return savant_calcul;
L'héritage simple
47
}
La fonction CalculVignette fait
float
Voiture::CalculVignette()
{
int code;
n'importe quoi, soyez certain que
if (chevaux_fiscaux < 2) code = 0 ; else code = chevaux_fiscaux - 1; return (SavantCalcul(code));
l'auteur en a parfaitement conscience. }
Listes d'initialisations
L e s lecteurs les p l u s alertes n e m a n q u e r o n t p a s d e c o n s t a t e r q u e l q u e s l o u r d e u r s d a n s l ' e x e m p l e p r é c é d e n t . E x a m i n e z les c o n s t r u c t e u r s de la classe Voiture. E x a m i n e z m a i n t e n a n t les c o n s t r u c t e u r s de la classe Véhicule. Je récapitule p o u r v o u s les d e u x c o n s t r u c t e u r s ci-dessous. N e r e m a r q u e z - v o u s pas une certaine ressemblance ? Véhicule::Vehicule(char *nom, int places) { sprintf(nom„vehicule, "%20s", nom); nombre_de_places = places; }
Voiture::Voiture(char *nom, int places, int chevaux) { sprintf(nom_vehicule, %20s", nom); nombre de_places = places; chevaux_fiscaux = chevaux; } n
O h , m a i s c'est b i e n sûr ! L e s d e u x p r e m i è r e s lignes s o n t i d e n t i q u e s ! N ' e s t - c e p a s là le s i g n e p r o v o c a t e u r d ' u n e red o n d a n c e inutile et d a n g e r e u s e p r o p r e à susciter u n e colère aussi v i v e q u e justifiée ? Le p r o b l è m e vient du fait q u e les c o n s t r u c t e u r s (ainsi q u e les d e s t r u c t e u r s , m a i s c e n ' e s t p a s l e p r o p o s ici) n e s o n t p a s hérités. C e l a signifie q u e , q u e l q u e part d a n s le c o n s t r u c t e u r de Voiture, v o u s d e v e z initialiser les v a r i a b l e s - m e m b r e s défin i e s d a n s Véhicule, et d o n t Voiture a hérité. Ici, il s'agit d e s v a r i a b l e s nom et places. Le p r o b l è m e est en fait s i m p l e : c o m m e n t faire p o u r appeler un c o n s t r u c t e u r de la c l a s s e de B a s e , d e p u i s un c o n s t r u c t e u r d ' u n e classe d é r i v é e ? D e u x solutions :
48
Pont entre C et C++
Vous êtes en train de paniquer ? Vous avez l'impression d'être noyé sous les n o u veautés syntaxiques du C + + ? Ne trem-
Faire c o m m e d a n s l ' e x e m p l e , c'est-à-dire recopier les lig n e s de c o d e du constructeur de la c l a s s e de b a s e qui v o u s intéresse. C ' e s t très laid : d u p l i q u e r b ê t e m e n t du c o d e , c'est alourdir le p r o g r a m m e , et si j a m a i s ce c o d e doit être modifié, v o u s devrez retrouver tous les e n d r o i t s où v o u s l ' a v e z recopié. I m a g i n e z u n p e u c e q u e cela peut d o n n e r d a n s u n e hiérarchie de p l u s i e u r s c e n t a i n e s de classes ! La m e i l l e u r e solution : utiliser u n e liste d'initialisation. Q u ' e s t - c e d o n c ? C ' e s t un m o y e n p r a t i q u e d ' a p p e l e r direct e m e n t des constructeurs p o u r des d o n n é e s - m e m b r e qu'il faut initialiser. Voici un e x e m p l e d'utilisation de la liste d'initialisation :
blez plus, car l'aidem é m o i r e C + + est arrivé ! Il est joli et il vous attend sur le
// constructeur de la classe Dérivée, amélioré Voiture ::Voiture(char *nom, int places, int chevaux) : Véhicule(nom, places) {
rabat de la couverture !
chevaux_fiscaux = chevaux; )
J u s t e après les p a r a m è t r e s du constructeur, tapez le caractère : (deux points) et a p p e l e z e n s u i t e les constructeurs d e s classes de b a s e s q u e v o u s a v e z c h o i s i s , a v e c leurs par a m è t r e s b i e n e n t e n d u . Si v o u s a p p e l e z p l u s i e u r s constructeurs, séparez-les p a r u n e virgule. D a n s l ' e x e m p l e ci-dessus, les p a r a m è t r e s nom e t p l a c e s utilisés p o u r appeler l e c o n s t r u c t e u r d e V é h i c u l e sont ceux que vous avez passés au constructeur de V o i t u r e . V o u s p o u v e z é g a l e m e n t utiliser u n e liste d'initialisation p o u r v o s variables s i m p l e s (entier, caractère, etc.) Exemple : // constructeur de la classe Dérivée, encore amélioré Voiture::Voiture(char *nom, int places, int chevaux) : Véhicule(nom, places), chevaux_fiscaux(chevaux) { }
L e s différents é l é m e n t s de la liste d'initialisation sont sép a r é s p a r u n e virgule. V o u s r e m a r q u e z q u e m a i n t e n a n t , il n ' y a plus d'instruction d a n s le corps de la fonction ! La liste d'initialisation est le seul m o y e n d'initialiser une d o n n é e - m e m b r e spécifiée constante. R a p p e l o n s qu'une
L'héritage simple
49
c o n s t a n t e ne p e u t être qu'' initialisée (et j a m a i s affectée), voir p a g e 67. Un autre a v a n t a g e d e s listes d'initialisation : elles n e créent p a s d'objets t e m p o r a i r e s c o m m e u n e fonction ordinaire. L e s objets sont initialisés d i r e c t e m e n t , d ' o ù gain de t e m p s certain. Résumé U n e liste d'initialisation p e u t appeler d e s c o n s t r u c t e u r s d e s classes d e b a s e o u initialiser des variables s i m p l e s . R é s u m é de la s y n t a x e : Classe::Classe(paramètres) : liste_ini { // corps du constructeur de Classe }
O ù l i s t e _ i n i contient u n o u p l u s i e u r s é l é m e n t s s u i v a n t s , séparés p a r d e s virgules : ConstructeurClasseDeBase(paramètres) ou Variable(valeur_initiale)
La surcharge
Le C++ offre aux programmeurs la possibilité de définir des fonctions homonymes. Ce principe est appelé « surcharge de fonction ».
Notions de base L'idée
Écrire d e u x fonctions qui portent l e m ê m e n o m est u n e c h o s e impensable en langage C. Pas en C + + . Imaginez que vous v o u l i e z écrire u n e fonction qui trie des tableaux d'entiers. L ' e n t ê t e de cette fonction ressemblerait à ceci : void
tri(int tab[], int taille_tableau);
Si p a r la suite v o u s a v e z b e s o i n d ' u n e fonction q u i trie des tableaux de réels, v o u s d e v e z réécrire la fonction, en lui donn a n t un autre n o m : void
tri_reels(float tab[], int taille_tableau);
S i v o u s êtes logiques avec v o u s - m ê m e , v o u s d e v r e z égalem e n t c h a n g e r le n o m de la fonction initiale : void
tri_entiers(int tab[], int taille_tableau);
52
Pont entre C et C++
Ce qui v o u s oblige à modifier tous les appels à cette fonction. G r â c e au m é c a n i s m e de surcharge de fonction, v o u s pouvez d o n n e r le n o m tri a u x d e u x fonctions, le compilateur pouv a n t les différencier grâce a u x types d ' a r g u m e n t s . N o t e z q u e le l a n g a g e C p r o p o s e déjà un type de surcharge p o u r les o p é r a t e u r s arithmétiques : le m é c a n i s m e mis en œ u v r e d é p e n d du type des objets sur l e s q u e l s on invoque l'opérateur.
Mise en œuvre C++
R e p r e n o n s notre e x e m p l e de tri, et appliquons-lui le principe de s u r c h a r g e . L e s d e u x entêtes d e v i e n n e n t : void void
tri(int tab[], int taille_tableau); triffloat tab[], int taille_tableau);
La seule différence étant le type du p r e m i e r paramètre. E x a m i n o n s m a i n t e n a n t les appels de fonctions suivants : void main() { float int
tab_f[] = { 1.5, 1.2, 7.2, 0.07 } ; tab_i[] = { 3, 2, 1, 0 };
tri(tab_f, 4 ) ; tri(tab_i, 4 ) ; }
Le p r e m i e r a p p e l à tri fait a u t o m a t i q u e m e n t référence à la d e u x i è m e fonction tri, qui accepte un tableau de flottants en p r e m i e r p a r a m è t r e . Le d e u x i è m e a p p e l à tri se fait natur e l l e m e n t avec la fonction tri qui s ' o c c u p e d e s entiers. C o m m e n t le c o m p i l a t e u r fait-il la différence e n t r e plusieurs fonctions h o m o n y m e s ? T o u t d ' a b o r d , seules les fonctions situées d a n s la portée de l'appel —c'est-à-dire « visibles »— sont p r i s e s en compte. E n s u i t e , les critères suivants sont e x a m i n é s : 1. Le type ou le n o m b r e de p a r a m è t r e s . D è s q u e d e u x fonctions h o m o n y m e s diffèrent p a r le t y p e ou le n o m b r e de l'un ou de p l u s i e u r s de leurs p a r a m è t r e s , le compilateur reconnaît la b o n n e fonction s e l o n les types réels passés à l'appel. C ' e s t l ' e x e m p l e q u e n o u s v e n o n s d e voir.
La surcharge
53
Attention ! Le type de retour de la fonction n ' e s t j a m a i s pris en c o m p t e ! Par e x e m p l e , il est illégal de déclarer les d e u x fonctions suivantes : int float
truc(int); truc(int);
Le c o m p i l a t e u r ne pourrait faire la différence. En revanc h e , il est toujours possible d'écrire : int float
truc(int); truc(float);
P u i s q u e , cette fois, le type des p a r a m è t r e s est différent.
* Les classes de base
2. Si l'étape 1 ne p e r m e t pas d'identifier la fonction à a p p e ler, le c o m p i l a t e u r essaie de convertir les p a r a m è t r e s réels vers les p a r a m è t r e s des fonctions h o m o n y m e s . A i n s i , u n c h a r o u u n s h o r t seront convertis v e r s u n int, les float seront convertis v e r s des double, les p o i n t e u r s de classes dérivées seront convertis en p o i n t e u r s de classes de base*, etc.
et les classes dérivées seront expliquées au chaprtre 3, sur l'héritage.
L'intérêt par rapport au C
En cas d ' a m b i g u ï t é insoluble, le c o m p i l a t e u r signale u n e erreur. D e toutes m a n i è r e s , m i e u x v a u t éviter les cas a m b i g u s qui sont s o u r c e d'affres éternelles.
O n pourrait croire q u e les fonctions h o m o n y m e s r e n d e n t les p r o g r a m m e s m o i n s lisibles et plus confus. C e r t e s , si v o u s définissez plusieurs fonctions « traite_donnees », qui font des c h o s e s t o t a l e m e n t différentes, le gain en clarté sera plutôt discutable. E n r e v a n c h e , d a n s les cas o ù v o u s d i s p o s e z d ' u n j e u d e fonctions qui fait p r a t i q u e m e n t la m ê m e c h o s e , m a i s d o n t certains p a r a m è t r e s diffèrent, v o u s a v e z intérêt à utiliser la s u r c h a r g e . Bref, c h a q u e fois q u e le sens des fonctions est s e n s i b l e m e n t identique, d o n n e z - l e u r l e m ê m e n o m . D a n s tous les c a s , le l a n g a g e C v o u s oblige à d o n n e r d e s n o m s différents, s i m p l e m e n t p o u r q u e l e c o m p i l a t e u r reconnaisse ses petits. R e m a r q u o n s q u e d a n s l ' e x e m p l e du tri,
54
Pont entre C et C + +
n o u s p o u r r o n s aussi n o u s servir du m é c a n i s m e de la généricité (mis en œ u v r e par les templates), e x p o s é au c h a p i t r e 10.
Compléments Surcharge d'opérateurs
E n C + + , les o p é r a t e u r s c o u r a n t s ( + , - , = = , etc.) p e u v e n t être s u r c h a r g é s . C o m m e n t est-ce p o s s i b l e ? En fait, q u a n d vous utilisez un opérateur op sur un objet, le c o m p i l a t e u r génère un appel à la fonction operator op. L e s trois écritures suiv a n t e s sont é q u i v a l e n t e s : objet_l = objet_2 + objet_3; objet_l = operatort (objet2, objet3); objet_l = objet_2.(operator+(objet_3));
// (1) // (2)
« o p e r a t o r op », où op est un o p é r a t e u r s t a n d a r d (ici +) est en réalité u n n o m d e fonction c o m m e u n autre, qui est défini p a r défaut p o u r les types d e b a s e d u C + + . D o n c , p o u r personnaliser le c o m p o r t e m e n t des o p é r a t e u r s , il v o u s suffit de s u r c h a r g e r la fonction « o p e r a t o r op » correspondante, c o m m e v o u s s a v e z déjà le faire p o u r v o s p r o p r e s fonctions. Attention ! Q u a n d v o u s s u r c h a r g e z un o p é r a t e u r , il faut veiller à choisir l'une ou l'autre des d e u x m é t h o d e s de l ' e x e m p l e ci-dessus : D a n s le cas (1), il s'agit de la fonction operator+ globale, définie en d e h o r s de toute classe. Elle p r e n d d e u x paramètres, c o r r e s p o n d a n t aux d e u x m e m b r e s d e l'addition. D a n s le cas ( 2 ) , il s'agit de la f o n c t i o n - m e m b r e operator+, définie d a n s la classe d'objet_2, q u i s u p p o s e que l'objet sur lequel elle est a p p e l é e est le p r e m i e r paramètre de l'opération d'addition (ici ob j et_2). Si v o u s utilisiez à la fois ces d e u x m é t h o d e s de surcharge p o u r la m ê m e fonction, le c o m p i l a t e u r ne pourrait p a s faire la différence. V o u s p o u v e z s u r c h a r g e r les o p é r a t e u r s s u i v a n t s :
La surcharge
+
-
*
/
+ = - = *= / = ! = < = >= &&
A
% A
%= = || ++
&
&= --
~
|= ,
i
=
55
<
<< » > > = < < = ->* -> () []
>
==
V o y o n s l ' e x e m p l e d ' u n p r o g r a m m e c o m p l e t redéfinissant l'opérateur + p o u r des objets de classe C o l o r : #include <stdio.h> class Color { private : int rouge; int vert; int bleu; public : // constructeurs (voir chapitre 2) Color(int r, int v, int b ) ; Color(); void Afficher)); Color operator+ (Color Sdroite); };
Color;:Color() { rouge = vert = bleu = 0 ; }
Color::Color(int r, int v, int b) { rouge = r; vert = v; bleu = b; )
void {
Color::Afficher() printf("rouge=%d vert=%d bleu=%d\n", rouge, vert, bleu);
}
* Le passage par référence est une exclusivité C++ ! Vous en saurez plus en lisant le chapitre 9, page 113.
Color Color::operator+ (Color Sdroite) // droite est passé par « référence »* {
// fonction redéfinissant l'opérateur + : // RESULTAT = A + B / / A est l'objet Color courant // B est le paramètre "droite" // RESULTAT est la valeur retournée Color
résultat) (rouge + droite.rouge) / 2, (vert + droite.vert) / 2, (bleu + droite.bleu) / 2 ) ;
56
Pont entre C et C++
return résultat; }
void {
main() Color
// //
rouge_pur(10, 0, 0 ) , bleu_pur(0, 0, 10), mixage ;
mixage = rouge_pur + bleu_pur; cette ligne éqruivaut à : mixage = rouge_pur.operator+ (bleu_pur) mixage.Afficher(); // affiche "rouge=5 vert=0 bleu=5"
}
Plusieurs r e m a r q u e s : Le p r e m i e r p a r a m è t r e d ' u n e fonction o p é r a t e u r (Color &droite d a n s l ' e x e m p l e ) est p a s s é p a r référence (voir chapitre 9, p a g e 1 1 3 ) . En d e u x m o t s : q u a n d v o u s p a s s e z un p a r a m è t r e p a r référence, cela é q u i v a u t à p a s s e r l'objet original. Ainsi, modifier un p a r a m è t r e p a s s é p a r référence revient à modifier le p a r a m è t r e réel, utilisé à l'appel de la fonction. U n o p é r a t e u r r e t o u r n e u n objet d e l a m ê m e classe q u e ses p a r a m è t r e s (ici, un objet de classe Color). V o u s devrez d o n c créer un objet de cette classe d a n s la fonction opérateur, l'initialiser en fonction des d e u x p a r a m è t r e s de l'opérateur, et le retourner.
P o u r q u o i passer droite par référence dans o p e r a t o r + ? E x p l i q u o n s plutôt p o u r q u o i un p a s s a g e p a r valeur habituel serait p r o b l é m a t i q u e . A d m e t t o n s q u e n o u s a y o n s défini operator+ c o m m e suit : Color Color::operator+ (Color droite) // droite est passé par valeur {
// . . . }
L e p a r a m è t r e d r o i t e étant p a s s é p a r v a l e u r , u n e copie s ' o p è r e entre le p a r a m è t r e effectif (l'objet b l e u _ p u r ) et le p a r a m è t r e formel utilisé d a n s l a fonction (l'objet d r o i t e ) .
La surcharge
57
D a n s c e cas, l a c o p i e n e p o s e p a s d e p r o b l è m e , m a i s si, p a r e x e m p l e , votre classe c o m p o r t a i t un p o i n t e u r alloué, la c o p i e d'objet p a r défaut serait m a u v a i s e . A v e c un p a s s a g e p a r référence, il n ' y a p a s de c o p i e , p u i s q u e l a fonction o p e r a t o r + agit d i r e c t e m e n t sur l e p a r a m è t r e effectif, b l e u _ p u r . E n d'autres t e r m e s , m a n i p u l e r l'objet d r o i t e é q u i v a u t à m a n i p u l e r l'objet b l e u _ p u r . Récapitulons : Soit v o u s utilisez u n p a s s a g e p a r référence e t v o u s n ' a v e z p a s b e s o i n de v o u s soucier de la c o p i e de v o t r e objet, Soit v o u s utilisez u n p a s s a g e par v a l e u r e t v o u s d e v e z vérifier q u e la copie s'effectue bien. Si n é c e s s a i r e , définissez un c o n s t r u c t e u r de c o p i e (voir p a g e 3 1 ) . Inutile de préciser — m a i s je le fais q u a n d m ê m e — q u e la p r e m i è r e solution est plus intéressante q u e la s e c o n d e .
Surcharge de l'opérateur =
P o u r q u o i redéfinir l'opérateur = ? L'affectation d ' u n objet, via l'opérateur = , est u n e o p é r a t i o n sensible qui mérite q u ' o n s'y attarde. Si v o u s ne définissez p a s d'opérateur = p o u r u n e classe, le c o m p i l a t e u r en g é n è r e un p a r défaut qui effectue u n e affectation « b ê t e », d o n n é e s membre à données membre. Bien que ce genre de comport e m e n t soit satisfaisant d a n s le cas de classes s i m p l e s , il devient parfois d a n g e r e u x si v o u s utilisez des p o i n t e u r s . Le p r o b l è m e est similaire à celui du constructeur-copie (voir page 31). C o n s i d é r o n s l ' e x e m p l e suivant : Sinclude <stdio.h> tinclude <string.h>
Le constructeur Exclamation ainsi que la fonction Affiche sont
class Exclamation { private: char *cri ; public : Exclamation(char *c) { cri = new char[strlen(c)+1]; strcpy(cri, c ) ; }
définis « inline »
void set_cri(char * n c ) ;
(voir page 21 ). new
void Affiche() { printf("%s\n", cri); )
et delete remplacent malloc et free (voir chapitre 6 page 83).
};
58
Pont entre C et C++
void Exclamation::set_cri(char *nc) { // vérifions si nous n'avons pas affaire aux // même pointeurs if (ne == cri) return; // libérons la mémoire de l'ancienne valeur de cri delete cri; // allouons de la mémoire pour la nouvelle valeur cri = new char[strlen(nc)+1]; // copions le paramètre dans la donnée cri. strcpy(cri, n e ) ; }
void main() { Exclamation
beurk("Beurk"), bof("Bof");
beurk.Affiche(); bof.Affiche();
// affiche "Beurk" // affiche "Bof"
bof = beurk;
// operateur= défini par défaut
beurk.Affiche(); bof.Affiche();
// affiche "Beurk" // affiche "Beurk"
bof.set_cri("Ecoeurant"); beurk.Affiche(); bof.Affiche();
/ / o n modifie bof et beurk
// affiche n'importe quoi // affiche "Ecoeurant"
}
La c o p i e p a r défaut a ici des effets d é s a s t r e u x : en copiant u n i q u e m e n t le p o i n t e u r cri et n o n ce qui est p o i n t é , n o u s n o u s r e t r o u v o n s avec d e u x objets d o n t la d o n n é e cri pointe sur la m ê m e z o n e m é m o i r e :
La surcharge
59
Le résultat de cette d é c a d e n c e ne se fait p a s attendre : il suffit d e modifier l'un des d e u x objets p o u r q u e s o n j u m e a u soit affecté. C ' e s t ce q u e n o u s faisons dans la ligne bof . s e t _ c r i ( " E c o e u r a n t ") ; Cette instruction libère l ' a n c i e n n e v a l e u r d e c r i e n a p p e l a n t d e l e t e . O r , cette anc i e n n e valeur est é g a l e m e n t p a r t a g é e p a r l'objet b e u r k . S a n s le savoir, n o u s a v o n s d o n c effacé d e u x cris au lieu d'un. C o m m e n t redéfinir l'opérateur = ? C e s constatations affreuses n o u s p o u s s e n t à écrire notre propre opérateur d'affectation, qui va se c h a r g e r de copier le c o n t e n u d e s p o i n t e u r s cris. R a p p e l o n s q u e l ' u s a g e de = appelle l a fonction o p é r â t o r = . Voici l'exemple complet : tinclude <stdio.h> #include <string.h> class Exclamation { private: char *cri ; public: Exclamation(char *c) { cri = new char[strlen(c)+1]; strcpy(cri, c ) ; } void set_cri(char * n c ) ; void Affiche() { printf("%s\n", cri); ) Exclamation };
&operator=(Exclamation &source);
60
Pont entre C et C++
void Exclamation::set_cri(char *nc) { if (ne == cri) return; delete cri; cri = new char(strlen(ne)+1]; strcpy(cri, n e ) ; } Cette fonction, qui retourne une référence, est expliquée à la suite
Exclamation &Exclamation::operator=(Exclamation Ssource) { // évitons d'affecter deux objets identiques if (this == &source) return *this; // retournons l'objet courant
du listing.
delete cri; cri = new char[ strlen(source.cri)+1 ]; strcpy(cri, source.cri); return *this; // retournons l'objet courant }
void main() { Exclamation
beurk("Beurk"), bof("Bof");
beurk.Affiche(); bof.Affiche();
// affiche "Beurk" // affiche "Bof"
bof = beurk;
// operateur= défini par nos soins
beurk.Affiche(); bof.Affiche();
// affiche "Beurk" // affiche "Beurk"
bof.set_cri("Ecoeurant"); beurk.Affiche(); bof.Affiche();
// on ne modifie que bof
// affiche "Beurk" : parfait ! // affiche "Ecoeurant"
}
P o u r q u o i ' r e t u r n *this'? L ' o p é r a t e u r = doit retourner un objet de type Exclamation. P l u s e x a c t e m e n t , i l doit retourner l'objet sur lequel o p e r a tor+ a été a p p e l é , a p r è s q u e l'affectation a été faite. En ret o u r n a n t *this, n o u s r e t o u r n o n s b i e n l'objet c o u r a n t q u e n o u s v e n o n s d e modifier. M a i s p o u r q u o i retourner u n e référence plutôt q u ' u n objet Exclamation tout s i m p l e ? P o u r r é p o n d r e c o r r e c t e m e n t au cas de figure suivant : (bof = beurk).Affiche();
La surcharge
61
Si n o u s r e t o u r n i o n s u n e s i m p l e copie du résultat, l ' o p é r a t i o n A f f i c h e n e s'effectuerait p a s sur b o f m a i s sur s a c o p i e . P o u r q u o i faire le test 'if (this == & s o u r c e ) ' ? Ce test a p o u r b u t de vérifier si n o u s ne v o u l o n s p a s affecter l'objet l u i - m ê m e (ce serait le cas de l'instruction a = a ; ). Si c e test n'était p a s réalisé, n o u s n o u s r e t r o u v e r i o n s d a n s u n e situation très désagréable. R e g a r d e z un instant la p r e m i è r e ligne après l e test. O u i , c'est b i e n d e l e t e c r i . I m a g i n e z m a i n t e n a n t q u ' u n c o q u i n ait écrit b e u r k = b e u r k . E n faisant d e l e t e c r i , n o u s effacerions l e c r i d e l'objet s o u r c e mais aussi, sans le vouloir, celui de l'objet destination. D è s lors, rien d ' é t o n n a n t à ce q u e n o u s n o u s e x p o s i o n s à l ' i m p r é v u e n a c c é d a n t a u c r i d e l'objet source.
Opérateurs de conversion de types
L'opération réciproque, à savoir la conversion d'un type quelconque en un objet de la classe, est tout à fait possible ! Il
P o u r autoriser et contrôler la c o n v e r s i o n d ' u n e classe A v e r s un type T q u e l c o n q u e , v o u s p o u v e z définir un o p é r a t e u r de c o n v e r s i o n de n o m operator T ( ). C h a q u e fois q u ' u n objet de classe A doit être c o n s i d é r é c o m m e u n e v a r i a b l e de t y p e T, l'appel à cet o p é r a t e u r se fera a u t o m a t i q u e m e n t . Attention ! Ce g e n r e d ' o p é r a t e u r ne doit pas avoir de type de retour, m a i s doit tout de m ê m e r e t o u r n e r u n e variable de t y p e T (on évite ainsi l a r e d o n d a n c e d u n o m d u t y p e e t d e s r i s q u e s d ' i n c o h é r e n c e liés à cette r e d o n d a n c e superflue). A d m e t t o n s q u e v o u s v o u l i e z m o d é l i s e r des articles d a n s u n m a g a s i n . La classe Article, qui contient diverses informations, doit p o u v o i r être utilisée d a n s d e s e x p r e s s i o n s de calcul de prix. Il faut d o n c définir operator float ( ) ;
suffit d'utiliser un constructeur spécial (voir page 35),
On ne devrait pas spécifier la taille du tableau directement dans la classe, mais plutôt utiliser une constante. Toutes mes excuses p o u r cette hérésie éhontée.
#include <string.h> #include <stdio.h> class Article { protected: char nom[50]; float prix; int nb_disponible; public: Article(char *n, float p, int nb) : prix(p), nb_disponible(nb) { strcpy(nom, n ) ; } operator float(); };
62
Pont entre C et C++
Article : :operator float() {
return prix; } void main() { float total; Article b("Bonne bière", 12., 3 0 ) ; total = 5 * b; // conversion de b en float printf("5 bonnes bières valent %.2f F", total); }
// affichage : 5 bonnes bières valent 60.00 F
Les petits + du C++
Pour conclure cette première partie, attardons-nous sur les petits changements qui rendent la vie du programmeur plus agréable. Contrairement aux autres chapitres, celui-ci ne se décompose pas en deux parties (notions de base et compléments). Chaque point est expliqué en une seule fois, sans distinction de « niveau de difficulté ».
Les commentaires V o u s êtes u n p r o g r a m m e u r c o n s c i e n c i e u x , v o u s c o m m e n t e z donc abondamment vos programmes. Le C + + a pensé à v o u s e t p r o p o s e u n e n o u v e l l e m a n i è r e d'introduire d e s c o m m e n t a i r e s d a n s les c o d e s - s o u r c e s : void
main()
{
// commentaire pertinent jusqu'à la fin de la ligne }
L e s d e u x caractères « / / » i n d i q u e n t au p r é - p r o c e s s e u r le d é b u t d ' u n c o m m e n t a i r e , qui s'arrête a u t o m a t i q u e m e n t à la fin de la ligne. V o u s n ' a v e z d o n c p a s b e s o i n de r é p é t e r / / p o u r m e t t r e fin au c o m m e n t a i r e .
64
Pont entre C et C++
B i e n e n t e n d u , v o u s p o u v e z toujours utiliser l ' a n c i e n style de c o m m e n t a i r e (entre / * e t * / ) , très p r a t i q u e p o u r les prog r a m m e u r s qui ont b e a u c o u p de c h o s e s à r a c o n t e r — sur p l u s i e u r s lignes. L e s d e u x styles d e c o m m e n t a i r e s peuvent être i m b r i q u é s : u n e section de c o d e entre / * et * / p e u t contenir d e s c o m m e n t a i r e s / / , et r é c i p r o q u e m e n t .
Ruse de sioux
Il existe un autre m o y e n de m e t t r e en c o m m e n t a i r e de nomb r e u s e s lignes, très utile si ces lignes c o n t i e n n e n t déjà des c o m m e n t a i r e s avec / * et * /. P e n s e z au p r é - p r o c e s s e u r et utilisez # i f d e f e t # e n d i f . Voici l e listing initial : void
fonction_boguee()
{
char
*pointeur;
// pointeur sur un truc
/* début de la fonction */ strcpy(pointeur, "Plantage"); }
V o u s v o u l e z m e t t r e en c o m m e n t a i r e tout le c o n t e n u de la fonction. A m o i n s de s u p p r i m e r le c o m m e n t a i r e intérieur, v o u s ne p o u v e z p a s utiliser / * et * / , p u i s q u e le p r e m i e r / * serait fermé par le * / du c o m m e n t a i r e intérieur. La solution : void { #if 0 char
fonction_boguee()
*pointeur;
// pointeur sur un truc
/* début de la fonction */ strcpy(pointeur, "Plantage"); #endif }
Rappelons que # i f expression est é v a l u é p a r l e prép r o c e s s e u r . Si expression est vrai, le c o d e qui suit (jusqu'à # e n d i f ) est c o m p i l é . Il suffit d o n c de d o n n e r u n e expression fausse p o u r m e t t r e en « c o m m e n t a i r e s » la z o n e de code v o u l u e . C ' e s t p o u r q u o i n o u s utilisons # i f 0 qui n ' e s t jam a i s vrai.
L e s petits + du C++
65
Paramètres par défaut Là aussi, u n e idée s i m p l e et efficace : v o u s p o u v e z d o n n e r u n e v a l e u r p a r défaut p o u r u n o u p l u s i e u r s p a r a m è t r e s d e fonction, d a n s la déclaration de cette fonction. A i n s i , à l'appel, v o u s p o u v e z o m e t t r e les p a r a m è t r e s qui p o s s è d e n t u n e v a leur p a r défaut. E x e m p l e :
Ici, on donne une
/* fichier fonction.h */ float calcul_tva(float montant, float tva = 18.6);
valeur par défaut (18.6) au deuxième paramètre de la fonction calcul_tva.
/* fichier fonction.cpp */ float calcul_tva(float montant, float tva) { if (tva > 0. && tva < 100.) return montant * (tva / 100.); else { /* traitement d'erreur... */ } }
/* fichier main.cpp */ #include "fonction.h" void
main()
{
float
res;
res = calcul_tva(100.); // 2ème argument vaut 18.6 res = calcul_tva(100., 5.5); res = calcul_tva(100., 18.6); }
I l existe d e u x possibilités d ' a p p e l e r l a fonction c a l cul_tva : A v e c d e u x p a r a m è t e s : c'est la solution c l a s s i q u e . D a n s ce cas, le p a r a m è t r e p a r défaut est « é c r a s é » p a r la v a l e u r d'appel du deuxième paramètre. A v e c un seul p a r a m è t r e : d a n s ce c a s , le c o m p i l a t e u r s u p p o s e q u e l e d e u x i è m e p a r a m è t r e p r e n d l a v a l e u r p a r défaut (dans l ' e x e m p l e , 1 8 . 6 ) . Attention : t o u s les p a r a m è t r e s qui d i s p o s e n t d ' u n e v a l e u r p a r défaut d o i v e n t être déclarés après les p a r a m è t r e s norm a u x , c'est-à-dire à la fin de la liste des p a r a m è t r e s .
66
Pont entre C et C++
P l u s i e u r s p a r a m è t r e s par défaut Il est p o s s i b l e de définir u n e v a l e u r par défaut p o u r plusieurs p a r a m è t r e s d'une fonction. Veillez c e p e n d a n t à les déclarer après les p a r a m è t r e s n o r m a u x . L ' e x e m p l e suivant est i m p o s s i b l e car a m b i g u : void phonquession(int a = 1, int b, int c = 3); // erreur !
C e t t e déclaration est i m p o s s i b l e : le p r e m i e r p a r a m è t r e a une v a l e u r p a r défaut, m a i s p a s le d e u x i è m e . P o u r q u o i n'est-ce p a s autorisé ? C ' e s t a s s e z l o g i q u e . I m a g i n e z q u e v o u s appeliez la fonction c o m m e ceci : phonquession(12 , 13);
M e t t e z - v o u s un instant d a n s la p e a u du p a u v r e compilateur : q u e v e u t dire cet appel ? Q u e le p r e m i e r p a r a m è t r e vaut 12, et le d e u x i è m e 13 ? Ou b i e n le d e u x i è m e 12 et le troisième 13 ? A l l o n s , s o y o n s r a i s o n n a b l e s , et c o n t e n t o n s - n o u s de déclarer les v a l e u r s par défaut à la file, en fin d ' e n t ê t e : void phonquession(int // mieux.
b, int a = 1, int c = 3 ) ;
A i n s i , n o u s l e v o n s toute a m b i g u ï t é . Voici c o m m e n t seraient interprêtés différents appels à p h o n q u e s s i o n : phonquession(10); // b:10 a:l phonquession(10, 11); // b:10 a:ll phonquession(10, 11, 1 2 ) ; // b:10 a:ll
c:3 c:3 c:12
Les petits + du C++
67
Les constantes Habitué(e) du l a n g a g e C, v o u s d e v e z c e r t a i n e m e n t utiliser # d e f ine à tour de b r a s p o u r définir v o s c o n s t a n t e s . L e s p l u s a u d a c i e u x d'entre v o u s a u r o n t peut-être a d o p t é le mot-clé const ( d i s p o n i b l e d e p u i s la n o r m e ANSI du l a n g a g e C ) . Le C + + v a p l u s loin e n é t e n d a n t les possibilités d e const. N o u s allons voir tout cela au travers des différents m o y e n s d'utiliser des c o n s t a n t e s e n C + + . U n e c o n s t a n t e s i m p l e , visible d a n s un b l o c ou un seul fichier s o u r c e C ' e s t la m a n i è r e la plus s i m p l e de déclarer u n e c o n s t a n t e : const
float
TVA = 0.186;
Il est i n d i s p e n s a b l e d'initialiser la c o n s t a n t e au m o m e n t de sa déclaration. On ne p e u t p a s s i m p l e m e n t déclarer const float TVA, puis, q u e l q u e s lignes en d e s s o u s , affecter u n e valeur à TVA. T o u t c o m p i l a t e u r C + + v o u s insultera si v o u s agissez de la sorte. D a n s le cas d ' u n tableau constant, ce sont tous ses é l é m e n t s qui sont c o n s t a n t s : const float tab[2] = 1.1;
tab[3] = { 2.2, 3.3, 4.4 } ; // impossible, refusé par le compilateur
C o m m e toutes les autres variables, la p o r t é e d ' u n e c o n s t a n t e d é p e n d de l'endroit où elle a été déclarée. C o n s t a n t e s et p o i n t e u r s Il faut b i e n distinguer ce qui est pointé du pointeur l u i - m ê m e . G r â c e à const, n o u s s o m m e s en m e s u r e de spécifier au c o m p i l a t e u r si n o u s v o u l o n s q u e le pointeur soit c o n s t a n t , ou b i e n q u e les données pointées soient c o n s t a n t e s , ou e n c o r e q u e les d e u x soient figés. Détaillons ces trois c a s :
68
Pont entre C et C++
char char char
chainel[] = "baratin"; chaine2 [ ] = "billevei/sées " ; chaine3[] = "balivernes";
// ce qui est en gras est constant const char * p = chainel; char * const p = chaine2; const char * const p = chaine3;
// (1) // (2) // (3)
D a n s (1), la c h a î n e " b a r a t i n " est c o n s t a n t e , m a i s pas le p o i n t e u r p. V o u s p o u v e z d o n c écrire p = 0 (après v o u s être assuré q u ' u n autre p o i n t e u r d é s i g n e " b a r a t i n " , sans quoi v o u s ne p o u v e z p l u s y a c c é d e r ) . D a n s (2), c'est le p o i n t e u r p qui est constant. Si v o u s utilisez u n autre p o i n t e u r p o u r a c c é d e r à " b i l l e v e u s é e s " , v o u s p o u r r e z la modifier librement. D a n s (3), tout est verrouillé. Ni p ni la chaîne " b a l i v e r n e s " n e p e u v e n t être modifiés. Illustrons u n e dernière fois ce qui est p o s s i b l e et ce qui ne l'est p o i n t : void
main()
{
char chainel[] = "baratin"; char chaine2[] = "billeveusées"; char chaine3[] = "balivernes"; const char * p = chainel; // (1) char * const p = chaine2; // (2) const char * const p = chaine3; // (3) pl[2] = 'X'; pl = new char[10];
// impossible // OK
p2[2] = 'X•; p2 = new char[10];
/ / OK // impossible
p3[2] = 'X'; p3 = new char[10];
// impossible // impossible
}
Pourquoi ne pas avoir déclaré les chaînes comme ceci : const char * p = "baratin";
En c o d a n t la c h a î n e " b a r a t i n " en « d u r » c o m m e ci-dessus, n o u s n ' a u r i o n s p a s pu la modifier. En effet, u n e chaîne dé-
Les petits + du C++
69
clarée ainsi est s t o c k é e d a n s u n e z o n e d e m é m o i r e p r o t é g é e . T o u t accès à cette z o n e e n g e n d r e un p l a n t a g e p l u s ou m o i n s i m m é d i a t . N o u s a v o n s d o n c été contraints d'utiliser d e s v a riables intermédiaires, qui stockent les d o n n é e s d a n s u n e z o n e d e m é m o i r e modifiable. U n e c o n s t a n t e s i m p l e , v i s i b l e d a n s p l u s i e u r s f i c h i e r s sources Un petit rappel de C s t a n d a r d s ' i m p o s e . Si v o u s v o u l e z déclarer u n e variable globale visible d a n s p l u s i e u r s fichiers sources, v o u s p r o c é d e z ainsi :
C o m m e v o u s l e v o y e z , l a variable g l o b a l e n ' e s t définie q u ' u n e seule fois, d a n s l e fichier m a i n . c p p . C h a q u e fois q u ' u n autre fichier v e u t p o u v o i r l'utiliser, il doit i n d i q u e r q u e cette variable existe et est définie « ailleurs », d a n s un autre fichier (c'est l e rôle d u m o t - c l é e x t e r n ) .
Les petits + du C++
71
D é c l a r e r q u ' u n e fonction r e t o u r n e u n e c o n s t a n t e N e lisez p a s c e p a r a g r a p h e avant d e c o n n a î t r e les références, e x p o s é e s a u chapitre 9 , p a g e 113. P o u r q u ' u n e fonction retourne u n e référence c o n s t a n t e , i l suffit d ' i n d i q u e r c o n s t a v a n t le type de retour de la fonction. L'intérêt de cette t e c h n i q u e est d'éviter la c o p i e d ' u n objet entre la fonction a p p e l é e et la fonction appelante, tout en p r é s e r v a n t l'intégrité de l'objet. E x e m p l e : const int &f() { static big_objet big; // manipulations sur big return big; }
void main() { f() = 0; // déclenche une erreur de compilation // à cause du const. }
D é c l a r e r u n e c o n s t a n t e interne à u n e classe C ' e s t un p r o b l è m e intéressant : c o m m e n t déclarer u n e c o n s tante qui ne sera visible et utilisable q u e d a n s sa classe ? V o u s seriez peut-êter tenté de p r o c é d e r ainsi : class MaClasse { private: const int MAX = 100; public :
// incorrect, hélas
// . . . } ;
C ' e s t oublier q u e l'on ne p e u t p a s affecter de v a l e u r directem e n t d a n s l a déclaration d ' u n e classe. V o u s n e p o u v e z q u e déclarer d e s variables, p a s les initialiser. M a l i n , v o u s v o u s dites q u e v o u s aller initialiser la c o n s t a n t e en d e h o r s de la classe : class MaClasse { private: const int MAX; public : // ...
// toujours incorrect
72
Pont entre C et C++
} ;
MaClasse::MAX = 100;
// toujours incorrect
C ' e s t e n c o r e i m p o s s i b l e , car v o u s n e p o u v e z affecter a u c u n e v a l e u r à u n e c o n s t a n t e , en d e h o r s de s o n initialisation. Et ce n ' e s t p a s u n e initialisation ici, car MAX est u n e variable d ' i n s t a n c e (liée à un objet précis) et n o n de classe. Elle ne p e u t d o n c p a s être initialisée sans être s t a t i c . Q u e faire, m e direz-vous ? I l existe d e u x solutions. U n e avec s t a t i c c o n s t , l'autre avec enum. La static const solution. U n e c o n s t a n t e p r o p r e à u n e classe doit être l o g i q u e m e n t déclarée s t a t i c c o n s t . Elle doit être s t a t i c car c'est u n e variable de classe, d o n t la valeur est c o m m u n e à tous les objets de cette classe ; et elle doit être c o n s t car on v e u t interdire les modifications de sa valeur. L a seule m a n i è r e d'initialiser u n e variable s t a t i c c o n s t déclarée d a n s u n e classe, c'est de l'initialiser en d e h o r s de la classe. En voici la d é m o n s t r a t i o n : class MaClasse { private: static const int MAX; public : MaClasse();
// constante de classe
} ;
const int MaClasse::MAX = 100;
L ' e n u m solution Si v o s c o n s t a n t e s sont de t y p e entier, v o u s p o u v e z utiliser e n u m p o u r les déclarer et les initialiser à l'intérieur d ' u n e classe. B i e n q u e cela ne m a r c h e q u e sur les entiers, cette solution p r é s e n t e l ' a v a n t a g e d'initialiser la c o n s t a n t e immédiatement. C ' e s t la seule m é t h o d e q u e je c o n n a i s s e pour initaliser u n e c o n s t a n t e de classe d a n s le corps de la c l a s s e , et d o n c qui p e r m e t t e de l'utiliser p o u r d i m e n s i o n n e r un tableau d a n s la classe. V o i c i un e x e m p l e :
Les petits + du C++
class MaClasse { private: enum { MAX = 100 }; int tab[MAX]; public :
73
// MAX est de type int // OK
// ...
>;
Déclarations E n C + + v o u s p o u v e z déclarer v o s variables o u objets n ' i m p o r t e où d a n s le c o d e , y c o m p r i s a p r è s u n e série d'instructions. La p o r t é e de telles variables va de l'endroit de leur déclaration à la fin du bloc c o u r a n t ( l ' a c c o l a d e f e r m a n t e en général). void
main()
{
int i; for (i=0; i<100; i++) { int j ; // traitement }
// ici j est inconnu }
V o u s p o u v e z déclarer d a n s u n sous-bloc u n e variable portant l e m ê m e n o m q u ' u n e autre variable d é c l a r é e d a n s u n bloc supérieur. D a n s c e c a s , l a variable l a p l u s p r o c h e d e l'endroit de l'instruction est utilisée. E x e m p l e : void
main()
{
int i; for (i=0; i<100; i++) { int i ; i = 1;
74
Pont entre C et C++
}
// ici i vaut 100 et pas 1 }
Variable de boucle Voici u n e construction intéressante : v o u s p o u v e z déclarer u n e variable de b o u c l e d i r e c t e m e n t dans l'instruction f or ( , , ), ce qui v o u s garantit q u e p e r s o n n e n'utilisera cette variable en d e h o r s de la b o u c l e : void main() { for (int i = 0; i < 100; i++) { // traitement } )
Résumé
La découverte du C++ est un peu déroutante, tant les termes et concepts inédits sont nombreux. C'est pourquoi nous vous proposons ici un résumé rapide de la première partie du livre. Il vous permettra d'avoir les idées claires avant d'aborder la suite, riche en rebondissements...
Résumé première partie
Encapsulation
77
Le p r i n c i p e fondateur de la p r o g r a m m a t i o n orientée-objet est l'encapsulation. Au lieu de r é s o u d r e un p r o b l è m e p a r d e s structures e t des fonctions s a n s lien explicite ( c o m m e e n C ) , l ' e n c a p s u l a t i o n p r o p o s e d'utiliser des objets, entités inform a t i q u e s r e g r o u p a n t d o n n é e s e t fonctions i n t i m e m e n t liées. L e s d o n n é e s d ' u n objet n e p e u v e n t être m a n i p u l é e s q u e p a r les fonctions créées s p é c i a l e m e n t p o u r cela. E n C + + , u n objet est u n e i n s t a n c e de classe, q u e l'utilisateur p e u t définir c o m m e il le veut. L e s fonctions qui m a n i p u l e n t les d o n n é e s d ' u n objet s o n t appelées fonctions-membres. De m u l t i p l e s objets p e u v e n t être créés à partir de la m ê m e classe, c h a c u n c o n t e n a n t des v a l e u r s différentes. L e s objets sont issus d ' u n e classe. class NomClasse { private: // variables et objets public : // fonctions de manipulation de ces variables / / e t objets } ;
void
main()
{
NomClasse NomClasse
objetl; objet2;
objetl.nom_de_fonction_public(); }
Constructeurs & destructeurs * qu'il sort simplement déclaré stàtiquement ou alloué dynamiquement par l'opérateur new.
C h a q u e classe doit p o s s é d e r au m o i n s u n e fonction constructeur et u n e fonction destructeur. Si le p r o g r a m m e u r n ' e n définit pas, le c o m p i l a t e u r en g é n è r e p a r défaut. Le c o n s t r u c t e u r est appelé à c h a q u e création d'objet*, et p e r m e t a u p r o g r a m m e u r d'initialiser c o r r e c t e m e n t l'objet. L e destructeur est a p p e l é dès q u ' u n objet sort de sa p r o p r e p o r tée o u est d é s a l l o u é e x p l i c i t e m e n t p a r u n a p p e l à d e l e t e .
78
Pont entre C et C++
Héritage
L'héritage p e r m e t de mettre en relation d e s classes. Q u a n d u n e classe B hérite d ' u n e classe A, A est a p p e l é e classe de b a s e et B classe dérivée. Au n i v e a u s é m a n t i q u e , B « est u n e sorte » de A (voir e x e m ple p a g e suivante). Au n i v e a u du C + + , la classe B « recopie » (en q u e l q u e sorte) les d o n n é e s et fonctions de la classe A. Un objet de classe B intègre donc les d o n n é e s et fonctions accessibles de A.
La classe dérivée B p e u t redéfinir des fonctions de la classe de b a s e A. On parle alors de redéfinition de fonction membre, ce qui signifie q u ' u n m ê m e entête de fonction est partagé p a r toute u n e hiérarchie de classes.
Résumé première partie
Surcharge
79
La surcharge de fonction p e r m e t au p r o g r a m m e u r de d o n n e r l e m ê m e n o m à d e s fonctions situées d a n s l a m ê m e c l a s s e , o u e n d e h o r s d e toute classe. L a seule c o n t r a i n t e est q u e l e c o m p i l a t e u r p u i s s e faire la distinction g r â c e au n o m b r e ou au t y p e des p a r a m è t r e s de ces fonctions « s u r c h a r g é e s ». int
carre(int a)
{
return (a * a) ; }
float carre(float a) { return (a * a ) ; }
Encore plus de C++ Vous connaissez maintenant les grands principes du C++. Mais votre voyage au pays des objets n'est pas fini : il vous reste à découvrir d'autres concepts qui enrichissent encore plus le langage. Une bonne connaissance des éléments présentés dans la première partie facilitera votre lecture.
La fin du malloc new et delete
Nous sommes au regret de vous annoncer le décès de malloc, survenu après de nombreuses années de bons et loyaux services... En réalité, bien que vous puissiez toujours utiliser malloc, calloc et free, le C++ met à votre disposition new et delete, leurs talentueux remplaçants.
Notions de base L'idée
L e C + + propose deux nouveaux mots-clés, new e t d e l e t e , p o u r r e m p l a c e r leurs aînés m a l l o c e t f r e e . P o u r q u o i u n tel c h a n g e m e n t ? P o u r s'adapter a u x n o u v e l l e s caractéristiques d u C + + q u e sont l'héritage, les c o n s t r u c t e u r s e t les destructeurs. E n effet, alors q u e m a l l o c e t f r e e s o n t d e u x fonctions d e l a b i b l i o t h è q u e standard d u C , n e w e t d e l e t e s o n t c o m p l è t e m e n t intégrés au l a n g a g e C + + . Ce sont à la fois d e s m o t s - c l é s (réservés) et d e s opérateurs. Le rôle de n e w est d o u b l e : il r é s e r v e l ' e s p a c e m é m o i r e q u ' o n lui d e m a n d e , p u i s l'initialise en a p p e l l a n t les c o n s t r u c t e u r s des objets créés.
84
Pont entre C et C++
L e rôle d e d e l e t e est s y m é t r i q u e : i l a p p e l l e les d e s t r u c t e u r s des objets créés p a r new, p u i s libère l ' e s p a c e m é m o i r e réservé.
Mise en œuvre C++
Dans les cas cicontre, la variable pt désigne toujours un pointeur de T (déclaré avec T *pt).
L'opérateur new C o m m e m a l l o c , l'opérateur n e w r e t o u r n e soit l ' a d r e s s e m é m o i r e n o u v e l l e m e n t allouée, soit 0 (zéro) si l'opération a échoué. On p e u t utiliser n e w de trois m a n i è r e s différentes :
pt = new T; L'opérateur new recherche un espace mémoire pour un seul é l é m e n t de type T. Ici, T p e u t d é s i g n e r un t y p e standard ( c h a r , i n t , etc.) o u u n e classe. D a n s c e dernier c a s , n e w appellera le c o n s t r u c t e u r par défaut de la c l a s s e T. pt = new T {arguments d'un constructeur de classe T) -, Ici, au lieu d ' i n v o q u e r le c o n s t r u c t e u r p a r défaut de la classe T, n e w appelera le c o n s t r u c t e u r c o r r e s p o n d a n t a u x paramètres indiqués. pt = new T [taille du tableau] ; C e t t e s y n t a x e d e m a n d e à n e w de c h e r c h e r un e s p a c e m é m o i r e p o u v a n t contenir t a i 1 2 e _ t a b l e a u é l é m e n t s consécutifs de type T (T p o u v a n t être un t y p e s t a n d a r d ou u n e classe). Le c o n s t r u c t e u r p a r défaut de la c l a s s e T est invoqué pour chaque élément.
A t t e n t i o n ! Détruire un objet individuel avec l'instruction delete [ ] p r o v o q u e quelque chose d'indéfini mais de généralement nuisible (loi de Murphy). De même, utiliser delete p o u r détruire un tableau entraine des conséquences t o u t aussi aléatoires !
L'opérateur delete I l est fortement r e c o m m a n d é d ' a p p e l e r d e l e t e p o u r libérer l ' e s p a c e - m é m o i r e alloué p a r new. L a s y n t a x e d e d e l e t e est simple : d e l e t e pt; A p p e l l e le destructeur de pt (si pt est un p o i n t e u r v e r s u n objet) p u i s libère l ' e s p a c e m é m o i r e p r é a l a b l e m e n t alloué p a r u n new. d e l e t e [ ] pt; M ê m e c h o s e , m a i s p o u r libérer u n tableau alloué a v e c new T [ taille tableau ].
La fin du malloc
85
L e s spécifications officielles d u C + + v o u s g a r a n t i s s e n t q u e rien d e m a u v a i s n'arrivera s i v o u s a p p e l e z d e l e t e s u r u n p o i n t e u r nul. Exemple R e g a r d o n s m a i n t e n a n t c o m m e n t utiliser n e w e t d e l e t e p o u r allouer et libérer des c h a î n e s de caractères. P r e n o n s c o m m e e x e m p l e la classe CDRom, définie ci-dessous : class CDRom { protected : char * titre; public : CDRom(); CDRom(char *tit); -CDRom(); } ;
// définition des constructeurs CDRom: :CDRom() {
titre = new char[8]; if (titre == 0) badaboum(); strcpy(titre, ""); }
CDRom::CDRom(char *tit) { titre = new char[ strlen(tit) + 1 ]; if (titre == 0) badaboum(); strcpy(titre, tit); } // définition du destructeur CDRom::-CDRom() {
delete [] titre; }
Vous constarerez que nous avons bien respecté la symétrie n e w / d e l e t e . L a fonction b a d a b o u m s'occupe d e traiter les erreurs, l o r s q u e n e w n ' e s t p a s p a r v e n u à allouer la m é m o i r e demandée. E x a m i n o n s m a i n t e n a n t c o m m e n t utiliser n e w e t d e l e t e a v e c d e s objets. P u i s q u e n o u s l ' a v o n s s o u s l a m a i n , g a r d o n s
la classe CDRom :
86
Pont entre C et C++
void
main()
{
CDRom CDRom
*pt_inconnu, *pt_cd_X; rebel_assault("Rebel Assault");
pt_inconnu = new CDRom; pt_cd_X = new CDRom("Virtual Escort"); // blabla delete pt_inconnu; delete pt_cd_X; }
// (1) // (2) // (3) // // // // // //
(4) (5) (6) (7) (8) (9)
L e s instructions (1) et (2) n ' a p p e l l e n t a u c u n c o n s t r u c t e u r , et ne r é s e r v e n t de la p l a c e q u e p o u r les p o i n t e u r s e u x - m ê m e s . La ligne (3) appelle le d e u x i è m e c o n s t r u c t e u r de la c l a s s e CDRom, et initialise l'objet rebel_assault. R i e n de n e u f ici. En r e v a n c h e , l'instruction (4) va trouver de la p l a c e en m é m o i r e p o u r u n objet de classe CDRom, i n i t i a l i s e p a r le p r e m i e r c o n s t r u c t e u r (le constructeur p a r défaut, p u i s q u ' i l ne p r e n d aucun paramètre). Vous pouvez maintenant utiliser pt_inconnu p o u r appeler d e s fonctions p u b l i q u e s de la classe CDRom. L'instruction (5) va créer un objet initialise p a r le d e u x i è m e constructeur, et r e t o u r n e r l ' a d r e s s e de cet objet. Enfin, les lignes (7) et (8) appellent le destructeur de la c l a s s e CDRom p o u r détruire les objets p o i n t é s p a r pt_inconnu et pt_cd_X. C e s destructeurs v o n t libérer les c h a î n e s de caractères allouées.
L'intérêt par rapport au C
* C'est une possibilité réservée aux experts...
C o m p a r o n s les d e u x lignes s u i v a n t e s : tab = (char *) malloc (TAILLE * sizeof(char)); tab = new char [TAILLE] ;
// C /,/ C + +
L'intérêt d u n e w devrait v o u s frapper d e plein fouet. C ' e s t vrai : n o n s e u l e m e n t n e w est p l u s s i m p l e à utiliser, m a i s en plus, il fait partie i n t é g r a n t e du C + + , d o n c il est p o r t a b l e et n e n é c e s s i t e p a s l'adjonction d ' u n e librairie o u l'inclusion d ' u n fichier d'entête. Par ailleurs, l'utilisation du n e w p e r m e t la vérification de type (ce q u e ne p e r m e t p a s malloc), d o n c u n e p l u s g r a n d e sûreté d u c o d e . E n outre, n e w a p p e l l e l e c o n s t r u c t e u r v o u l u , c'est un opérateur, il p e u t être redéfini p a r v o s soins* et il peut être hérité. Q u e v o u l e z - v o u s de p l u s ?
La fin du malloc
Que celui qui n'a pas pesté la première fois qu'il a découvert le profil de malloc se lève.
87
C e r t e s , v o u s p o u v e z toujours utiliser m a l l o c e n C + + . M a i s d a n s c e cas, v o u s d e v r e z appeler v o u s - m ê m e les c o n s t r u c teurs v o u l u s , et, ce qui est n e t t e m e n t p l u s f â c h e u x , v o u s risq u e r e z de p a s s e r au travers de n e w spécifiques à certaines c l a s s e s . Je m ' e x p l i q u e : si un gentil p r o g r a m m e u r réécrit un joli n e w p o u r s a classe e t q u e v o u s i g n o r e z s u p e r b e m e n t ses efforts e n utilisant m a l l o c , n o n s e u l e m e n t v o t r e objet risq u e r a d'être m a l initialise, m a i s , p l u s g r a v e , v o u s v o u s attirerez i n é v i t a b l e m e n t son inimitié. Un conseil : m e t t e z - v o u s au new, v o u s ne le r e g r e t t e r e z p a s .
La fin du printf cout, cin et cerr
Nous venons famille printf. nération : la jets issus de réutilisabilité.
d'enterrer la famille malloc, passons maintenant à Et donnons maintenant sa chance à la nouvelle famille cout. Notons que cout, cin et cerr sont des classes C++, et qu'ils illustrent très bien le concept
la géobde
Notions de base L e C + + intègre d a n s s a librairie s t a n d a r d différents m é c a n i s m e s d ' e n t r é e s e t d e sorties. I l s'agit d e s flux c o u t , c i n e t c e r r , c o u p l é s a u x o p é r a t e u r s > > e t < < . L ' o p é r a t e u r > > perm e t de lire un flux, alors q u e « v o u s a u t o r i s e à écrire dedans. V o i c i le rôle des trois flux s t a n d a r d s du C + + * :
90
Pont entre C et C++
t r e a m , e t c i n u n objet d e classe i s t r e a m . T o u t c e b e a u m o n d e est défini d a n s l e fichier i o s t r e a m . h . N o u s allons voir c o m m e n t utiliser les flux C + + e t p o u r q u o i ils s o n t p l u s intéressants q u e c e u x d e C .
Mise en œuvre C++
P r e n o n s un e x e m p l e s i m p l e : n o u s allons écrire un petit p r o g r a m m e qui calcule l a m o y e n n e d e s n o t e s d ' u n étudiant. #include "iostream.h" void { char float float int
maint) nom[50]; notes[3]; cumul = 0 ; i;
cout << "Entrez le nom de l'étudiant : cin >> nom; for (i=0; i<3; i++) {
cout << "Entrez la note n°" << i+1 << ' '; // (1) cin >> notes[ i ] ; cumul += notes[i];
N o t e z que, t o u t }
c o m m e p o u r scanf, vous ne pouvez pas
cout << nom << " a pour moyenne " << cumul/3. << endl; }
entrer d'espace(s) pendant la saisie d'une chaîne. En fait, p o u r être exact, vous pouvez le faire, mais vous rendrez v o t r e programme complètement fou, et
/ / C e programme affiche ceci : // (les caractères gras sont tapés par l'utilisateur) Entrez le Entrez la Entrez la Entrez la GaryMad a
nom de l'étudiant : GaryMad note n°l 0.5 note n°2 7 note n°3 19.5 pour moyenne 9
vous avec.
Remarque V o u s p o u v e z afficher p l u s i e u r s variables o u c o n s t a n t e s d a n s l a m ê m e instruction, d u m o m e n t q u e v o u s les s é p a r e z p a r < < . C ' e s t l e cas e n ( 1 ) , d a n s l a p r e m i è r e instruction d e l a b o u c l e , o ù n o u s affichons u n e c h a î n e , p u i s l'entier i , p u i s l e caractère e s p a c e . C e t t e r e m a r q u e est é g a l e m e n t v a l a b l e p o u r les lectures a v e c c i n e t > > .
La fin du printf
91
M i s e en forme automatique V o u s n o t e r e z q u e v o u s n ' a v e z pas b e s o i n d e préciser l e form a t d'affichage, celui étant défini p a r défaut p o u r les types d e b a s e d e C . Si, n é a n m o i n s , v o u s s o u h a i t e z a p p l i q u e r d e s contraintes sur l'affichage ( n o m b r e de chiffres d é c i m a u x d ' u n réel par e x e m p l e ) , inutile de revenir à printf, cout sait le faire très bien. R e p o r t e z v o u s a u x c o m p l é m e n t s de ce chapitre p o u r savoir c o m m e n t . Sans adresse V o u s a v e z noté l'abscence de & d a n s la s y n t a x e du cin. Ce dernier n ' a p a s b e s o i n de connaître l'adresse de la variable à lire, c o m m e c'était l e cas p o u r s c a n f .
L'intérêt par rapport au C
L'intérêt principal de cout, cin, cerr, << et >> est la vérification de type. En effet, c h a q u e objet est affiché en fonction de sa classe ; il n ' y a p a s de r i s q u e de confusion ou de quip r o q u o . L e s fonctions printf et c o m p a g n i e s o n t i n c a p a b l e s de vérifier le type des d o n n é e s q u ' e l l e s traitent (ce qui accroît les r i s q u e s d'erreurs). P l u s j a m a i s les c h a î n e s de c a r a c t è r e s ne seront affichées c o m m e d e v u l g a i r e s entiers, j e v o u s l e p r o m e t s ... Par ailleurs, u n e s i m p l e c o m p a r a i s o n de lignes suffit p o u r q u e la vérité a p p a r a i s s e de m a n i è r e éclatante à l ' h o n n ê t e h o m m e : cout et ses frères sont r e m a r q u a b l e m e n t p l u s s i m p l e s à utiliser q u e leurs a i n e s d e la famille printf. // C standard (infâme, n'est-ce pas ?) printf("%s%d%c%f\n", "Voici i:", entier, '+', réel); scanf("%f", &reel); // C++ (mieux, tout de même !) cout << "Voici i:" << entier << '+' << réel << endl; cin >> réel;
M a i s l'intérêt ne se limite p a s là : v o u s p o u v e z surtout redéfinir les opérateur << et » p o u r c h a c u n e de v o s c l a s s e s ! A i n s i , les o p é r a t i o n s d ' e n t r é e s / s o r t i e s g a r d e r o n t toujours la m ê m e s y n t a x e , i n d é p e n d a m m e n t d e s classes utilisées ! Par e x e m p l e , u n e classe FilmHorreur p o u r r a afficher le n o m du film et s o n réalisateur, opération réalisée de la m ê m e m a n i è r e q u e p o u r un entier :
92
Pont entre C et C++
Utilisateurs de MSD O S , prenez garde ! Le n o m de fichier
#include "FilmHorreur.h" void
// voir dans la marge
main()
{
utilisé dans l'include
FilmHorreur
fait plus de huit caractères !
film("Doume", "Aïdi software");
cout << film; }
On s u p p o s e b i e n e n t e n d u q u e la classe est définie, et que « l'on a fait ce qu'il faut » p o u r q u e l ' o p é r a t e u r « prenne en c o m p t e l'affichage de la classe F i l m H o r r e u r . V o u s allez a p p r e n d r e c o m m e n t réaliser ce petit m i r a c l e d a n s les comp l é m e n t s d e c e chapitre.
Compléments Surcharger « et » Avant de lire ce complément, vous devez connaître les fonctions amies (page 123), ainsi que les principes de la surcharge, développés page 5 1 .
V o u s p o u v e z s u r c h a r g e r les o p é r a t e u r s < < e t > > p o u r affic h e r v o s p r o p r e s objets. En d'autres t e r m e s , il faut que les fonctions operator<< e t o p e r a t o r » s a c h e n t quoi faire l o r s q u ' e l l e s sont a p p l i q u é e s à des objets de v o t r e classe. A v a n t de p o u r s u i v r e les e x p l i c a t i o n s , un petit rappel s ' i m p o s e : q u a n d on utilise un o p é r a t e u r @, cela revient à app e l e r la fonction operator® c o r r e s p o n d a n t e . V o i l à ce que cela d o n n e p o u r + : int résultat, a = 1, b = 1; résultat = a + b; // équivaut à résultat = operatort (a, b ) ;
T r a n s p o s o n s l ' e x e m p l e ci-dessus à « : NomClasse
mon_objet;
cout << mon_objet; // équivaut à operator<< (cout, mon__objet);
Il s'agit ici de l'opérateur << global. R a p p e l o n s q u e cout et cin sont r e s p e c t i v e m e n t d e s objets de classe ostream et
La fin du printf
93
istream. P o u r s u r c h a r g e r « , i l suffit d o n c d'écrire l a fonction hors-classe suivante : ostream operator<< (ostream, NomClasse)
La fonction à s u r c h a r g e r est identifiée. Il ne reste p l u s q u ' à l'écrire. Un p r o b l è m e se p o s e alors : c o m m e n t a c c é d e r a u x d o n n é e s de l'objet de classe NomClasse ? D e u x solutions : p a s s e r p a r les fonctions m e m b r e s public d ' a c c è s a u x d o n n é e s , ou b i e n déclarer d a n s la classe NomClasse q u e la fonction operator<< est u n e amie. D a n s ce dernier c a s , o p e r a t o r « p e u t a c c é d e r l i b r e m e n t a u x d o n n é e s private e t
protected des objets de NomClasse. Ce qui d o n n e : Vous trouverez page 176 un exemple de surcharge de << qui ne déclare pas de fonction amie.
#include class NomClasse { private: int n; public : NomClasse(int i) : n(i) {} int get_n() const { return n; } // déclarons que l'opérateur global << est // un ami de notre classe (NomClasse) : friend ostreamS operator<< (ostream& out, const NomClasse& obj); };
Nous utilisons ici des passages par
// surchargeons l'opérateur global << pour qu'il // sache traiter des objets de classe NomClasse : ostream& operator<< (ostreamk out, const NomClasse& obj) {
référence (voir
// out est l'objet comparable à cout // obj est l'objet à afficher return out << '[' << obj.n << ' ] ' << endl;
chapitre 9, page I 13). }
void {
maint)
NomClasse
objet(1);
cout << objet; } /*
Pour redéfinir >>, utilisez : istream& operator>> (istreamk in, NomClasse& obj);
94
Pont entre C et C++
*/
// affichage: // [1]
L a fonction o p e r a t o r < < r e t o u r n e u n e référence sur u n objet d e classe o s t r e a m , p o u r q u e v o u s p u i s s i e z utiliser d e s instructions à la file : cout << objl << obj2; // équivaut à : // operator<< (operator<<( cout, objl), obj2);
D a n s l a ligne ci-dessus, o p e r a t o r < < ( c o u t , o b j l ) doit être l u i - m ê m e l'objet d e classe o s t r e a m qui v i e n t d'être appelé ; c'est p o u r cela q u e n o u s r e t o u r n o n s u n e référence v e r s cet objet.
Formater les sorties Ces formatages o n t été vérifiés sur le compilateur T u r b o C + + p o u r PC, mais devraient fonct i o n n e r sur tous les autres compilateurs. * sauf contre indicat i o n dans le texte.
L e s indications de formatage (présentation) d e s sorties doiv e n t être p a s s é e s à c o u t d e l a m ê m e m a n i è r e q u e les d o n n é e s à afficher, à savoir avec l'opérateur < < . Q u a n d v o u s utilisez u n e instruction d e f o r m a t a g e , v o u s dev e z inclure au d é b u t de votre c o d e le fichier i o m a n i p . h en plus d u fichier i o s t r e a m . h . L e s p a r a g r a p h e s qui suivent d o n n e n t des e x e m p l e s p o u r c h a q u e type d e f o r m a t a g e différent. V o u s t r o u v e r e z e n s u i t e un tableau récapitulatif de toutes ces fonctions. Attention : toutes ces instructions de formatage* restent v a lables p o u r tous les c o u t qui les suivent d a n s l e p r o g r a m m e . A f f i c h e r au m o i n s n caractères L'identificateur s e t w ( n ) force c o u t à afficher a u m o i n s n caractères, et plus si nécessaire, s e t w est l'abbréviation de set zuidth (« spécifie la largeur »). Par défaut les affichages s o n t c a d r é s à droite. E x e m p l e : #include "iostream.h" #include "iomanip.h" void
main()
{
cout << setw(3) << 1 << endl; cout << 1.2345 << endl; }
La fin du printf
95
// affiche : 1 1.2345
P o u r q u e c o u t affiche autant d e caractères q u e n é c e s s a i r e , spécifiez u n e l o n g u e u r m i n i m a l e d e 0 ( s e t w ( 0 ) ) . C ' e s t d'ailleurs la valeur par défaut. A l i g n e r les sorties à g a u c h e Utilisez ici l e barbare s e t i o s f l a g s ( i o s : : l e f t ) . E x e m ple : #include "iostream.h" finclude "iomanip.h" void
main()
{
cout << setw(5) << "Wax" << endl; //(l) cout << setiosflags(ios::left) << "Wax" << endl; 11(2) II setw(5) est toujours valable ici ! }
// affiche : Wax Wax
La l i g n e (1) i n d i q u e qu'il faut afficher au m o i n s cinq caractères. Par défaut, l ' a l i g n e m e n t se fait à droite, d ' o ù la p r e m i è r e ligne affichée (deux e s p a c e s et les trois lettres). La ligne (2) p r é c i s e qu'il faut aligner le texte à g a u c h e . R a p p e l e z - v o u s q u e la c o n s i g n e d'afficher au m o i n s cinq caractères est toujours valable. Cette fois-ci, les trois lettres de « W a x » sont affichées, suivies de d e u x e s p a c e s . A l i g n e r les s o r t i e s à droite C ' e s t l e c o m p o r t e m e n t p a r défaut d e c o u t . S i v o u s v o u l e z l e rétablir, utilisez s e t i o s f l a g s ( i o s : : r i g h t ) — v o i r p a r a g r a p h e précédent. A f f i c h e r n chiffres après la v i r g u l e Utilisez s e t p r e c i s i o n ( n ) : #include "iostream.h" #include "iomanip.h" void
main()
96
Pont entre C et C++
{
cout << setprecision(2) << 1.23456 << endl; }
// affiche : 1.23
Afficher les zéros après la virgule Utilisez s e t i o s f l a g s ( i o s : : s h o w p o i n t ) . #include "iostream.h" #include "iomanip.h" void
main()
{
cout << setw(5) << setiosflags(ios::showpoint) << 1.2 << endl; } // affiche : 1.200
Ne pas afficher les zéros après la virgule C ' e s t l a fonction r é c i p r o q u e d e l a p r é c é d e n t e . Utilisez s e tiosf lags(ios::showbase) : #include "iostream.h" #include "iomanip.h" void
main()
{
cout << setw(5) << setiosflags(ios::showpoint) << 1.2 << endl; cout << setiosflags(ios::showbase) << 1.2 << endl; }
// affiche : 1.200 1.2
Afficher u n ' + ' p o u r les n o m b r e s positifs Utilisez s e t i o s f l a g s ( i o s : : s h o w p o s ) : #include "iostream.h" #include "iomanip.h" void {
maint)
La fin du printf
97
cout << setiosflags(ios::showpos) << 1.2 << endl; } // affiche : + 1.2
R e m p l a c e r le caractère blanc par un autre caractère N o u s p a r l o n s ici d e s e s p a c e s affichés p o u r c o m b l e r les v i d e s . P a r e x e m p l e , si v o u s a v e z spécifié s e t w ( 5 ) et q u e v o u s affic h e z le chiffre 1, quatre e s p a c e s seront affichés en p l u s . Si v o u s n ' a i m e z p a s les e s p a c e s , utilisez s e t f i l l ( c a r ) p o u r les r e m p l a c e r p a r un caractère de votre c h o i x : #include "iostream.h" #include "iomanip.h" void main() { cout << setfill('-') << setw(5) << 1 << endl; }
// affiche : 1
A f f i c h e r les n o m b r e s d a n s u n e autre b a s e S o n t définies trois b a s e s : l'octal (base 8 ) , le d é c i m a l (par défaut) et l ' h e x a d é c i m a l (base 16). il suffit de spécifier l'abréviation ( o c t , d e c , h e x ) p o u r q u e tous les affichages suivants se fassent d a n s cette b a s e : #include "iostream.h" #include "iomanip.h" void
main()
{
cout << hex << 12 << endl; }
// affiche : c
A f f i c h e r les n o m b r e s e n n o t a t i o n s c i e n t i f i q u e Un rappel p o u r les n o n - m a t h e u x s ' i m p o s e : la n o t a t i o n scientifique est de la forme n Ee, où n est un n o m b r e réel entre 0 et 1 n o n inclus, et où e est é g a l e m e n t un n o m b r e réel.
98
Pont entre C et C++
U n e telle n o t a t i o n r e p r é s e n t e le n o m b r e n m u l t i p l i é p a r 10 p u i s s a n c e e. P o u r formater l'affichage en n o t a t i o n scientifiq u e , utilisez s e t i o s f l a g s ( i o s : : scientific). E x e m ple : #include "iostream.h" #include "iomanip.h" void
main()
{
cout << setiosflags(ios::scientific) << 12.3 << endl; }
// affiche : .123 e2
A f f i c h e r l e s n o m b r e s e n n o t a t i o n fixe C ' e s t l'affichage p a r défaut d e s n o m b r e s , il s'obtient en utilisant s e t i o s f l a g s ( i o s : : fixed). E x e m p l e : #include "iostream.h" #include "iomanip.h" void
main()
{
cout << setiosflags(ios::scientific) << 12.3 << endl; cout << setiosflags(ios::fixed) << 12.3 << endl; } // affiche : .123 e2 12 .3
La fin du printf
Tableau récapitulatif
99
Ce tableau r e p r e n d les différentes options d'affichage valables pour c o u t .
Effet voulu
cout « ...
au moins n caractères
setw(n)
aligner à gauche
setiosflags(ios::left)
aligner à droite
setiosflags(ios::right)
n caractères après la virgule
setprecision(n)
afficher les 0 après la virgule
setiosf lags(ios: :showpoint)
ne pas afficher les 0 après la virgule
setiosflags(ios::showbase)
'+' devant les nombres positifs
setiosf lags(ios: :showpos)
changer le caractère de remplissage
setfill(c); // c est un char
afficher en décimal, octal ou hexadécimal
dec, oct ou hex
notation scientifique
setiosf lags(ios::scientific)
notation à virgule fixe
setiosflags(ios::fixed)
Le polymorphisme et la virtualité
Vous connaissez déjà les vertus de l'héritage. Découvrez maintenant celles du polymorphisme, mis en œuvre par la virtualité en C++. Si vos notions sur l'héritage sont encore un peu floues, vous gagnerez à relire le chapitre 3 avant d'aborder celui-ci.
Notions de base L'idée
Le p r i n c i p e de p o l y m o r p h i s m e p e u t s ' a p p a r e n t e r à u n e surc h a r g e e t u n e redéfinition d e f o n c t i o n - m e m b r e é t e n d u e . R a p p e l o n s q u e la surcharge consiste à p o u v o i r d o n n e r le m ê m e n o m à p l u s i e u r s fonctions ; le c o m p i l a t e u r faisant la différence grâce aux p a r a m è t r e s . La redéfinition de fonctionmembre a p p l i q u e ce m ê m e p r i n c i p e à travers u n e arboresc e n c e d'héritage, e t p e r m e t e n p l u s d e d o n n e r e x a c t e m e n t l e m ê m e entête d e fonction. Le p o l y m o r p h i s m e va p l u s loin : il p e r m e t de trouver dynamiquement à quel objet s ' a d r e s s e u n e fonction, p e n d a n t l ' e x é c u t i o n d u p r o g r a m m e . A l o r s q u e l a redéfinition d e f o n c t i o n - m e m b r e d é t e r m i n a i t cela à la c o m p i l a t i o n , c'est-àdire de m a n i è r e statique, le p o l y m o r p h i s m e m è n e s o n en-
202
Pont entre C et C++
quête et va jusqu'à retrouver la classe réelle d'un objet pointé. Limites de la redéfinition de fonction-membre Reprenons l'exemple d'une gestion de textes bruts et mis en forme. Si l'on utilise un pointeur de T e x t e B r u t qui pointe réellement vers un T e x t e M i s E n F o r m e , la redéfinition de fonction-membre seule montre ses limites : #include class TexteBrut
{
| public: void
j Imprimer(int n b ) ;
} ;
void TexteBrut::Imprimer(int nb) { cout << "Imprimer de la classe TexteBrut" << endl;
;
}
class TexteMisEnForme : public TexteBrut
// héritage
{
!
I public: void
! Imprimer(int n b ) ;
} ;
void TexteMisEnForme::Imprimer(int nb) { cout << "Imprimer de la classe TexteMisEnForme" < < endl;
i
}
|
void main() { TexteBrut TexteMisEnForme
i i
}
i
*txt; joli_t;xt;
txt = &joli_txt; txt -> Imprimer(1);
// affichage : // Imprimer de la classe TexteBrut
j
i i
Ce n'est pas le résultat espéré : il faudrait que la fonction Imp r i m e r définie dans la classe T e x t e M i s E n F o r m e soit appelée, car l'objet réellement pointé appartient à cette classe. Le polymorphisme permet justement de résoudre ce problème.
Le polymorphisme et la virtualité
Mise en œuvre C++
103
L e p o l y m o r p h i s m e est m i s e n œ u v r e p a r l a virtualité e n C + + . R e p r e n o n s l ' e x e m p l e p r é c é d e n t . Il suffit d'ajouter le m o t - c l é virtual d e v a n t l'entête de la fonction Imprimer, d a n s la classe TexteBrut. Ce qui d o n n e : #include class TexteBrut { public : virtual void
Imprimer(int n b ) ;
} ;
// ... le reste est identique à l'exemple précédent ... void main() { TexteBrut TexteMisEnForme
*txt; joli_txt;
txt = &joli_txt; // txt pointe sur un TexteMisEnForme txt -> Imprimer(1); } // affichage : // Imprimer de la classe TexteMisEnForme
E x a m i n o n s le rôle du mot-clé virtual d a n s n o t r e petite architecture de classes. Le rôle d'une fonction virtuelle Q u a n d l e c o m p i l a t e u r r e n c o n t r e d e s f o n c t i o n s - m e m b r e s virtuelles, il sait qu'il faut attendre l ' e x é c u t i o n du p r o g r a m m e p o u r savoir quelle fonction appeler. Il d é t e r m i n e r a en t e m p s v o u l u la bonne fonction, c'est-à-dire celle qui est définie d a n s la classe de l'objet réel a u q u e l la fonction est a p p l i q u é e . M a i s r e v e n o n s à n o t r e e x e m p l e p o u r illustrer c e s p r o p o s : le petit b o u t de m a i n ( ) a b e a u être petit, il n ' e n est p a s m o i n s très riche en e n s e i g n e m e n t s . L'utilisation d ' u n p o i n t e u r sur classe TexteBrut m é r i t e q u e l q u e s é c l a i r c i s s e m e n t s .
204
Pont entre C et C++
P o i n t e u r s sur u n e c l a s s e d e b a s e * O u i oui, en C c'est possible, car le C est un grand laxiste. Mais en C + + , le contrôle des types est plus serré, et vos incartades ne passeront plus : finies les convertions automatiques hasardeuses de truc_machin vers bidule_chose. De la discipline, que diable !
Résumé * C'est-à-dire m ê m e n o m b r e et mêmes types d'arguments.
* Pour la clarté du listing, on pourra répéter le mot-clé virtual devant chaque fonction concernée, t o u t au long de la hiérarchie.
L'intérêt par rapport au C
En temps normal, quand vous déclarez un pointeur de type A, vous ne pouvez pas le faire pointer sur une donnée de type B*. En tous cas, cela devrait être contraire à votre éthique. M a i s ici, nous sommes dans une situation spéciale : nous déclarons un pointeur sur une classe de base. D a n s ce cas précis, et le C + + nous y autorise, vous pouvez faire pointer ce pointeur vers une classe dérivée de cette classe de base. C'est exactement ce que nous faisons dans l'avant-dernière ligne du listing. La ligne qui suit (txt -> Imprimer ( 1 ) ) devrait donc faire appel à la fonction Imprimer définie dans la classe TexteBrut, puisque txt est un pointeur sur TexteBrut. M a i s NON, puisque le C + + aura vu que txt pointe maintenant sur joli_txt, objet de classe TexteMisEnForme. C e t t e ligne appelera donc la fonction Imprimer de la classe TexteMisEnForme. D a n s u n e hiérarchie de classe, p o u r déclarer des fonctions qui o n t l e m ê m e n o m e t les m ê m e s a r g u m e n t s * e t d o n t les appels sont résolus d y n a m i q u e m e n t , il faut qu'elles soient virtuelles. Il suffit de déclarer u n e fonction virtuelle d a n s la classe de b a s e p o u r q u e toutes les fonctions i d e n t i q u e s des classes dérivées soient elles aussi virtuelles*. P e n d a n t l'exécution, la fonction appelée d é p e n d de la classe de l'objet qui l'appelle (et n o n du type de variable qui pointe sur l'objet). D a n s le cas d'un pointeur sur u n e classe qui appelle u n e fonction virtuelle, c'est la classe de l'objet pointé réellement par ce pointeur qui d é t e r m i n e la fonction a p p e l é e . C e m é c a n i s m e c o m m u n à d e n o m b r e u x l a n g a g e s orientésobjets est appelé polymorphisme. La virtualité est sa m i s e en œuvre C + + . N o u s avions déjà v u q u ' a v e c l'héritage, l e C + + proposait q u e l q u e c h o s e de très intéressant, totalement a b s e n t du C. G r â c e a u p o l y m o r p h i s m e , v o u s d i s p o s e z d ' u n m o y e n nouv e a u , d ' u n e s o u p l e s s e et d ' u n e p u i s s a n c e é t o n n a n t e : utiliser des fonctions h o m o n y m e s , d o n t les a r g u m e n t s ont l e m ê m e profil, tout au l o n g d ' u n e hiérarchie de classe, en bénéficiant de la r é s o l u t i o n d y n a m i q u e des appels.
Le polymorphisme et la virtualité
Rions ensemble. Un utilisateur de Macintosh déclarait récemment : « Windows p o u r PC, c'est mettre du rouge
105
E x e m p l e : v o u s d é v e l o p p e z un e n v i r o n n e m e n t g r a p h i q u e . V o u s le baptisez Fenêtres pour Groupes de Travail 96*. G r â c e à la virtualité, c h a q u e objet g r a p h i q u e de v o t r e s y s t è m e (bouton, m e n u déroulant, liste, etc.) p o s s é d e r a u n e fonctionm e m b r e Afficher, a v e c les m ê m e s p a r a m è t r e s . Il v o u s sera p o s s i b l e d'écrire q u e l q u e c h o s e c o m m e :
à lèvre à un poulet »
ObjetGraphiqueGenerique *obj[10]; // tableau de 10 pointeurs // ... affectation d'objets divers aux pointeurs du tableau // (nous ne détaillons pas cette partie) for (i = 0; i < 10; i++) { obj[i] -> Afficher(paramètres); }
V o u s n ' a v e z plus à v o u s soucier du t y p e e x a c t de l'objet grap h i q u e p o i n t é p a r obj [ i ] , p u i s q u e l a b o n n e fonction Aff i c h e r sera a p p e l é e a u t o m a t i q u e m e n t . En C, il aurait fallu p a s s e r p a r d e s p o i n t e u r s sur void*, définir p l u s i e u r s structures figées c o n t e n a n t d e s p o i n t e u r s sur d e s fonctions, e t autres bidouilles p e u c a t h o l i q u e s . U n e autre solution consisterait à utiliser des s w i t c h , m a i s cela rendrait l ' e x t e n s i o n du c o d e existant très é p i n e u s e . Le C n ' é t a n t p a s c o n ç u p o u r cela, v o u s v o u s e x p o s e r i e z à de m u l t i p l e s prob l è m e s de portabilité, de fiabilité, de m a i n t e n a n c e . C a r la v é ritable force d e frappe d u C + + , c'est l'héritage. S e u l l'héritage, et s o n c o m p l i c e la virtualité, p e r m e t t e n t de m e t t r e e n œ u v r e des s y s t è m e s c o m p l e x e s e t — r e l a t i v e m e n t — facile à modifier.
Le polymorphisme et la virtualité
Rions ensemble. Un utilisateur de Macintosh déclarait récemment : « Windows p o u r PC, c'est mettre du rouge
105
E x e m p l e : v o u s d é v e l o p p e z un e n v i r o n n e m e n t g r a p h i q u e . V o u s le baptisez Fenêtres pour Groupes de Travail 96*. G r â c e à la virtualité, c h a q u e objet g r a p h i q u e de v o t r e s y s t è m e (bouton, m e n u déroulant, liste, etc.) p o s s é d e r a u n e fonctionm e m b r e A f f i c h e r , a v e c les m ê m e s p a r a m è t r e s . I l v o u s sera p o s s i b l e d'écrire q u e l q u e c h o s e c o m m e :
à lèvre à un poulet »
ObjetGraphiqueGenerique *obj[10]; // tableau de 10 pointeurs // ... affectation d'objets divers aux pointeurs du tableau // (nous ne détaillons pas cette partie) for (i = 0; i < 10; i++) { obj[i] -> Afficher(paramètres) ; )
V o u s n ' a v e z plus à v o u s soucier du t y p e e x a c t de l'objet grap h i q u e p o i n t é p a r o b j [ i ] , p u i s q u e l a b o n n e fonction A f f i c h e r sera a p p e l é e a u t o m a t i q u e m e n t . E n C , i l aurait fallu p a s s e r p a r d e s p o i n t e u r s sur v o i d * , définir p l u s i e u r s structures figées c o n t e n a n t d e s p o i n t e u r s sur d e s fonctions, e t autres bidouilles p e u c a t h o l i q u e s . U n e autre solution consisterait à utiliser des s w i t c h , m a i s cela rendrait l ' e x t e n s i o n du c o d e existant très é p i n e u s e . Le C n ' é t a n t p a s c o n ç u p o u r cela, v o u s v o u s e x p o s e r i e z à de m u l t i p l e s p r o b l è m e s de portabilité, de fiabilité, de m a i n t e n a n c e . C a r la v é ritable force d e frappe d u C + + , c'est l'héritage. S e u l l'héritage, et s o n c o m p l i c e la virtualité, p e r m e t t e n t de m e t t r e e n œ u v r e des s y s t è m e s c o m p l e x e s e t — r e l a t i v e m e n t — facile à modifier.
106
Pont entre C et C++
Compléments Destructeurs virtuel
Il est tout à fait p o s s i b l e de définir un d e s t r u c t e u r virtuel p o u r u n e classe. A quoi sert un d e s t r u c t e u r virtuel ? A détruire l'objet r é e l l e m e n t pointé, et n o n un objet i m a g i n a i r e qui aurait l e m ê m e type q u e celui d u p o i n t e u r . U n petit exemple : #include class Véhicule { public : virtual -Véhicule!) { cout << "-Véhicule" << endl; } >;
class Camion : public Véhicule { public : virtual -Camion() { cout << "-Camion" << endl; } };
void main() { Véhicule *pt_inconnu; Camion *mon_beau_camion = new Camion; pt_inconnu = mon_beau_camion; delete pt_inconnu; }
// affichage : /•/ -Camion // -Véhicule
Le delete en dernière ligne appelle le d e s t r u c t e u r de Camion (puis le d e s t r u c t e u r de Véhicule, car les d e s t r u c t e u r s s o n t a p p e l é s en série de la classe d é r i v é e à la classe de b a s e ) . Si les d e s t r u c t e u r s n ' é t a i e n t p a s virtuels, seul le d e s t r u c t e u r de Véhicule aurait été a p p e l é . P o u r e x p l i q u e r cela a u t r e m e n t , pt_inconnu est d é c l a r é c o m m e un p o i n t e u r sur Véhicule, m a i s p o i n t e en réalité sur un objet de classe Camion. C o m m e les d e s t r u c t e u r s s o n t ici virtuels, le p r o g r a m m e a p p e l l e le destructeur qui à la classe de l'objet r é e l l e m e n t pointé, c'est-à-dire Camion.
Le polymorphisme et la virtualité
Classes abstraites et fonctions virtuelles pures
107
Le concept de classe abstraite (ou classe abstraite de base) est c o m m u n à de n o m b r e u x l a n g a g e s orientés-objets. De quoi s'agit-il ? U n e classe est dite abstraite si elle est c o n ç u e d a n s le b u t de servir de c a d r e g é n é r i q u e à ses classes dérivées. De p l u s , u n e classe abstraite ne p e u t e n g e n d r e r aucun objet. C ' e s t un p o i n t capital, qui p e r m e t de s o u l i g n e r q u e la raison d'être d ' u n e classe abstraite réside d a n s ses classes dérivées. Fonctions virtuelles pures L e C + + autorise l a m i s e e n œ u v r e des classes abstraites, p a r l e biais d e f o n c t i o n s - m e m b r e s virtuelles p u r e s . E n C + + , u n e classe est déclarée abstraite q u a n d elle contient au moins une fonction virtuelle pure. U n e fonction virtuelle p u r e est u n e f o n c t i o n - m e m b r e d o n t seul le profil (valeur retournée, n o m et p a r a m è t r e s ) est défini. On déclare u n e fonction virtuelle en ajoutant « = 0; » après l'entête d e l a fonction, e n plus d u m o t - c l é v i r t u a l . Conséquences Q u a n d v o u s déclarez u n e fonction virtuelle p u r e d a n s u n e classe, voilà ce qui se p a s s e : la classe est décrétée « classe abstraite » : elle ne p e u t d o n c pas être utilisée p o u r créer un objet. la fonction virtuelle p u r e ne peut être définie : elle n ' a q u ' u n profil, u n entête, m a i s pas d e c o r p s les classes futures qui hériteront de cette classe abstraite devront définir les f o n c t i o n s - m e m b r e s virtuelles p u r e s de la classe abstraite. I n t é r ê t du v i r t u e l p u r L'intérêt d e s fonctions virtuelles p u r e s et d e s c l a s s e s abstraites de b a s e , c'est de s'assurer q u e les classes d é r i v é e s r e s p e c teront l'interface d e votre classe. V o u s i m p o s e z u n e n t ê t e p o u r u n e fonction g é n é r i q u e , et v o u s forcez les classes dériv é e s à redéfinir cette fonction. C ' e s t un m o y e n s u p p l é m e n taire d e garantir u n e b o n n e h o m o g é n é i t é d e votre architecture de classes.
108
Pont entre C et C++
E x e m p l e court D a n s le listing qui suit, n o u s déclarons la f o n c t i o n - m e m b r e • a f f i c h e r virtuelle p u r e , e n ajoutant = 0 après s o n entête. N o u s n e p o u v o n s d o n c p a s créer d'objet d e cette classe. E n r e v a n c h e , n o u s p o u v o n s créer d e s objets de la classe Dériv é e qui définit l a fonction a f f i c h e r . class AbstraiteBase { protected : // blabla public : virtual void afficher(int x, int y) = 0; // c'est ce « = 0 » qui rend la fonction // virtuelle pure } ;
class Dérivée : { protected : public : virtual //
public AbstraiteBase // blabla void afficher(int x, int y ) ; (virtuelle simple)
} ;
void
Dérivée::afficher(int x, int y)
{
// traitement } void
main()
{
AbstraiteBase toto; Dérivée tata;
// impossible !!! // possible.
tata.afficher(15, 1 2 ) ; // possible. }
Exemple complet Voici u n petit e x e m p l e c o m p l e t m e t t a n t e n é v i d e n c e l'intérêt de la virtualité p u r e . A v a n t d ' e n d é c o u v r i r le listing, quelques é c l a i r c i s s e m e n t s : D a n s l e c a d r e d u d é v e l o p p e m e n t d ' u n e interface g r a p h i q u e , n o u s a v o n s choisi de définir u n e classe Obj etGraphique, qui contient le m i n i m u m i n d i s p e n s a b l e à tous les objets grap h i q u e s de notre interface. Par objet g r a p h i q u e , n o u s entend o n s tout ce qui est utilisé d a n s l'interface (bouton, a s c e n s e u r s , m e n u s , m a i s aussi z o n e p o u v a n t contenir d'autres objets, z o n e de dessin, etc.) D a n s un souci de sim-
Le polymorphisme et la virtualité
109
plification, n o u s retiendrons trois caractéristiques : l'objet g r a p h i q u e parent, les c o o r d o n n é e s en x et en y de l'objet d a n s l ' e s p a c e de c o o r d o n n é e s a s s o c i é e s à l'objet parent. A q u o i sert l'objet p a r e n t ? A savoir d a n s quel autre objet est situé l'objet courant. P a r e x e m p l e , u n b o u t o n aura c o m m e o b jet p a r e n t u n e fenêtre. D a n s l e c a d r e d e l ' e x e m p l e , n o u s décrirons é g a l e m e n t l a classe B o u t o n O n O f f , c o r r e s p o n d a n t à l'objet « c a s e à coc h e r ». P o u r offrir un m i n i m u m de r é a l i s m e s a n s c o m p l i q u e r , n o u s s u p p o s e r o n s q u e n o u s travaillons e n m o d e texte, e t utiliserons les primitives d e T u r b o C + + sur P C p o u r p o s i tionner le c u r s e u r à l'écran. R a s s u r e z - v o u s , il n ' y a rien de sorcier là dedans. Voici le listing : #include
// propre a Turbo C++, // permet de positionner le curseur
enum Boolean { False = (1==0), True = (1==1) } ; // type booléen classe ObjetGraphique
//
class ObjetGraphique { protected : ObjetGraphique *pere; int x, y;
// Objet pere // coord. par rapport // au pere
public : // constructeur ObjetGraphique(ObjetGraphique *pi = 0, int xi = 0, int yi = 0) : pere(pi), x(xi), y(yi) { }
Nous utilisons ici plusieurs techniques propres au C + + et indépendantes des
// fonctions d'accès ObjetGraphique *get_pere() { return pere; ) int get_x() { return x; } int get_y() { return y; }
fonctions virtuelles : les paramètres par défaut (voir page 65), la liste d'initialisation (voir page 47), et la
// fonctions de modification virtual Boolean set_x(int me) = 0; virtual Boolean set„y(int ny) = 0;
définrtion inline des fonctions (voir page 21),
// fonctions de haut niveau virtual void Redessine() = 0; } ;
110
Pont entre C et C++
classe BoutonOnOff
//
Les constantes sont traitées page 67.
class BoutonOnOff : public ObjetGraphique { private : const char CAR_ON; //caractère affiché si ON const char CAR_OFF; //caractère affiché si OFF Boolean état; //bouton ON (True) ou OFF public : // constructeur BoutonOnOff(ObjetGraphique *pi = 0, int xi = 0, int yi = 0, Boolean ei = False) : ObjetGraphique(pi, xi, y i ) , etat(ei), CAR_ON('x'), CAR_OFF('0') { } // fonctions virtuelles pures a définir // fonctions de modification virtual Boolean set_x(int nx) ; virtual Boolean set_y(int n y ) ; // fonctions de haut niveau virtual void Redessine!); // fonctions nouvelles de BoutonOnOff Boolean get_etat() { return état; } void set_etat(Boolean ne) { état = ne; Redessine(); } } ;
// définitions des fonctions de BoutonOnOff Boolean BoutonOnOff::set„x(int nx) {
i f (nx != x) { x = nx ; Redessine(); } return True; }
Boolean
BoutonOnOff;:set v(int ny)
{
if (ny != y) { y = ny; Redessine( ) ; }
return True; }
void
BoutonOnOff::Redessine()
{
// positionnons le curseur en x, y gotoxy(x, y ) ; // affichons un caractère à l'endroit du curseur
Le polymorphisme et la virtualité
111
putch( état ? CAR_ON : CAR_OFF ); }
//
programme principal
void
main()
{
BoutonOnOff
bouton(0, 10, 1 0 ) ;
bouton.Redessine(); getch(); // attend qu'une touche soit frappée bouton.set_etat(True); )
Ce programme met en évidence l'intérêt de la virtualité pure. D a n s la classe de base O b j e t G r a p h i q u e , trois fonctions sont déclarées virtuelles pures : s e t _ x , s e t _ y , et R e d e s s i n e . Pourquoi ? D a n s l'idée, p o u r fournir un c a d r e g é n é r a l i d e n t i q u e à tous les objets g r a p h i q u e s . En forçant les classes d é r i v é e s à définir ces trois fonctions, on s'assure q u e leurs e n t ê t e s d e v r o n t se c o n f o r m e r à un certain profil. Un p r o g r a m m e u r sachant utiliser u n objet g r a p h i q u e s a u r a a u t o m a t i q u e m e n t utiliser tous les autres, y c o m p r i s c e u x qui ne sont pas e n c o r e écrits. s e t _ x e t s e t _ y d é p e n d e n t bel e t b i e n d e c h a q u e objet g r a p h i q u e , car il c o n v i e n t de s'assurer q u e les n o u v e l l e s v a l e u r s sont correctes, n o t a m m e n t , o n p e u t l ' i m a g i n e r , p a r r a p p o r t à l'objet p è r e qui contient n o t r e objet graphique. R e d e s s i n e d é p e n d aussi d e c h a q u e objet g r a p h i q u e . I l n e p r e n d a u c u n p a r a m è t r e car u n objet doit être c a p a b l e de se r e d e s s i n e r en fonction de ses d o n n é e s i n t e r n e s (ici ses c o o r d o n n é e s en x et y ) .
Les références
Seul le passage par valeur est autorisé en C standard. Même lorsque vous passez un pointeur à une fonction, vous passez la valeur de ce pointeur. Le C++ introduit une nouvelle possibilité : le passage par référence (connu depuis longtemps en Pascal). Par ailleurs, ce même mécanisme de référence vous permet de déclarer des synonymes de variables : il s'agit des références libres.
Notions de base L'idée
Q u a n d v o u s p a s s e z à u n e fonction un p a r a m è t r e p a r référence, cette fonction reçoit un s y n o n y m e du p a r a m è t r e réel. En o p é r a n t sur le p a r a m è t r e p a s s é p a r référence, la fonction p e u t d o n c modifier d i r e c t e m e n t l e p a r a m è t r e réel. V o u s n ' a v e z p a s b e s o i n d'utiliser l'adresse d u p a r a m è t r e p o u r l e modifier, c o m m e v o u s d e v r i e z le faire en C s t a n d a r d .
Mise en
Le caractère & est utilisé d a n s l'entête de la fonction p o u r sig n a l e r q u ' u n p a s s a g e p a r référence est d é c l a r é :
œuvre C++
void plus_l(int Snombre) // passage par référence attendu { nombre++;
114
Pont entre C et C++
)
void {
main()
int i = 3 ; plus_l(i); // i vaut maintenant 4 }
C o m m e v o u s le constatez, l'appel se fait de m a n i è r e très s i m p l e . L a fonction p l u s _ l sait q u e l e p a r a m è t r e entier q u ' e l l e attend est u n e référence vers l e p a r a m è t r e réel i . E n c o n s é q u e n c e , toute modification d e n o m b r e d a n s l a fonction p l u s _ l s e répercute sur l a variable i d u p r o g r a m m e principal. Déclarer une référence libre V o u s p o u v e z utiliser les références en d e h o r s de toute fonction : en déclarant q u ' u n e variable est « s y n o n y m e » d'une autre, c'est-à-dire q u e l ' u n e est u n e référence v e r s l'autre. D è s lors, u n e modification sur l ' u n e q u e l c o n q u e de ces variables affectera le c o n t e n u de l'autre. P a r e x e m p l e : void main() { int nombre = 1 ; int &bis = nombre; // bis est une référence à nombre int *pointeur; bis = 1 0 ; nombre = 20; pointeur = &bis; (*pointeur) = 3 0 ;
// bis et nombre valent 10 // bis et nombre valent 20 // bis et nombre valent 3 0
}
L'intérêt par rapport au C
* les constructeurs copie sont vus au chapitre 2, page 3 I.
Le fait d'utiliser le caractère & p o u r autre c h o s e q u e ce qu'il signifie en l a n g a g e C doit v o u s paraître étrange. S o y e z rassuré : v o u s p o u v e z p r e s q u e t o t a l e m e n t v o u s p a s s e r d'utiliser les références, tout en bénéficiant des substanciels a v a n t a g e s du C + + . P o u r q u o i presque totalement ? Eh b i e n , p o u r être franc, il faut a v o u e r q u e les c o n s t r u c t e u r s de copie* ont b e soin de types p a s s é s p a r référence. V o u s r e m a r q u e r e z tout d e m ê m e q u e , p a s s é e l a p r e m i è r e a n g o i s s e , les références se révèlent très p r a t i q u e s !
Les références
115
Compléments Références et objets provisoires * Petite précision de vocabulaire : dans la ligne int &ref = ini; ref
P o u r q u ' u n e référence soit c o r r e c t e m e n t utilisée, il faut q u e l'objet référence et l'objet initial* soient de m ê m e type. S i n o n , plusieurs cas p e u v e n t se présenter : L'objet initial est u n e c o n s t a n t e int &ref = 1 ; const int &ref = 1 ;
// (1) NON (voir ci-dessous) // (2) OUI
désigne l'objet référence et ini l'objet initial.
L'objet initial peut être converti v e r s l'objet r é f é r e n c e int const
initial = 5; float &ref = initial; float &ref = initial;
// (1) NON // (2) OUI
L e s d e u x cas ci-dessus e n g e n d r e n t les m ê m e s r e m a r q u e s : (1) La référence n ' e s t p a s constante. V o t r e c o m p i l a t e u r doit n o r m a l e m e n t générer u n e erreur d e type. (2) La référence est constante. Un objet t e m p o r a i r e est créé ; la référence le désigne.
Fonction retournant une référence
D a n s s o n désir infini de r e p o u s s e r toujours plus loin les limites d u C , l e C + + p e r m e t , grâce a u x références, d'utiliser les fonctions c o m m e des variables ordinaires a u x q u e l l e s o n p e u t affecter u n e valeur. Ainsi, q u a n d u n e fonction r e t o u r n e u n e v a l e u r par référence, on p e u t d i r e c t e m e n t agir s u r cette v a l e u r d e retour. M a i s u n petit e x e m p l e v a u t m i e u x q u ' u n e explication o b s c u r e : // variables globales int t a b [ ] = { 0 , 1, 2, 4, 8 } ; const int nb_elem = 5 ; // nbr d'éléments de tab int &fonction(int j) // retourne une référence { if (j >= 0 && j < nb_elem) return tab[j]; else return tab[0]; }
void main()
116
Pont entre C et C++
{
fonction (0) = Ï0 ; // tab[0] vaut -ïb fonction(l) = fonction(4); // tab[l] vaut 8 }
La p r e m i è r e ligne du m a i n est e x é c u t é e c o m m e ceci : a p p e l à f o n c t i o n , qui retourne u n e référence vers t a b [ 0 ] . D o n c , d a n s l ' e x p r e s s i o n f o n c t i o n (0) = 10;, tout se p a s s e c o m m e si f o n c t i o n ( 0 ) était r e m p l a c é p a r t a b [ 0 ]. On affecte d o n c b i e n la valeur 10 à t a b [ 0 ]. L a d e u x i è m e ligne fonctionne selon l e m ê m e principe.
Les templates ou la mise en œuvre de la généricité
Toujours plus, toujours : dans sa C + + nous offre les templates, qui les fonctions ou les classes par des sants, mais souvent délicats à mettre teurs relativement récents autorisent et fiable.
débauche d'effets spéciaux, le vous permettent de paramétrer types. Les templates sont puisen œuvre : seuls les compilaleur usage de manière simple
Notions de base L'idée Juste une remarque de vocabulaire : la
généricité est le concept orientéobjet, alors que les
templates sont la solution apportée par le C + + pour mettre en œuvre ce concept.
J u s q u ' à présent, les fonctions p o u v a i e n t avoir c o m m e p a r a m è t r e s des variables o u des objets. M a i s j a m a i s v o u s n ' a v e z eu la possibilité de les p a r a m é t r e r p a r d e s types. C ' e s t j u s t e m e n t le rôle et l'intérêt de la généricité et des t e m p l a t e s . D e s classes p e u v e n t é g a l e m e n t être déclarées « t e m p l a t e s », si v o u s j u g e z q u ' e l l e s doivent d é p e n d r e d ' u n type. Par e x e m p l e , u n e classe qui gère u n e liste d'objets q u e l c o n q u e s n ' a p a s à se soucier du type réel de ces é l é m e n t s : elle ne doit s ' o c c u p e r q u e de les classer, d'en ajouter ou d ' e n s u p p r i m e r à sa liste, etc. U n e telle classe a d o n c intérêt à être p a r a m é t r é e
118
Pont entre C et C++
p a r le type des é l é m e n t s q u ' e l l e s t o c k e . Il serait j u d i c i e u x d'en faire u n e classe « t e m p l a t e ».
Mise en œuvre C++
Il est i m p o r t a n t de distinguer d e u x types de t e m p l a t e s : celles qui c o n c e r n e n t les fonctions et celles qui c o n c e r n e n t les classes. E x a m i n o n s d ' a b o r d les t e m p l a t e s de fonctions.
Templates de fonctions
Exemple P r e n o n s l ' e x e m p l e classique de la fonction qui r e t o u r n e le m i n i m u m de d e u x chiffres. Il s e m b l e l é g i t i m e de v o u l o i r r e n d r e cette fonction i n d é p e n d a n t e du t y p e d e s objets. V o y o n s la m i s e en œ u v r e d ' u n e telle fonction, g r â c e a u x templates : tinclude / / pour l'affichage avec cout template TYPE min(TYPE a, TYPE b) { return ( a < b ) ? a : b; } main() { int char
il = 10, i2 = 20; cl = 'x', c2 = ' a ; 1
cout << "min entier : " << min(il, i2) << endl; cout << "min char : " << min(cl, c2) << endl; }
Commentaires Observez bien la déclaration de A v a n t le type de retour, v o u s d e v e z
la fonction. préciser tem-
piate où NomType est un identificateur de votre cru, q u e v o u s utiliserez e n s u i t e p o u r d é s i g n e r le type « p a r a m è t r e » de votre fonction. V o u s êtes obligés d'utiliser ce n o m de type d a n s au moins un p a r a m è t r e de votre fonction. Ici, les d e u x é l é m e n t s à c o m p a r e r sont de t y p e MonType, et la fonction r e t o u r n e é g a l e m e n t un objet de type MonType. À l'appel, le c o m p i l a t e u r reconnaît le type des p a r a m è t r e s et r e m p l a c e d a n s la fonction toutes les o c c u r e n c e s de
Les templates
119
MonType par le t y p e v o u l u (ici, int d a n s le p r e m i e r appel e t c h a r d a n s l e d e u x i è m e ) . T e m p l a t e s de f o n c t i o n s : cas g é n é r a l
Templates de classes
* Il faudrait bien entendu gérer un
Exemple L e s t e m p l a t e s v o u s seront é g a l e m e n t utiles d a n s d e s c l a s s e s . R e p r e n o n s l ' e x e m p l e de la classe qui gère u n e liste d'objets de type (ou de classe) q u e l c o n q u e . P o u r simplifier le p r o p o s , n o u s utiliserons u n tableau p o u r stocker les objets*. C o m m e n t la déclarer ?
nombre variable d'objets, avec allocations et désallocations « à la demande ». Cela compliquerait la classe et nuirait à l'explication des
tinclude // pour affichage avec cout #include <stdlib.h> // pour la fonction exit template class Liste { private: enum { MAX = 3 ,} ; // constante TYPE elem[MAX];
templates. Par ailleurs,
public :
la gestion d'erreurs est réduite ici à sa
TYPE void void
plus simple expression. N o u s verrons au chapitre I 3, page I 37, comment implanter une véritable gestion des erreurs.
Liste() { } get_elem(int n ) ; set_elem(int n, TYPE e ) ; affiche();
} ;
template TYPE Liste::get_elem(int n) // retourne le nième élément classé {
if (n >= 1 && n <= MAX) return elem[ n-1 ]; else { cerr <<"Liste::get_elem - Indice hors limite :" << n << endl;
120
Pont entre C et C++
exit(1); } }
template void Liste: :set_elem(int n, TYPE e) // stocke e à la nième place dans la liste { if ( n >= 1 && n <= MAX) elem[n-l] = e; else { cerr <<"Liste : :set_elem - Indice hors limite :" << n << endl; exit(1); } }
template { int i;
void
Liste::affiche()
for (i = 0; i < MAX ; i + +) cout << "Num " << i+1 << ':' << elem[i] << endl; }
main() {
Liste
liste;
liste.set_elem(1, 1 0 ) ; liste.set_elem(3, 3 0 ) ; liste.set_elem(2, 2 0 ) ; liste.affiche(); // affichage : // Num 1 : 10 // Num 2 : 20 // Num 3 : 30 }
Q u e l q u e s e x p l i c a t i o n s s u r cet e x e m p l e N o u s a v o n s choisi d e stocker les é l é m e n t s d a n s u n tableau de taille MAX, MAX étant u n e c o n s t a n t e spécifique à la classe Liste, définie à l'aide d ' u n enum (voir p a g e 7 1 ) . La déclaration de la classe i n d i q u e q u e c'est u n e t e m p l a t e par a m é t r é e p a r le type TYPE : template
class Liste
Les templates
121
N o u s utilisons d o n c le type g é n é r i q u e TYPE d a n s la c l a s s e , p o u r i n d i q u e r ce q u e n o u s s t o c k o n s d a n s le t a b l e a u e l e m : TYPE
elem[MAX];
N o t e z é g a l e m e n t la définition des f o n c t i o n s - m e m b r e s en deh o r s de la classe : template TYPE Liste: :get_elem(int n)
TYPE est le type de retour de la fonction, d o n t le n o m de classe est Liste. La seule c h o s e qui c h a n g e vraim e n t par r a p p o r t à u n e f o n c t i o n - m e m b r e n o r m a l e , c'est le
préfixe « template ». Utilisation d'une classe t e m p l a t e L'utilisation d ' u n e classe t e m p l a t e se fait en d o n n a n t le n o m d u type entre c h e v r o n s . D a n s l ' e x e m p l e ci-dessus, cela donne : Liste
liste;
où int aurait pu être r e m p l a c é p a r n ' i m p o r t e quel autre type o u classe. T e m p l a t e s de classes : cas g é n é r a l Voici c o m m e n t l'on déclare u n e classe t e m p l a t e :
122
Pont entre C et C++
Et voici c o m m e n t l'on définit u n e f o n c t i o n - m e m b r e d ' u n e classe t e m p l a t e :
Compléments Attention à l'ambiguïté
I l p e u t v o u s arriver d'avoir b e s o i n d ' i m b r i q u e r d e s t e m p l a tes. Par e x e m p l e , v o u s v o u l e z gérer u n e liste d'arbres, où la classe Liste et la classe A r b r e sont des t e m p l a t e s . Il n ' y a en théorie p a s de p r o b l è m e , m a i s je tiens à attirer v o t r e attention sur la m a n i è r e de déclarer un objet Liste : Liste>
ma_liste_d_arbres;
// NON
D a n s cette déclaration, l e type d e v o t r e liste t e m p l a t e n ' e s t autre q u ' u n e classe A r b r e , e l l e - m ê m e t e m p l a t e , d o n t l e t y p e est i n t . C e t t e déclaration p r o v o q u e i n s t a n t a n é m e n t u n e erreur de c o m p i l a t i o n , b i e n q u e , à p r e m i è r e v u e , elle s e m b l e correcte. La seule c h o s e qui c l o c h e , c'est la p r é s e n c e d e » . E n C + + , les d e u x caractères s u p é r i e u r s r e p r é s e n t e n t un opérateur. Il y a d o n c m é p r i s e . P o u r y r e m é d i e r , t a p e z un e s p a c e entre les d e u x c h e v r o n s . L a b o n n e déclaration d e v i e n t donc : Liste< Arbre >
ma_liste_d_arbres;
// OUI
Les classes et fonctions amies
L'amitié n'est pas une vertu réservée aux humains. Les classes et les fonctions C++ peuvent, elles aussi, se lier d'amitié. Heureusement, vous seul pouvez les autoriser à s'enticher ainsi les unes des autres.
Notions de base L'idée * Rappelons que l'encapsulation c o n siste à manipuler des objets composés de données et de fonctions de manipulation de ces données. Les données sont cachées au monde extérieur.
L o r s q u ' u n e fonction est déclarée a m i e ( f r i e n d ) d ' u n e c l a s s e , elle p e u t a c c é d e r directement à toutes les d o n n é e s p r i v a t e o u p r o t e c t e d d e cette classe. L o r s q u ' u n e classe est déclarée a m i e d ' u n e autre c l a s s e , ce s o n t toutes les fonctions de la p r e m i è r e classe qui p e u v e n t a c c é d e r a u x d o n n é e s p r i v é e s d e l a d e u x i è m e classe. C'est bien beau, mais si vous avez bien compris le premier chapitre de ce livre, v o u s devriez b o n d i r de v o t r e siège, et v o u s écrier « M a i s c'est u n e entorse i n s u p p o r t a b l e à la sacrosainte règle de l'encapsulation* ! » Et v o u s auriez raison. A u s s i s o m m e s - n o u s en droit de n o u s d e m a n d e r pourquoi le créateur du C + + a p e r m i s q u e l'on viole — le m o t n ' e s t p a s
224
Pont entre C et C++
trop fort — l ' e n c a p s u l a t i o n ? E s s e n t i e l l e m e n t p o u r d e s rais o n s d'architecture, d u e s a u fait q u e l e C + + n ' e s t p a s u n lang a g e e n t i è r e m e n t orienté objet (il co-existe a v e c le C ) , m a i s aussi p o u r p e r m e t t r e à un p r o g r a m m e u r d ' a c c o r d e r des « droits » entre ses classes internes, et é v e n t u e l l e m e n t p o u r des raisons d ' o p t i m i s a t i o n (voir aussi les q u e s t i o n s - r é p o n s e s , a u chapitre 16). V o u s t r o u v e r e z l'utilité d e s classes a m i e s d a n s l ' e x e m p l e d e s u r c h a r g e d e l'opérateur « , p a g e 92. C e l a dit, méfiez-vous et n'utilisez les « a m i e s » q u e si v o u s y êtes contraint...
Mise en œuvre C++
La déclaration de l'amitié d ' u n e classe A p o u r u n e autre entité se fait n ' i m p o r t e où d a n s la classe A, g r â c e au m o t - c l é
friend: class A { friend void fonction_B(); friend class C; //
// (1) pour une fonction 1/(2) pour une classe
...
> ;
La l i g n e (1) signifie q u e fonction_B p e u t a c c é d e r a u x d o n n é e s private ou protected de la classe A. R e m a r q u e z q u e si f onction_B était u n e fonction m e m b r e d ' u n e autre c l a s s e , il faudrait spécifier de quelle classe elle est m e m b r e , avec u n e ligne du style : friend void AutreClasse: :fonction_B();
La l i g n e (2) signifie q u e toutes les fonctions de la c l a s s e C ont le droit d ' a c c é d e r d i r e c t e m e n t a u x d o n n é e s private ou
protected de la classe A. C o m m e v o u s l'avez constaté, la déclaration f riend... se fait d a n s la classe qui accorde le droit a u x autres de v e n i r tripatouiller ses p r o p r e s d o n n é e s . R é c a p i t u l o n s :
Classes et fonctions amies
Attention !
125
Utilisez avec la p l u s g r a n d e p r é c a u t i o n les fonctions f r i e n d : elles tordent le c o u à l ' e n c a p s u l a t i o n , et sont d o n c d a n g e r e u s e s p o u r la stabilité de votre application. Elles ne sont intéressantes q u e p o u r o p t i m i s e r des c l a s s e s , o u p o u r accorder d e s droits entre classes internes qui n ' i n t é r e s s e r o n t p a s d'autres p r o g r a m m e u r s . E n r é s u m é , bidouillez v o s c l a s ses à v o u s , m a i s faites du travail p r o p r e sur les classes qui seront u t i l i s é e s / r e p r i s e s p a r d'autres. G a r d e z b i e n p r é s e n t à l'esprit q u ' u n e modification d a n s les d o n n é e s p r i v é e s d ' u n e classe qui a déclaré d'autres c l a s s e s a m i e s doit se répercuter sur ces autres classes ! A u s s i , m é fiez-vous et i n d i q u e z toujours c l a i r e m e n t d a n s la d o c u m e n tation quelles classes sont a m i e s d e qui, p o u r q u e c e u x qui s e p e n c h e r o n t sur v o t r e travail d a n s q u e l q u e s m o i s ( v o u s y c o m p r i s ) ne s'arrachent p a s les c h e v e u x en se c o g n a n t la tête c o n t r e les m u r s .
L'héritage multiple
L'héritage déjà pour différence classes ! tors...
simple vous permet d'utiliser des classes qui existent en créer d'autres. L'héritage multiple est identique, à la près que vous pouvez hériter simultanément de plusieurs Ce qui n'est pas sans engendrer quelques problèmes re-
Notions de base L'idée
L ' h é r i t a g e m u l t i p l e v o u s p e r m e t d e créer des classes d é r i v é e s qui héritent e n m ê m e t e m p s d e p l u s i e u r s classes d e b a s e . I l n e s'agit pas d ' u n héritage s i m p l e e n c h a î n e m a i s bel e t b i e n d'un héritage m u l t i p l e s i m u l t a n é :
128
Pont entre C et C++
D a n s le cas p r é s e n t é à droite du s c h é m a ci-dessus, la classe C hérite du c o n t e n u des classes A et B (mais cela d é p e n d touj o u r s d e s spécificateurs p r o t e c t e d , p r i v a t e e t p u b l i c ) .
Mise en œuvre C++
U n héritage multiple s e spécifie c o m m e u n héritage s i m p l e , en séparant p a r d e s virgules les classes de b a s e s désirées. P o u r déclarer l'héritage multiple du s c h é m a p r é c é d e n t , il faudrait écrire : class A { // blabla public : void
fonction_a();
} ;
class B { // blabla public : void
fonction_b();
} ;
class C : public A, public B {
// blabla public : void
fonction_c();
} ;
E n admettant que f o n c t i o n _ a , f o n c t i o n _ b e t f o n c t i o n ^ ont été définies, v o u s pourriez écrire q u e l q u e chose comme : void
main()
{
C
objet_c;
objet_c.fonction_a(); // appel à A::fonction_a() objet_c.fonction_b() ; // appel à B: :fonction_b() objet_c.fonction_c(); // appel à C: :fonction_c() }
Il n ' y a là rien de b i e n surprenant. On pourrait m ê m e croire q u e c'est simple. M a i s c o m m e d e n o m b r e u x c o n c e p t s C + + , l e d e s s u s d e l'iceberg c a c h e u n certain n o m b r e d e complications difficiles à déceler au p r e m i e r a b o r d . . .
L'héritage multiple
129
O r d r e d'appel des constructeurs D a n s le cas d ' u n héritage m u l t i p l e , la création d ' u n objet de la classe dérivée appelle les c o n s t r u c t e u r s des classes de b a s e d a n s l'ordre de déclaration de l'héritage. P a r e x e m p l e : class A { } ;
class B { } ;
class C : public B, public A // la ligne ci-dessus détermine l'ordre d'appel // des constructeurs { } ;
void
main()
{
C objet_c; // appel à B ( ) , puis A ( ) , puis C() }
Les listes d'initialisations sont présentées page 47.
Par ailleurs, si v o u s p r é c i s e z u n e liste d'initialisation*, l'ordre d ' a p p e l des c o n s t r u c t e u r s d é p e n d r a toujours de l'ordre de d é c l a r a t i o n des classes de b a s e s . P a r e x e m p l e : class C : public B, public A { public : C() ; } ;
C::C() : A(param), B(param) // liste d'initialisation { }
void
main()
{
C
objet_c; // appel à B(param), A(param) puis C()
}
L'intérêt par rapport au C
Le l a n g a g e C ne p r o p o s e p a s d ' é q u i v a l e n t de la n o t i o n d'héritage s i m p l e , il est d o n c v a i n de c h e r c h e r un é q u i v a l e n t d'héritage m u l t i p l e . Le principal atout de ce dernier est q u ' i l p e r m e t d e m o d é l i s e r d e façon p l u s juste l e m o n d e réel.
730
Pont entre C et C++
Par e x e m p l e , si v o u s é l a b o r e z u n e classification des a n i m a u x , v o u s p o u v e z être a m e n é à considérer la b a l e i n e c o m m e un m a m m i f è r e m a i s aussi c o m m e u n a n i m a l v i v a n t s o u s l'eau. L'héritage multiple vous permettra de garder un schéma cohérent :
exemple
d'héritage multiple
Il faut n é a n m o i n s t e m p é r e r cette excitation : l'héritage m u l tiple est c o m p l e x e à m a n i p u l e r ; n o u s a v o n s vu à q u e l point il faut faire attention a u x a m b i g u ï t é s difficiles à déceler. E s s a y e z autant q u e p o s s i b l e d'éviter l'héritage multiple. N ' o u b l i e z p a s q u e m ê m e s i v o u s l e m a î t r i s e z parfaitement, c e n ' e s t peut-être p a s le cas d e s futurs utilisateurs de v o s classes. Or la c o m p r é h e n s i o n d ' u n e hiérarchie de classes est vitale p o u r c o n c e v o i r du c o d e fiable et c o m p r é h e n s i b l e .
L'héritage multiple
131
Compléments Duplication de données d'une classe de base
A t t a r d o n s - n o u s sur c e qui s e p a s s e e n m é m o i r e d a n s certains cas d ' h é r i t a g e s multiples. A d m e t t o n s q u e les d e u x classes d e b a s e d ' u n héritage m u l t i p l e héritent e l l e s - m ê m e s d ' u n e m ê m e classe de b a s e :
héritage multiple « double »
Traduisons ce schéma en C + + : class Base { int variable_base; // blabla } ;
class A : public Base { int variable_A; // blabla };
class B : public Base { int variable_B; // blabla } ;
class C : public A, public B { int variable_C; // blabla };
C o n s é q u e n c e s : un objet de classe C c o n t i e n d r a d e u x fois les d o n n é e s héritées de la classe Base. P o u r q u o i ? P a r c e q u e à la fois l'objet A et l'objet B c o n t i e n n e n t les d o n n é e s héritées de la classe Base. Or la classe C hérite de la c l a s s e A et de la
132
Pont entre C et C++
classe B. C bénéficie donc de d e u x copies distinctes des donn é e s héritées de la classe Base. En m é m o i r e , cela r e s s e m b l e à ceci :
C o m m e n t , dès lors, a c c é d e r à l ' u n e ou l'autre c o p i e d e s donn é e s de la classe Base ? T o u t s i m p l e m e n t en utilisant l'opérateur de résolution de p o r t é e : :. Il suffit de spécifier le n o m de la classe à laquelle appartient la d o n n é e v o u l u e . A i n s i , p o u r a c c é d e r à variable_base hérité d a n s la classe B, il faut utiliser B: :variable_base. De m ê m e , p o u r acc é d e r à la c o p i e de variable_base s t o c k é e d a n s la classe A, il faut utiliser A : : va r i ab 1 e_ba s e. Voici un e x e m p l e illustrant la duplicité d e s d o n n é e s et leur utilisation : class Base
* Pour un exemple
public : int variable_base; // désolé *
simple et court, la variable est déclarée
class A : public Base
public. C'est un exemple à ne pas
// blabla
suivre (en temps normal). Il faudrait bien entendu la déclarer private ou
class B : public Base // blabla
protected et définir des fonctions public d'accès et de modification.
class C : public B, public A // blabla
void main()
L'héritage multiple
133
{
C
objet_c;
objet_c.variable_base = 1; objet_c.B: :variable_base = 1; objet_c.A::variable_base = 2;
// (1) erreur!!! // (2) mieux // (3) mieux
}
L a ligne (1) n e précise p a s à quelle v a r i a b l e _ b a s e elle recourt. L e c o m p i l a t e u r n e c o m p r e n d p a s e t v o u s d e m a n d e d e lever l ' a m b i g u ï t é . En r e v a n c h e , les lignes (2) et (3) sont s a n s é q u i v o q u e . R a p p e lons que les variables B : :variable_base et A : : v a r i a b l e _ b a s e s o n t différentes e t b i e n distinctes e n mémoire. Masquage du polymorphisme
S o u s c e titre s e c a c h e u n p r o b l è m e é p i n e u x e n g e n d r é par l'héritage multiple. Le fait d'être obligé de p r é c i s e r e x a c t e m e n t à quelle fonction on fait référence va à l ' e n c o n t r e m ê m e du p r i n c i p e de virtualité. P r e n o n s un e x e m p l e où cette faiblesse est m i s e en é v i d e n c e :
N o u s considérons qu'un périphérique est à la fois une ressource et un objet réel, physique. N o u s définissons une fonction virtuelle I n f o pour les classes R e s s o u r c e , O b j e t R e e l e t P é r i p h é r i q u e . Voici l e listing C + + qui correspond à cette architecture : class Ressource { public : virtual void
Info();
};
void
Ressource::Info() { }
class ObjetReel
134
Pont entre C et C++
{ public : virtual void Info(); } ;
void
ObjetReel: : Info() { }
class Périphérique : public Ressource, public ObjetReel {
} ;
class Imprimante : public Périphérique {
public : virtual void Info(); };
void
Imprimante::Info() { }
void main() { Périphérique Imprimante
* inconnu; hp5 00;
inconnu = &hp500; inconnu -> Info(); inconnu -> Ressource::Info();
// (1) // (2) inattendu // (3) OK
)
Le problème
* En effet inconnu est un pointeur de Périphérique, mais il pointe en réalité sur un objet de classe Imprimante.
Il existe un moyen d'éviter ce problème : c'est l'héritage virtuel, détaillé ci-après.
Voici le problème : en temps normal, grâce au mécanisme de virtualité, la ligne (2) du m a i n devrait appeler la fonction I n f o définie dans la classe I m p r i m a n t e * . M a i s hélas, à cause de l'héritage multiple, c'est la fonction-membre définie par la classe P é r i p h é r i q u e qui est appelée, à notre grand dam ! P o u r plus de clarté, je vais reprendre l'explication d'une autre manière. Quand une classe, ici P é r i p h é r i q u e , utilise l'héritage multiple, cela peut poser un problème d'ambiguité : il suffit que les classes de base héritées (ici R e s s o u r c e et O b j e t R e e l ) possèdent des fonctions homonymes (ici I n f o ) et toc, le compilateur C + + renonce au polymorphisme. Il appelle donc la fonction correspondant au type statique du pointeur (ici P é r i p h é r i q u e ) . O r , la souplesse de la virtualité réside justement dans cette espèce de flou permis au pro-
L'héritage multiple
135
g r a m m e u r , qui p e u t utiliser des p o i n t e u r s d e classes p o u r a p p e l e r d e s fonctions virtuelles. L a b o n n e fonction étant app e l é e p e n d a n t l'exécution d u p r o g r a m m e , s e l o n l'objet réell e m e n t pointé.
Héritage virtuel
Il vaut mieux lire les paragraphes précédents avant de
C o m m e n o u s l ' a v o n s v u p r é c é d e m m e n t , les d o n n é e s d ' u n e classe d e b a s e p e u v e n t être d u p l i q u é e s d a n s certains cas d ' h é r i t a g e multiple. M a l h e u r e u s e m e n t , tel n ' e s t p a s forcém e n t le désir du p r o g r a m m e u r . R e m é d i a n t à ce p r o b l è m e , l'héritage virtuel p e r m e t de n'avoir qu'une seule occurrence d e s d o n n é e s héritées d ' u n e classe d e b a s e .
s'aventurer dans celui-ci.
héritage multiple en diamant
P o u r q u e la classe C ne p o s s è d e q u ' u n seul e x e m p l a i r e d e s d o n n é e s de la classe Base, il faut q u e les classes A et B héritent v i r t u e l l e m e n t de la classe Base. Voici l ' e x e m p l e C + + qui c o r r e s p o n d à cette explication (c'est le m ê m e e x e m p l e q u e le p a r a g r a p h e p r é c é d e n t , à l ' e x c e p t i o n des m o t s - c l é s virtual h a b i l e m e n t placés) : class Base { public : int variable_base; // blabla };
class A : virtual public Base { int variable_A; // blabla };
class B : virtual public Base { int variable_B; // blabla } ;
136
Pont entre C et C++
class C : public A, public B { int variable_C; // blabla } ;
O b s e r v e z m a i n t e n a n t sur c e s c h é m a l'état d e l a m é m o i r e q u a n d un objet de classe C est créé :
Voilà, il n ' y a p l u s q u ' u n e seule o c c u r r e n c e d e s d o n n é e s de B a s e . C o n t r a i r e m e n t a u p a r a g r a p h e p r é c é d e n t o ù v o u s étiez obligé de spécifier le « c h e m i n » de v o s d o n n é e s , v o u s n ' a v e z p l u s à v o u s en soucier ici. Illustrons cette j o v i a l e c o n c l u s i o n p a r un b o u t de p r o g r a m m e : void main() { C objet_c; ,objet_c.variable_base = 1 ; >
// parfait !
Les exceptions
Le traitement des erreurs est depuis toujours une vraie plaie pour les programmeurs. Le C++ nous propose le mécanisme des « exceptions » pour coordonner la lutte contre ces erreurs indésirables. Attention cependant : les exceptions sont encore peu répandues, tant au niveau des compilateurs que des programmeurs C++. C'est donc en quelque sorte un secteur « en chantier »...
Notions de base L'idée
U n p r o g r a m m e , m ê m e b i e n écrit, p e u t rencontrer d e s erreurs d ' e x é c u t i o n s : disque plein, saturation de la m é m o i r e , etc. C o m m e n t traiter ces p r o b l è m e s ? L e C + + r é p o n d s i m p l e m e n t par le c o n c e p t d'exception. Qu'est-ce qu'une « exception » ? U n e e x c e p t i o n est u n e variable, d e n ' i m p o r t e quel type, qui s y m b o l i s e u n e erreur. Q u a n d u n e partie d e p r o g r a m m e rencontre un p r o b l è m e , elle p e u t lancer u n e e x c e p t i o n , qui p o u r ra être interceptée p a r un autre b o u t de p r o g r a m m e . L'intérêt de ce s y s t è m e est de séparer la détection d'erreur d e s t r a i t e m e n t s associés. P a r e x e m p l e , q u a n d v o u s é c r i v e z u n e hiérarchie de classes, il est j u d i c i e u x de définir les e x -
138
Pont entre C et C++
ceptions q u e v o s classes p o u r r o n t lancer e n cas d e problème. De s o n côté, l'utilisateur de v o s classes aura c o n n a i s s a n c e de la liste de ces e x c e p t i o n s , et p o u r r a e n v i s a g e r différents trait e m e n t s p o u r é r a d i q u e r le mal.
Mise en œuvre C++
A t t e n t i o n : les exceptions f o n t uniquement référence aux événements
Utiliser des exceptions existantes P o u r ce faire, il faut b i e n distinguer d e u x b l o c s de c o d e : celui qui réalisera les opérations « d a n g e r e u s e s », c'est-à-dire susceptibles de générer des e x c e p t i o n s , et celui q u i traitera ces e x c e p t i o n s si elles surviennent. Le p r e m i e r bloc est précédé d u mot-clé t r y , l e s e c o n d d u mot-clé c a t c h . V o y o n s u n petit e x e m p l e :
internes au programme, et n o n aux événements extérieurs c o m m e par exemple un clic souris ou l'appui sur une
#include #include <except.h> void main() { try
t o u c h e du clavier.
{
int *p = new int[1E99]; // difficile ! cout << "Fin normale" << endl; }
catch(xalloc) {
cout << "Fin chaotique" << endl; L'exemple de xalloc fonctionne avec
} )
T u r b o C + + sur PC. Consultez la documentation de
// affichage : // Fin chaotique
votre compilateur p o u r obtenir la liste des exceptions définies.
E
A l l o u e r 1 9 9 entiers est u n e opération délicate, en tout cas p o u r m o n ordinateur. Il faut savoir é g a l e m e n t q u ' e n cas d ' é c h e c d'allocation m é m o i r e , u n e e x c e p t i o n d e t y p e x a l l o c est g é n é r é e . A p r è s l e bloc t r y , n o u s détaillons u n bloc c a t c h qui intercepte j u s t e m e n t les e x c e p t i o n s x a l l o c . L ' e x é c u t i o n m o n t r e b i e n q u e l'exception a été g é n é r é e et traitée. F o n c t i o n n e m e n t de try et catch Q u a n d u n e e x c e p t i o n est détectée d a n s u n bloc t r y , l ' e x é c u t i o n de ce dernier est s t o p p é e , p u i s d é r o u t é e s u r le bloc c a t c h c o r r e s p o n d a n t . A l a fin d u bloc c a t c h , o n n e retourne pas d a n s le bloc t r y fautif. En r e v a n c h e , le pro-
Les exceptions
139
g r a m m e suit son cours comme si le bloc t r y avait été exécuté. Q u a n d u n e e x c e p t i o n est r e n c o n t r é e , les d e s t r u c t e u r s d e s objets du bloc t r y sont appelés avant d'entrer d a n s le bloc catch. U n b l o c t r y doit o b l i g a t o i r e m e n t être suivi d ' a u m o i n s u n bloc c a t c h . Plusieurs b l o c s c a t c h p e u v e n t être décrits à la file, si chac u n i n t e r c e p t e u n type d ' e x c e p t i o n différent. S i a u c u n bloc c a t c h n'intercepte l'exception é m i s e , l a fonction t e r m i n a t e est appelée. Par défaut, elle fait appel à l a fonction a b o r t qui m e t fin a u p r o g r a m m e . C e p o i n t est a b o r d é p l u s e n détail d a n s les c o m p l é m e n t s d e ce chapitre. Précisions sur catch U n bloc c a t c h doit être p r é c é d é d ' u n bloc t r y . L a s y n t a x e d e c a t c h p e u t être l ' u n e des trois s u i v a n t e s : catch (Type)
Intercepte les exceptions de type Type, ainsi que les exceptions de classes dérivées si Type est une classe.
catch (Type obj)
Idem que ci-dessus, mais lèxception est représentée
catch (...)
dans le bloc catch par un objet réel : obj. Intercepte toutes les exceptions non-traitées par les blocs catch précédents.
Intercepter plusieurs exceptions I l suffit p o u r cela d'écrire plusieurs b l o c s c a t c h après l e bloc t r y . C o m m e n o u s l ' a v o n s v u d a n s l e tableau c i - d e s s u s , v o u s p o u v e z intercepter toutes les e x c e p t i o n s , s a n s distinction, e n utilisant c a t c h ( . . . ) . tinclude #include <except.h> void main() { try {
// quelque chose qui va déclencher // Exceptionl, Exception2 ou une autre exception } catch(Exceptionl) {
140
Pont entre C et C++
cout << "Fin chaotique 1" << endl; }
catch(Exception2) { cout << "Fin chaotique 2" << endl; }
catch (...) { cout << "Fin très chaotique" << endl; } }
Créer et lancer ses propres exceptions I n t e r c e p t e r les erreurs des a u t r e s , c'est b i e n , m a i s p r é v o i r les s i e n n e s , c'est m i e u x . N o u s a v o n s v u q u ' u n e e x c e p t i o n p o u v a i t être de n ' i m p o r t e q u e l type : u n e v a r i a b l e ou un objet. La p r e m i è r e é t a p e c o n s i s t e d o n c à créer les t y p e s c o r r e s p o n d a n t à v o s e x c e p t i o n s , c'est-à-dire a u x e r r e u r s q u e v o u s p o u r r e z d é c l e n c h e r . E n s u i t e , q u a n d v o u s é c r i v e z u n e fonction, v o u s p o u r r e z « lancer » u n e de v o s e x c e p t i o n s g r â c e au m o t - c l é
throw: throw obj; throw;
Lance l'exception obj. Relance la dernière exception lancée.
throw s a n s p a r a m è t r e p e u t s'utiliser q u a n d v o u s n ' a r r i v e z p a s à r é s o u d r e le p r o b l è m e d a n s un b l o c catch. V o u s relancez donc la dernière exception, en espérant qu'il existe un autre b l o c catch d a n s la ou les fonctions a p p e l a n t e s . Exemple D a n s l ' e x e m p l e qui suit, la classe MaClasse va l a n c e r u n e
e x c e p t i o n de classe MonErreur, dès q u e la fonction FonctionBoguee est a p p e l é e . #include #include <except.h> // définissons une classe exception (en fait une classe // comme une autre) class MonErreur { public : MonErreur() {cout << "Constructeur de MonErreur" << endl;} };
Les exceptions
141
// définissons une classe qui va lancer des exceptions class MaClasse {
public : void FonctionBoguee!); };
* Dans la ligne cicontre, MonErreurO crée un objet temporaire de classe MonErreur. C'est
void MaClasse::FonctionBoguee() { // admettons que cette fonction soit boguée; // elle générera donc une exception : throw MonErreur(); // * voir dans la marge } void main() { try
donc bel et bien un
{
MaClasse obj;
objet qui est spécifié après t h r o w .
obj.FonctionBoguee(); }
catch (MonErreur) { cout << "Panique" << endl; } }
// affichage : // Constructeur de MonErreur // Panique
C e t exemple nous montre que l'exception générée dans F o n c t i o n B o g u e e , de la classe M a C l a s s e , est interceptée dans le m a i n par l'instruction c a t c h ( M o n E r r e u r ) . N o u s avons donc mis en évidence le fait que c'est le concepteur de la classe M a C l a s s e qui détecte les erreurs et lance une exception ; alors que c'est l'utilisateur de cette classe, ici la fonction main, qui envisage les solutions selon l'exception rencontrée. L'intérêt par rapport au C
Le C ne p r o p o s e a u c u n s y s t è m e de g e s t i o n des erreurs. La p r a t i q u e la p l u s c o u r a n t e c o n s i s t e à r e n v o y e r u n e v a l e u r signifiant q u ' u n e erreur est i n t e r v e n u e . C e l a oblige à tester cette valeur à c h a q u e appel, ce qui alourdit et ralentit c o n s i d é r a b l e m e n t l e p r o g r a m m e . Par ailleurs, q u e faire q u a n d u n e telle erreur est détectée ? G r â c e a u x e x c e p t i o n s , les erreurs sont d e s objets à p a r t entière, d é c l e n c h a b l e s puis récupérables en c a s c a d e , du b l o c le
142
Pont entre C et C++
p l u s p r o c h e — en fait le p l u s c o m p é t e n t p o u r traiter l'erreur — a u x b l o c s les p l u s g é n é r a u x . M a i s p l u s e n c o r e , c'est u n e tentative de formaliser les t r a i t e m e n t s d'erreur, un c a d r e général qui p e r m e t a u x p r o g r a m m e u r s d e m i e u x s'y retrouver.
Résumé
L e C + + p r o p o s e d e gérer les erreurs p a r l e m é c a n i s m e d e s e x c e p t i o n s . U n e e x c e p t i o n n ' e s t rien d'autre q u ' u n e v a r i a b l e qui r e p r é s e n t e u n e erreur. T r o i s m o t s - c l é s p e r m e t t e n t d e les mettre en œuvre : t r y , c a t c h et t h r o w . Q u a n d une exception est l a n c é e p a r t h r o w q u e l q u e part d a n s u n b l o c t r y , l ' e x é c u t i o n de ce bloc est s t o p p é e et d é r o u t é e v e r s le b l o c c a t c h correspondant. S i aucun bloc c a t c h n'intercepte l'exception, l a fonction t e r m i n a t e est a p p e l é e (voir c o m p l é m e n t s ) . P a r défaut, elle m e t s o b r e m e n t fin a u p r o g r a m m e par un appel à a b o r t ( ).
Exemple complet
N o u s allons m a i n t e n a n t détailler u n c a s c o n c r e t d'utilisation d e s e x c e p t i o n s : u n e classe tableau qui n ' a c c e p t e p a s q u ' o n é c r i v e en d e h o r s de ses b o r n e s . #include #include <except.h> class ErreurBorne { protected: int borne_fautive; public: ErreurBorne(int b) { borne_fautive = b; } int get_borne_fautive() { return borne_fautive; } } ;
class Tableau { protected: enum { MAX = 10 }; // constante int tab[MAX]; public : int get_MAX() { return MAX; } int &operator[](int i ) ; } ;
Les exceptions
pour que l'utilisateur
int&Tableau: :operator[] (int i) { if (i<0 || i>=MAX) throw ErreurBorne(i); else return tab[i];
puisse consulter mais
}
* L'opérateur [ ] retourne ici une référence (un synonyme) du tableau
aussi modifier sa valeur dans une expression du style t[n] = i. V o i r le
143
// * voir dans la marge
void main() { Tableau t; int i;
chapitre sur les
try {
références, page I I 3.
t[9] = 1; t[99] = 2;
// instruction fautive
i = t[9]; // ligne jamais exécutée } catch (ErreurBorne err) { cout << "Erreur de borne : " << err.get_borne_fautive() << endl; } }
// affichage : // Erreur de borne : 99
Compléments Spécifier des exceptions
V o u s p o u v e z i n d i q u e r a u c o m p i l a t e u r e t a u x futurs utilisateurs d e v o s classes quelles e x c e p t i o n s v o s fonctions s o n t susceptibles de lancer. P o u r ce faire, il faut n o m m e r c e s e x c e p t i o n s d a n s un throw, a p r è s l'entête n o r m a l . E x e m p l e :
Attention : il faut que le throw soit spécifié dans l'entête de déclaration et dans
void int void void
fl(int a) throw (MonErreur); f 2 ( ) throw (MonErreur, MaCrainte); f3(char *s) throw(); // aucune exception f4(float f ) ; // toutes les exceptions
l'entête de définition de la fonction.
V o i l à la signification de ces trois lignes : f 1 ne p e u t lancer q u e d e s e x c e p t i o n s de classe MonErreur ou de c l a s s e s dériv é e s . f2 p e u t lancer des e x c e p t i o n s MonErreur ou Ma-
144
Pont entre C et C++
C r a i n t e ou de classes dérivées, f 3 ne p e u t l a n c e r a u c u n e exception, f 4 p e u t se p e r m e t t r e de lancer toutes les e x c e p tions, c a r a u c u n e limitation n ' e s t e x p r i m é e . Fonction unexpected S i , m a l g r é tout, u n e fonction lance u n e e x c e p t i o n qui ne fig u r e p a s d a n s l e t h r o w d e son entête, l a fonction u n e x p e c t e d (inattendue) est appelée. Par défaut, cette fonction fait appel à la fonction t e r m i n â t e qui fait e l l e - m ê m e appel à a b o r t . V o u s p o u v e z , s i v o u s l e désirez, r e m p l a c e r l a fonction u n e x p e c t e d p a r défaut p a r u n e fonction d e v o t r e cru. I l suffit d'utiliser s e t _ u n e x p e c t e d avec c o m m e seul param è t r e un n o m de fonction respectant l'entête s u i v a n t : PFV VotreNomDeFonction(void);
où P F V est un type défini p a r : typedef void(*PFV) () ;
Ce qui c o r r e s p o n d à un p o i n t e u r sur u n e fonction qui ne p r e n d a u c u n p a r a m è t r e e t qui r e t o u r n e v o i d . Exemple complet : #include #include <except.h> // si PFV est inconnu, définissons-le #ifndef PFV typedef void( *PFV) (); #endif class MonErreur {}; class MonDilemne {}; class MaClasse { public : void FonctionBoguee)) throw(MonDilemne); };
void MaClasse::FonctionBoguee() throw(MonDilemne) { throw MonErreur(); //n'est pas autorisé par l'entête }
PFV {
Monlnconnue()
Les exceptions
145
/ / n e doit pas retourner à son appelant // doit sortir du programme cout << "Je suis dans l'inconnue..." << endl; exit(1); }
void main() { try { MaClasse ob j ; set_unexpected((PFV)Monlnconnue); obj.FonctionBoguee(); }
catch (MonErreur) { cout << "Panique" << endl; } }
// affichage : / / J e suis dans l'inconnue
Exception non interceptée
Il p e u t arriver q u ' u n e e x c e p t i o n l a n c é e ne c o r r e s p o n d e à auc u n bloc c a t c h qui suit l e bloc t r y . D a n s c e c a s , l a fonction t e r m i n a t e est a p p e l é e . Par défaut, elle m e t fin a u p r o g r a m m e p a r u n appel à a b o r t ( ) . V o u s p o u v e z c e p e n d a n t définir v o t r e p r o p r e fonction terminate, à l'aide de s e t _ t e r m i n a t e . V o t r e fonction doit c o r r e s p o n d r e à l'entête suivant (où P F V est défini c o m m e i n d i q u é ci-dessus) : PFV VotreNomDeFonction(void);
V o i c i u n e x e m p l e d e fonction t e r m i n a t e p e r s o n n e l l e : #include #include <except.h> // si PFV est inconnu, définissons-le #ifndef PFV typedef void( *PFV ) (); #endif class MonErreur {}; class MonDilemne {}; class MaClasse { public : void FonctionBoguee() throw(MonErreur);
146
Pont entre C et C++
) ;
void MaClasse::FonctionBoguee() { throw MonErreur();
throw(MonErreur)
}
PFV
MonTerminator()
{
// ne doit pas lancer d'exception // ne doit pas retourner à son appelant cout << "On dirait qu'il y a un problème" << endl; exit(1); }
void main() { try { MaClasse obj; set„terminate((terminate_function)MonTerminator) ; obj.FonctionBoguee(); }
catch (MonDilemne.) { cout << "Panique" << endl; }
}
// affichage : // On dirait qu'il y a un problème
Exceptions en cascade
Q u e s e passe-t-il q u a n d u n e e x c e p t i o n est l a n c é e d a n s u n b l o c catch ? R é p o n s e en d e u x t e m p s : Le b l o c A c o n t e n a n t le b l o c catch est s t o p p é . Si ce bloc A était l u i - m ê m e d a n s un bloc try, l ' e x c e p t i o n serait traitée p a r un d e s blocs catch c o r r e s p o n d a n t au bloc try qui contient A. L a b r u m e d e ces explications sera peut-être d i s s i p é e p a r l ' e x e m p l e s u i v a n t : u n e e x c e p t i o n de classe MonDilemne est l a n c é e d a n s un b l o c catch. #include #include <except.h> class MonErreur { ) ; class MonDilemne {}; class MaClasse {
Les exceptions
public : void FonctionBoguee() throw(MonErreur); } ;
void MaClasse::FonctionBoguee() throw(MonErreur) { throw MonErreur(); // autorisé par l'entête } void fonction!) { try { MaClasse obj ; obj.FonctionBoguee(); } catch (MonErreur) { cout << "Erreur" << endl; throw MonDilemne(); } catch (MonDilemne) { // ce catch n'est pas appelé cout << "Dilemne dans fonction" << endl; }
} void { try {
main()
fonction!);
// déclenchera un MonDilemne
}
catch (MonDilemne) {
// ce catch est appelé cout << "Dilemne dans le main" << endl; } }
// affichage : // Erreur // Dilemne dans le main
147
La compilation séparée
Bien que la compilation séparée ne constitue pas une nouveauté du C++, un chapitre lui est consacré car beaucoup de programmeurs C en ignorent les principes. Par ailleurs, le C++ diffère légèrement du C dans ce domaine.
Notions de base L'idée
La c o m p i l a t i o n séparée p e r m e t de répartir le c o d e - s o u r c e d ' u n e application d a n s plusieurs fichiers. C e t t e t e c h n i q u e p r o c u r e u n e meilleure clarté d a n s l'organisation du projet. V o u s g a g n e z aussi du t e m p s : seuls les fichiers m o d i f i é s d e puis la dernière c o m p i l a t i o n sont r e c o m p i l é s . A v a n t d ' a b o r d e r la m i s e en œ u v r e de la c o m p i l a t i o n s é p a r é e , rappelons brièvement le fonctionnement d'un compilateur. P r i n c i p e d'un c o m p i l a t e u r D e u x g r a n d e s étapes sont n é c e s s a i r e s p o u r transformer votre c o d e - s o u r c e en e x é c u t a b l e : la c o m p i l a t i o n et l'édition de liens (parfois appelé « l i n k a g e » p a r certains fous qui refusent d ' e m p l o y e r les t e r m e s français officiels — mea culpa...). La compilation d ' u n fichier C ou C + + d o n n e un fichier objet,
250
Pont entre C et C++
U n e « librairie » est en quelque sorte un ensemble de fichiers .o que vous incluez à l'édition des liens.
en général suffixe p a r . o. C e s fichiers . o sont e n s u i t e « liés » entre e u x , é v e n t u e l l e m e n t avec d e s librairies*, p o u r former l ' e x é c u t a b l e p r o p r e m e n t dit. D a n s l ' e x e m p l e ci-dessous, d e u x fichiers s o u r c e s suffixes p a r . c p p s o n t c o m p i l é s s é p a r é m e n t puis « linkés » a v e c u n e librairie i n d é p e n d a n t e p o u r d o n n e r l ' e x é c u t a b l e m a i n . e x e . S i , après u n e p r e m i è r e compilation, v o u s n e modifiez q u e l e fichier o u t i l s . c p p , v o u s n ' a u r e z b e s o i n d e r e c o m p i l e r q u e o u t i l s . c p p ; v o u s p o u r r e z réutiliser l'ancien m a i n . o p o u r l ' é d i t i o n des liens.
Mise en œuvre
P o u r réaliser effectivement u n e c o m p i l a t i o n s é p a r é e , il faut savoir d e u x c h o s e s : quoi m e t t r e , et d a n s quels fichiers c o m m e n t lancer la c o m p i l a t i o n
Quoi mettre, et dans quels fichiers ?
U n projet C + + s e d é c o m p o s e g é n é r a l e m e n t e n b e a u c o u p d ' é l é m e n t s de natures différentes : des c l a s s e s , d e s fonctions globales (hors-classes), des c o n s t a n t e s , d e s structures, etc. C o m m e n t savoir où placer c h a q u e é l é m e n t ?
La compilation séparée
On parlera ici de fichiers .cpp pour désigner les fichiers contenant du codesource C + + . Votre compilateur utilise peut-être une autre extension (.cxx ou .C
151
Principe général D a n s la majorité d e s c a s , les fichiers fonctionnent p a r c o u ple : un fichier s u f f i x e p a r . h p o u r déclarer u n e classe ou d e s entêtes de fonctions, et un fichier s u f f i x e p a r . cpp p o u r la définition d e ces fonctions ( m e m b r e s o u n o n ) . A d m e t t o n s q u e v o u s écriviez d a n s un fichier A q u e l c o n q u e . Si v o u s a v e z b e s o i n d'utiliser u n e classe ou u n e fonction définie ailleurs, il faut inclure au d é b u t de A le fichier . h qui déclare ce d o n t v o u s a v e z b e s o i n . P o u r utiliser u n e autre i m a g e , le . h est l ' a m b a s s a d e u r de v o tre travail, il p r é s e n t e c o m m e n t utiliser ce q u e v o u s a v e z fait, alors q u e le . cpp décrit c o m m e n t c'est implanté.
par exemple).
Conseils La déclaration d ' u n e classe se fait g é n é r a l e m e n t d a n s un fichier . h qui p o r t e s o n n o m . La définition d e s f o n c t i o n s - m e m b r e s d ' u n e c l a s s e , ainsi q u e la définition des c o n s t a n t e s et variables de classe (static) se fait d a n s un fichier . cpp p o r t a n t le n o m de la classe. L e s objets, variables ou fonctions globales (c'est-à-dire accessibles d e p u i s n ' i m p o r t e quelle fonction d u p r o g r a m m e ) sont définis d a n s un fichier d o n t le n o m r e s s e m b l e à glo-
bal . cpp La déclaration de ces objets g l o b a u x se fait d a n s un fichier d o n t le n o m r e s s e m b l e à extern.h, où c h a c u n de ces objets est p r é c é d é du m o t - c l é extern. L e s fonctions hors-classe, C standard, figurent d a n s d e s fichiers . c La déclaration de ces fonctions se fait d a n s des fichiers . h Si v o u s utilisez de n o m b r e u s e s classes, v o u s g a g n e r e z à reg r o u p e r plusieurs classes d a n s un seul fichier . h. Il est cep e n d a n t conseillé de g a r d e r un seul fichier . cpp p a r classe — les corps des fonctions p r e n n e n t en effet plus de p l a c e q u e les déclarations de classes.
V o u s trouverez ci-dessous u n s c h é m a qui r é s u m e ces c o n seils à travers un e x e m p l e . L e s listings c o m p l e t s sont situés u n p e u plus loin d a n s c e chapitre.
252
Pont entre C et C++
Compilation
séparée
-
exemple
Remarques A v e c l a c o m p i l a t i o n séparée, l e p r o g r a m m e u r est contraint de respecter certaines règles : C h a q u e fichier . c p p utilisant d e s é l é m e n t s d ' u n autre fichier doit inclure le fichier . h déclarant les é l é m e n t s en question. Ici, m a i n . c p p inclut les fichiers e x t e r n . h , f o n c _ c . h e t C l a s s i . h , car i l v a utiliser d e s variables e t c o n s t a n t e s
La compilation séparée
153
globales déclarées d a n s e x t e r n . h , des fonctions C déclarées d a n s f o n c _ c . h , e t des classes d é c l a r é e s d a n s
Classl . h . Q u a n d v o u s modifiez l'entête d ' u n e fonction d a n s u n fichier . cpp, il faut veiller à mettre à j o u r le fichier . h correspondant. L ' o r d r e d'inclusion des fichiers . h est i m p o r t a n t . S i , p a r e x e m p l e , v o u s utilisez un objet global de classe Classl déclaré d a n s e x t e r n , h, il faut q u e p a r t o u t Classl. h soit inclus a v a n t e x t e r n . h (sinon l e t y p e Classl sera inconnu dans e x t e r n . h). Listing Voici le listing qui détaille l'architecture p r é s e n t é e d a n s le s c h é m a de la p a g e p r é c é d e n t e : // fichier main.cpp #include "extern.h" extern "C" { // voir compléments #include "fonc_c.h" }
#include "Classl.h" void main() { Classl objetl; _statut = 0; }
// fichier global.cpp // constantes globales const float PI = 3.1415; // variables globales int _statut;
Cette manière d'opérer (avec #ifndef) est expliquée dans les compléments, page I 57.
// fichier extern.h tifndef _EXTERN_H #define _EXTERN_H // constantes globales extern const float // variables globales extern int _statut;
PI;
754
Pont entre C et C++
#endif
// fichier Classl.h #ifndef _CLASS1_H #define _CLASS1_H class Classl { protected: // ... public : Classl ( ) ; void FoncMemb ( ) ; } ;
#endif
// fichier Classl.cpp #include "Classl.h" // constructeurs Classl::Classl() { }
// autres fonctions-membre de Classl void Classl::FoncMemb() { }
// fichier fonc_c.h #ifndef _FONC_C_H #define _FONC_C_H #define CONST_C void
100
fonction_c(int a ) ;
#endif
// fichier fonc c.c #include "fonc_c.h" int fonction_c(int a) { return a; }
La compilation séparée
Comment lancer la compilation ?
155
L e s m é t h o d e s d e c o m p i l a t i o n varient b e a u c o u p selon les s y s t è m e s . C e r t a i n s c o m p i l a t e u r s s ' o c c u p e n t de p r e s q u e tout : il v o u s suffit d'indiquer quels fichiers font partie de v o t r e p r o jet. D ' a u t r e s en r e v a n c h e , r e p o s e n t sur l'utilitaire m a k e . Make C e t utilitaire v o u s p e r m e t lancer u n e c o m p i l a t i o n s é p a r é e d ' u n e seule instruction. P o u r cela, il est n é c e s s a i r e d'écrire un script de compilation, a p p e l é makefile. Ce script idendifie les fichiers à c o m p i l e r , ainsi q u e les d é p e n d e n c e s entre ces fichiers. Si v o t r e makefile est écrit c o r r e c t e m e n t , seuls les fichiers n é c e s s a i r e s seront r e c o m p i l é s . Le format d ' u n e paire de lignes d ' u n fichier makefile est le suivant (ici, les crochets i n d i q u e n t des é l é m e n t s o p t i o n n e l s ) : fichier_cible : fichier_l [fichier_2 ...] instruction à exécuter
Si une ligne de votre Makefile est t r o p longue, vous pouvez la couper en deux à condition que la première ligne se termine par un backslash (\).
C e qui signifie : p o u r obtenir f i c h i e r _ c i b l e , j ' a i b e s o i n d e f i c h i e r _ l , f i c h i e r _ 2 , etc. e t d ' e x é c u t e r l'instruction de la d e u x i è m e ligne. M a k e en déduit q u e si l'un d e s fichiers f i c h i e r _ l , f i c h i e r _ 2 , etc. est plus récent (au n i v e a u d e s dates d e dernière modification) q u e f i c h i e r _ c i b l e , i l faut e x é c u t e r l'instruction de la d e u x i è m e ligne. Ce format sert p r i n c i p a l e m e n t à d e u x g e n r e s d'instructions (on s u p p o s e r a q u e v o t r e c o m p i l a t e u r C + + s'appelle CC) : •
c o m p i l e r un . c p o u r obtenir un . o fichier.o : fichier.c fichier.h autre_fichier.h CC -c fichier.c
C e s lignes signifient q u e s i f i c h i e r . c , f i c h i e r . h o u a u t r e _ f i c h i e r . h ont été modifiés à u n e date p o s t é r i e u r e d e celle d e f i c h i e r . o , i l faut r e c o m p i l e r f i c h i e r . c p o u r o b tenir un n o u v e a u f i c h i e r , o. L ' o r d r e de c o m p i l a t i o n dép e n d b i e n e n t e n d u d e votre c o m p i l a t e u r . En fait, on i n d i q u e après les : le n o m du fichier s o u r c e c o n cerné, ainsi q u e la liste d e s fichiers . h qu'il utilise.
156
Pont entre C et C++
• lier les . o et les librairies p o u r former l ' e x é c u t a b l e exécutable : fichier.o autre_fichier.o lib.a CC -o exécutable fichier.o autre_fichier.o -llib.a
C e qui v e u t d i r e : s i f i c h i e r , o , a u t r e _ f i c h i e r . o o u l i b . a s o n t p l u s r é c e n t s q u e e x é c u t a b l e , i l faut refaire u n e édition d e s liens. N o u s utilisons ici u n e librairie à titre d ' e x e m p l e ; il est tout à fait p o s s i b l e q u e v o u s n ' e n n ' a y e z p a s b e s o i n . Il faudrait d a n s ce cas écrire : exécutable : fichier.o autre_fichier.o CC -o exécutable fichier.o autre_fichier.o
Exemple Voici l'allure du fichier makefile l ' e x e m p l e p r é s e n t é p a g e 152.
permettant
de
compiler
# fichier makefile appli : main.o global.o classl.o fonc_c.o CC -o appli main.o global.o classl.o fonc_c.o main.o : main.cpp extern.h fonc_c.h classl.h CC -c main.cpp global.o : global.cpp CC -c global.cpp classl.o : classl.cpp classl.h CC -c global.cpp fonc_c.o : fonc_c.c fonc_c.h CC -c fonc_c.c
L a n c e r la c o m p i l a t i o n (enfin!) A s s u r e z - v o u s q u e le fichier makefile est d a n s le r é p e r t o i r e où sont situés les fichiers-sources de v o t r e projet, et t a p e z make -f nom_fichier_makefile
Si v o u s n o m m e z le fichier makefile M a k e f i l e (avec un M m a j u s c u l e ) , il v o u s suffira de taper m a k e p o u r lancer la c o m p i l a t i o n s é p a r é e de votre projet.
La compilation séparée
157
N o t e z q u e certains s y s t è m e s a c c e p t e n t i n d i f f é r e m m e n t m a k e f i l e o u M a k e f i l e (avec o u s a n s m a j u s c u l e ) , d o n n a n t l a préférence au fichier qui c o m m e n c e p a r u n e m a j u s c u l e si les d e u x sont p r é s e n t s d a n s le répertoire courant.
Compléments Comment éviter les redéclarations ?
L e c o m p i l a t e u r n ' a i m e p a s les déclarations m u l t i p l e s d ' u n m ê m e é l é m e n t (objet, variable, c o n s t a n t e , fonction, etc.) Il faut d o n c veiller à ne p a s inclure d e u x fois le m ê m e fichier . h d a n s le m ê m e fichier. C e l a arrive facilement d a n s un c a s de t y p e « p o u p é e s r u s s e s » :
E n traitant a p p l i . c p p , l e p r é - p r o c e s s e u r r e m p l a c e l a ligne #include " e x t e r n . h " p a r l e c o n t e n u d u fichier e x tern.h. On obtient d o n c deux lignes #include " toto . h", qui inclueront d e u x fois le fichier toto . h, ce qui va g é n é r e r des erreurs de redéclaration. Il existe un m o y e n s i m p l e d'éviter ces r e d é c l a r a t i o n s , tout en p e r m e t t a n t l'inclusion m u l t i p l e du m ê m e . h : utiliser les directives d e p r é c o m p i l a t i o n # i f n d e f , # d e f i n e e t # e n d i f . Le truc consiste à ne p r e n d r e en c o m p t e le c o n t e n u du . h q u e la p r e m i è r e fois q u e le p r é c o m p i l a t e u r le r e n c o n t r e . Voici un fichier . h qui utilise cette t e c h n i q u e : // début du fichier extern.h #ifndef _EXTERN_H #define EXTERN_H // corps complet du fichier #endif // fin du fichier extern.h
158
Pont entre C et C++
En fait, on associe à c h a q u e fichier . h un s y m b o l e p o r t a n t s o n n o m (ici _EXTERN_H). A U début, c e s y m b o l e n ' e x i s t e pas. La p r e m i è r e fois q u e le p r é c o m p i l a t e u r traite le fichier, il r e n c o n t r e la ligne #ifndef _EXTERN_H. C o m m e le s y m b o l e _EXTERN_H n ' e x i s t e p a s , le p r é c o m p i l a t e u r traite le fichier j u s q u ' à rencontrer u n # e n d i f (ou u n #elif d'ailleurs). Le p r é - p r o c e s s e u r traite d o n c tout le c o r p s du fichier j u s q u ' à n o t r e #endif final. D a n s cette partie d e c o d e prise e n c o m p t e , l a p r e m i è r e ligne est # de fi ne _EXTERN_H, qui définit le symbole _EXTERN_H, ce qui servira au p r o c h a i n p a s s a g e à r e n d r e la c o n d i t i o n # i f n d e f _EXTERN_H fausse, et à éviter d'inclure u n e n o u v e l l e fois l e fichier e x t e r n . h .
Static, inline et portée
* C'est-à-dire en dehors de t o u t e fonction
Le g a i n de t e m p s et de clarté n ' e s t p a s le seul intérêt de la c o m p i l a t i o n s é p a r é e . L a d é c o m p o s i t i o n e n p l u s i e u r s fichiers a aussi d e s c o n s é q u e n c e s sur la p o r t é e de certaines v a r i a b l e s et fonctions : Static global T o u t e fonction déclarée s t a t i c ainsi q u e toute v a r i a b l e déclarée s t a t i c e n global*, n ' e s t accessible q u e d a n s l e fichier où elle est définie. E x e m p l e : PORTÉE DE VAR ET FONCI
On ne p e u t a c c é d e r ni à var, ni à foncl d e p u i s f i c h 2 . cpp, car ils ont été déclarés static d a n s f i c h l . c p p . Ils ne sont a c c e s s i b l e s q u ' à l'intérieur de f ichl. cpp.
La compilation séparée
159
Attention ! Ici, s t a t i c n ' a p a s l e m ê m e sens q u e lorsqu'il qualifie les v a r i a b l e s - m e m b r e s ou f o n c t i o n s - m e m b r e s ! R a p p e l o n s q u e d a n s ces derniers c a s , s t a t i c signifie qu'il s'agit de variables ou fonctions de classe, c o m m u n e s à tous les o b jets issus de la classe. F o n c t i o n s inlines S i v o u s déclarez u n e fonction i n l i n e e n d e h o r s d ' u n e classe, la p o r t é e de celle-ci est a u t o m a t i q u e m e n t r é d u i t e au fichier d a n s lequel elle est déclarée. Constantes globales U n e c o n s t a n t e déclarée en global n ' e s t visible q u e d a n s le fichier où elle est définie, à m o i n s de spécifier e x p l i c i t e m e n t c o n s t d a n s l a décla'ration externe. E x e m p l e 1, où PI est déclaré l o c a l e m e n t à f i c h l . c p p :
E x e m p l e 2, où PI est visible à la fois d a n s f i c h l . c p p et fich2.cpp :
7 60
Pont entre C et C++
Travailler avec d'autres langages
L e C + + p e r m e t l ' u s a g e d e fonctions p r o v e n a n t d ' a u t r e s lang a g e s : le C b i e n e n t e n d u , m a i s aussi le P a s c a l , le Fortran, etc. C o m m e c h a q u e l a n g a g e a d o p t e s a p r o p r e l o g i q u e d'édition d e s liens ( n o t a m m e n t e n c e qui c o n c e r n e l a m a n i è r e d e stocker les p a r a m è t r e s de fonctions sur la pile d ' e x é c u t i o n ) , il faut l'indiquer d ' u n e m a n i è r e ou d ' u n e autre d a n s le c o d e C + + . V o u s p o u v e z l e faire grâce à l a c l a u s e e x t e r n " n o m " , où nom représente un type d'édition de liens ( c o n s u l t e z la d o c u m e n t a t i o n d e votre c o m p i l a t e u r ) . Il existe trois m a n i è r e s d'utiliser e x t e r n "nom" : extern
"nom" élément;
déclare que l'édition des liens de élément (un entête de fonction ou une variable) se fait selon la convention nom.
extern
"nom"{
bloc de déclarations;
Idem que ci-dessus, mais tous les éléments déclarés dans bloc sont concernés.
} extern "nom" { #include "fichier.h"
tions faites dans fichier.h sont concernées.
Idem que ci-dessus, mais toutes les déclara-
) Prenons l'exemple d'un programme C + + ayant besoin de fonctions C. On utilise e x t e r n " C " : // fichier sériai.h (C standard) int int
serial_output(int, unsigned char); serial_input(int, unsigned char*);
// fichier main.cpp (C++) possibilité 1 extern "C" int serial_output(int, unsigned char); extern "C" int serial_input(int, unsigned char*); void main { /*...*/ }
Utilisons la d e u x i è m e possibilité p o u r déclarer les d e u x fonctions sans répéter e x t e r n "C" : // fichier main.cpp (C++) possibilité 2 extern "C" { int int }
serial_output(int, unsigned char); serial_input(int, unsigned char*);
La compilation séparée
161
void main { /*...*/ }
Enfin, a d o p t o n s la dernière solution, la p l u s é l é g a n t e , la p l u s é c o n o m i q u e , bref, la m e i l l e u r e d a n s n o t r e e x e m p l e : // fichier main.cpp (C++) possibilité 3 extern "C" { #include "sériai.h" } void main { /*...*/ }
U t i l i s e r du c o d e C + + à partir du C P o u r utiliser d u c o d e C + + à partir d ' u n c o d e C , s u i v e z l'exemple suivant : // fichier C extern "C" double racine(double d) { // code C++ Math monObjetMath; return monObjetMath.racine(d); } void main() { double res; res = racine(22.); }
Guide de survie Les concepts présentés dans les deux premières parties vous laissent peut-être perplexe : comment utiliser un tel arsenal pour venir à bout d'un problème réel ? Cette partie expose quelques conseils de base pour concevoir une application en C++, ainsi qu'une série de questions/réponses sur le langage.
Conseils
Ce chapitre aborde quelques conseils de programmation orientéeobjets en C++. Il ne s'agit pas d'énoncer des règles d'or, mais plutôt de donner quelques pistes pour débuter. Par la suite, l'expérience aidant, vous pourrez concevoir et coder dans un style qui ne vous sera dicté par personne...
Les étapes d'un projet U n projet i n f o r m a t i q u e p e u t s e d é c o m p o s e r e n q u a t r e p h a s e s grossières : 1. 2. 3. 4.
D é t e r m i n a t i o n du cahier des c h a r g e s fonctionnel Conception Codage Maintenance
M a l g r é tout le soin p o u r les séparer, ces p h a s e s ont t e n d a n c e à se m é l a n g e r de m a n i è r e subtile. C ' e s t p o u r q u o i les tentativ e s d e formalisation d e l a p r o g r a m m a t i o n ont s o u v e n t é c h o u é , car il s'agit là d ' u n e activité p r o f o n d é m e n t h u m a i n e , v o i r e artistique.
166
Pont entre C et C++
L a p r e m i è r e p h a s e consiste à d é t e r m i n e r c e q u e v o u s (ou v o s clients) a t t e n d e n t de l'application à d é v e l o p p e r . Il est en effet capital d e n e j a m a i s p e r d r e d e v u e les b e s o i n s a u x q u e l s l e p r o g r a m m e doit r é p o n d r e . M a l h e u r e u s e m e n t (ou h e u r e u s e m e n t ) , c e s b e s o i n s s o n t susceptibles d ' é v o l u e r tout a u l o n g d u projet, m e t t a n t e n c a u s e c e qui était c o n s i d é r é c o m m e acquis d è s l e début. N o u s n e n o u s p e n c h e r o n s p a s p l u s sur cette p h a s e qui n e d é p e n d p a s d u C + + . A t t a r d o n s - n o u s m a i n t e n a n t sur l a c o n c e p t i o n p r o p r e m e n t dite.
Conception La c o n c e p t i o n orientée-objets consiste e s s e n t i e l l e m e n t à trouver les classes qui m o d é l i s e n t v o t r e p r o b l è m e et les relations qui se tissent entre elles.
Trouver les classes
C ' e s t u n e p h a s e essentielle. Pourtant, il est difficile de d o n n e r d e s c o n s e i l s g é n é r a u x tant les solutions d é p e n d e n t d u p r o b l è m e . O n isole p l u s facilement les classes q u a n d o n p r e s s e n t (du v e r b e pressentir) les relations qui les u n i r o n t ; aussi la lecture de la section suivante pourra-t-elle v o u s aider. Q u e l q u e s r e m a r q u e s : u n e classe p e u t être v u e c o m m e u n e entité, u n e idée a u n i v e a u d e votre projet. R a p p e l o n s q u ' u n e classe est un tout, formé de d o n n é e s et de fonctions qui traitent ces d o n n é e s . P o u r créer u n e c l a s s e , il faut d o n c q u e d e s liens étroits existent entre ses c o m p o s a n t s . Si cela n ' e s t p a s le cas, il se p e u t q u ' e l l e se d é c o m p o s e en s o u s - c l a s s e s . Un autre m o y e n consiste à formuler le p r o b l è m e en français. V o u s a u r e z alors d e b o n n e s c h a n c e s p o u r q u e les n o m s c o m m u n s r e p r é s e n t e n t des objets, e t q u e les v e r b e s s y m b o l i sent d e s f o n c t i o n s - m e m b r e s (c'est l a m é t h o d e d e B o o c h ) . À ce n i v e a u de c o n c e p t i o n , ne p r e n e z p a s en c o m p t e les classes liées à l ' i m p l é m e n t a t i o n , m a i s c o n c e n t r e z - v o u s sur celles qui r é p r é s e n t e n t d i r e c t e m e n t la réalité du p r o b l è m e . Inutile,
Conseils
167
p a r e x e m p l e , de p e n s e r à des listes ou des tableaux à ce niveau.
Architecturer les classes
C ' e s t p r a t i q u e m e n t la p h a s e la p l u s difficile. Il existe princip a l e m e n t quatre sortes de relations entre d e u x c l a s s e s A et B: 1. 2. 3. 4.
A A A A
est u n e sorte de B est c o m p o s é de B utilise B n ' a a u c u n lien avec B (eh oui!)
La différence entre les points d e u x et trois n ' e s t p a s toujours é v i d e n t e , m a i s n o u s allons voir cela d a n s les p a r a g r a p h e s qui suivent. N o u s ne détaillerons p a s le dernier point, d o n t le rôle est s i m p l e m e n t d e v o u s rappeler q u ' i l n e faut p a s a b s o l u m e n t s ' a c h a r n e r à trouver un lien entre d e u x c l a s s e s . A est u n e s o r t e de B Il s'agit d ' u n e relation d'héritage. La f o r m u l a t i o n « est u n e sorte de » fonctionne très b i e n p o u r l'identifier. Si m a l g r é tout cela ne suffit p a s , il faut p e n s e r q u e la classe d é r i v é e (ici A) p e u t être u n e spécialisation de la classe de b a s e . D a n s certains cas, u n e relation n e p e u t p a s s e m o d é l i s e r s o u s forme « est u n e sorte de », b i e n q u ' i n t u i t i v e m e n t v o u s sentiez q u ' i l existe u n e relation d'héritage entre les d e u x . Il est alors p o s s i b l e q u e les d e u x entités soient « s œ u r s », c'est-àdire q u ' e l l e s aient des p o i n t s c o m m u n s et héritent toutes d e u x d ' u n e m ê m e classe d e b a s e . Exemple Q u e l l e s sont les relations entre camion et voiture ? U n e v o i t u r e « est-elle u n e sorte » de c a m i o n ou est-ce l'inverse ? D a n s ce c a s , s e l o n l e p r o b l è m e , o n p e u t p e n c h e r p l u t ô t p o u r l a solution « un c a m i o n est u n e sorte de v o i t u r e », car les v o i t u r e s s o n t p l u s fréquentes, et elles ont été i n v e n t é e s a v a n t les c a m i o n s . L e s c a m i o n s sont d o n c u n e spécialisation d e s v o i t u res. On p e u t aussi créer u n e classe véhicule, ou v é h i c u l e terrestre, qui servira de classe de b a s e à v o i t u r e et c a m i o n .
168
Pont entre C et C++
A est c o m p o s é de un ou p l u s i e u r s B C e t t e relation r e p r é s e n t e u n objet qui p e u t s e d é c o m p o s e r e n d'autres objets, qui p o s s è d e n t c h a c u n leur vie p r o p r e . D e tels objets B sont déclarés c o m m e d o n n é e s - m e m b r e s de l'objet principal A . Exemple U n e v o i t u r e est c o m p o s é e d ' u n m o t e u r e t d e q u a t r e p n e u s . C e m o t e u r e t ces p n e u s sont des objets qui p e u v e n t e x i s t e r indépendamment. C e n ' e s t v i s i b l e m e n t p a s u n e relation d ' h é r i t a g e , car u n p n e u (ou un m o t e u r ) n ' e s t p a s « u n e sorte de » v o i t u r e .
A utilise B Il existe d e u x m a n i è r e s de représenter cette relation en t e r m e de l a n g a g e orienté-objet. Soit l'objet B est défini c o m m e donn é e - m e m b r e de l'objet A, soit A utilise d e s objets B g l o b a u x o u p a s s é s c o m m e p a r a m è t r e s d e fonctions. P a r r a p p o r t à la relation p r é c é d e n t e (A est c o m p o s é de B ) , la n u a n c e se situe au n i v e a u du s e n s : ici, les objets B ne font
Conseils
169
p a s partie d e l'objet A . Ils sont c o m p l è t e m e n t i n d é p e n d a n t s , m a i s s o n t utilisés p a r A p o u r r é s o u d r e un p r o b l è m e . Exemple Dans un environnement graphique, le gestionnaire d ' é v é n e m e n t s utilise les informations en p r o v e n a n c e de la souris. Il n ' e s t ni c o m p o s é d ' u n e souris, p a s p l u s qu'il n ' e s t l u i - m ê m e u n e sorte d e souris.
B i e n e n t e n d u , en t e r m e d ' i m p l é m e n t a t i o n , il se p e u t q u e la classe g e s t i o n n a i r e d ' é v é n e m e n t ait c o m m e d o n n é e - m e m b r e un objet de classe souris. M a i s il se p e u t é g a l e m e n t qu'il c o m m u n i q u e a u t r e m e n t avec l'objet souris, p a r e x e m p l e par l'intermédiaire d ' u n e autre classe qui centralise les entrées.
Détailler les classes
L e s classes sont c o n n u e s et leurs relations identifiées. Il faut m a i n t e n a n t détailler les f o n c t i o n s - m e m b r e s q u ' e l l e s contiennent. Ce qu'il est b o n de g a r d e r à l'esprit : M i n i m i s e r les é c h a n g e s e n t r e les c l a s s e s P l u s v o s classes sont i n d é p e n d a n t e s , m o i n s elles é c h a n g e n t d'informations a v e c les autres, et p l u s facile sera la m a i n t e n a n c e d e v o t r e projet. R é d u i s e z autant q u e p o s s i b l e l e n o m bre de p a r a m è t r e s d a n s les f o n c t i o n s - m e m b r e s . En dévoiler un m i n i m u m D a n s l e m ê m e ordre d'idées, u n e classe doit s'efforcer d e cac h e r un m a x i m u m de ses p r o p r e s d o n n é e s , et surtout la manière d o n t ces d o n n é e s sont s t o c k é e s . I m a g i n e z toujours ce qui se passerait si v o u s c h a n g i e z la m a n i è r e de r e p r é s e n t e r ces d o n n é e s . L'interface avec l'extérieur, c'est-à-dire les fonctions p u b l i c , n e doit p a s c h a n g e r — e n tout c a s aussi p e u que possible.
2 70
Pont entre C et C++
Évaluer l'ensemble
C o n f r o n t e z plusieurs scénarios à votre architecture de class e s , e t vérifiez q u e tout p e u t être m i s e n œ u v r e . V o u s g a g n e rez à p a s s e r en r e v u e les fonctionnalités a t t e n d u e s ( p h a s e 1). Si v o t r e c o n c e p t i o n s'avère trop c o m p l e x e à utiliser, r e m e t tez-là en c a u s e et e s s a y e r d ' e n i m a g i n e r u n e autre. Il faut savoir q u e s i v o u s a v e z des p r o b l è m e s d e c o m p r é h e n s i o n d e l'architecture à ce n i v e a u , ils ne feront q u e s'amplifier à l'étape suivante.
Codage C++ * C'est une façon de parler.
Quels outils ?
V o t r e projet est détaillé sur le papier, il ne reste plus* q u ' à le p r o g r a m m e r . A v e c l e C + + , v o u s d i s p o s e z d e n o m b r e u x outils d o n t l'utilisation s i m u l t a n é e n ' e s t p a s toujours é v i d e n t e . Ce p a r a g r a p h e va tenter de v o u s faciliter la tâche. C e t t e é t a p e consiste à choisir les classes-outils ou les bibliot h è q u e s q u e v o u s utiliserez p o u r m e t t r e e n œ u v r e v o t r e c o n ception. C e n e s o n t p a s d e s classes c o n c e p t u e l l e s , m a i s d e s classes p r o p r e s a u x t e c h n i q u e s d e p r o g r a m m a t i o n . Par e x e m p l e , toutes les classes « c o n t e n e u r » (tableaux, listes, arb r e s , g r a p h e s ) en font partie. Elles servent à s t o k e r d'autres objets. Si v o u s d e v e z c o n c e v o i r d e s classes-outils p o u r votre projet, p e n s e z à la réutilisabilité, et faites en sorte q u ' e l l e s soient aussi universelles q u e possible. V o u s a u r e z peut-être p l u s d e m a l , m a i s v o u s g a g n e r e z d u t e m p s sur v o t r e p r o c h a i n projet. B i e n e n t e n d u , c e g e n r e d e classe-outils n é c e s s i t e u n e d o c u m e n t a t i o n : c o m m e n t e z - l e s ou décrivez leur f o n c t i o n n e m e n t à part, si p o s s i b l e a v e c des e x e m p l e s d'utilisation. Factoriser des classes R e p r e n e z votre g r a p h e d'héritage. V o u s p o u v e z e s s a y e r d e trouver d e s p o i n t s c o m m u n s entre c l a s s e s , s u f f i s a m m e n t n o m b r e u x p o u r former u n e classe d e b a s e . L ' a v a n t a g e d ' u n e
Conseils
171
telle factorisation réside d a n s la m i n i m i s a t i o n d e s r e d o n d a n c e s . D o n c , les modifications éventuelles n ' a u r o n t à se faire q u ' à un endroit si elles c o n c e r n e n t la classe de b a s e . C e t t e é t a p e ne figure p a s d a n s la c o n c e p t i o n , car il s'agit là d ' u n e m é t h o d e d e p r o g r a m m a t i o n qui n ' a p a s f o r c é m e n t d e s e n s conceptuel.
* Rappelons que le C+ + fart la conversion de Dérivée* vers Base*.
Mise en œuvre de la réutilisabilité
Exemple Vous concevez un environnement graphique composé de divers objets : fenêtres, b o u t o n s , etc. Si c h a q u e objet c o m p o r t e u n identifiant, u n e référence vers l'objet p a r e n t , e t j e n e sais q u o i d'autre, il e s t j u d i c i e u x de c r é e r u n e classe ObjetG e n e r i q u e qui r e g r o u p e r a tous ces p o i n t s c o m m u n s . Ici n o u s bénéficions d ' u n autre atout : tous les objets fenêtres, b o u tons et autres p o u r r o n t êtres d é s i g n é s p a r un p o i n t e u r s u r ObjetGenerique*.
L ' u n d e s c h e v a u x d e bataille d e s l a n g a g e s orientés-objets est l a réutilisabilité d u c o d e produit. L e C + + est p a r t i c u l i è r e m e n t b i e n a r m é p o u r m e n e r à b i e n cette croisade. O u t r e l'héritage q u e n o u s a v o n s détaillé d a n s la partie c o n c e p t i o n , p a r l o n s ici du p o l y m o r p h i s m e , de la généricité et des e x c e p t i o n s . L e p o l y m o r p h i s m e e t les f o n c t i o n s v i r t u e l l e s U n e fonction virtuelle doit garder le m ê m e s e n s d a n s toute la b r a n c h e d'héritage. U n e x e m p l e parfait serait u n e fonction d e rotation virtuelle définie p o u r un objet g r a p h i q u e de b a s e . U n b o n m o y e n d ' i m p o s e r u n m o u l e p o u r u n e série d e c l a s s e s consiste à créer u n e classe de b a s e abstraite, où des fonctions virtuelles p u r e s sont déclarées. C e g e n r e d e c l a s s e fixe u n c a dre général d'utilisation : les classes dérivées d o i v e n t définir les fonctions virtuelles p u r e s , en r e s p e c t a n t leur entête. N o t r e e x e m p l e s'appliquerait b i e n à cela : u n e classe abstraite ObjetVirtuel, qui ne créerait d o n c a u c u n objet, m a i s qui définirait les entêtes des fonctions affichage, rotation, etc. On i m a g i n e facilement q u e des classes R e c t a n g l e o u C e r c l e héritent d'ObjetVirtuel (voir s c h é m a p a g e suivante).
172
Pont entre C et C++
La généricité et les templates Les templates permettent de paramétrer des classes par des types de d o n n é e s , ces derniers p o u v a n t être d'autres classes. L e s c o n t e n e u r s (les classes de s t o c k a g e ) sont tout i n d i q u é s p o u r utiliser les templates. U n b o n exercice serait d e c o n c e voir u n e classe Liste template, qui accepterait c o m m e param è t r e le type des objets q u ' e l l e stocke. Les exceptions Q u a n d v o u s c o n c e v e z u n e classe, v o u s n e s a v e z p a s forcém e n t qui v a l'utiliser, n i q u a n d o n v a l'utiliser. E n a d o p t a n t l e m é c a n i s m e d ' e x c e p t i o n , v o u s p o u v e z signaler des erreurs ( t h r o w ) à l'utilisateur de la classe qui agira en c o n s é q u e n c e s ( t r y / c a t c h ) . D o c u m e n t e z e t signalez c l a i r e m e n t les c l a s s e s d ' e x c e p t i o n s q u e v o u s p o u v e z lancer. L ' u n d e s m o y e n s c o n siste à faire suivre l'entête d ' u n e fonction p a r les e x c e p t i o n s q u ' e l l e est susceptible de lancer (voir p a g e 1 4 3 ) .
Implantation des classes
E n c o n c e v a n t e t c o d a n t u n e classe C + + , i l est intéressant d e c o n s i d é r e r q u e l q u e s points : Respectez la cohérence des fonctions d'accès U n e classe c o m p t e très s o u v e n t d e s fonctions d ' a c c è s p o u r consulter ou modifier d e s d o n n é e s - m e m b r e s (à m o i n s q u e
Conseils
173
v o u s n ' a y e z déclaré des d o n n é e s - m e m b r e s public, m a i s v o u s devriez écarter d ' e m b l é e cette possibilité). P e n s e z à garder u n e c o h é r e n c e p o u r distinguer ces fonctions d e s a u tres. Faites p a r e x e m p l e p r é c é d e r les fonctions de c o n s u l t a tion p a r g e t _ e t les fonctions d e modification p a r s e t _ , e n les faisant suivre du libellé exact des d o n n é e s - m e m b r e s . Prat i q u e m e n t tous les e x e m p l e s de ce livre r e s p e c t e n t ce format. R e n d e z « const » les fonctions qui ne m o d i f i e n t rien S i u n e f o n c t i o n - m e m b r e d ' u n e classe n e modifie a u c u n e d o n n é e - m e m b r e , p e n s e z à l a r e n d r e c o n s t . V o u s garantissez ainsi a u x utilisateurs de votre classe q u e l'objet ne sera p a s modifié en faisant appel à cette fonction. Tous les exemples de ce livre ne respectent pas cette convention, par souci d'allégement et de clarté.
class Awesome { protected: int a; public : int get_a() const { return a; }; } ;
Préférez protected à private R a p p e l o n s q u e des d o n n é e s private ne s o n t p a s héritées. L e s d o n n é e s protected, en r e v a n c h e , s o n t accessibles a u x classes dérivées m a i s restent inaccessibles a u x autres c l a s s e s . A u s s i est-il g é n é r a l e m e n t plus intéressant d'utiliser pro-
tected. Respectez la symétrie des constructeurs et destructeurs Si un c o n s t r u c t e u r alloue de la m é m o i r e ou d e s r e s s o u r c e s diverses, faites en sorte q u e le destructeur les libère. Attention : r a p p e l e z - v o u s q u e la s y n t a x e de delete est différente p o u r u n e s i m p l e variable (delete v a r ) q u e p o u r u n ta-
b l e a u (delete [ ] t a b ) . I n i t i a l i s e z t o u t e s les d o n n é e s - m e m b r e s , d a n s c h a q u e c o n s tructeur Il n ' y a rien de p l u s d é s a g r é a b l e q u e de confier la tâche d'initialisation à un c o n s t r u c t e u r irresponsable. Le rôle du
Ï74
Pont entre C et C++
c o n s t r u c t e u r est de p r é p a r e r un n o u v e l objet, et ce rôle c o m p r e n d son initialisation. F a u t - i l r e d é f i n i r le c o n s t r u c t e u r c o p i e ? Il suffit q u ' u n e d o n n é e - m e m b r e soit un p o i n t e u r et v o u s aurez c e r t a i n e m e n t b e s o i n d u c o n s t r u c t e u r c o p i e . C e dernier s e c h a r g e d'initialiser un n o u v e l objet à partir d ' u n objet de m ê m e classe déjà existant. R a p p e l o n s l e format d ' u n c o n s tructeur c o p i e : NomClasse::NomClasse(const NomClass &a_copier) {/*...*/}
L e c o n s t n ' e s t p a s obligatoire, m a i s fortement conseillé : v o u s viendrait-il à l'idée de modifier l'objet à c o p i e r ? Faut-il r e d é f i n i r l ' o p é r a t e u r d ' a f f e c t a t i o n = ? Si l ' u n e d e s d o n n é e s - m e m b r e s est un pointeur, redéfinissez l'opérateur = p o u r v o u s a s s u r e r q u e les é l é m e n t s d é s i g n é s p a r le p o i n t e u r s o n t b i e n r e c o p i é s . A t t e n t i o n : l ' o p é r a t e u r = n ' e s t p a s hérité p a r les sous-classes ! F a u t - i l r e d é f i n i r d e s o p é r a t e u r s de c o m p a r a i s o n ? D a n s d e n o m b r e u x c a s , i l v a u t m i e u x q u e v o u s redéfinissiez l'opérateur de c o m p a r a i s o n = = . Si v o u s êtes a m e n é s à trier d e s objets de votre classe, redéfinissez é g a l e m e n t < ou >, car o n n ' e s t j a m a i s m i e u x servi q u e p a r s o i - m ê m e . V o u s s a u r e z ce s u r quoi la c o m p a r a i s o n s'effectue : a d r e s s e d e s objets, ou b i e n v a l e u r d ' u n identifiant, o u e n c o r e é q u i v a l e n c e l o g i q u e , etc.
Questions-Réponses
Si vous n'avez pas trouvé les réponses à vos questions dans les chapitres précédents ou dans l'index, tentez votre chance ici. Même si vous ne vous posez pas de question, il peut être instructif de lire les réponses... Q u ' e s t - c e q u ' u n c o n s t r u c t e u r par défaut ? C ' e s t u n c o n s t r u c t e u r qui n e p r e n d a u c u n p a r a m è t r e , o u d o n t les p a r a m è t r e s p o s s è d e n t tous u n e v a l e u r p a r défaut. L e C + + g é n è r e u n c o n s t r u c t e u r p a r défaut s i v o u s n ' a v e z défini a u c u n constructeur. Un c o n s t r u c t e u r p a r défaut est appelé « en silence » p o u r les objets qui ne s o n t p a s e x p l i c i t e m e n t initialisés. Il est é g a l e m e n t a p p e l é q u a n d v o u s allouez un n o u v e l objet a v e c new obj ; ou un tableau a v e c new obj [TAILLE] ;. C o m m e n t appeler un constructeur d'une classe de base dans le constructeur d'une classe dérivée Utilisez u n e liste d'initialisation (plus de détails p a g e 4 7 ) . class Dérivée : public Base { public : Dérivée ( ) : B a s e O { /* ... */ } } ;
î 76
Pont entre C et C++
J ' a i r e d é f i n i l ' o p é r a t e u r = m a i s il n ' e s t pas a p p e l é d a n s la d é c l a r a t i o n Classe obj2 = objl; Q u ' a i - j e fait de m a l ? R i e n . D a n s l ' e x e m p l e cité, o b j 2 est initialisé avec un autre objet ( o b j 1 ) . D a n s c e c a s , c e n ' e s t p a s l'opérateur = q u i est a p p e l é , m a i s l e c o n s t r u c t e u r copie. L e rôle d e c e dernier c o n siste p r é c i s é m e n t à initialiser un objet à partir d ' u n autre o b jet existant. Il s'agit ici d ' u n e initialisation de variable, et n o n d ' u n e affectation, qui serait réalisée p a r l'opérateur -. P o u r q u o i l ' o p é r a t e u r « doit-il être d é c l a r é f r i e n d ? Il ne doit p a s forcément être déclaré f r i e n d , m a i s cela facilite les c h o s e s . Q u e l q u e s explications : l'opérateur « s ' a d r e s s e à l'objet c o u t d e l a classe o s t r e a m . C o m m e n o u s n e p o u v o n s p a s ajouter notre o p é r a t e u r « d i r e c t e m e n t d a n s la classe o s t r e a m , il faut surcharger l'opérateur « global. O r , ce dernier n ' a p a s accès a u x d o n n é e s c a c h é e s d e notre classe. D e u x solutions s'offrent à n o u s : déclarer n o t r e o p é r a t e u r « f r i e n d d a n s notre classe, o u n e p a s l e faire [ha h a ] . D a n s c e dernier c a s , l e c o r p s d e l'opérateur < < d e v r a s e c o n t e n t e r d'utiliser les fonctions p u b l i q u e s de notre classe. Voici comment se passer du f r i e n d : #include class NomClasse { private: int n; public : NomClasse(int i) : n(i) {) int get_n() const { return n; } // vous voyez, on se passe du friend : // friend ostream &operator<< // (ostream& out, const NomClasse& obj); } ;
// nous devons utiliser get_n() au lieu de n : ostream &operator<< (ostreamk out, const NomClassek obj) { return out << '[' << obj.get_n() << ' ] ' << endl; } void main() { NomClasse
obj1(1), obj2(2);
Questions-Réponses
177
cout << objl << obj 2; }
// affichage: // [1] // [2]
P o u r q u o i u t i l i s e r cout et c i n au l i e u de p r i n t f et s c a n f ? E n utilisant c o u t , c i n , c e r r e t les autres objets définis d a n s i o s t r e a m . h , v o u s bénéficiez d e plusieurs a v a n t a g e s : 1. V o u s n ' a v e z p a s b e s o i n de préciser le t y p e d e s objets q u e v o u s utilisez (plus de % d o n c m o i n s d'erreurs p o s s i b l e s ) , car c o u t , c i n e t c e r r intègrent les vérifications d e type ! 2. V o u s p o u v e z redéfinir « et » p o u r qu'ils traitent v o s classes. D e cette m a n i è r e , elles s'utiliseront c o m m e les typ e s prédéfinis, ce qui est un g a g e de c o h é r e n c e . 3. printf et s c a n f sont c o n n u s p o u r leur lenteur, d u e à l ' a n a l y s e s y n t a x i q u e qu'ils sont obligés d ' o p é r e r sur la c h a î n e d e format. R a s s u r e z - v o u s , c o u t e t ses frères s o n t plus rapides. 4 . Utiliser c o u t a u lieu d e p r i n t f est plus chic. Q u e l est l'intérêt d e d é c l a r e r d e s o b j e t s a u b e a u m i l i e u d'une fonction plutôt qu'au début ? P e r s o n n e l l e m e n t , je préfère déclarer tous les objets au d é b u t d ' u n e fonction. C e l a évite de parcourir tout le c o d e p o u r trouver un objet précis. M a i s les déclarations tardives ont leur c h a r m e : elles p e u v e n t accélérer le p r o g r a m m e . En effet, la création d ' u n objet est c o û t e u s e p u i s q u ' i l faut allouer la m é m o i r e e t appeler l e constructeur. I m a g i n e z u n e fonction c o m p o r t a n t un bloc A e x é c u t é u n e fois sur cent. Si A utilise dix objets, v o u s a u r e z intérêt à les déclarer d a n s A plutôt q u ' a u d é b u t de la fonction. C e l a dit, c'est u n e q u e s t i o n de g o û t p e r s o n n e l . C o m m e n t faire p o u r a l l o u e r n octects a v e c n e w ? char *pointeur; pointeur = new char[n];
// alloue n octets
1 78
Pont entre C et C++
Dois-je b a n i r friend de ma religion ? O u i , autant q u e possible. N ' u t i l i s e z f r i e n d q u e s i v o u s n e p o u v e z p a s raisonnablement faire a u t r e m e n t . P a r e x e m p l e , i m a g i n o n s q u ' u n e classe p r é c i s e (et u n e seule) a b e s o i n d ' a c c é d e r a u x d o n n é e s c a c h é e s d ' u n e autre classe. D a n s c e cas d e figure, f r i e n d garantit q u e seule l a c l a s s e v o u l u e p e u t a c c é d e r a u x d o n n é e s c a c h é e s . C e l a évite d ' a v o i r à déclarer d e s fonctions d'accès « p u b l i q u e s » qui ne serviraient q u ' à u n e seule autre classe, et q u e les autres d e v r a i e n t i g n o rer. Q u e s i g n i f i e c o n s t d a n s u n e d é f i n i t i o n du s t y l e void f() const {I* ... */ i ? L e m o t clé c o n s t , inséré ici entre les p a r a m è t r e s d ' u n e fonction et s o n corps, signifie q u e cette fonction ne modifie p a s les d o n n é e s - m e m b r e s de la classe à laquelle elle appartient : class Awesome { protected: int a ; public : int get_a() const { return a; }; };
D a n s cet e x e m p l e , la fonction g e t _ a ( ) ne modifie p a s la d o n n é e - m e m b r e a . Elle peut d o n c être déclarée c o n s t . Q u e l l e est la capitale du K a z a k h s t a n ? Alma-Ata. P o u r q u o i d i a b l e n ' a r r i v é - j e pas à a c c é d e r a u x d o n n é e s de ma c l a s s e de b a s e d e p u i s ma c l a s s e d é r i v é e ? V o u s avez peut-être déclaré ces d o n n é e s p r i v a t e a u lieu d e p r o t e c t e d . R a p p e l o n s q u e les d o n n é e s p r i v a t e n e sont p a s accessibles d a n s les classes dérivées. Il se p e u t é g a l e m e n t q u e votre héritage soit p r i v a t e o u p r o t e c t e d a u lieu d e p u b l i c . V o y e z l e chapitre sur l'héritage, s p é c i a l e m e n t page 39.
Index
Symboles
« surcharger 92 surcharger 57
» surcharger 92 []
comment allouer n octets? 177 new et delete 83 amies classes et fonctions 123
B base classe de... 38 bases du C + + 11 C
surcharger 142 A accès aux membres dans un héritage 39 affectation opérateur= 57 affichage cout, cin et cerr 89 formater 94 surcharger « 92 allocation mémoire
catch 138 cerr 89 surcharger « 92 cin 89 surcharger » 92 classe 13 abstraite de base 179 amie 123 comment les architecturer? 167 comment les trouver? 166 conseils d'implantation 172
182
Pont entre C et C++
conversion de pointeurs 104 de base 38 déclaration 25 définir 15 dérivée 38 différences avec les objets 14 template 119 commentaires 63 ruse de sioux 64 compilateur principes de base 149 compilation séparée 149 conseils 151 make 155 conception conseils 166 conseils de codage 170 de conception 166 constantes 67 et compilation séparée 159 paramètres 70 propres à une classe 71 constructeurs 27 copie 31 et héritage 43 ordre d'appel dans un héritage multiple 129 par défaut 175 vue d'ensemble 27 conversion constructeur 35 de classe vers un type 61 de type vers une classe 35 dérivée* vers base* 42; 104 copie constructeur 31 par défaut 58 cout 89 surcharger « 92 tableau récapitulatif 99 D dec 97
déclaration de classe 25 n'importe où 73 définir classe 15 fonction-membre 16 delete 83 syntaxe 84 démarrer en C + + 20 dérivée classe... 38 destructeurs 27 et héritage 44 virtuels 106 vue d'ensemble 27 E
encapsulation 14 erreurs gestion des... 137 exceptions 137 créer ses propres... 140 en boucle 146 intercepter plusieurs... 139 non interceptée 145 spécifier dans l'entête 143 extern "nom" 160 F
fichiers séparation en plusieurs... 149 fonctions amies 123 définir fonction-membre 16 inline 21 redéfinition de fonction-membre 41 retournant une référence 115 spécifier les exceptions dans un entête 143 templates 118 virtuelles 101 virtuelles pures 107
Index
formater les sorties 94 friend classes et fonctions 123 et o p é r a t e u r « 176 et religion 178 G
généricité 117 guide de survie 163 H
héritage accès aux membres 39 conseils 167 conversion de pointeurs 104 et constructeurs 43 et destructeurs 44 exemple complet 45 multiple 127 multiple, duplication de données 131 multiple, masquage de la virtualité 133 résumé 42; 78 simple 37 virtuel 135 hex 97
I implantation conseils 172 initialisation listes 47 inline 21 iomanip.h 94 ios::fixed 98 ios::left 95 ios::right 95 ios::scientific 98 ios::showbase 96 ios::showpoint 96 ios::showpos 96 iostream.h Voir cout et cin
183
L
langages travailler avec d'autres... 160 listes d'initialisations 47 M make 155 malloc la fin du... 83 modèles Voir templates N new 83 syntaxe 84 notation scientifique 97 O objet accéder à la partie public 17 créer 17 différences avec les classes 14 pourquoi comparer deux objets identiques 61 provisoire et références 115 oct 97 opérateurs Voir Symboles, au début de l'index de conversion de classe vers un type 61 surcharge (vue d'ensemble) 54
P paramètres par défaut 65 passage par référence 113 patrons Voir templates pointeurs sur classe de base 104 polymorphisme 101 printf la fin du... 89 protected 40 conseils 173
184
Pont entre C et C++
d'opérateurs 54 de l'opérateur « 92 de l'opérateur = 57 de l'opérateur » 93 de l'opérateur [] 142
Q questions 175 R redéclarations comment éviter les... 157 redéfinition d'une fonction-membre 41 références 113 et objets provisoires 115 retournées par une fonction 115 résumé 1ère partie 75 réutilisabilité 171 S set_terminate 145 set_unexpected 144 setfill 97 setiosflags 95 setprecision 95 setw 94 sioux ruse de 64 static 24 portée 158 surcharge 51
T template ambiguïté 122 templates 117 terminate 145 this 23 pourquoi retourner *this? 60 throw 140 try 138
U unexpected 144 V virtualité 101 destructeurs 106 héritage virtuel 135 masquage dans un héritage multiple 133 pure 107