Le carnet On est
Séance 4 · encre prune

Héritage & polymorphisme

La relation « est un », les modes d'héritage, les fonctions virtuelles, les classes abstraites, et les mots-clés override / final pour verrouiller le contrat.

~ 65 min Source : Cours4 Héritage Polymorphisme + formes.zip 16 flashcards · 9 QCM

📄 Ressources du cours

📥 Télécharger les slides  ·  🔗 Ouvrir dans un nouvel onglet  ·  📝 Ouvrir le TD/TP

🎥 Vidéos recommandées

1. L'héritage — la relation « est un »

Définition L'héritage permet de partir d'une classe existante (classe de base, parente) pour en obtenir une nouvelle, plus spécialisée (classe dérivée, fille). Relation logique : « est un ».

Exemple classique : un étudiant est une personne. Il a un nom et un prénom (de Personne), plus un numéro d'étudiant en propre.

class Personne {
   private:
      string nom;
   public:
      string getNom() const { return nom; }
};

class Etudiant : public Personne {     // Etudiant dérive de Personne
   private:
      string id;
   public:
      string getId() const { return id; }
};

La classe dérivée hérite tous les membres (données et méthodes) de la classe de base. Trois bénéfices :

  • réutilisation du code déjà écrit
  • ajout de nouvelles fonctionnalités spécifiques
  • redéfinition d'un comportement existant

Hiérarchies simples vs multiples

Une classe peut hériter de plusieurs classes (héritage multiple) :

class A { ... };
class B { ... };
class C : public A, public B { ... };  // C hérite de A et de B

Un objet C possède simultanément les données et les méthodes de A et de B. À utiliser avec parcimonie (problème du diamant si A et B partagent un ancêtre commun).

2. Redéfinir une méthode héritée

La classe dérivée peut réécrire une méthode de la classe de base. Pour appeler la version d'origine, on préfixe avec Personne::.

class Personne {
   private: string nom;
   public:
      void afficher() const { cout << nom << endl; }
};

class Etudiant : public Personne {
   private: string id;
   public:
      void afficher() const {        // redéfinition
         Personne::afficher();           // réutilisation du parent
         cout << id << endl;             // + ajout spécifique
      }
};
⚠ Redéfinition ≠ surcharge
  • Surcharge (CM3) : plusieurs fonctions de même nom dans la même classe, différenciées par leurs paramètres.
  • Redéfinition : une classe dérivée fournit sa propre version d'une méthode déjà héritée. Même signature.

3. Constructeurs et destructeurs en cascade

Ordre d'appel
  • Construction : parent d'abord, puis enfant
  • Destruction : enfant d'abord, puis parent (ordre inverse)

Quand on instancie un Etudiant, le constructeur de Personne est appelé avant celui d'Etudiant. Soit on l'appelle explicitement dans la liste d'initialisation, soit le compilateur appelle le constructeur par défaut (et râle s'il n'existe pas).

class Personne {
   private: string nom;
   public:
      Personne(string n) : nom{n} { }
};

class Etudiant : public Personne {
   private: string id;
   public:
      Etudiant(string nom, string id) : Personne(nom), id{id} { }
      //                                ↑ appel explicite du constructeur parent
};
📌 Métaphore
Pour construire une maison, on commence par les fondations puis on monte les étages. Pour démolir, on commence par le toit. Mêmes intuitions pour les classes.

4. Modes d'héritage — public, protected, private

Quand on dérive, on précise un mode :

class ClasseDerivee : public|protected|private ClasseBase {
   /* ... */
};

Le mode change le statut hérité des membres. Tableau des droits d'accès dans la classe dérivée :

Modepublic (base)protected (base)private (base)
publicpublicprotectedinaccessible
protectedprotectedprotectedinaccessible
privateprivateprivateinaccessible
Lecture du tableau Les attributs privés de la base sont toujours inaccessibles depuis la dérivée. Le mode protected existe précisément pour exposer aux enfants sans casser l'encapsulation vis-à-vis du reste du programme.
🔑 En pratique
L'héritage public est le seul qui correspond vraiment à la relation « est un ». protected et private servent à des cas d'implémentation rares (héritage technique). Par défaut, écrire : public Base.

5. Polymorphisme — choisir la bonne méthode au runtime

Définition Un objet polymorphe peut prendre plusieurs formes pendant l'exécution. Le polymorphisme est la capacité du système à choisir dynamiquement la méthode qui correspond au type réel de l'objet (et pas au type du pointeur).

En C++ il s'implémente avec deux ingrédients : l'héritage et le mot-clé virtual.

Exemple — calculer une aire

class Polygon {
   protected:
      int width, height;
   public:
      virtual int getArea() { return 0; }
};

class Rectangle : public Polygon {
   public:
      int getArea() override { return width * height; }
};

class Triangle : public Polygon {
   public:
      int getArea() override { return width * height / 2; }
};

Utilisation polymorphe

vector<const Polygon*> vp;
Polygon* p1 = new Rectangle(4, 5);
Polygon* p2 = new Triangle(4, 5);
vp.push_back(p1);
vp.push_back(p2);

for (auto p : vp) {
   cout << p->getArea() << endl;    // affiche 20 puis 10
}

// n'oubliez pas delete !
delete p1; delete p2;
⚠ Sans virtual ? Si getArea() n'était pas virtuelle, l'appel p->getArea() appellerait toujours la version de Polygon (le type du pointeur), retournant 0 pour toutes les formes. Le mot-clé virtual active la résolution dynamique (au runtime).
🔑 Lien virtual + pointeur (ou référence)
Le polymorphisme ne fonctionne qu'à travers un pointeur ou une référence sur la classe de base. Sur une variable de type valeur, le compilateur connaît le type exact à la compilation : pas de dispatch dynamique.

6. Le mot-clé override — vérification

Quand une dérivée redéfinit une virtuelle, ajouter override demande au compilateur de vérifier qu'on remplace bien quelque chose. Sans override, une faute de frappe crée silencieusement une nouvelle méthode.

class BaseClass {
   virtual void funcA();
   virtual void funcB() const = 0;
   virtual void funcC(int = 0);
   void         funcD();
};

class DerivedClass : public BaseClass {
   virtual void funcA() override;              // ✓ OK
   virtual void funcB() override;              // ✗ erreur : manque const
   virtual void funcC(double = 0.0) override;  // ✗ erreur : type ≠
   void         funcD() override;              // ✗ erreur : base non-virtuelle
};
Bonne pratique Toujours mettre override quand on redéfinit. Sans override, ces erreurs compilent et produisent silencieusement des bugs en runtime — la version « surchargée » n'est jamais appelée par le mécanisme polymorphe.

7. Fonction virtuelle pure & classe abstraite

Définitions
  • Une fonction est virtuelle pure si elle est déclarée avec = 0 : virtual type nom(args) = 0;
  • Une classe contenant au moins une virtuelle pure est dite abstraite.
  • Une classe abstraite ne peut pas être instanciée.
  • Les classes dérivées doivent fournir une implémentation des virtuelles pures pour devenir concrètes (instanciables).
class Polygon {                          // classe abstraite
   protected:
      int width, height;
   public:
      virtual int getArea() = 0;          // virtuelle pure — pas de corps
};

Polygon p;       // ✗ ERREUR : classe abstraite, instanciation interdite

class Rectangle : public Polygon {
   public:
      int getArea() override { return width * height; }  // ✓ concrétisée
};

Rectangle r;     // ✓ OK : Rectangle est concrète
📌 Quand utiliser une classe abstraite ?
Pour exprimer un contrat : « toute forme doit savoir calculer son aire ». Polygon ne sait pas — c'est aux Rectangle, Triangle, Cercle… de répondre. La classe abstraite force ce contrat à la compilation.

8. Le spécificateur final

final verrouille un mécanisme d'héritage. Deux usages :

Méthode final — interdire la redéfinition

class BaseClass {
   public:
      virtual void afficher() final {
         cout << "classe de base";
      }
};

class DerivedClass : public BaseClass {
   public:
      virtual void afficher() {           // ✗ ERREUR — afficher() est final
         cout << "classe dérivée";
      }
};

Classe final — interdire l'héritage

class BaseClass final { };

class DerivedClass : public BaseClass { };  // ✗ ERREUR — BaseClass est non-héritable

final est un mot-clé contextuel : il n'a sa signification spéciale qu'après une déclaration de méthode ou un nom de classe. Ailleurs, c'est un identifiant comme un autre.

9. Synthèse — trois types d'héritage de méthode

TypeSyntaxeHéritage
Virtuelle pure virtual T f() = 0; Interface seule. Les dérivées doivent implémenter.
Virtuelle virtual T f(); Interface + implémentation par défaut que les dérivées peuvent remplacer.
Non-virtuelle T f(); Interface + implémentation obligatoire. Pas de polymorphisme — invariance.
Règle de design (Scott Meyers, encore) Distinguer le quoi (interface) du comment (implémentation). Une virtual = 0 dit « je veux que tu saches faire ça ». Une virtual dit « voici une façon par défaut, libre à toi de la changer ». Une non-virtuelle dit « ne touche pas, c'est invariant ».

Réviser le chapitre

Pour vérifier ta compréhension

Quelle est la différence entre héritage public et privé ?

Public (le plus courant) : relation « est-un ». Les membres publics de la base restent publics dans la dérivée, le polymorphisme dynamique est possible. Privé : relation « implémenté en termes de ». Les membres publics de la base deviennent privés — la dérivée ne peut être traitée comme une instance de la base depuis l'extérieur.

Pourquoi rendre un destructeur virtual ?

Pour permettre le polymorphisme à la destruction. Sans virtual, delete sur un pointeur de type base appelle seulement le destructeur de la base, pas celui de la dérivée → fuites mémoire. Règle : si une classe a au moins une méthode virtuelle, son destructeur doit l'être aussi.

Qu'est-ce qu'une méthode virtuelle pure ?

Une méthode déclarée avec = 0 à la fin : virtual void afficher() = 0;. Elle n'a pas d'implémentation dans la classe et oblige les classes dérivées à la redéfinir. Une classe contenant au moins une méthode virtuelle pure est abstraite — elle ne peut pas être instanciée.

Comment fonctionne la résolution dynamique des méthodes ?

Grâce à la VTable (table des fonctions virtuelles). Chaque classe avec des méthodes virtuelles a une VTable, et chaque objet contient un pointeur vers la VTable de sa classe réelle. Quand on appelle obj->methode(), le compilateur regarde la VTable de l'objet pour trouver la bonne implémentation — déterminée à l'exécution, pas à la compilation.

Quelle est la différence entre override et simplement redéfinir une méthode ?

Sans override, si tu te trompes de signature (ex. paramètre int au lieu de const int&), tu crées une nouvelle méthode au lieu de redéfinir l'ancienne — bug silencieux. Avec override (C++11+), le compilateur vérifie que la méthode existe bien dans la base avec cette signature et te le signale sinon.

🃏 Flashcards

Quelle relation logique l'héritage exprime-t-il ?
La relation « est un » (ou « est une sorte de »). Un Etudiant est une Personne — donc Etudiant dérive de Personne.
Syntaxe d'une classe dérivée publique ?
class Fille : public Mere { ... };. Les trois mots-clés autorisés sont public, protected, private — public est presque toujours le bon choix.
Quel est l'ordre d'appel des constructeurs et destructeurs ?
Construction : parent puis enfant (de haut en bas). Destruction : enfant puis parent (ordre inverse). Comme une maison : fondations d'abord, démolition par le toit.
Différence redéfinition vs surcharge ?
Surcharge : plusieurs fonctions de même nom dans la même classe, paramètres différents. Redéfinition : la classe dérivée fournit sa propre version d'une méthode héritée, même signature.
À quoi sert protected ?
À exposer un membre aux classes dérivées tout en le cachant du reste du programme. Compromis entre private (rien) et public (tout).
Les attributs private du parent sont-ils accessibles depuis la fille ?
Non — jamais, quel que soit le mode d'héritage. C'est ce que garantit l'encapsulation. Pour qu'une fille accède au père, utiliser protected ou des accesseurs publics.
Définis « polymorphisme » en une phrase
Capacité d'un objet à réagir différemment au même appel selon son type réel. En C++ : héritage + virtual + appel via pointeur/référence sur la classe de base.
Quel mot-clé active le polymorphisme dynamique ?
virtual. Sans lui, l'appel p->f() sur un Base* p appelle toujours Base::f, même si p pointe sur une dérivée.
Le polymorphisme fonctionne-t-il sur une variable de type valeur ?
Non. Uniquement via pointeur ou référence sur la classe de base. Avec une valeur, le compilateur connaît le type à la compilation et fait du static dispatch.
Qu'est-ce qu'une fonction virtuelle pure ?
Une virtuelle sans implémentation, déclarée avec = 0 : virtual T f() = 0;. Les dérivées doivent la définir, sinon elles restent abstraites.
Qu'est-ce qu'une classe abstraite ?
Une classe avec au moins une fonction virtuelle pure. Elle ne peut pas être instanciée : MaClasse obj; est rejeté par le compilateur.
À quoi sert override ?
Demande au compilateur de vérifier qu'on redéfinit bien une virtuelle existante. Sans override, une faute de frappe crée silencieusement une nouvelle méthode (bug subtil).
À quoi sert final ?
Après une méthode : interdit sa redéfinition dans les dérivées. Après un nom de classe : interdit l'héritage à partir de cette classe.
Pour exprimer « toute forme doit calculer son aire » sans imposer la formule, quel outil ?
Une fonction virtuelle pure dans une classe de base abstraite. Le contrat est exprimé (aire obligatoire), l'implémentation laissée aux dérivées.
Trois types d'héritage de méthode ?
Virtuelle pure : interface seule. ② Virtuelle : interface + implémentation par défaut. ③ Non-virtuelle : interface + implémentation invariante.
Comment réutiliser la version parente dans une méthode redéfinie ?
Préfixer par le nom de la classe parente : Personne::afficher(); à l'intérieur de la méthode d'Etudiant.

✎ Quiz éclair

1.Dans class Etudiant : public Personne, que désigne public ?
  • La visibilité du constructeur
  • Le mode d'héritage — comment les membres hérités sont exposés
  • Que Personne est une classe publique
  • Que Etudiant peut être instanciée hors du module
Le mot-clé devant le nom de la base précise comment les statuts public/protected/private des membres de la base seront vus depuis la dérivée et l'extérieur.
2.L'ordre d'appel à la création d'un Etudiant héritant de Personne :
  • Constructeur de Personne, puis constructeur d'Etudiant
  • Constructeur d'Etudiant, puis constructeur de Personne
  • Les deux en parallèle
  • Seulement celui d'Etudiant
Le parent se construit avant l'enfant — on ne peut pas spécialiser ce qui n'existe pas encore. Pour la destruction : ordre inverse.
3.Avec Polygon* p = new Rectangle(...) et getArea non virtuelle, p->getArea() appelle :
  • La version de Rectangle (le type réel)
  • Les deux versions
  • Erreur de compilation
  • La version de Polygon (le type du pointeur)
Sans virtual, le compilateur résout l'appel en se fiant au type statique du pointeur (Polygon). C'est précisément ce que virtual est fait pour défaire.
4.Une classe avec virtual void draw() = 0; est :
  • Une interface au sens Java
  • Une classe finale
  • Une classe abstraite, non instanciable
  • Une classe vide
Toute classe avec au moins une virtuelle pure est abstraite. On ne peut pas écrire MaClasse obj;. Les dérivées doivent fournir le corps de toutes les virtuelles pures pour devenir concrètes.
5.Pourquoi ajouter override aux redéfinitions ?
  • C'est obligatoire en C++11
  • Pour que le compilateur signale les fautes de frappe ou les changements de signature
  • Pour la performance
  • Pour rendre la méthode publique
Sans override, une signature qui diverge (un const oublié, un type changé) compile silencieusement et crée une nouvelle méthode jamais appelée polymorphiquement.
6.Une variable Polygon (par valeur, pas pointeur) appelée polymorphiquement, ça marche comment ?
  • Ça ne marche pas — pas de dispatch dynamique, en plus du risque d'object slicing
  • Comme un pointeur — virtual fonctionne aussi
  • Erreur de compilation
  • Crash au runtime
Le polymorphisme exige un pointeur ou une référence. Sur une valeur, le compilateur connaît le type exact, et copier une dérivée dans une base provoque du slicing (les attributs supplémentaires disparaissent).
7.Que fait class A final { }; ?
  • Rend A abstraite
  • Rend toutes les méthodes virtuelles
  • Interdit l'héritage à partir de A
  • Rend A immutable
final sur une classe interdit toute dérivation. Sur une méthode virtuelle, interdit sa redéfinition dans les dérivées.
8.Dans une méthode redéfinie, comment appeler la version du parent ?
  • super.afficher();
  • parent::afficher();
  • this->afficher();
  • Personne::afficher();
C++ n'a pas de mot-clé super. On utilise le nom explicite de la classe parente avec ::.
9.En mode d'héritage public, un attribut private de la base devient dans la dérivée :
  • protected
  • Inaccessible
  • public
  • private accessible
Quel que soit le mode d'héritage, les private de la base restent toujours inaccessibles depuis la dérivée. C'est le principe fondamental de l'encapsulation.

Score : 0 / 9 ·