Travaux Dirigés 2 : L'Héritage Simple, Multiple et en Diamant
Cette séance de travaux dirigés vous permet de mettre en pratique l'ensemble des notions abordées dans le
Chapitre 2 :
sémantique de la relation « EST-UN », délégation au constructeur parent, contrôle fin des visibilités (public, protected, private),
ordre strict de construction/destruction (LIFO), résolution des collisions de noms en héritage multiple et maîtrise indispensable de l'héritage virtuel face au redouté piège du diamant.
Objectifs pédagogiques de la séance
- Exercice 1 (★☆☆☆☆) : Mettre en œuvre l'héritage simple public : chaînage des constructeurs dans la liste d'initialisation, distinction entre membres
privateetprotected, et spécialisation de méthodes en appelant la classe parente. - Exercice 2 (★★☆☆☆) : Tracer rigoureusement le cycle de vie (LIFO) dans une chaîne d'héritage à plusieurs niveaux, et démasquer les surcharges cachées par le mécanisme de Name Hiding grâce au mot-clé
using. - Exercice 3 (★★★☆☆) : Comparer expérimentalement les trois modes de dérivation (
public,protected,private), comprendre l'héritage d'implémentation et observer l'interdiction de conversion de type (upcasting) hors héritage public. - Exercice 4 (★★★★☆) : Pratiquer l'héritage multiple, mesurer les décalages d'adresses mémoires (offsets) des sous-objets, et résoudre les conflits d'ambiguïté homonyme via l'opérateur de portée
::et des méthodes de synthèse. - Exercice 5 (★★★★★) : Diagnostiquer le problème du diamant (duplication mémoire, incohérence d'état et ambiguïtés), et concevoir une architecture robuste avec l'héritage virtuel (
virtual public) où la classe terminale assume directement l'initialisation de l'ancêtre commun.
Consignes de Travail & Outils de Compilation
Commandes de compilation recommandées
Compilez avec la norme C++20 et tous les drapeaux d'avertissements activés :
g++ -std=c++20 -Wall -Wextra -pedantic exercice.cpp -o exo ./exo
Détection d'anomalies mémoire & Undefined Behavior
Pour inspecter la hiérarchie mémoire et traquer les accès invalides ou destructeurs manquants :
g++ -std=c++20 -fsanitize=address,undefined -g exercice.cpp -o exo ./exo
AddressSanitizer et UBSan détectent à l'exécution les destructions polymorphiques invalides, les cast illicites et les corruptions de mémoire.
Héritage Simple & Spécialisation : Hiérarchie d'Employés
1. Contexte
Une entreprise souhaite modéliser les membres de son personnel. Tout employé possède une identité de base (nom, matricule et salaire mensuel brut). Cependant, les différentes fonctions dans l'entreprise introduisent des spécificités :
- Un Manager est un employé qui encadre une équipe et perçoit une prime forfaitaire de management.
- Un Développeur est un employé spécialisé dans un langage de programmation et pouvant toucher un bonus de projet.
La relation entre Manager et Employe est une relation stricte de type « EST-UN » (IS-A). Il est donc tout à fait légitime de modéliser cette situation par un héritage simple public.
2. Travail à réaliser
-
Définissez la classe de base
Employecontenant trois attributs :std::string m_nom,int m_matriculeetdouble m_salaireBase. Pourquoi est-il judicieux de placer ces attributs en sectionprotectedplutôt qu'enprivatesi l'on souhaite que les classes dérivées y accèdent directement, tout en les interdisant au reste du programme ? -
Écrivez le constructeur paramétré de
Employeutilisant obligatoirement la liste d'initialisation. Ajoutez les accesseursconst(getNom(),getMatricule(),getSalaireBase()), un mutateur sécurisésetSalaireBase(double), une méthodedouble calculerSalaire() constet une méthode d'affichagevoid afficher() const. -
Déclarez la classe
Managerdérivant publiquement deEmploye(class Manager : public Employe). Ajoutez-lui deux attributs privés :double m_primeManagementetint m_nbCollaborateurs. -
Écrivez le constructeur de
Manager. Pourquoi devez-vous impérativement invoquer le constructeur deEmployedans la liste d'initialisation du constructeur deManager? Que se passerait-il si vous oubliez cet appel ? -
Spécialisez la méthode
calculerSalaire() constdansManagerpour qu'elle renvoie la somme du salaire de base et de la prime. -
Redéfinissez la méthode
afficher() constdansManager. Faites en sorte de réutiliser l'affichage générique de la base grâce à l'opérateur de portée (Employe::afficher();) avant d'afficher la prime et le nombre de collaborateurs. -
Créez une seconde classe dérivée
Developpeurajoutantstd::string m_langagePrincipaletdouble m_bonusProjet, avec sa propre version decalculerSalaire()etafficher(). -
Dans le
main(), instanciez un employé, un manager et un développeur. Vérifiez que le développeur peut appeler directement les méthodes de la classe de base (commesetSalaireBase()) pour augmenter sa rémunération.
En C++, les membres d'une classe de base ne peuvent PAS être initialisés directement dans la liste d'initialisation de la fille comme s'ils lui appartenaient. L'instruction suivante est invalide :
Manager(...) : m_nom(nom), m_salaireBase(s) {} // ❌ ERREUR
Il faut obligatoirement déléguer la construction de la base en appelant son constructeur :
Manager(...) : Employe(nom, matricule, s), m_primeManagement(prime), m_nbCollaborateurs(nb) {} // ✅ CORRECT
Correction détaillée & Analyse de conception
Voici l'implémentation complète en C++20. Notez la clarté apportée par l'appel Employe::afficher(); au sein des méthodes redéfinies : cela évite toute duplication de code de formatage tout en étendant fidèlement les fonctionnalités de la base.
/** * TD 2 - Exercice 1 : Héritage Simple, Spécialisation et Visibilités * Modélisation d'une hiérarchie d'employés en C++20 */ #include <iostream> #include <string> #include <iomanip> // 1. Classe de Base class Employe { protected: // Attributs protégés : accessibles directement par les classes dérivées // mais protégés contre toute manipulation directe depuis l'extérieur. std::string m_nom; int m_matricule; double m_salaireBase; public: // Constructeur avec liste d'initialisation Employe(const std::string& nom, int matricule, double salaireBase) : m_nom(nom), m_matricule(matricule), m_salaireBase(salaireBase) {} // Accesseurs const std::string getNom() const { return m_nom; } int getMatricule() const { return m_matricule; } double getSalaireBase() const { return m_salaireBase; } // Mutateur avec contrôle de validité void setSalaireBase(double nouveauSalaire) { if (nouveauSalaire >= 0.0) { m_salaireBase = nouveauSalaire; } } // Calcul de rémunération mensuelle double calculerSalaire() const { return m_salaireBase; } // Affichage des informations générales void afficher() const { std::cout << "[Employe #" << m_matricule << "] " << m_nom << " | Salaire base: " << std::fixed << std::setprecision(2) << m_salaireBase << " €"; } }; // 2. Classe Dérivée : Manager (Héritage public : "Un Manager EST-UN Employe") class Manager : public Employe { private: // Attributs spécifiques au manager double m_primeManagement; int m_nbCollaborateurs; public: // Le constructeur de la dérivée DOIT invoquer le constructeur de la base // dans sa propre liste d'initialisation. Manager(const std::string& nom, int matricule, double salaireBase, double primeManagement, int nbCollaborateurs) : Employe(nom, matricule, salaireBase), m_primeManagement(primeManagement), m_nbCollaborateurs(nbCollaborateurs) {} // Accesseurs spécifiques double getPrime() const { return m_primeManagement; } int getNbCollaborateurs() const { return m_nbCollaborateurs; } void setPrime(double prime) { if (prime >= 0.0) m_primeManagement = prime; } // Spécialisation du calcul de salaire (salaire de base hérité + prime) double calculerSalaire() const { return m_salaireBase + m_primeManagement; } // Redéfinition de la méthode afficher() en réutilisant la méthode parente void afficher() const { // Réutilisation de l'affichage de la base via l'opérateur de résolution de portée :: Employe::afficher(); std::cout << " | Prime: " << m_primeManagement << " €" << " | Équipe: " << m_nbCollaborateurs << " membres" << " | Total: " << calculerSalaire() << " €"; } }; // 3. Classe Dérivée : Developpeur class Developpeur : public Employe { private: std::string m_langagePrincipal; double m_bonusProjet; public: Developpeur(const std::string& nom, int matricule, double salaireBase, const std::string& langage, double bonusProjet = 0.0) : Employe(nom, matricule, salaireBase), m_langagePrincipal(langage), m_bonusProjet(bonusProjet) {} std::string getLangage() const { return m_langagePrincipal; } double getBonus() const { return m_bonusProjet; } void ajouterBonus(double bonus) { if (bonus > 0.0) m_bonusProjet += bonus; } double calculerSalaire() const { return m_salaireBase + m_bonusProjet; } void afficher() const { Employe::afficher(); std::cout << " | Langage: " << m_langagePrincipal << " | Bonus: " << m_bonusProjet << " €" << " | Total: " << calculerSalaire() << " €"; } }; int main() { std::cout << "=== TD 2 - Exercice 1 : Héritage Simple (Employe, Manager, Developpeur) ===\n\n"; // 1. Instanciation d'un Employe standard Employe e1("Claire Dupont", 1001, 2400.0); std::cout << "--- Employé de base ---\n"; e1.afficher(); std::cout << "\n\n"; // 2. Instanciation d'un Manager Manager m1("Alice Bernard", 2001, 3800.0, 750.0, 6); std::cout << "--- Manager (dérivée de Employe) ---\n"; m1.afficher(); std::cout << "\n\n"; // 3. Instanciation d'un Développeur Developpeur d1("Bob Martin", 3001, 3100.0, "C++20", 350.0); std::cout << "--- Développeur (dérivée de Employe) ---\n"; d1.afficher(); std::cout << "\n"; // 4. Utilisation des méthodes héritées de la base std::cout << "\n--- Modification via les méthodes héritées de Employe ---\n"; std::cout << "Augmentation du salaire de base du développeur Bob Martin de 3100 € à 3300 €...\n"; d1.setSalaireBase(3300.0); // Méthode de la classe de base Employe d1.ajouterBonus(150.0); // Méthode propre à Developpeur d1.afficher(); std::cout << "\n"; return 0; }
Résultat de l'exécution :
Chaîne d'Héritage, Cycle de Vie Mémoire (LIFO) & Masquage de Noms
1. Contexte
Lorsqu'une classe dérive d'une classe qui dérive elle-même d'une autre classe, nous formons une chaîne d'héritage multiniveau. Deux questions fondamentales se posent alors :
- Dans quel ordre exact la mémoire est-elle initialisée puis détruite ?
- Que se passe-t-il si une classe fille déclare une méthode portant le même nom qu'une méthode de sa mère, mais avec une liste de paramètres différente ?
En C++, contrairement à Java ou C#, la déclaration d'une méthode dans une classe dérivée masque (Name Hiding) toutes les méthodes portant le même nom dans les classes ancêtres, même si les signatures (paramètres) sont différentes !
2. Travail à réaliser
-
Créez une hiérarchie matérielle à trois niveaux :
- Niveau 1 (Base) :
Composantavec attribut protégéint m_idSerie. - Niveau 2 (Intermédiaire) :
Peripherique : public Composantavec attribut protégéstd::string m_interface(ex:"USB-C"). - Niveau 3 (Dérivée finale) :
ClavierRGB : public Peripheriqueavec attribut privéstd::string m_retroEclairage.
- Niveau 1 (Base) :
-
Dans chacune des trois classes, écrivez un constructeur et un destructeur affichant un message explicite sur
std::coutcomprenant le nom de la classe et l'adresse mémoire de l'objet (this). -
Dans la classe
Composant, écrivez une méthode sans argument :void configurer(). -
Dans la classe
Peripherique, écrivez une méthode avec argument :void configurer(int debitMbps). -
Dans le
main(), instanciez unClavierRGBet tentez d'appelerclavier.configurer();(sans argument). Constatez l'erreur de compilation : pourquoi le compilateur refuse-t-il l'appel alors que la méthode existe dansComposant? -
Corrigez cette anomalie dans
Peripheriqueen utilisant la clauseusing Composant::configurer;pour réintégrer la méthode dans la portée de surcharge. -
Encadrez l'instanciation du clavier dans un bloc délimité par des accolades
{ ... }dansmain(). Avant d'exécuter, écrivez sur une feuille l'ordre exact d'affichage des constructeurs et des destructeurs. Vérifiez ensuite la règle LIFO (Last In, First Out).
En C++, la résolution de nom s'arrête dès que le compilateur trouve une correspondance dans la classe la plus dérivée. S'il trouve configurer(int) dans Peripherique, il ne cherchera pas plus haut dans Composant pour voir s'il existe une surcharge configurer() ! Pour forcer le compilateur à considérer les surcharges de la base, il faut écrire explicitement :
using Composant::configurer;
Correction détaillée & Analyse de l'ordre d'exécution
Remarquez deux observations majeures lors de l'exécution :
1. L'adresse this est rigoureusement identique pour Composant, Peripherique et ClavierRGB car il s'agit d'un objet unique alloué d'un seul tenant sur la pile.
2. Les constructeurs s'exécutent de la base vers la feuille (1 → 2 → 3), tandis que les destructeurs s'exécutent dans l'ordre strictement inverse (3 → 2 → 1).
/** * TD 2 - Exercice 2 : Chaîne d'Héritage, Ordre d'Exécution & Masquage de Noms * Observation pas à pas de l'ordre LIFO et rétablissement de la surcharge avec using */ #include <iostream> #include <string> // Niveau 1 : Classe de Base class Composant { protected: int m_idSerie; public: Composant(int id) : m_idSerie(id) { std::cout << " [1. Composant] Constructeur (#" << m_idSerie << ") @" << this << "\n"; } ~Composant() { std::cout << " [1. Composant] Destructeur (#" << m_idSerie << ") @" << this << "\n"; } int getId() const { return m_idSerie; } void configurer() { std::cout << " -> Configuration générique du composant #" << m_idSerie << "\n"; } }; // Niveau 2 : Classe Intermédiaire class Peripherique : public Composant { protected: std::string m_interface; public: Peripherique(int id, const std::string& interfaceConnexion) : Composant(id), m_interface(interfaceConnexion) { std::cout << " [2. Peripherique] Constructeur (Interface: " << m_interface << ") @" << this << "\n"; } ~Peripherique() { std::cout << " [2. Peripherique] Destructeur (Interface: " << m_interface << ") @" << this << "\n"; } // Rétablissement de la méthode de la base masquée par la surcharge locale using Composant::configurer; // Surcharge de configurer avec un paramètre (débit en Mbps) // En C++, définir cette méthode masque Composant::configurer() à moins d'utiliser 'using' ! void configurer(int debitMbps) { std::cout << " -> Configuration du périphérique #" << m_idSerie << " sur bus " << m_interface << " bridé à " << debitMbps << " Mbps\n"; } }; // Niveau 3 : Classe Dérivée Finale class ClavierRGB : public Peripherique { private: std::string m_retroEclairage; public: ClavierRGB(int id, const std::string& interfaceConnexion, const std::string& profilRGB) : Peripherique(id, interfaceConnexion), m_retroEclairage(profilRGB) { std::cout << " [3. ClavierRGB] Constructeur (RGB: " << m_retroEclairage << ") @" << this << "\n"; } ~ClavierRGB() { std::cout << " [3. ClavierRGB] Destructeur (RGB: " << m_retroEclairage << ") @" << this << "\n"; } void changerRGB(const std::string& nouveauProfil) { m_retroEclairage = nouveauProfil; std::cout << " -> Clavier #" << m_idSerie << " basculé sur le profil RGB : " << m_retroEclairage << "\n"; } void afficherFiche() const { std::cout << "Clavier #" << m_idSerie << " | Bus: " << m_interface << " | Éclairage: " << m_retroEclairage << "\n"; } }; int main() { std::cout << "=== TD 2 - Exercice 2 : Cycle de Vie & Masquage de Noms ===\n\n"; std::cout << "--- 1. Observation de la cascade de construction et destruction ---\n"; std::cout << "Début du bloc local...\n"; { std::cout << "Instanciation d'un ClavierRGB (pile) :\n"; ClavierRGB clavier(4501, "USB-C", "Aura Rainbow"); std::cout << "\nObjet opérationnel en mémoire :\n"; clavier.afficherFiche(); std::cout << "\nTest des méthodes de configuration (grâce à 'using Composant::configurer') :\n"; // Appel de la version sans argument (héritée du niveau 1 Composant) : clavier.configurer(); // Appel de la version avec paramètre (héritée du niveau 2 Peripherique) : clavier.configurer(1000); clavier.changerRGB("Rouge Pulsant"); std::cout << "\nFin du bloc local, sortie de portée imminente...\n"; } std::cout << "Fin du bloc local atteinte (tous les destructeurs ont été exécutés).\n"; return 0; }
Résultat de l'exécution :
Modes de Dérivation (Public, Protected, Private) & Rupture du « EST-UN »
1. Contexte
En C++, le mot-clé placé devant la classe de base lors de la dérivation modifie profondément l'accessibilité des membres hérités et la nature de la relation entre les types :
| Mode d'héritage | Sémantique de conception | Membres publics de la base | Conversion implicite (Upcasting) |
|---|---|---|---|
public |
Sous-typage : relation stricte « EST-UN » | Restent publics | Autorisée (Base* ptr = &fille;) |
protected |
Spécialisation restreinte aux sous-classes | Deviennent protégés | Refusée pour l'extérieur |
private |
Héritage d'implémentation : « Implémenté en termes de » | Deviennent privés | Strictement interdite |
Nous souhaitons concevoir un conteneur générique TableauFixe, puis observer ce qui se produit lorsqu'on en dérive une structure TableauStatistique (héritage public légitime) et une structure PileEntiers (où l'accès aléatoire aux indices violerait les invariants d'une pile LIFO).
2. Travail à réaliser
-
Créez la classe
TableauFixeencapsulant un tableau statiqueint m_donnees[50]etstd::size_t m_taille. Proposez les méthodes publiquesinsererFin(int),insererIndex(index, val),supprimerFin(),get(index) const,getTaille() constetafficher() const. -
Dérivez la classe
TableauStatistiquepar héritage public :class TableauStatistique : public TableauFixe. Ajoutez-lui des méthodes de calcul statistique :moyenne() const,minimum() const,maximum() const. -
Écrivez une fonction libre :
void inspecterConteneur(const TableauFixe& tab);. Vérifiez qu'on peut lui passer une instance deTableauStatistiquesans la moindre erreur. Pourquoi est-ce possible ? -
Dérivez la classe
PileEntierspar héritage privé :class PileEntiers : private TableauFixe. Pourquoi le choix de l'héritage privé est-il indispensable ici pour préserver la cohérence d'une pile ? -
Dans
PileEntiers, implémentez l'interface d'une pile stricte :empiler(val),depiler(),sommet() const,estVide() constetafficherPile() consten appelant les méthodes internes deTableauFixe. -
Utilisez la clause
using TableauFixe::getTaille;dans la section publique dePileEntierspour réexposer uniquement la taille aux utilisateurs de la pile, sans réexposer les méthodes d'insertion indicée. -
Dans le
main(), tentez d'appelerpile.insererIndex(1, 999);et observez l'erreur du compilateur. Tentez ensuite d'appelerinspecterConteneur(pile);. Expliquez le refus du compilateur concernant l'upcasting.
L'héritage privé ne modélise PAS un sous-typage. Il signifie : "J'utilise le code et les algorithmes de la classe de base pour m'implémenter, mais je refuse absolument que le monde extérieur me traite comme une instance de cette base". Dans la plupart des architectures modernes, la composition (avoir un membre TableauFixe m_tab;) est préférée à l'héritage privé, sauf si la classe dérivée a besoin de redéfinir des méthodes virtuelles de la base.
Correction détaillée & Analyse de conception
Ce corrigé met en évidence la puissance du contrôle d'accès en C++ : la pile bénéficie de l'implémentation de stockage de TableauFixe sans exposer ses failles d'accès indicé, et le compilateur bloque net toute tentative de cast illégitime.
/** * TD 2 - Exercice 3 : Modes de Dérivation (public, protected, private) * Rupture de la relation « EST-UN » et héritage d'implémentation */ #include <iostream> #include <stdexcept> #include <numeric> #include <algorithm> // 1. Classe de Base : Conteneur brut avec accès indicé libre class TableauFixe { protected: static constexpr std::size_t CAPACITE_MAX = 50; int m_donnees[CAPACITE_MAX]; std::size_t m_taille; public: TableauFixe() : m_taille(0) {} std::size_t getTaille() const { return m_taille; } std::size_t getCapacite() const { return CAPACITE_MAX; } void insererFin(int valeur) { if (m_taille >= CAPACITE_MAX) { throw std::overflow_error("TableauFixe plein : capacité maximale atteinte."); } m_donnees[m_taille++] = valeur; } void insererIndex(std::size_t index, int valeur) { if (m_taille >= CAPACITE_MAX) { throw std::overflow_error("TableauFixe plein."); } if (index > m_taille) { throw std::out_of_range("Index hors limites lors de l'insertion."); } for (std::size_t i = m_taille; i > index; --i) { m_donnees[i] = m_donnees[i - 1]; } m_donnees[index] = valeur; ++m_taille; } void supprimerFin() { if (m_taille == 0) { throw std::underflow_error("Tableau vide, impossible de supprimer."); } --m_taille; } int get(std::size_t index) const { if (index >= m_taille) { throw std::out_of_range("Index hors limites."); } return m_donnees[index]; } void afficher() const { std::cout << "[ "; for (std::size_t i = 0; i < m_taille; ++i) { std::cout << m_donnees[i] << " "; } std::cout << "] (taille = " << m_taille << ")\n"; } }; // 2. Dérivation PUBLIQUE : "Un TableauStatistique EST-UN TableauFixe" // Tous les membres publics de TableauFixe restent publics pour les clients. class TableauStatistique : public TableauFixe { public: // Constructeur par défaut appelant le constructeur de la base TableauStatistique() : TableauFixe() {} // Ajout de calculs statistiques double moyenne() const { if (m_taille == 0) return 0.0; double somme = 0.0; for (std::size_t i = 0; i < m_taille; ++i) { somme += m_donnees[i]; } return somme / m_taille; } int minimum() const { if (m_taille == 0) throw std::logic_error("Tableau vide"); int minVal = m_donnees[0]; for (std::size_t i = 1; i < m_taille; ++i) { if (m_donnees[i] < minVal) minVal = m_donnees[i]; } return minVal; } int maximum() const { if (m_taille == 0) throw std::logic_error("Tableau vide"); int maxVal = m_donnees[0]; for (std::size_t i = 1; i < m_taille; ++i) { if (m_donnees[i] > maxVal) maxVal = m_donnees[i]; } return maxVal; } }; // 3. Dérivation PRIVÉE : "Une Pile N'EST PAS un TableauFixe, elle est implémentée avec" // L'accès direct aux indices intermédiaires violerait le principe fondamental LIFO. // Tous les membres hérités deviennent privés dans PileEntiers. class PileEntiers : private TableauFixe { public: PileEntiers() : TableauFixe() {} // Exposition sélective et contrôlée de la méthode getTaille de la base using TableauFixe::getTaille; // Interface stricte de pile LIFO void empiler(int val) { insererFin(val); // Appel autorisé en interne } int depiler() { if (estVide()) { throw std::underflow_error("Dépilement impossible : la pile est vide !"); } int val = m_donnees[m_taille - 1]; supprimerFin(); return val; } int sommet() const { if (estVide()) { throw std::underflow_error("Pile vide, aucun sommet disponible."); } return m_donnees[m_taille - 1]; } bool estVide() const { return m_taille == 0; } void afficherPile() const { std::cout << "Pile (sommet -> bas) : [ "; for (std::size_t i = m_taille; i > 0; --i) { std::cout << m_donnees[i - 1] << " "; } std::cout << "]\n"; } }; // Fonction externe attendant un TableauFixe par référence void inspecterConteneur(const TableauFixe& tab) { std::cout << "Conteneur inspecté : taille=" << tab.getTaille() << " -> "; tab.afficher(); } int main() { std::cout << "=== TD 2 - Exercice 3 : Modes d'Héritage (public vs private) ===\n\n"; // 1. Dérivation Publique std::cout << "--- 1. TableauStatistique (Héritage public : IS-A TableauFixe) ---\n"; TableauStatistique stats; stats.insererFin(12); stats.insererFin(45); stats.insererFin(8); stats.insererFin(27); stats.insererIndex(2, 99); // Méthode publique de la base accessible ! std::cout << "Données : "; stats.afficher(); std::cout << "Moyenne : " << stats.moyenne() << "\n"; std::cout << "Min : " << stats.minimum() << " | Max : " << stats.maximum() << "\n"; std::cout << "\nConversion implicite vers la base (Upcasting autorisé car public) :\n"; inspecterConteneur(stats); // ✅ Valide : TableauStatistique EST un TableauFixe // 2. Dérivation Privée std::cout << "\n--- 2. PileEntiers (Héritage privé : Implémentation masquée) ---\n"; PileEntiers pile; pile.empiler(10); pile.empiler(20); pile.empiler(30); pile.empiler(40); pile.afficherPile(); std::cout << "Taille de la pile (via 'using TableauFixe::getTaille') : " << pile.getTaille() << "\n"; std::cout << "Sommet actuel : " << pile.sommet() << "\n"; std::cout << "Dépilement : " << pile.depiler() << "\n"; std::cout << "Dépilement : " << pile.depiler() << "\n"; std::cout << "Après 2 dépilements : "; pile.afficherPile(); // DÉMONSTRATION DES SÉCURITÉS À LA COMPILATION : // pile.insererIndex(1, 999); // ❌ ERREUR COMPILATION : 'insererIndex' is a private member of 'TableauFixe' // // inspecterConteneur(pile); // ❌ ERREUR COMPILATION : 'TableauFixe' is an inaccessible base of 'PileEntiers' // Car la relation EST-UN n'existe pas pour l'extérieur ! std::cout << "\nProtection vérifiée : l'utilisateur externe ne peut pas corrompre la pile\n" << "ni forcer un upcasting vers TableauFixe.\n"; return 0; }
Résultat de l'exécution :
Héritage Multiple : Fusion de Comportements, Conflits de Noms & Décalages Mémoire
1. Contexte
En C++, une classe peut hériter de plusieurs classes mères indépendantes. C'est l'héritage multiple :
class CopieurMultifonction : public Imprimante, public Scanner
Ce mécanisme très puissant soulève deux défis majeurs pour le développeur :
- Le conflit de noms : si
ImprimanteetScannerdéfinissent toutes deux une méthodereinitialiser()ouafficherStatut(), l'appel directcopieur.afficherStatut();est rejeté pour ambiguïté par le compilateur. - L'organisation physique en mémoire : un objet multifonction regroupe en mémoire deux sous-objets distincts placés l'un après l'autre. Le pointeur vers la seconde base subit donc un décalage d'adresse (offset) non nul !
2. Travail à réaliser
-
Créez la classe
Imprimanteavec les attributs protégésint m_resolutionDPIetint m_pagesImprimees. Équipez-la des méthodesimprimer(const std::string& doc, int copies),reinitialiser()etafficherStatut() const. -
Créez la classe
Scanneravec les attributs protégésint m_vitessePPMetint m_pagesScannees. Équipez-la des méthodesnumeriser(const std::string& doc),reinitialiser()etafficherStatut() const. -
Concevez la classe
CopieurMultifonctionhéritant publiquement à la fois deImprimanteet deScanner. Ajoutez-lui un attribut proprestd::string m_modele. -
Dans le constructeur de
CopieurMultifonction, initialisez les deux classes de base. Dans quel ordre précis les constructeurs de base sont-ils exécutés ? L'ordre d'écriture dans la liste d'initialisation a-t-il une influence ? -
Implémentez une méthode combinée
photocopier(const std::string& doc, int copies)qui appelle successivementnumeriser()puisimprimer(). -
Résolution de collision n°1 (Appel externe ciblé) :
Montrez comment un utilisateur externe peut cibler uniquement la réinitialisation du module scanner grâce à l'opérateur de portée :copieur.Scanner::reinitialiser();. -
Résolution de collision n°2 (Redéfinition unificatrice) :
Redéfinissez la méthodeafficherStatut() constetreinitialiser()dansCopieurMultifonctionpour qu'elles fédèrent les actions des deux sous-systèmes sans ambiguïté. -
Exploration de la mémoire :
Récupérez les adresses de l'objet complet et des deux sous-objets via conversion de pointeurs :const CopieurMultifonction* pC = &copieur;const Imprimante* pI = &copieur;const Scanner* pS = &copieur;
Affichez ces adresses et calculez l'écart (offset) en octets entre le début du copieur et le sous-objetScanner.
Lorsqu'un pointeur CopieurMultifonction* est converti en Scanner*, le compilateur C++ effectue automatiquement une arithmétique de pointeur à l'exécution en ajoutant la taille du sous-objet Imprimante qui précède en mémoire. L'adresse change de valeur tout en désignant le même objet composite !
Correction détaillée & Explication des mécanismes
Le code ci-dessous illustre la résolution des deux collisions et démontre expérimentalement le décalage d'adresse de 8 octets correspondant à l'espace occupé par la première base Imprimante (deux entiers de 4 octets) :
/** * TD 2 - Exercice 4 : Héritage Multiple, Conflits de Noms & Agencement Mémoire * Combinaison d'interfaces et désambiguïsation par l'opérateur de portée :: */ #include <iostream> #include <string> // 1. Première classe de base : Imprimante class Imprimante { protected: int m_resolutionDPI; int m_pagesImprimees; public: Imprimante(int dpi) : m_resolutionDPI(dpi), m_pagesImprimees(0) { std::cout << " [+] Constructeur Imprimante @" << this << " (DPI: " << m_resolutionDPI << ")\n"; } ~Imprimante() { std::cout << " [-] Destructeur Imprimante @" << this << "\n"; } void imprimer(const std::string& doc, int copies = 1) { m_pagesImprimees += copies; std::cout << " [Imprimante] Impression de \"" << doc << "\" (" << copies << " copie(s) à " << m_resolutionDPI << " DPI)\n"; } // Méthode générant un conflit de nom avec Scanner void reinitialiser() { m_pagesImprimees = 0; std::cout << " [Imprimante] Compteur de pages imprimées remis à zéro.\n"; } // Deuxième méthode en conflit void afficherStatut() const { std::cout << " [Statut Imprimante @" << this << "] DPI: " << m_resolutionDPI << " | Pages imprimées: " << m_pagesImprimees << "\n"; } }; // 2. Deuxième classe de base : Scanner class Scanner { protected: int m_vitessePPM; // Pages par minute int m_pagesScannees; public: Scanner(int ppm) : m_vitessePPM(ppm), m_pagesScannees(0) { std::cout << " [+] Constructeur Scanner @" << this << " (Vitesse: " << m_vitessePPM << " PPM)\n"; } ~Scanner() { std::cout << " [-] Destructeur Scanner @" << this << "\n"; } void numeriser(const std::string& doc) { ++m_pagesScannees; std::cout << " [Scanner] Numérisation du document \"" << doc << "\" (débit " << m_vitessePPM << " PPM)\n"; } // Méthode homonyme en conflit void reinitialiser() { m_pagesScannees = 0; std::cout << " [Scanner] Compteur de pages numérisées remis à zéro.\n"; } // Deuxième méthode en conflit void afficherStatut() const { std::cout << " [Statut Scanner @" << this << "] Débit: " << m_vitessePPM << " PPM | Pages numérisées: " << m_pagesScannees << "\n"; } }; // 3. Classe Dérivée par Héritage Multiple : CopieurMultifonction // Note : L'ordre de déclaration ci-dessous détermine l'ordre d'appel des constructeurs ! class CopieurMultifonction : public Imprimante, public Scanner { private: std::string m_modele; public: // Constructeur : invoque Imprimante puis Scanner CopieurMultifonction(const std::string& modele, int dpi, int ppm) : Imprimante(dpi), Scanner(ppm), m_modele(modele) { std::cout << "[+] CopieurMultifonction \"" << m_modele << "\" initialisé @" << this << "\n"; } ~CopieurMultifonction() { std::cout << "[-] Destructeur CopieurMultifonction \"" << m_modele << "\" @" << this << "\n"; } // Opération combinée utilisant les deux facettes héritées void photocopier(const std::string& docOriginal, int nbCopies) { std::cout << "\n>>> Démarrage d'une photocopie combinée (" << nbCopies << " copies) <<<\n"; // Appel de la méthode héritée de Scanner numeriser(docOriginal); // Appel de la méthode héritée d'Imprimante imprimer("Copie de " + docOriginal, nbCopies); } // Résolution de l'ambiguïté pour afficherStatut() : // On redéfinit la méthode dans la classe dérivée pour fédérer les deux statuts void afficherStatut() const { std::cout << "\n=== Statut Synthétique Copieur [" << m_modele << "] ===\n"; Imprimante::afficherStatut(); Scanner::afficherStatut(); } // Résolution de l'ambiguïté pour reinitialiser() void reinitialiser() { std::cout << "\n=== Réinitialisation globale de tous les modules ===\n"; Imprimante::reinitialiser(); Scanner::reinitialiser(); } }; int main() { std::cout << "=== TD 2 - Exercice 4 : Héritage Multiple & Conflits de Noms ===\n\n"; std::cout << "--- 1. Instanciation et analyse de la disposition mémoire (Offsets) ---\n"; { CopieurMultifonction copieur("OfficeJet Pro 9000", 1200, 35); // Analyse avancée des pointeurs et décalage d'adresse (Offset) const CopieurMultifonction* ptrCopieur = &copieur; const Imprimante* ptrImprimante = &copieur; const Scanner* ptrScanner = &copieur; std::cout << "\nAnalyse des adresses des sous-objets en mémoire :\n"; std::cout << " Adresse de l'objet complet CopieurMultifonction : " << ptrCopieur << "\n"; std::cout << " Adresse du sous-objet Imprimante (1ère base) : " << ptrImprimante << "\n"; std::cout << " Adresse du sous-objet Scanner (2ème base) : " << ptrScanner << "\n"; std::cout << " -> Décalage (offset) de Scanner : " << (reinterpret_cast<const char*>(ptrScanner) - reinterpret_cast<const char*>(ptrCopieur)) << " octets.\n"; // Utilisation des services combinés copieur.photocopier("Contrat_Confidentialite.pdf", 3); // Affichage synthétique via la méthode redéfinie unificatrice copieur.afficherStatut(); // 2. Levée d'ambiguïté explicite depuis l'extérieur sans redéfinition std::cout << "\n--- 2. Appel ciblé avec l'opérateur de portée (::) ---\n"; std::cout << "Réinitialisation UNIQUEMENT du module scanner :\n"; copieur.Scanner::reinitialiser(); copieur.afficherStatut(); std::cout << "\nSortie de portée : destruction en ordre inverse...\n"; } return 0; }
Résultat de l'exécution :
Le Problème du Diamant (Diamond of Death) & Résolution par Héritage Virtuel
1. Contexte
Le problème du diamant (Diamond Problem) se produit lorsqu'une classe terminale D dérive par héritage multiple de deux classes intermédiaires B et C, qui héritent elles-mêmes d'une base commune ancêtre A :
m_immatriculation, m_puissanceKW
m_tirantDEau
m_nbPlaces
m_heliceDeployee, m_nomModele
Sans précaution particulière, un objet VehiculeAmphibie hébergerait deux exemplaires distincts de Vehicule en mémoire ! Cela entraînerait :
- Une duplication inutile des attributs d'immatriculation et de puissance.
- Des incohérences graves (modifier la puissance du côté
Bateaune modifierait pas celle du côtéAutomobile!). - L'impossibilité d'effectuer un Upcasting vers
Vehicule&sans ambiguïté.
2. Travail à réaliser
-
Créez la classe de base sommet
Vehiculecontenantstd::string m_immatriculationetint m_puissanceKW. Munissez-la d'un constructeur avec message de traçage, d'un destructeur virtuelvirtual ~Vehicule(), d'accesseurs/mutateurs et d'une méthodeafficherVehicule() const. -
Créez la classe intermédiaire
Bateauavec son attributdouble m_tirantDEau. Faites-la dériver virtuellement deVehicule:class Bateau : virtual public Vehicule. -
Créez la classe intermédiaire
Automobileavec son attributint m_nbPlaces. Faites-la dériver également virtuellement deVehicule:class Automobile : virtual public Vehicule. -
Créez la classe terminale
VehiculeAmphibie : public Bateau, public Automobile. Ajoutez-lui les attributsstd::string m_nomModeleetbool m_heliceDeployee, ainsi qu'une méthode de basculebasculerModeNautique(). -
Règle fondamentale de l'Héritage Virtuel :
Dans le constructeur deVehiculeAmphibie, appelez explicitement le constructeur deVehicule(immat, kw), en plus de ceux deBateauetAutomobile. Expliquez pourquoi c'est la classe terminale qui doit obligatoirement initialiser la base virtuelle commune, et ce qu'il advient des appels àVehicule(...)écrits dans les constructeurs deBateauetAutomobile. -
Vérifiez l'unicité de l'état : modifiez la puissance du véhicule amphibie via
amph.setPuissanceKW(220);et constatez que cette modification est partagée sans la moindre désambiguïsation. -
Écrivez une fonction externe
void controleTechnique(const Vehicule& v);. Passez-lui directement votre instance deVehiculeAmphibie. Pourquoi cette conversion polymorphique ascendante (upcasting) est-elle instantanément acceptée par le compilateur, alors qu'elle aurait été refusée sans le mot-clévirtual? -
Affichez les adresses mémoire des sous-objets
ptrAmph,ptrBateau,ptrAutoetptrVehicule. Que remarquez-vous sur la position de l'ancêtre virtuel ?
Correction détaillée & Analyse architecturale
Voici la solution complète résolvant le diamant d'héritage. Observez attentivement la sortie console : le constructeur de Vehicule n'est exécuté qu'une seule fois, au tout début de la création de VehiculeAmphibie.
/** * TD 2 - Exercice 5 : Le Problème du Diamant & L'Héritage Virtuel * Résolution du "Diamond of Death", unicité de l'ancêtre commun * et responsabilité d'initialisation de la classe la plus dérivée. */ #include <iostream> #include <string> // 1. Sommet du Diamant : Classe de Base Commune class Vehicule { protected: std::string m_immatriculation; int m_puissanceKW; public: Vehicule(const std::string& immat, int kw) : m_immatriculation(immat), m_puissanceKW(kw) { std::cout << " [1. SOMMET Vehicule] Constructeur @" << this << " | Immat: " << m_immatriculation << ", Puissance: " << m_puissanceKW << " kW\n"; } virtual ~Vehicule() { std::cout << " [1. SOMMET Vehicule] Destructeur @" << this << " (Immat: " << m_immatriculation << ")\n"; } std::string getImmatriculation() const { return m_immatriculation; } int getPuissanceKW() const { return m_puissanceKW; } void setPuissanceKW(int kw) { m_puissanceKW = kw; } void afficherVehicule() const { std::cout << " [Base Vehicule @" << this << "] Immat: " << m_immatriculation << " (" << m_puissanceKW << " kW)\n"; } }; // 2. Branche Gauche : Bateau avec Héritage VIRTUEL class Bateau : virtual public Vehicule { protected: double m_tirantDEau; // en mètres public: Bateau(const std::string& immat, int kw, double tirantDEau) : Vehicule(immat, kw), m_tirantDEau(tirantDEau) { std::cout << " [2. BRANCHE GAUCHE Bateau] Constructeur @" << this << " | Tirant d'eau: " << m_tirantDEau << " m\n"; } ~Bateau() override { std::cout << " [2. BRANCHE GAUCHE Bateau] Destructeur @" << this << "\n"; } void naviguer() const { std::cout << " -> Navigation maritime : " << m_immatriculation << " navigue avec " << m_tirantDEau << " m de tirant d'eau.\n"; } }; // 3. Branche Droite : Automobile avec Héritage VIRTUEL class Automobile : virtual public Vehicule { protected: int m_nbPlaces; public: Automobile(const std::string& immat, int kw, int nbPlaces) : Vehicule(immat, kw), m_nbPlaces(nbPlaces) { std::cout << " [2. BRANCHE DROITE Automobile] Constructeur @" << this << " | Places: " << m_nbPlaces << "\n"; } ~Automobile() override { std::cout << " [2. BRANCHE DROITE Automobile] Destructeur @" << this << "\n"; } void rouler() const { std::cout << " -> Conduite sur route : " << m_immatriculation << " roule (" << m_nbPlaces << " places à bord).\n"; } }; // 4. Pointe du Diamant : Classe Terminale VehiculeAmphibie class VehiculeAmphibie : public Bateau, public Automobile { private: std::string m_nomModele; bool m_heliceDeployee; public: // ⚡ RÈGLE FONDAMENTALE DE L'HÉRITAGE VIRTUEL : // C'est obligatoirement la classe LA PLUS DÉRIVÉE (ici VehiculeAmphibie) // qui doit invoquer le constructeur de la base virtuelle commune Vehicule(immat, kw). // Les appels Vehicule(...) situés dans Bateau et Automobile sont purement ignorés ! VehiculeAmphibie(const std::string& immat, int kw, double tirantDEau, int nbPlaces, const std::string& modele) : Vehicule(immat, kw), // 1. Initialisation de la base virtuelle unique Bateau(immat, kw, tirantDEau), // 2. Initialisation branche Bateau Automobile(immat, kw, nbPlaces), // 3. Initialisation branche Automobile m_nomModele(modele), m_heliceDeployee(false) { std::cout << "[3. POINTE VehiculeAmphibie] Prêt : \"" << m_nomModele << "\" @" << this << "\n"; } ~VehiculeAmphibie() override { std::cout << "[-] Destructeur VehiculeAmphibie \"" << m_nomModele << "\" @" << this << "\n"; } void basculerModeNautique() { m_heliceDeployee = !m_heliceDeployee; std::cout << " -> Mode nautique " << (m_heliceDeployee ? "ACTIVÉ (Hélice déployée)" : "DÉSACTIVÉ (Roues motrices)") << "\n"; } void afficherFicheComplete() const { std::cout << "\n================ Fiche Technique Amphibie ================\n"; std::cout << "Modèle : " << m_nomModele << "\n"; // ✅ Aucun conflit d'accès ! getImmatriculation() et m_puissanceKW sont uniques ! std::cout << "Immat : " << getImmatriculation() << "\n"; std::cout << "Puissance : " << getPuissanceKW() << " kW (" << m_puissanceKW << ")\n"; std::cout << "Tirant d'eau : " << m_tirantDEau << " m\n"; std::cout << "Capacité : " << m_nbPlaces << " passagers\n"; std::cout << "Hélice active : " << (m_heliceDeployee ? "Oui" : "Non") << "\n"; std::cout << "==========================================================\n"; } }; // Fonction démontrant le polymorphisme ascendant (Upcasting) vers le sommet du diamant void controleTechnique(const Vehicule& v) { std::cout << ">> [Contrôle Technique] Validation du véhicule Immat : " << v.getImmatriculation() << " (" << v.getPuissanceKW() << " kW)\n"; } int main() { std::cout << "=== TD 2 - Exercice 5 : Le Diamant d'Héritage et sa Résolution Virtuelle ===\n\n"; std::cout << "--- 1. Instanciation du véhicule amphibie (construction de la hiérarchie) ---\n"; { VehiculeAmphibie amph("FR-AMPH-2026", 180, 0.85, 6, "AquaTerra Cruiser"); std::cout << "\n--- 2. Analyse de l'unicité du sous-objet virtuel Vehicule ---\n"; // Vérification des adresses : const VehiculeAmphibie* ptrAmph = &h; const Bateau* ptrBat = &h; const Automobile* ptrAuto = &h; const Vehicule* ptrVehicule = &h; // ✅ Possible UNIQUEMENT grâce à virtual ! std::cout << "Adresse globale VehiculeAmphibie : " << ptrAmph << "\n"; std::cout << "Adresse sous-objet Bateau : " << ptrBat << "\n"; std::cout << "Adresse sous-objet Automobile : " << ptrAuto << "\n"; std::cout << "Adresse sous-objet commun Vehicule: " << ptrVehicule << "\n"; amph.afficherFicheComplete(); std::cout << "\n--- 3. Démonstration de cohérence d'état unique ---\n"; std::cout << "Modification de la puissance à 220 kW...\n"; amph.setPuissanceKW(220); // Modifie l'unique exemplaire de m_puissanceKW ! // Actions combinées des deux branches amph.rouler(); amph.basculerModeNautique(); amph.naviguer(); std::cout << "\n--- 4. Upcasting vers la base commune Vehicule ---\n"; // Sans héritage virtuel, l'instruction suivante provoquerait une erreur : // "Vehicule is an ambiguous base of VehiculeAmphibie" controleTechnique(amph); std::cout << "\n--- 5. Destruction automatique (LIFO) en sortie de bloc ---\n"; } return 0; }
Résultat de l'exécution :
🎉 Félicitations : Vous maîtrisez l'Héritage C++ sous toutes ses formes !
Au terme de ce TD 2, vous possédez une compréhension solide des arcanes de la modélisation hiérarchique en C++ :
- Vous savez concevoir des hiérarchies en héritage simple en exploitant
protected, la liste d'initialisation et la spécialisation de comportements. - Vous maîtrisez l'ordonnancement rigoureux du cycle de vie (LIFO) et savez résoudre le masquage de noms avec
using. - Vous comprenez la distinction capitale entre sous-typage public (relation « EST-UN ») et héritage d'implémentation privé.
- Vous savez gérer l'héritage multiple, lever les ambiguïtés homonymes et anticiper les décalages de pointeurs en mémoire.
- Vous maîtrisez la résolution sans faille du problème du diamant grâce à l'héritage virtuel.