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.
📄 Ressources du cours
🎥 Vidéos recommandées
-
🇫🇷
C++ #19 - héritage
FormationVidéo · 33:53 · 13k vues — L'héritage, où la manière de définir des relations entre plusieurs classes. Voyons ensemble comment faire dériver une classe d'une autre.00:00 Introduction00...
-
🇫🇷
C++ Polymorphisme (Exemple complet avec une classe de gestion)
Zied ALAYA · 32:06 · 11k vues — Ce tuto introduit la notion de polymorphisme en C++ et vous montre comment l'utiliser avec utilisation de constructeur de copie, destructeur et opérateur =. ...
-
🇬🇧
A Journey Into Non-Virtual Polymorphism in C++ - Rudyard Merriam - CppCon 2023
CppCon · 48:50 · 30k vues — https://cppcon.org/---A Journey Into Non-Virtual Polymorphism addressing std::any, std::variant and visit, std::tuple and apply, and CRTP. - Rudyard Merriam ...
1. L'héritage — la relation « 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
}
};
- 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
- 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
};
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 :
| Mode | public (base) | protected (base) | private (base) |
|---|---|---|---|
| public | public | protected | inaccessible |
| protected | protected | protected | inaccessible |
| private | private | private | inaccessible |
protected existe précisément pour exposer aux enfants sans casser l'encapsulation vis-à-vis du reste du programme.
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
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;
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).
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
};
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
- 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
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
| Type | Syntaxe | Hé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. |
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
class Fille : public Mere { ... };. Les trois mots-clés autorisés sont public, protected, private — public est presque toujours le bon choix.protected ?private (rien) et public (tout).private du parent sont-ils accessibles depuis la fille ?protected ou des accesseurs publics.virtual + appel via pointeur/référence sur la classe de base.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.= 0 : virtual T f() = 0;. Les dérivées doivent la définir, sinon elles restent abstraites.MaClasse obj; est rejeté par le compilateur.override ?override, une faute de frappe crée silencieusement une nouvelle méthode (bug subtil).final ?Personne::afficher(); à l'intérieur de la méthode d'Etudiant.✎ Quiz éclair
class Etudiant : public Personne, que désigne public ?Polygon* p = new Rectangle(...) et getArea non virtuelle, p->getArea() appelle :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.virtual void draw() = 0; est :MaClasse obj;. Les dérivées doivent fournir le corps de toutes les virtuelles pures pour devenir concrètes.override aux redéfinitions ?override, une signature qui diverge (un const oublié, un type changé) compile silencieusement et crée une nouvelle méthode jamais appelée polymorphiquement.class A final { }; ?final sur une classe interdit toute dérivation. Sur une méthode virtuelle, interdit sa redéfinition dans les dérivées.super. On utilise le nom explicite de la classe parente avec ::.public, un attribut private de la base devient dans la dérivée :private de la base restent toujours inaccessibles depuis la dérivée. C'est le principe fondamental de l'encapsulation.Score : 0 / 9 ·