Programmation C++ : CM séance 04
8. Héritage et polymorphisme
Le polymorphisme est la capacité pour une classe d'effectuer une liaison dynamique, c'est-à-dire de déterminer au runtime quelle méthode virtuelle il faut appeler.
8.1. Hiérarchies
Une classe peut hériter d'une ou plusieurs autres classes :
- la classe qui hérite est dite classe dérivée ou classe fille ;
- une classé héritée est appelée classe de base, classe mère ou super-classe.
Une classe dérivée hérite de toutes les données membre et fonctions membre de ses classes mère, et ainsi de suite.
Exemple de hiérarchie d'héritage :
graph BT
E[Classe E] --> C[Classe C]
C --> A[Classe A]
D[Classe D] --> A
D --> B[Classe B]
- La classe E dérive (hérite) de la classe C, qui elle-même dérive de la classe A.
- C est la classe de base directe de E, A est une classe de base indirecte de E.
- Les classes C et A sont les ancêtres de E.
On parle d'héritage simple ou multiple selon la cardinalité des classes de base directes :
- E et C n'ont chacune qu'une seule classe de base directe, il s'agit d'un héritage simple ;
- la classe D dérive de deux classes, A et B, ce qui constitue un héritage multiple.
Syntaxe de la déclaration :
class ClasseDérivée : public ClasseBase1, public ClasseBase2, .... { .... };
(On verra plus loin les alternatives à public
que sont private
et protected
).
Dans notre exemple :
graph BT
E[Classe E] --> C[Classe C]
C --> A[Classe A]
D[Classe D] --> A
D --> B[Classe B]
class A { .... };
class B { .... };
class C : public A { .... };
class E : public C { .... };
class D : public A, public B { .... };
Il ne faut pas oublier public
car par défaut elles sont private
.
8.2. Héritage simple
Soit la hiérarchie suivante :
graph RL
E[Dog] --> C[Pet]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
Reprenons l'exemple en rajoutant des données membre :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
Étant donné que le constructeur de la classe de base Pet
demande un paramètre
(ligne 4), il faut le fournir dans le constructeur de la classe dérivée Dog
(ligne 11).
Comment le faire ? L'appel des constructeurs avec paramètres se fait de la même façon que pour les constructeurs des objets membres :
class ClasseDérivée: public ClasseBase1, .... {
public:
ClasseDérivée (type_paramètre paramètre, ....)
: ClasseBase1{valeur1}, .... // {} ou ()
objet_membre{valeur2}, donnée_membre{valeur3}, ....
{ .... }
};
Remarque : toutes les références ou pointeurs vers des instances de classes dérivées sont implicitement convertis vers des référence ou pointeurs de classe de base lorsque c'est nécessaire. Les membres de la classe dérivée, toujours présents, seront alors inatteignables.
int main()
{
Dog d {"Odie", 2}; // Affiche :
d.say_name(); // Odie
d.bark(); // Bark! Bark!
Pet& p = d; // conversion implicite de référence
p.say_name(); // Odie
p.bark(); // erreur, p n'est pas un Dog
}
8.3. Héritage multiple
L'héritage multiple a du sens lorsque les classes de base sont totalement disjointes.
Par exemple on considère la hiérarchie suivante :
graph BT
E[Dog] --> C[Pet]
E --> B[Trainable]
(certains animaux de compagnie ne sont pas dressables, et certains animaux dressables ne sont pas des animaux de compagnie).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
Remarque 1 : l'ordre de déclaration des classes mère induit l'appel des constructeurs des classes mère ; les destructeurs sont appelés dans l'ordre inverse.
Remarque 2 : il est préférable d'éviter les problèmes de l'héritage en diamant.
8.4. Méthode virtuelle
Lors de l'héritage, une classe dérivée peut redéfinir une méthode membre :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
Les fonctions membre à appeler sont déterminées lors de la compilation ; il s'agit d'une liaison statique. Mais il y a des situations où on souhaite que le choix des fonctions membre à appeler soit effectué à l'exécution ; on appelle cela une liaison dynamique.
Ainsi, l'exemple suivant compile parfaitement :
void yell (Pet& pet)
{
pet.yell();
}
int main()
{
Pet p {"Kiwi"}; // Affiche :
yell(p); // The pet Kiwi is yelling!
Dog d {"Odie"}; //
yell(d); // The pet Odie is yelling!
}
mais l'effet escompté n'est pas atteint, car le compilateur sait uniquement
que la fonction yell
reçoit un Pet&
, et il réalise une liaison statique.
Pour demander au compilateur de réaliser une liaison dynamique :
-
la fonction membre de la classe de base doit être déclarée virtuelle, avec le mot réservé
virtual
; -
la fonction membre de la classe dérivée doit avoir la même signature (nom et types des paramètres), et doit être décorée avec le mot réservé
override
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
Stockage dans un conteneur
Attention, si on stocke ces éléments dans un std::vector<Pet>
il n'y aura
plus de liaison dynamique, car l'insertion dans le conteneur est faite par
recopie sur la classe de base (slicing).
La solution est de les stocker dans un vector
de pointeurs :
1 2 3 4 5 6 7 8 9 10 11 |
|
affiche :
The pet Kiwi is yelling!
The pet Odie is yelling!
The pet Kiwi is yelling!
The dog Odie is barking!
Classe polymorphe
Une classe qui déclare ou hérite d'au moins une fonction membre virtuelle est dite polymorphe.
En interne, chaque classe polymorphe est dotée d'une table de fonctions virtuelles (VTBL) pour effectuer la résolution au runtime. Le coût en mémoire et en durée d'exécution est très faible.
Un constructeur ne peut pas être virtuel, car à l'appel du constructeur, la VTBL est inachevée ; par contre un destructeur peut l'être.
Lorsqu'un constructeur ou destructeur appelle une méthode virtuelle, les sous-classes dérivées sont ignorées (VTBL inachevée).
8.5. Méthode virtuelle pure
Dans certains cas il est intéressant de rentre une méthode virtuelle pure,
ce qui signifie qu'elle ne sera pas implémentée dans la classe de base,
mais devra être implémentée dans une classe dérivée.
La syntaxe consiste à remplacer le corps par = 0
:
class ClasseBase1 {
public:
....
virtual ... méthode_virtuelle_pure (types paramètres....) = 0;
....
};
Une classe qui possède au moins une méthode virtuelle pure est qualifiée de classe abstraite ; une classe abstraite ne peut pas être instanciée. Par opposition, une classe concrète est une classe qui n'est pas abstraite et peut être instanciée.
Exemple :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
9. Encapsulation
Le but de l'encapsulation est de protéger le fonctionnement interne d'une classe en ne donnant l'accès qu'à sa partie publique, qui constitue l'API de la classe. Ce principe est essentiel pour la séparation et la maintenance du code.
9.1. Spécificateurs d'accès
Le C++ fournit les spécificateurs d'accès public
, private
et protected
au niveau
de la déclaration d'une classe. Lorsqu'un membre est déclaré :
-
public
, il sera accessible de n'importe quelle fonction ; -
private
, il ne sera accessible que depuis la classe elle-même, ou des classes explicitement autorisées avec le mot réservéfriend
; -
protected
, il aura les mêmes restrictions queprivate
, mais pourra aussi être utilisé dans les classes dérivées.
Ces mots réservés peuvent être utilisés plusieurs fois dans n'importe quel ordre.
On rappelle que par défaut les membres d'une class
sont private
et les membres d'un
struct
sont public
.
Exemple :
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
On aura aussi une erreur ligne 12 si on met protected
ligne 4. Quelle est la différence ?
On l'observe en rajoutant une classe dérivée :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
Si ligne 4 on met private
, alors ligne 10 la méthode quack
ne pourra pas
appeler tell
.
9.2. Limiter l'accès par héritage
À quoi sert le spécificateur public
dans class Duck : public Bird
?
Il sert à indiquer pour la classe dérivée Duck
quel est le plus fort niveau
d'accès des membres de la classe de base Bird
:
-
Comme
public
est le plus haut niveau, les niveaux d'accès des membres de la classe de base sont conservés pour la classe dérivée ; -
Avec
protected
, les membrespublic
de la classe de base deviennentprotected
pour la classe dérivée, lesprivate
restentprivate
; -
Avec
private
, tous les membres deviennentprivate
pour la classe dérivée : on ne pourra donc pas les appeler.
9.3. Accesseurs
Le principe consiste à placer toutes les données membre en private
(ou à la rigueur
en protected
pour les classes dérivées éventuelles), et donner l'accès
public
avec des accesseurs, c'est-à-dire par des setters et des getters.
Exemple :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
En C++ moderne, les accesseurs sont réalisés en faisant une
surcharge de fonction (au lieu d'écrire des méthodes get_....
et set_....
).
On peut réutiliser le nom de la variable (sans le préfixe m_
) si c'est plus clair.
Méthode const:
il convient de décorer les getter avec un const
à droite pour dire au compilateur
que la méthode ne modifie pas l'instance, cela évitera des warnings lors de
l'utilisation lorsque l'instance doit rester const
.
Avantages de l'encapsulation :
elle permet par la suite de modifier le nom des données membre sans modifier
l'API (par exemple m_s
→ m_secondes
).
Elle permet aussi de valider les données :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Fonctions inline :
Toutes les fonctions membre définies au niveau de la classe (autrement dit,
non forward), sont automatiquement inlinées (déclarées inline
), c'est-à-dire que
chaque appel est remplacé par le code du corps de la fonction.
Par conséquent, le surcoût en temps d'exécution des accesseurs définis dans la classe est nul.
Pour inliner une fonction forward, il suffit de la décorer à gauche avec
le mot réservé inline
. Exemple :
inline int Time::s() const { return m_s; } // getter
inline
n'est qu'une indication (hint ) pour le compilateur.
Le compilateur g++
accepte des options d'optimisation :
-O1
active -finline-functions-called-once
,
-O2
active -finline-functions
et -finline-small-functions
.
9.4. Fonctions et classes amies
On a vu que les membres protégés ou privés ne sont pas accessibles en dehors de
la classe (éventuellement dérivée pour protected
) où ils sont définis.
Le mot réservé friend
permet d'assouplir cette règle.
La déclaration peut être faite n'importe où dans la classe (public
, private
ou
protected
), cela n'a pas d'incidence car ce n'est pas dans la portée de
la classe.
On distingue les classes amies et les fonctions amies :
Classe amie : on peut donner à une classe B l'accès au membres protégés ou privés d'une classe A, en déclarant dans A que B est une classe amie : exemple ligne 9
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
Si on ne met pas la ligne 9, la compilation échouera pour la fonction
Segment::show
qui a besoin d'accéder aux données membre des points.
Note : dans cet exemple on aurait pu faire des accesseurs.
Fonction amie : pour autoriser une fonction (non membre) à accéder à des membres protégés ou privés il suffit de la déclarer comme amie dans la classe : exemple lignes 12 et 24.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
|
La fonction length
a besoin d'accéder aux données membre privées des
classes Segment
et Point
, c'est pourquoi on les déclare amies avec la
fonction. De plus ligne 12 on a besoin de la définition de Segment
,
donc on déclare la classe en avance ligne 1.
Note : dans cet exemple on aurait pu faire des accesseurs dans Point
et une méthode
length
dans Segment
, et il n'y a plus besoin d'amis :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|