Module 5 - cours du 15 septembre (quiz 4)

Héritage et polymorphisme (partie 1)

Leçons Moodle « Héritage », « Polymorphisme » | Vidéos « L'héritage en programmation orientée objet », « Résolution statique des liens en C++ », « Polymorphisme et liaisons dynamiques » | Mis en pratique au TP3 (hiérarchie Composante → Moteur, Capteur, SourceEnergie...)

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 ?

Droits d'accès selon le mode d'héritage

Le cours utilise presque toujours l'héritage public. Les modes protected et private restreignent ce que les utilisateurs de la classe dérivée voient 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).

Ordre de construction d'un objet dérivé
  1. la classe de base (récursivement : sa propre base d'abord) ;
  2. les attributs de la classe, dans l'ordre de leur déclaration ;
  3. 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).

Pas à pas : exercice 4 de l'intra A17 (« Donnez l'affichage »)

Base, attributs, corps : suivez la construction de D unObjet(1, 2) puis sa destruction.

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.

Intra H16 : MetSucre, Tarte, TarteAuSucre

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.

Sans virtual : liaison statique

Le compilateur choisit la méthode d'après le type déclaré du pointeur (Composante*), à la compilation.

Avec virtual : liaison dynamique

La méthode choisie dépend du type réel de l'objet, déterminé à l'exécution (table des méthodes virtuelles).

Quelle méthode s'exécute ?

Animal déclare parler() virtuelle et dormir() non virtuelle ; Chien redéfinit les deux ; Chiot hérite de Chien sans rien redéfinir. Choisissez un code et une méthode.

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 »).

Vocabulaire des vidéos du cours

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

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
Question vrai/faux de l'intra H16. Pour donner accès aux dérivées sans l'ouvrir à tous, on déclare l'attribut 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
Intra H16 : VRAI. C'est même le seul endroit où l'on peut choisir quel constructeur de la base appeler.

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
Et la destruction suit l'ordre inverse : corps du destructeur, attributs du dernier au premier, base.

Quelles sont les deux premières lignes affichées par FF uneInstance; ?

  • CC, BB
  • FF, DD
  • AA, BB
  • BB, CC
FF construit d'abord sa base CC ; CC construit d'abord son attribut BB, puis son corps. Suite complète : BB CC AA AA EE DD FF (intra H15).

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
C'est ce qui se passe pour B dans l'exercice A17 : 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
La méthode de la base est appelée explicitement avec l'opérateur de portée. Intra H16.

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
Liaison statique. Pour obtenir « derivee », il faut 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)
Sans override, 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 delete via 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
Sans lui, 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:
protected est l'intermédiaire : visible dans la hiérarchie, invisible depuis main.

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)
La conversion dérivée → base est toujours implicite, pour les pointeurs bruts comme pour les pointeurs intelligents.

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
Idem pour les destructeurs et l'opérateur d'affectation (qui est toutefois appelé automatiquement par la version par défaut de la dérivée).

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
~D, ~C (attribut), ~B, ~A (attribut de B), ~A (base de B) : autant que de constructeurs.

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
La surcharge est résolue à la compilation ; la redéfinition d'une méthode virtuelle est résolue à l'exécution.

Exercices à développement

Intra H15Affichage

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

Vérifiez votre réponse

Explication

FF : base CC d'abord → CC construit son attribut BB (« BB ») puis son corps (« CC »). Puis l'attribut DD de FF : base AA (« AA »), puis l'attribut EE de DD : base AA (« AA ») puis corps (« EE »), puis corps de DD (« DD »). Enfin le corps de FF.

Intra H16Constructeurs

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

Simulateur de hockeyConception

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

TP3Polymorphisme

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.

AffichageLiaison

Virtuelle ou pas ?

Donnez l'affichage.

Vérifiez votre réponse

Explication

p->a() : virtuelle, type réel Derivee. p->b() : non virtuelle, type déclaré Base, donc Base::b ; mais à l'intérieur, a() est un appel virtuel sur le même objet : Derivee::a. delete p : le destructeur est virtuel, donc ~Derivee puis ~Base. Sans virtual ~Base, seul « ~Base() » s'afficherait.

AccèsCompilation

Quelles lignes compilent ?

Solution
  • Ligne 1 : erreur, solde_ est privé dans Compte, inaccessible à Epargne. Correction : ajouter(getSolde() * taux_).
  • Ligne 2 : compile, ajouter est protégée (accessible à la dérivée) et getSolde publique.
  • Ligne 3 : erreur, ajouter est protégée : inaccessible depuis main, même via un Epargne.
  • Lignes 4 et 5 : compilent (méthodes publiques).