Chapitre 2 : L'Héritage Simple, Multiple et en Diamant
Comprendre la factorisation de code et la spécialisation de types, maîtriser les modes d'héritage, gérer l'ordre de construction/destruction et résoudre les ambiguïtés critiques de l'héritage multiple grâce à l'héritage virtuel.
Objectifs pédagogiques de la séance
- Distinguer la relation d'héritage (« est-un ») de la composition (« a-un »).
- Mettre en œuvre l'héritage simple et maîtriser les modificateurs de dérivation (
public,protected,private). - Comprendre l'ordre rigoureux d'exécution des constructeurs et destructeurs dans une hiérarchie.
- Pratiquer l'héritage multiple et résoudre les conflits de noms par résolution de portée (
::). - Diagnostiquer le problème du diamant et le résoudre avec l'héritage virtuel (
virtual).
L'Héritage Simple : Spécialisation et Réutilisation
1.1. Motivation : Factoriser et Spécialiser
L'héritage est un mécanisme fondamental de la POO permettant de créer une nouvelle classe (appelée classe dérivée, fille ou sous-classe) à partir d'une classe existante (appelée classe de base, mère ou super-classe).
La classe dérivée hérite des caractéristiques (attributs et méthodes) de la classe de base, tout en pouvant y ajouter ses propres attributs, de nouvelles méthodes, ou modifier le comportement de méthodes existantes.
On n'utilise l'héritage public que s'il existe une relation stricte de type « EST-UN » :
• Une Voiture est un Vehicule (Héritage légitime ✅)
• Une Voiture a un Moteur (Composition : attribut Moteur m_moteur; dans Voiture ✅, PAS d'héritage ❌)
std::string m_marque;int m_annee;void klaxonner();
- Hérite de tout Vehicule
int m_nbPortes;(Ajout)void afficherDetails();(Spécialisation)
La modélisation UML correspondante est :
1.2. Syntaxe en C++
En C++, l'héritage se déclare après le nom de la classe dérivée, précédé du symbole deux-points : et du modificateur d'accès (généralement public) :
#include <iostream> #include <string> // 1. Classe de Base class Vehicule { protected: std::string m_marque; int m_annee; public: Vehicule(std::string marque, int annee) : m_marque(std::move(marque)), m_annee(annee) { std::cout << "[Vehicule] Construction : " << m_marque << std::endl; } virtual ~Vehicule() { std::cout << "[Vehicule] Destruction : " << m_marque << std::endl; } void klaxonner() const { std::cout << m_marque << " : Pouet pouet !" << std::endl; } }; // 2. Classe Dérivée (Héritage public) class Voiture : public Vehicule { private: int m_nbPortes; public: // Appel explicite du constructeur de la base dans la liste d'initialisation Voiture(std::string marque, int annee, int nbPortes) : Vehicule(std::move(marque), annee), m_nbPortes(nbPortes) { std::cout << "[Voiture] Construction (" << m_nbPortes << " portes)" << std::endl; } ~Voiture() override { std::cout << "[Voiture] Destruction" << std::endl; } void afficher() const { // Accès direct possible aux membres protected de la base std::cout << "Voiture : " << m_marque << " de " << m_annee << " avec " << m_nbPortes << " portes." << std::endl; } }; int main() { Voiture v("Renault", 2023, 5); v.klaxonner(); // Méthode héritée de Vehicule v.afficher(); // Méthode propre à Voiture return 0; }
Visibilités d'Héritage et Cycle de Vie
2.1. Les 3 Modes d'Héritage en C++
En C++, le mot-clé placé devant la classe de base (public, protected ou private) définit la façon dont la visibilité des membres de la base est transformée dans la classe dérivée :
| Visibilité dans la Base | Héritage public (Le standard) |
Héritage protected |
Héritage private |
|---|---|---|---|
public |
Reste public | Devient protected | Devient private |
protected |
Reste protected | Reste protected | Devient private |
private |
Inaccessible dans la fille | Inaccessible dans la fille | Inaccessible dans la fille |
Dans 99% des cas en conception orientée objet, on utilise l'héritage public (qui modélise le sous-typage et le polymorphisme). L'héritage privé ou protégé est une technique avancée d'implémentation masquée, souvent avantageusement remplacée par la composition.
2.2. L'Ordre d'Exécution des Constructeurs et Destructeurs
Lorsqu'une instance de classe dérivée est créée ou détruite, l'ordre d'appel des fonctions de gestion de mémoire suit une règle absolue en C++ :
🏗️ À la Construction (De la Base vers la Dérivée)
- 1. Allocation de la mémoire globale de l'objet.
- 2. Exécution du constructeur de la classe de base.
- 3. Initialisation des attributs propres à la classe dérivée.
- 4. Exécution du corps du constructeur de la classe dérivée.
💥 À la Destruction (Ordre strictement inverse)
- 1. Exécution du corps du destructeur de la classe dérivée.
- 2. Destruction des attributs membres de la classe dérivée.
- 3. Exécution du destructeur de la classe de base.
- 4. Libération de l'emplacement mémoire.
Si la classe de base ne possède pas de constructeur par défaut (sans argument), la classe dérivée DOIT OBLIGATOIREMENT appeler un de ses constructeurs paramétrés dans sa liste d'initialisation :
Voiture(...) : Vehicule(marque, annee), m_nbPortes(nb) {}.
Si vous l'omettez, le compilateur refusera de compiler avec le message : « no default constructor exists for class Vehicule ».
L'Héritage Multiple
3.1. Définition et Syntaxe
Contrairement à d'autres langages comme Java ou C# qui interdisent l'héritage multiple de classes, le **C++ permet à une classe d'hériter directement de plusieurs classes de base**.
void prendrePhoto();
void appeler();
Possède toutes les méthodes de AppareilPhoto ET de Telephone.
La modélisation UML correspondante est :
3.2. Conflit de Noms et Ambiguïté
Le premier danger de l'héritage multiple survient lorsque deux classes de base possèdent une méthode portant exactement le même nom et la même signature :
#include <iostream> class EmetteurAudio { public: void diffuser() const { std::cout << "Diffusion du son..." << std::endl; } }; class EmetteurVideo { public: void diffuser() const { std::cout << "Diffusion de la vidéo..." << std::endl; } }; // Héritage multiple de deux bases ayant une méthode 'diffuser()' class LecteurMultimedia : public EmetteurAudio, public EmetteurVideo { public: void toutDiffuser() const { // diffuser(); ❌ ERREUR COMPILATION : Appel ambigu ! // Solution : Levée d'ambiguïté explicite avec l'opérateur de portée :: EmetteurAudio::diffuser(); EmetteurVideo::diffuser(); } }; int main() { LecteurMultimedia lecteur; // lecteur.diffuser(); // ❌ ERREUR : 'diffuser' is ambiguous // Appel direct via résolution de portée : lecteur.EmetteurAudio::diffuser(); lecteur.EmetteurVideo::diffuser(); std::cout << "--- Diffusion combinée ---\n"; lecteur.toutDiffuser(); return 0; }
Le Problème du Diamant & L'Héritage Virtuel
4.1. Anatomie du Problème du Diamant (Diamond of Death)
Le cas le plus problématique de l'héritage multiple en C++ se produit lorsqu'une classe D hérite de deux classes B et C, qui dérivent elles-mêmes toutes les deux d'une même classe ancêtre commune A.
int m_idSerie;
Hérite de Appareil
Hérite de Appareil
⚠️ Problème : Reçoit Appareil en double exemplaire !
La modélisation UML correspondante est :
4.2. Les Conséquences Catastrophiques sans Héritage Virtuel
1. Duplication de Mémoire & Incohérence
L'objet TerminalPortable contient en mémoire deux sous-objets Appareil distincts :
- La partie
Appareilprovenant deClavier. - La partie
Appareilprovenant deEcran.
Si on modifie m_idSerie via le clavier, l'ID de l'écran n'est pas synchronisé !
2. Ambiguïté Totale à la Compilation
Si on tente d'accéder directement à un membre de l'ancêtre :
TerminalPortable tp; tp.getId(); // ❌ ERREUR DE COMPILATION // 'getId' is ambiguous : // tp.Clavier::getId() ou tp.Ecran::getId() ?
4.3. La Solution C++ : L'Héritage Virtuel (virtual)
Pour indiquer au compilateur C++ que la classe de base doit être partagée en un seul exemplaire unique dans toute la hiérarchie descendante, les classes intermédiaires doivent hériter avec le mot-clé virtual :
#include <iostream> // 1. Classe de base au sommet du diamant class Appareil { protected: int m_idSerie; public: Appareil(int id) : m_idSerie(id) { std::cout << "[Appareil] Créé avec ID: " << m_idSerie << std::endl; } virtual ~Appareil() = default; int getId() const { return m_idSerie; } }; // 2. Héritage VIRTUEL pour Clavier et Ecran (partage de l'ancêtre unique Appareil) class Clavier : virtual public Appareil { public: Clavier(int id) : Appareil(id) { std::cout << "[Clavier] Initialisé" << std::endl; } void taperTexte() const { std::cout << "Saisie clavier..." << std::endl; } }; class Ecran : virtual public Appareil { public: Ecran(int id) : Appareil(id) { std::cout << "[Ecran] Initialisé" << std::endl; } void afficherImage() const { std::cout << "Affichage à l'écran..." << std::endl; } }; // 3. Classe terminale dérivée : UN SEUL sous-objet Appareil en mémoire ! class TerminalPortable : public Clavier, public Ecran { public: // ⚡ RÈGLE CRITIQUE : Dans un héritage virtuel, c'est la classe LA PLUS DÉRIVÉE // (ici TerminalPortable) qui est chargée d'appeler directement le constructeur // de la base virtuelle Appareil ! TerminalPortable(int id) : Appareil(id), Clavier(id), Ecran(id) { std::cout << "[TerminalPortable] Prêt." << std::endl; } void statut() const { // ✅ Aucun conflit ! Appareil::getId() est unique et sans ambiguïté. std::cout << "Terminal #" << getId() << " en ligne." << std::endl; } }; int main() { TerminalPortable tp(98765); tp.taperTexte(); tp.afficherImage(); tp.statut(); // Affiche bien l'ID unique 98765 sans ambiguïté return 0; }
Synthèse & Auto-évaluation
5.1. Fiche Mémo Récapitulative
| Concept | Syntaxe C++ | Règle essentielle à retenir |
|---|---|---|
| Héritage simple | class B : public A |
Modélise la relation « EST-UN ». Constructeur de A exécuté avant B. |
Membres protected |
protected: int m_x; |
Accessibles dans la classe et ses descendantes, inaccessibles de l'extérieur. |
| Héritage multiple | class C : public A, public B |
Conflits de noms résolus avec l'opérateur de portée A::nom(). |
| Héritage virtuel (Diamant) | class B : virtual public A |
Garantit un sous-objet A unique. Initialisé par la classe la plus dérivée. |
5.2. QCM de Réflexion (Niveau L3)
Voiture dérivant de Vehicule ?
Voiture d'abord, puis de Vehicule).
B hérite de A via un héritage private (class B : private A), que deviennent les méthodes publiques de A pour un utilisateur de B ?
private dans la classe fille. La relation « EST-UN » est brisée pour l'extérieur (on ne peut pas convertir implicitement un B* en A*).
virtual public Base, quelle classe a la responsabilité d'invoquer le constructeur de Base ?
📝 Exercice d'application pour le TD
Modélisez une hiérarchie de véhicules pour une société de transport multimodal :
- Classe de base
Vehicule(attributs:m_immatriculation,m_puissanceKW). - Classe
Bateau(attribut:m_tirantDEau) et classeAutomobile(attribut:m_nbPlaces) héritant virtuellement deVehicule. - Classe
Amphibiehéritant à la fois deBateauet deAutomobile. - Écrivez les constructeurs complets garantissant l'unicité de
Vehiculeet une méthodeafficherDetails().