class B :
où le mode de dérivation permet de fixer le statut des membres
(attributs, fonctions) de la classe B en
fonction du statut des membres de la classe A(voir le tableau [TAB. 4.1] pour
un récapitulatif des différents
Statut des membres de base
public protected private
Mode
de
dérivation
public public protected inaccessible
protected protected protected inaccessible
private private private inaccessible
TAB. 4.1 ? Statut des membres de la classe dérivée en fonction
du statut des membres de la classe de base
et du mode de dérivation.
cas possibles). Le mode de dérivation par défaut est le mode
private, mais le mode de dérivation le plus
courant est le mode public: c?est celui qui permet de donner les même
statuts aux membres dérivés que
ceux des membres de base. Une définition de la classe Boissondans ce
contexte est donnée [PROG. 4.2].
1 #include
2
3 //
4 // Classe Article
5 //
6
7 class Article {
8 public:
9 // Constructeur
10 Article(const char* nom, float prixBrut,
11 int quantite = 0, float tva = 1.206)
12 : _prixBrut(prixBrut), _quantite(quantite), _tva(tva)
13 {
14 _nom = new char[strlen(nom) + 1];
15 strcpy(_nom, nom);
16 }
17
18 // Prix brut de l?article
19 float prixBrut() const;
20
21 // Prix net de l?article
22 float prixNet() const {return _prixBrut * _tva;}
23
24 protected:
25 char* _nom; // Nom de l?article
26 float _prixBrut; // Prix brut de l?article
27 int _quantite; // Quantité en stock
28 float _tva; // TVA appliquée à l?article
29 };
30
31 //
32 // Classe Boisson
33 //
34
35 class Boisson : public Article {
36 public:
37 // Constructeur
38 Boisson(const char* nom, float prixBrut, int volume, int quantite = 0)
39 : Article(nom, prixBrut, quantite), _volume(volume) {}
40
41 // Accès au volume
42 int volume() const {return _volume;}
43
44 protected:
45 int _volume; // Volume de la boisson (cl)
46 };
PROG. 4.2 ? Rappel de la classe Articleet définition de la classe
Boisson. Note : un attribut correspondant à la TVA a également
été ajouté au niveau de la classe Article.
Lors de la création d?une boisson (c?est-à-dire d?un
objet de ce type), tous les constructeurs de la
hiérarchie d?héritage sont automatiquement appelés,
du plus général (ici Article) au plus particulier (ici
Boisson). Le type d?appel fait au constructeur de la classe Article est
implanté dans le constructeur
de la classe Boisson, en utilisant la même syntaxe que pour initialiser
les attributs de classe. Si cet appel
n?est pas spécifié, c?est le constructeur par défaut
de la classe Articlequi est invoqué.
Lors de la destruction de cette même boisson, les différentes
destructeurs en présence dans la hiérarchie
d?héritage sont successivement appelés, dans l?ordre
inverse de celui des constructeurs, c?est-à-dire du plus
particulier (Boisson) au plus général (Article).
Note : C++ permet également d?utiliser l?héritage multiple. Cette partie n?est pas abordé dans ce polycopié.
4.3 Redéfinition de méthodes
L?héritage permet de bénéficier d?autres fonctionnalités,
telles que la redéfinition de méthodes. Supposons, pour illustrer
cette technique, que nous souhaitions implanter une classe BoissonAlcoolisee.
Cette classe hérite de la classe Boissonet introduit deux nouveaux attributs
:
? _degrequi correspond au degré d?alcool contenu dans la
boisson,
? _accisequi contient le montant des accises 6.
Cette classe hérite donc des attributs et méthodes définis
successivement dans les classes Article
et Boisson. Cependant, le calcul effectué pour calculer le prix net de
la boisson doit être modifié pour
prendre en compte les droits d?accise. Pas de problème, l?héritage
offre la possibilité de redéfinir la méthode
prixNetdans la classe BoissonAlcoolisee[PROG. 4.3]. Pour cela, l?entête
de la méthode redéfinie
doit être absolument identique à l?original. La méthode
prixNet originale reste applicable sur un objet
la classe BoissonAlcoolisee, mais il faut pour cela la préfixer du nom
de la classe où elle a été
définie [PROG. 4.4].
L?avantage de cette technique est qu?il est ainsi possible de disposer
de deux méthodes prixNet
différentes sur des articles ? en fonction du type exact de l?article
?, effectuant leur calcul de deux
manières différentes, tout en conservant une homogénéité
de nom.
Attention : lorsqu?une méthode surchargée est redéfinie
dans une classe dérivée, la redéfinition masque
toutes les définitions de la méthode de base, et pas seulement
celles redéfinies.
4.4 Le polymorphisme
Un certain nombre de conversions standards sont automatiquement définies
entre une classe de base et
ses classes dérivées de façon publique. Ainsi, pour une
classe A de base et une classe B dérivée de A, des
6. Taxe appliquée à certains alcools, proportionnelle au degré
d?alcool et au volume.
1 class BoissonAlcoolisee : public Boisson {
2 public:
3 // Constructeur
4 BoissonAlcoolisee(const char *nom, float prixBrut, int volume,
5 float degre, float accise, int quantite = 0)
6 : Boisson(nom, prixBrut, volume, quantite),
7 _degre(degre), _accise(accise) {}
8
9 // Prix net de l?article
10 float prixNet() const
11 {
12 return ((_prixBrut + _accise * _degre * _volume) * _tva);
13 }
14
15 protected:
16 float _degre; // Degrés de la boisson
17 float _accise; // Droits d?accise par degré et litre
18 };
PROG. 4.3 ? Définition de la classe BoissonAlcoolisee.
1 #include "BoissonAlcoolisee.H"
2
3 int main()
4 {
5 Article table("Table", 250, 1);
6 BoissonAlcoolisee biere("Bière", 3.40, .25, 4.7, .062);
7
8 cout << "Prix de la table : " << table.prixNet() <<
endl;
9 cout << "Prix de la bière : " << biere.prixNet()
<< endl;
10 cout << "Prix de la bière si elle n?était
pas alcoolisée : "
11 << biere.Article::prixNet() << endl;
12 }
PROG. 4.4 ? Utilisation de la classe BoissonAlcoolisee.
? d?un objet de type Bvers un objet de type A,
? d?un pointeur sur un objet de type Bvers un pointeur sur un objet
de type A,
? d?une référence sur un objet de type Bvers une référence
sur un objet de type A.
Ce type de conversions n?est pas risqué puisqu?un objet de
type B est avant tout un objet de type A.
Dans le premier cas (celui des objets), c?est une conversion d?objet
qui est effectuée : l?objet de type Aest
affecté à l?objet de type B. Dans ce cas, seul les attributs
définis dans Asont pris en compte, les éventuels
attributs supplémentaires définis dans B ne sont pas pris en considération.
Dans les deux autres cas (ceux
des pointeurs et références), c?est seulement une conversion
de type qui est réalisée : l?objet en lui-même
n?est pas affecté. Ce qui implique qu?un pointeur sur type
Apeut pointer vers un objet de type B. C?est ce
qu?on appelle le polymorphisme 7. Voir [PROG. 4.5] pour des exemples pratiques
de polymorphisme.
1 #include
2 #include "Article.H"
3 #include "BoissonAlcoolisee.H"
4
5 int main()
6 {
7 // Avec des objets
8
9 Article a("Sucre", 4.55);
10 BoissonAlcoolisee b("Biere", 3.4, .25, 4.7, .062);
11
12 cout << a.nom() << endl; // Sucre
13 cout << b.nom() << endl; // Biere
14
15 a = b;
16
17 cout << a.nom() << endl; // Biere
18
19 // Avec des pointeurs
20
21 Article *pa = new Article("Sucre", 4.55);
22 BoissonAlcoolisee *pb = new BoissonAlcoolisee("Biere", 3.4, .25,
23 4.7, .062);
24
25 cout << pa->prixNet() << endl; // Article::prixNet()
26 cout << pb->prixNet() << endl; // BoissonAlcoolisee::prixNet()
27
28 pa = pb;
29 cout << pa->prixNet() << endl; // Article::prixNet()
30
31 pb = (BoissonAlcoolisee*) pa; // Juste car pa pointe vers un alcool
32 cout << pb->prixNet() << endl; // BoissonAlcoolisee::prixNet()
33 }
PROG. 4.5 ? Exemples de polymorphisme.
Quelques remarques :
? On ne peut pas accéder aux membres spécifiques à
B à travers un pointeur ou une référence de
type A. Les seuls membres accessibles sont donc ceux qui sont définis
au niveau de la classe A. Un
7. En biologie, c?est la caractéristique d?un organisme qui
peut se présenter sous diverses formes sans changer de nature.
4. L?héritage
exemple pratique de cette remarque se trouve ligne 29 : même si pa pointe
vers un objet de type
BoissonAlcoolisee, c?est bien la méthode Article::prixNetqui est
appelée.
? Si la conversion implicite marche très bien du type Bvers le
type A, ce n?est pas le cas de la conversion inverse (d?un objet
de type A vers un objet de type B) qui nécessite une conversion de type
explicite (un cast). Voir en particulier l?exemple de la ligne 31 qui
illustre cette situation.
D?autre part, pour réaliser cette conversion, le programmeur doit
être sûr qu?elle a un sens, c?està-dire que pa
pointe effectivement vers une boisson alcoolisée. Il existe plusieurs
moyens d?avoir
cette assurance (ce qui est utile sur des exemples plus complets que celui présenté)
:
1. Définir dans la classe de base un attribut permettant de stocker un
identificateur unique pour
chaque classe de la hiérarchie. En interrogeant préalablement
cet identificateur avant une conversion de type, on sait exactement quel est
le type de l?objet en présence, et on peut donc appliquer la conversion
ad hoc. Cette technique a longtemps été la seule disponible dans
ce genre
de situation.
2. Utiliser les nouveaux opérateurs de conversion définis dans
la norme finale C++, qui permettent
de réagir dynamiquement face aux types des objets manipulés, sans
avoir à implanter quelque
chose de plus. Ces nouveaux opérateurs ne sont cependant pas encore supportés
par tous les
compilateurs C++ et il est toujours recommandé d?utiliser l?ancienne
méthode si le programme
C++ est destiné à être porté sur d?autres compilateurs
que celui utilisé lors de la conception.
Ces opérateurs sont décrits [§ C.2].
4.5 La liaison dynamique
L?héritage et le polymorphisme sont des fonctionnalités
très puissantes pour représenter et manipuler
non seulement des objets, mais également des familles d?objets.
Il existe cependant une entrave à la bonne
maniabilité de ces familles d?objets : c?est la liaison statique
des méthodes à la compilation. En effet,
dans les exemples que nous avons étudiés jusqu?à
présent, le choix de la méthode à appliquer à un
objet est
déterminé à la compilation. C?est pour cette raison
d?ailleurs que l?appel de la ligne 29 présent [PROG. 4.5]
a provoqué l?appel de la fonction Article::prixNet. Le compilateur
s?est basé sur le type du pointeur
papour déterminer la méthode concernée et a effectué
une liaison statique.
Ce comportement est parfois assez contraignant. Imaginons en effet que nous
ayons un tableau de
pointeurs sur des objets de type Article, et que ces pointeurs pointent vers
des articles ou des objets
issus des classes dérivées de Article (comme des alcools par exemple),
ceci grâce au polymorphisme.
Comment faire pour afficher le prix net de chacun des articles contenus dans
ce tableau? Une des solutions
est de convertir chacun de ces pointeurs, afin d?accorder leur type avec
celui de l?objet pointé, et de calculer
ensuite le prix net [PROG. 4.6]. Cette solution n?est pas très
élégante, et le programme doit être modifié
à chaque ajout d?un article redéfinissant la méthode
prixNet, ce qui pose des problèmes évidents de
maintenance...
Pour éviter ce genre de situations, C++ permet de bénéficier
de la liaison dynamique. Pour cela, il
suffit de définir les méthodes qui sont concernées par
ce type de liaison comme virtuelles. Cette définition
s?effectue en préfixant la méthode concernée du mot-clé
virtual8 lors de la première déclaration de la
méthode [PROG. 4.7]. Une méthode déclarée virtuelle
dans une classe de base le reste en effet dans toutes
les classes dérivées de la hiérarchie. Dès lors,
il n?est plus nécessaire de convertir au fur et à mesure
les
différents articles. La méthode qui correspond au type d?objet
manipulé est dynamiquement déterminée,
c?est-à-dire pendant l?exécution du programme et non
pas lors de sa compilation [PROG. 4.8].
Remarque importante : afin de pouvoir détruire tous les objets comme indiqué ligne 11 [PROG. 4.8],
8. Attention : ce mot-clé n?est utilisé que lors de déclarations,
il ne faut pas le reprendre lors de la définition des méthodes.
1 #define MAXELTS 1000
2
3 int main()
4 {
5 Article *lesArticle[MAXELTS];
6
7 // Initialisation du tableau
8 // avec des articles hétérogènes
9
10 for (int i = 0; i < MAXELTS; i++)
11 switch (lesArticle[i]->type()) {
12 case ARTICLE:
13 cout << lesArticle[i]->prixNet() << endl;
14 break;
15
16 case ALCOOL:
17 BoissonAlcoolisee *ba;
18
19 ba = (BoissonAlcoolisee*) lesArticle[i];
20 cout << ba->prixNet() << endl;
21 break;
22
23 // Et ainsi de suite pour les autres articles...
24 }
25 }
PROG. 4.6 ? Résolution statique de l?application d?une méthode à une famille d?objets.
1 class Article {
2 public:
3
4 virtual float prixNet() const;
5 };
PROG. 4.7 ? Déclaration d?une méthode virtuelle dans la classe Article.
1 #define MAXELTS 1000
2
3 int main()
4 {
5 Article *lesArticle[MAXELTS];
6
7 ...
8
9 for (int i = 0; i < MAXELTS; i++) {
10 cout << lesArticle[i]->prixNet() << endl;
11 delete lesArticle[i];
12 }
13
14 // Plus de distinction à faire : la bonne méthode est
15 // automatiquement appelée en fonction du type
16 // dynamique de l?article courant
17 }
PROG. 4.8 ? Résolution dynamique de l?application d?une méthode à une famille d?objets.
il est nécessaire de déclarer comme virtuel le destructeur de
la classe de base (Article). Dans le cas
contraire, c?est uniquement le destructeur de la classe de base qui sera
appelé sur chacun des articles,
quelque soit leur type réel... On déclare donc conventionnellement
tous 9 les destructeurs de classe comme
virtuels, à moins d?avoir de bonnes raisons de ne pas le faire.
1 class FigureGeometrique {
2 public:
3 // Constructeur
4 FigureGeometrique();
5
6 // Dessin (Fonction virtuelle pure)
7 virtual void dessin() const = 0;
8
9 ...
10 };
PROG. 4.9 ? Déclaration d?une fonction virtuelle pure.
à être la classe de base de toutes les figures géométriques.
Suivant leurs caractéristiques, ces figures vont
être implantées dans diverses classes dérivées (on
pourrait imaginer les figures en 2D, en 3D, etc.). Dans
ce contexte, la classe FigureGeometrique sert de cadre générique
pour la définition des méthodes
dans les classes dérivées. Ainsi, chaque figure doit pouvoir être
dessinée, et c?est ce qui est exprimé par
l?intermédiaire de la fonction virtuelle pure dessin.
La fonction dessin, qui est une fonction virtuelle pure, n?a pas de définition.
Cette définition devra être donnée dans les classes dérivées.
Lorsqu?une classe, comme la classe FigureGeometrique,
possède au moins une méthode virtuelle pure, elle ne peut pas
être instanciée. Elle est alors qualifiée de
classe abstraite. Si les classes abstraites ne peuvent être instanciées,
elles peuvent cependant être utilisées
dans le cadre du polymorphisme, ce qui est très pratique pour manipuler
de façon homogène une famille
d?objets [PROG. 4.10].
9. Même sur des classes qui ne sont pas destinées à être
classe de base d?une hiérarchie. Cela peut éviter des problèmes
si ces
classes sont finalement dérivées.
1 #include "FigureGeometrique.H"
2
3 #define NBFIG 1000
4
5 int main()
6 {
7 FigureGeometrique *lesFigures[NBFIG];
8 ...
9 // Dessin de toutes les figures géométriques
10
11 for (int i = 0; i < NBFIG; i++)
12 lesFigures[i]->dessin();
13 ...
14 }
PROG. 4.10 ? Utilisation d?une classe abstraite dans le cadre du
polymorphisme.