Niveau : Licence 3 Module : Programmation Orientée Objet Volume : 1h20 (CM/TD) Coloration : GeSHi

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

L'Héritage Simple : Spécialisation et Réutilisation

⏱️ 25 minutes

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.

💡 La règle d'or : La relation « EST-UN » (IS-A)

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

Hiérarchie d'héritage simple
🚗 Classe de Base : Vehicule
  • std::string m_marque;
  • int m_annee;
  • void klaxonner();
▲ Dérive de
🏎️ Classe Dérivée : Voiture
  • Hérite de tout Vehicule
  • int m_nbPortes; (Ajout)
  • void afficherDetails(); (Spécialisation)

La modélisation UML correspondante est :

EST-UN héritage

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

CPPheritage_simple.cpp
#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;
}
2

Visibilités d'Héritage et Cycle de Vie

⏱️ 20 minutes

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
🎯 Règle pratique pour le niveau L3

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. 1. Allocation de la mémoire globale de l'objet.
  2. 2. Exécution du constructeur de la classe de base.
  3. 3. Initialisation des attributs propres à la classe dérivée.
  4. 4. Exécution du corps du constructeur de la classe dérivée.

💥 À la Destruction (Ordre strictement inverse)

  1. 1. Exécution du corps du destructeur de la classe dérivée.
  2. 2. Destruction des attributs membres de la classe dérivée.
  3. 3. Exécution du destructeur de la classe de base.
  4. 4. Libération de l'emplacement mémoire.
⚠️ Piège classique : Appel du constructeur de base

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

3

L'Héritage Multiple

⏱️ 15 minutes

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

Schéma d'héritage multiple
📷 AppareilPhoto
  • void prendrePhoto();
📞 Telephone
  • void appeler();
▼ Héritage Multiple
📱 Smartphone

Possède toutes les méthodes de AppareilPhoto ET de Telephone.


La modélisation UML correspondante est :

EST-UN héritage multiple

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 :

CPPconflit_noms.cpp
#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;
}
4

Le Problème du Diamant & L'Héritage Virtuel

⏱️ 20 minutes

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.

Structure du Diamant d'Héritage
🏛️ Base Commune : Appareil
int m_idSerie;
↙ dérive de ↘ dérive de
⌨️ Clavier

Hérite de Appareil

🖥️ Ecran

Hérite de Appareil

↘ dérive de ↙ dérive de
📱 TerminalPortable

⚠️ Problème : Reçoit Appareil en double exemplaire !


La modélisation UML correspondante est :

EST-UN héritage multiple en diamant

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 Appareil provenant de Clavier.
  • La partie Appareil provenant de Ecran.

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 :

CPPdiamant_resolu.cpp
#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;
}

📌 Règle d'or de l'Héritage Virtuel en C++

Lors de l'instanciation de la classe la plus dérivée (TerminalPortable) :
1. Le constructeur de la base virtuelle Appareil est exécuté une seule fois au tout début, appelé directement par TerminalPortable.
2. Les appels à Appareil(id) présents dans les constructeurs intermédiaires Clavier et Ecran sont automatiquement ignorés par le compilateur pour éviter la double initialisation.

5

Synthèse & Auto-évaluation

⏱️ 10 minutes

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)

❓ Question 1 : Dans quel ordre sont exécutés les constructeurs lors de l'instanciation d'un objet de classe Voiture dérivant de Vehicule ?
A. D'abord le constructeur de Voiture, puis le constructeur de Vehicule.
B. D'abord le constructeur de Vehicule (la base), puis le constructeur de Voiture (la dérivée).
C. L'ordre dépend de la façon dont le compilateur optimise le code.
Réponse B : En C++, la base doit être entièrement construite et valide avant que la classe dérivée ne commence à initialiser ses propres membres. À la destruction, l'ordre est exactement l'inverse (destructeur de Voiture d'abord, puis de Vehicule).
❓ Question 2 : Si la classe 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 ?
A. Elles restent publiques et appelables sur un objet de type B.
B. Elles deviennent protégées et ne sont visibles que par les sous-classes de B.
C. Elles deviennent privées dans B et sont totalement inaccessibles depuis l'extérieur.
Réponse C : L'héritage privé restreint tous les membres hérités (publics et protégés) au niveau 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*).
❓ Question 3 : Dans une architecture en diamant résolue avec virtual public Base, quelle classe a la responsabilité d'invoquer le constructeur de Base ?
A. Les classes intermédiaires uniquement.
B. La classe la plus dérivée (celle au bas du diamant).
C. Le compilateur C++ l'interdit et oblige à utiliser un constructeur sans paramètre.
Réponse B : Puisque le sous-objet de base virtuel est unique et partagé par toutes les branches intermédiaires, le C++ impose que ce soit la classe terminale (la plus dérivée) qui transmette les arguments au constructeur de la base virtuelle dans sa liste d'initialisation.

📝 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 classe Automobile (attribut: m_nbPlaces) héritant virtuellement de Vehicule.
  • Classe Amphibie héritant à la fois de Bateau et de Automobile.
  • Écrivez les constructeurs complets garantissant l'unicité de Vehicule et une méthode afficherDetails().
📝 Ouvrir le TD 2 complet (Exercice 5 détaillé & 4 autres exercices) ➔