Héritage et polymorphisme (partie 1)
est unpublic / protected / privateOrdre de constructionRedéfinition et Base::methode()virtual et overrideLiaison statique ou dynamiqueDestructeur virtuel
Deux questions reviennent à chaque intra : « donnez l'affichage » d'une hiérarchie de constructeurs et destructeurs, et « quelle méthode est appelée » selon que la méthode est virtuelle ou non. Les deux démos interactives de ce module servent exactement à ça.
1. L'héritage : une relation « est un »
Une classe dérivée hérite de tous les membres d'une classe de base et en ajoute ou en redéfinit. class Moteur : public Composante signifie qu'un Moteur est une Composante : partout où une Composante est attendue (référence ou pointeur), on peut fournir un Moteur. On choisit l'héritage quand la phrase « A est un B » est vraie, la composition quand « A a un B » est vraie (intra H15, question 7).
Qui voit quoi ?
public: accessible à tout le monde.protected: accessible dans la classe et dans ses classes dérivées, pas depuis l'extérieur.private: accessible dans la classe seulement. Un attribut privé de la classe de base est hérité mais inaccessible à la classe dérivée (intra H16, vrai/faux : FAUX). La dérivée passe par les méthodes publiques ou protégées de la base.
Constructeurs, destructeurs et ordre d'exécution
Les constructeurs et destructeurs ne sont pas hérités. Le constructeur de la dérivée doit construire sa partie « base » : on appelle le constructeur de la base dans la liste d'initialisation (: Composante(nom, masseKg)). Si on ne le fait pas, le constructeur par défaut de la base est appelé (erreur de compilation s'il n'existe pas).
- la classe de base (récursivement : sa propre base d'abord) ;
- les attributs de la classe, dans l'ordre de leur déclaration ;
- le corps du constructeur.
Destruction dans l'ordre inverse : corps du destructeur de la dérivée, puis ses attributs (du dernier au premier), puis la classe de base. L'ordre dans la liste d'initialisation n'y change rien (seulement un avertissement -Wreorder).
Redéfinir une méthode et appeler la version de la base
Une méthode de la dérivée portant le même nom et la même signature qu'une méthode de la base la redéfinit (elle la masque). Pour réutiliser le travail de la base, on l'appelle explicitement avec l'opérateur de portée : Composante::afficher(out);. C'est le schéma attendu au TP3 : Moteur::afficher appelle Composante::afficher puis ajoute sa consommation.
2. Polymorphisme : liaison statique ou dynamique
Le polymorphisme, c'est manipuler des objets de classes dérivées différentes à travers un pointeur ou une référence vers leur classe de base, en obtenant pour chaque objet le comportement de son type réel. Pour cela, la méthode doit être déclarée virtual dans la base.
Le compilateur choisit la méthode d'après le type déclaré du pointeur (Composante*), à la compilation.
La méthode choisie dépend du type réel de l'objet, déterminé à l'exécution (table des méthodes virtuelles).
virtualse met dans la classe de base ; la méthode reste virtuelle dans toutes les dérivées (on peut répéter le mot, ce n'est pas obligatoire).override(dans la dérivée) demande au compilateur de vérifier qu'on redéfinit bien une méthode virtuelle de la base : une faute de frappe ou unconstoublié devient une erreur de compilation au lieu d'une méthode silencieusement différente. Exigé au TP3.- La liaison dynamique ne joue qu'à travers un pointeur ou une référence. Un objet copié dans une variable de type base (
Composante c = moteur;) perd sa partie Moteur : c'est le découpage (slicing), détaillé au module 6. - Dans un constructeur ou un destructeur, un appel virtuel utilise la version de la classe en cours de construction (la partie dérivée n'existe pas encore, ou déjà plus).
finalinterdit de redéfinir plus bas ; une méthode virtuelle a un petit coût (indirection), les autres aucun.
Le destructeur virtuel
Si on détruit un objet dérivé à travers un pointeur de base (Composante* p = new Moteur; delete p;, ou un unique_ptr<Composante>), seul le destructeur de la base s'exécute à moins qu'il ne soit virtual : les ressources de la partie Moteur fuient (comportement indéfini). Règle : toute classe destinée à être une classe de base avec des méthodes virtuelles déclare virtual ~Base() = default; (énoncé du TP3 : « Composante possède un destructeur virtuel »).
Résolution statique des liens : le compilateur fixe l'appel d'après le type déclaré (surcharge, méthodes non virtuelles). Liaison dynamique : l'appel est résolu à l'exécution d'après le type réel (méthodes virtuelles via pointeur ou référence). Le polymorphisme est le second mécanisme.
3. À retenir
- Héritage public = « est un ». La dérivée hérite de tout, mais les membres privés de la base lui sont inaccessibles ;
protectedest fait pour elle. - Construction : base, attributs (ordre de déclaration), corps. Destruction : inverse. La base se construit dans la liste d'initialisation, sinon son constructeur par défaut est appelé.
- Redéfinir = même signature dans la dérivée ;
Base::methode()pour réutiliser la version de la base. virtualdans la base +overridedans la dérivée = liaison dynamique à travers pointeurs et références. Sansvirtual, le type déclaré décide.- Destructeur virtuel dans toute classe de base polymorphe.
Quiz rapide - Module 5
Un attribut privé de la classe de base est accessible aux classes dérivées.
- Vrai
- Faux : il est hérité mais inaccessible ; il faut passer par des méthodes publiques ou protégées
- Vrai, seulement avec l'héritage public
- Vrai, seulement dans les constructeurs
protected.La liste d'initialisation d'un constructeur permet d'initialiser les attributs composites et la classe de base.
- Faux : la base s'initialise dans le corps
- Faux : seuls les types fondamentaux y sont permis
- Vrai
- Vrai pour les attributs, faux pour la base
Dans quel ordre un objet d'une classe dérivée est-il construit ?
- Classe de base, puis attributs dans l'ordre de déclaration, puis corps du constructeur
- Attributs, puis classe de base, puis corps
- Corps du constructeur, puis attributs, puis base
- Dans l'ordre de la liste d'initialisation
Quelles sont les deux premières lignes affichées par FF uneInstance; ?
- CC, BB
- FF, DD
- AA, BB
- BB, CC
Si la classe dérivée n'appelle aucun constructeur de la base dans sa liste d'initialisation :
- La partie base n'est pas construite
- Le constructeur par défaut de la base est appelé (erreur de compilation s'il n'existe pas)
- Le compilateur choisit le constructeur le plus proche
- C'est une erreur de compilation dans tous les cas
A() s'affiche en premier.Comment la méthode Tarte::afficher() peut-elle afficher quantiteSucre_, attribut privé de MetSucre, sans accesseur ?
- En écrivant
quantiteSucre_directement - En déclarant Tarte friend de MetSucre
- En appelant
MetSucre::afficher();qui a accès à l'attribut - C'est impossible
Qu'affiche ce code ?
- base : f n'est pas virtuelle, le type déclaré Base* décide
- derivee : l'objet est une Derivee
- Erreur de compilation
- basederivee
virtual void f() const dans Base.À quoi sert le mot-clé override ?
- À rendre une méthode virtuelle
- À appeler la méthode de la base
- À interdire la redéfinition
- À faire vérifier par le compilateur que la méthode redéfinit bien une méthode virtuelle de la base (même signature, même constance)
void afficher() (sans const) dans la dérivée créerait une nouvelle méthode au lieu de redéfinir virtual void afficher() const, et le polymorphisme ne fonctionnerait pas en silence.Pourquoi une classe de base polymorphe doit-elle avoir un destructeur virtuel ?
- Pour que ses attributs soient détruits
- Pour que
deletevia un pointeur de base (ou un unique_ptr<Base>) exécute aussi le destructeur de la classe dérivée - Pour empêcher la copie
- Parce que le compilateur l'exige
Composante* p = new Moteur; delete p; n'appelle que ~Composante : comportement indéfini, fuites.Quel est l'affichage ?
- ...zzz
- woufZZZ
- woufzzz
- ...ZZZ
parler est virtuelle : liaison dynamique via la référence, version Chien. dormir ne l'est pas : type déclaré Animal, version Animal.Quelle déclaration permet à une classe dérivée d'accéder à un attribut sans l'exposer à l'extérieur ?
protected:private:public:friend:
Dans une collection vector<shared_ptr<Composante>>, pourquoi peut-on faire push_back(make_shared<Moteur>(...)) ?
- Parce que make_shared convertit tout en Composante
- Parce que le vecteur accepte n'importe quel type
- On ne peut pas, il faut un cast
- Parce qu'un shared_ptr<Moteur> se convertit implicitement en shared_ptr<Composante> (conversion ascendante : un Moteur est une Composante)
Les constructeurs sont-ils hérités ?
- Oui, comme toutes les méthodes
- Non : la dérivée définit les siens et appelle ceux de la base dans sa liste d'initialisation
- Oui, mais seulement le constructeur par défaut
- Oui, si la base est abstraite
Combien de destructeurs s'exécutent à la destruction de D unObjet(1, 2) dans l'exercice A17 (A, B avec un attribut A, C, D avec un attribut C) ?
- 2
- 4
- 5
- 3
Quelle est la différence entre surcharge et redéfinition ?
- Surcharge : même nom, paramètres différents, dans la même portée ; redéfinition : même signature dans une classe dérivée
- Ce sont des synonymes
- La surcharge exige virtual
- La redéfinition n'existe qu'avec les opérateurs
Exercices à développement
Ordre de construction avec attributs et bases
Donnez l'affichage complet de FF uneInstance; avec les classes du quiz (AA, BB, CC avec attribut BB, EE : AA, DD : AA avec attribut EE, FF : CC avec attribut DD).
MetSucre, Tarte, TarteAuSucre
Soit les déclarations suivantes. Écrivez l'implémentation : (1) du constructeur de MetSucre ; (2) du constructeur de Tarte qui initialise taille_ et quantiteSucre_ ; (3) du constructeur de TarteAuSucre ; (4) de MetSucre::afficher() et Tarte::afficher() qui affichent les attributs sans utiliser les accesseurs ; (5) de TarteAuSucre::estBon() : vraie si la tarte est très sucrée et d'au moins 13,8 cm.
Solution
Pièges : Tarte ne peut pas écrire quantiteSucre_(quantiteSucre) dans sa liste (l'attribut appartient à MetSucre : on appelle le constructeur de la base) ; TarteAuSucre ne peut pas appeler MetSucre(...) directement (seule la base directe Tarte est permise dans la liste).
Patineur, Joueur, Arbitre
Un patineur a un nom, une vitesse (0 à 10) et une position (x, y). Un arbitre est un patineur. Un joueur est un patineur qui possède une habileté (0 à 10) et une équipe (bool). Un match contient des joueurs et des arbitres. Faites le diagramme de classes (en mots), écrivez les déclarations avec les relations d'héritage et les constructeurs, puis Joueur::jouer() qui déplace le joueur (fourni par Patineur::jouer()) et compte un but si rand() % 600000 < ((getX() + getY()) * habilete_) / 10.
Solution
Diagramme : Patineur est la base ; Arbitre et Joueur en héritent (flèches vers Patineur). Match contient (composition par valeur, ou vecteurs) des Joueur et des Arbitre.
Version polymorphe (module 6) : virtual void jouer() dans Patineur et un seul vector<unique_ptr<Patineur>>.
Composante, Moteur et Pince avec méthodes virtuelles
Écrivez la classe de base Composante (nom, masse ; destructeur virtuel ; getConsommationW() virtuelle retournant 0 par défaut ; afficher(ostream&) virtuelle qui écrit « nom (masse kg) » et retourne le flux), puis Moteur (puissance nominale, charge ; consommation = produit) et Pince (puissance de maintien, état fermé ; consomme seulement fermée), qui redéfinissent les deux méthodes avec override en réutilisant l'affichage de la base. Ajoutez un operator<< unique qui délègue à afficher pour préserver le polymorphisme.
Solution
Au TP3, getConsommationW est pure (= 0) et Composante devient abstraite : module 6.
Virtuelle ou pas ?
Donnez l'affichage.
Quelles lignes compilent ?
Solution
- Ligne 1 : erreur,
solde_est privé dans Compte, inaccessible à Epargne. Correction :ajouter(getSolde() * taux_). - Ligne 2 : compile,
ajouterest protégée (accessible à la dérivée) etgetSoldepublique. - Ligne 3 : erreur,
ajouterest protégée : inaccessible depuis main, même via un Epargne. - Lignes 4 et 5 : compilent (méthodes publiques).