Rechercher sur le site

[cpp] Introduction au C++ Chap4 : L'héritage

  • Auteur :Philippe Dosch Mise à jour :30-12-2006



Chapitre 4 L?héritage


4.1 Introduction


L?héritage, qui a été évoqué [§ 1.4.3], est une technique de plus permettant de servir la réutilisabilité. Son objectif est de permettre la définition aisée de sous-types, correspondant à une spécialisation (à
l?extension) de types existants. Imaginons en effet que nous souhaitions implanter une classe Boisson.
Les boissons sont des sortes d?articles, et doivent comporter à ce titre les même attributs que tout article
(_nom, _prixBrut, _quantite), ainsi qu?un autre qui est spécifique aux boissons : _volume.
L?héritage permet de réutiliser la définition de la classe Articlepour définir la classe boisson.

4.2 La dérivation


La dérivation de classe est la technique qui consiste à faire hériter une classe d?une autre classe en C++.
Au niveau terminologique, on dit ainsi qu?une classe Best dérivée d?une classe Asi elle en hérite. La classe
A est alors appelée superclasse ou classe de base de la classe B. Dans l?exemple évoqué au paragraphe
précédent, la classe Boissonest une classe dérivée de la classe Articlepuisqu?elle la spécialise. Pour
spécifier en C++ qu?une classe Bhérite d?une classe A, on déclare la classe Bde la manière suivante :

class B : class A { ... };


où le mode de dérivation permet de fixer le statut des membres (attributs, fonctions) de la classe B en
fonction du statut des membres de la classe A(voir le tableau [TAB. 4.1] pour un récapitulatif des différents

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.

4.3. Redéfinition de méthodes


La classe Boisson, qui est avant tout un article, hérite des attributs et des méthodes définis dans la
classe Article, en dehors des constructeurs, du destructeur, du constructeur de copie et de l?opérateur
d?affectation. Le statut de ces membres hérités est fixé par le mode de dérivation. La classe Boisson
peut aussi définir de nouveaux membres, comme l?attribut _volumeet la nouvelle méthode d?accès à cet
attribut.

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.

4.4. Le polymorphisme


conversions implicites sont définies :

? 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.

4.6 Les classes abstraites


Nous avons vu dans le paragraphe précédent comment déclarer des fonctions virtuelles. C++ autorise
la déclaration de fonctions virtuelles pures, c?est-à-dire de fonctions virtuelles dont la définition n?est pas
donnée. Dans l?exemple présenté [PROG. 4.9], une classe FigureGeometrique est définie, destinée

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.

Cours en relation :


Comparez les offres de Credit auto
Erreur de connexion :