Niveau : Licence 3 Fiche TD n°2 Volume : 2h00 Pratique C++20

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 private et protected, 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.

Exercice 1

Héritage Simple & Spécialisation : Hiérarchie d'Employés

★☆☆☆☆ Difficulté : Débutant ⏱️ 20 min Concepts : Héritage public, protected, Liste d'initialisation, Redéfinition

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

  1. Définissez la classe de base Employe contenant trois attributs : std::string m_nom, int m_matricule et double m_salaireBase. Pourquoi est-il judicieux de placer ces attributs en section protected plutôt qu'en private si l'on souhaite que les classes dérivées y accèdent directement, tout en les interdisant au reste du programme ?
  2. Écrivez le constructeur paramétré de Employe utilisant obligatoirement la liste d'initialisation. Ajoutez les accesseurs const (getNom(), getMatricule(), getSalaireBase()), un mutateur sécurisé setSalaireBase(double), une méthode double calculerSalaire() const et une méthode d'affichage void afficher() const.
  3. Déclarez la classe Manager dérivant publiquement de Employe (class Manager : public Employe). Ajoutez-lui deux attributs privés : double m_primeManagement et int m_nbCollaborateurs.
  4. Écrivez le constructeur de Manager. Pourquoi devez-vous impérativement invoquer le constructeur de Employe dans la liste d'initialisation du constructeur de Manager ? Que se passerait-il si vous oubliez cet appel ?
  5. Spécialisez la méthode calculerSalaire() const dans Manager pour qu'elle renvoie la somme du salaire de base et de la prime.
  6. Redéfinissez la méthode afficher() const dans Manager. 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.
  7. Créez une seconde classe dérivée Developpeur ajoutant std::string m_langagePrincipal et double m_bonusProjet, avec sa propre version de calculerSalaire() et afficher().
  8. 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 (comme setSalaireBase()) pour augmenter sa rémunération.
💡 Indice : Liste d'initialisation et classe de base

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.

CPPtd2_ex1_heritage_simple.cpp
  1. /**
  2.  * TD 2 - Exercice 1 : Héritage Simple, Spécialisation et Visibilités
  3.  * Modélisation d'une hiérarchie d'employés en C++20
  4.  */
  5.  
  6. #include <iostream>
  7. #include <string>
  8. #include <iomanip>
  9.  
  10. // 1. Classe de Base
  11. class Employe {
  12. protected:
  13. // Attributs protégés : accessibles directement par les classes dérivées
  14. // mais protégés contre toute manipulation directe depuis l'extérieur.
  15. std::string m_nom;
  16. int m_matricule;
  17. double m_salaireBase;
  18.  
  19. public:
  20. // Constructeur avec liste d'initialisation
  21. Employe(const std::string& nom, int matricule, double salaireBase)
  22. : m_nom(nom), m_matricule(matricule), m_salaireBase(salaireBase) {}
  23.  
  24. // Accesseurs const
  25. std::string getNom() const { return m_nom; }
  26. int getMatricule() const { return m_matricule; }
  27. double getSalaireBase() const { return m_salaireBase; }
  28.  
  29. // Mutateur avec contrôle de validité
  30. void setSalaireBase(double nouveauSalaire) {
  31. if (nouveauSalaire >= 0.0) {
  32. m_salaireBase = nouveauSalaire;
  33. }
  34. }
  35.  
  36. // Calcul de rémunération mensuelle
  37. double calculerSalaire() const {
  38. return m_salaireBase;
  39. }
  40.  
  41. // Affichage des informations générales
  42. void afficher() const {
  43. std::cout << "[Employe #" << m_matricule << "] " << m_nom
  44. << " | Salaire base: " << std::fixed << std::setprecision(2) << m_salaireBase << " €";
  45. }
  46. };
  47.  
  48. // 2. Classe Dérivée : Manager (Héritage public : "Un Manager EST-UN Employe")
  49. class Manager : public Employe {
  50. private:
  51. // Attributs spécifiques au manager
  52. double m_primeManagement;
  53. int m_nbCollaborateurs;
  54.  
  55. public:
  56. // Le constructeur de la dérivée DOIT invoquer le constructeur de la base
  57. // dans sa propre liste d'initialisation.
  58. Manager(const std::string& nom, int matricule, double salaireBase,
  59. double primeManagement, int nbCollaborateurs)
  60. : Employe(nom, matricule, salaireBase),
  61. m_primeManagement(primeManagement),
  62. m_nbCollaborateurs(nbCollaborateurs) {}
  63.  
  64. // Accesseurs spécifiques
  65. double getPrime() const { return m_primeManagement; }
  66. int getNbCollaborateurs() const { return m_nbCollaborateurs; }
  67.  
  68. void setPrime(double prime) {
  69. if (prime >= 0.0) m_primeManagement = prime;
  70. }
  71.  
  72. // Spécialisation du calcul de salaire (salaire de base hérité + prime)
  73. double calculerSalaire() const {
  74. return m_salaireBase + m_primeManagement;
  75. }
  76.  
  77. // Redéfinition de la méthode afficher() en réutilisant la méthode parente
  78. void afficher() const {
  79. // Réutilisation de l'affichage de la base via l'opérateur de résolution de portée ::
  80. Employe::afficher();
  81. std::cout << " | Prime: " << m_primeManagement << " €"
  82. << " | Équipe: " << m_nbCollaborateurs << " membres"
  83. << " | Total: " << calculerSalaire() << " €";
  84. }
  85. };
  86.  
  87. // 3. Classe Dérivée : Developpeur
  88. class Developpeur : public Employe {
  89. private:
  90. std::string m_langagePrincipal;
  91. double m_bonusProjet;
  92.  
  93. public:
  94. Developpeur(const std::string& nom, int matricule, double salaireBase,
  95. const std::string& langage, double bonusProjet = 0.0)
  96. : Employe(nom, matricule, salaireBase),
  97. m_langagePrincipal(langage),
  98. m_bonusProjet(bonusProjet) {}
  99.  
  100. std::string getLangage() const { return m_langagePrincipal; }
  101. double getBonus() const { return m_bonusProjet; }
  102.  
  103. void ajouterBonus(double bonus) {
  104. if (bonus > 0.0) m_bonusProjet += bonus;
  105. }
  106.  
  107. double calculerSalaire() const {
  108. return m_salaireBase + m_bonusProjet;
  109. }
  110.  
  111. void afficher() const {
  112. Employe::afficher();
  113. std::cout << " | Langage: " << m_langagePrincipal
  114. << " | Bonus: " << m_bonusProjet << " €"
  115. << " | Total: " << calculerSalaire() << " €";
  116. }
  117. };
  118.  
  119. int main() {
  120. std::cout << "=== TD 2 - Exercice 1 : Héritage Simple (Employe, Manager, Developpeur) ===\n\n";
  121.  
  122. // 1. Instanciation d'un Employe standard
  123. Employe e1("Claire Dupont", 1001, 2400.0);
  124. std::cout << "--- Employé de base ---\n";
  125. e1.afficher();
  126. std::cout << "\n\n";
  127.  
  128. // 2. Instanciation d'un Manager
  129. Manager m1("Alice Bernard", 2001, 3800.0, 750.0, 6);
  130. std::cout << "--- Manager (dérivée de Employe) ---\n";
  131. m1.afficher();
  132. std::cout << "\n\n";
  133.  
  134. // 3. Instanciation d'un Développeur
  135. Developpeur d1("Bob Martin", 3001, 3100.0, "C++20", 350.0);
  136. std::cout << "--- Développeur (dérivée de Employe) ---\n";
  137. d1.afficher();
  138. std::cout << "\n";
  139.  
  140. // 4. Utilisation des méthodes héritées de la base
  141. std::cout << "\n--- Modification via les méthodes héritées de Employe ---\n";
  142. std::cout << "Augmentation du salaire de base du développeur Bob Martin de 3100 € à 3300 €...\n";
  143. d1.setSalaireBase(3300.0); // Méthode de la classe de base Employe
  144. d1.ajouterBonus(150.0); // Méthode propre à Developpeur
  145. d1.afficher();
  146. std::cout << "\n";
  147.  
  148. return 0;
  149. }

Résultat de l'exécution :

=== TD 2 - Exercice 1 : Héritage Simple (Employe, Manager, Developpeur) === --- Employé de base --- [Employe #1001] Claire Dupont | Salaire base: 2400.00 € --- Manager (dérivée de Employe) --- [Employe #2001] Alice Bernard | Salaire base: 3800.00 € | Prime: 750.00 € | Équipe: 6 membres | Total: 4550.00 € --- Développeur (dérivée de Employe) --- [Employe #3001] Bob Martin | Salaire base: 3100.00 € | Langage: C++20 | Bonus: 350.00 € | Total: 3450.00 € --- Modification via les méthodes héritées de Employe --- Augmentation du salaire de base du développeur Bob Martin de 3100 € à 3300 €... [Employe #3001] Bob Martin | Salaire base: 3300.00 € | Langage: C++20 | Bonus: 500.00 € | Total: 3800.00 €
Exercice 2

Chaîne d'Héritage, Cycle de Vie Mémoire (LIFO) & Masquage de Noms

★★☆☆☆ Difficulté : Facile à Intermédiaire ⏱️ 25 min Concepts : Cascade LIFO, Name Hiding, using Base::methode

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 :

  1. Dans quel ordre exact la mémoire est-elle initialisée puis détruite ?
  2. 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

  1. Créez une hiérarchie matérielle à trois niveaux :
    • Niveau 1 (Base) : Composant avec attribut protégé int m_idSerie.
    • Niveau 2 (Intermédiaire) : Peripherique : public Composant avec attribut protégé std::string m_interface (ex: "USB-C").
    • Niveau 3 (Dérivée finale) : ClavierRGB : public Peripherique avec attribut privé std::string m_retroEclairage.
  2. Dans chacune des trois classes, écrivez un constructeur et un destructeur affichant un message explicite sur std::cout comprenant le nom de la classe et l'adresse mémoire de l'objet (this).
  3. Dans la classe Composant, écrivez une méthode sans argument : void configurer().
  4. Dans la classe Peripherique, écrivez une méthode avec argument : void configurer(int debitMbps).
  5. Dans le main(), instanciez un ClavierRGB et tentez d'appeler clavier.configurer(); (sans argument). Constatez l'erreur de compilation : pourquoi le compilateur refuse-t-il l'appel alors que la méthode existe dans Composant ?
  6. Corrigez cette anomalie dans Peripherique en utilisant la clause using Composant::configurer; pour réintégrer la méthode dans la portée de surcharge.
  7. Encadrez l'instanciation du clavier dans un bloc délimité par des accolades { ... } dans main(). 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).
⚠️ Piège classique : Le Masquage de Portée (Name Hiding)

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

CPPtd2_ex2_cycle_vie.cpp
  1. /**
  2.  * TD 2 - Exercice 2 : Chaîne d'Héritage, Ordre d'Exécution & Masquage de Noms
  3.  * Observation pas à pas de l'ordre LIFO et rétablissement de la surcharge avec using
  4.  */
  5.  
  6. #include <iostream>
  7. #include <string>
  8.  
  9. // Niveau 1 : Classe de Base
  10. class Composant {
  11. protected:
  12. int m_idSerie;
  13.  
  14. public:
  15. Composant(int id) : m_idSerie(id) {
  16. std::cout << " [1. Composant] Constructeur (#" << m_idSerie
  17. << ") @" << this << "\n";
  18. }
  19.  
  20. ~Composant() {
  21. std::cout << " [1. Composant] Destructeur (#" << m_idSerie
  22. << ") @" << this << "\n";
  23. }
  24.  
  25. int getId() const { return m_idSerie; }
  26.  
  27. void configurer() {
  28. std::cout << " -> Configuration générique du composant #" << m_idSerie << "\n";
  29. }
  30. };
  31.  
  32. // Niveau 2 : Classe Intermédiaire
  33. class Peripherique : public Composant {
  34. protected:
  35. std::string m_interface;
  36.  
  37. public:
  38. Peripherique(int id, const std::string& interfaceConnexion)
  39. : Composant(id), m_interface(interfaceConnexion) {
  40. std::cout << " [2. Peripherique] Constructeur (Interface: " << m_interface
  41. << ") @" << this << "\n";
  42. }
  43.  
  44. ~Peripherique() {
  45. std::cout << " [2. Peripherique] Destructeur (Interface: " << m_interface
  46. << ") @" << this << "\n";
  47. }
  48.  
  49. // Rétablissement de la méthode de la base masquée par la surcharge locale
  50. using Composant::configurer;
  51.  
  52. // Surcharge de configurer avec un paramètre (débit en Mbps)
  53. // En C++, définir cette méthode masque Composant::configurer() à moins d'utiliser 'using' !
  54. void configurer(int debitMbps) {
  55. std::cout << " -> Configuration du périphérique #" << m_idSerie
  56. << " sur bus " << m_interface
  57. << " bridé à " << debitMbps << " Mbps\n";
  58. }
  59. };
  60.  
  61. // Niveau 3 : Classe Dérivée Finale
  62. class ClavierRGB : public Peripherique {
  63. private:
  64. std::string m_retroEclairage;
  65.  
  66. public:
  67. ClavierRGB(int id, const std::string& interfaceConnexion, const std::string& profilRGB)
  68. : Peripherique(id, interfaceConnexion), m_retroEclairage(profilRGB) {
  69. std::cout << " [3. ClavierRGB] Constructeur (RGB: " << m_retroEclairage
  70. << ") @" << this << "\n";
  71. }
  72.  
  73. ~ClavierRGB() {
  74. std::cout << " [3. ClavierRGB] Destructeur (RGB: " << m_retroEclairage
  75. << ") @" << this << "\n";
  76. }
  77.  
  78. void changerRGB(const std::string& nouveauProfil) {
  79. m_retroEclairage = nouveauProfil;
  80. std::cout << " -> Clavier #" << m_idSerie
  81. << " basculé sur le profil RGB : " << m_retroEclairage << "\n";
  82. }
  83.  
  84. void afficherFiche() const {
  85. std::cout << "Clavier #" << m_idSerie
  86. << " | Bus: " << m_interface
  87. << " | Éclairage: " << m_retroEclairage << "\n";
  88. }
  89. };
  90.  
  91. int main() {
  92. std::cout << "=== TD 2 - Exercice 2 : Cycle de Vie & Masquage de Noms ===\n\n";
  93.  
  94. std::cout << "--- 1. Observation de la cascade de construction et destruction ---\n";
  95. std::cout << "Début du bloc local...\n";
  96. {
  97. std::cout << "Instanciation d'un ClavierRGB (pile) :\n";
  98. ClavierRGB clavier(4501, "USB-C", "Aura Rainbow");
  99.  
  100. std::cout << "\nObjet opérationnel en mémoire :\n";
  101. clavier.afficherFiche();
  102.  
  103. std::cout << "\nTest des méthodes de configuration (grâce à 'using Composant::configurer') :\n";
  104. // Appel de la version sans argument (héritée du niveau 1 Composant) :
  105. clavier.configurer();
  106. // Appel de la version avec paramètre (héritée du niveau 2 Peripherique) :
  107. clavier.configurer(1000);
  108.  
  109. clavier.changerRGB("Rouge Pulsant");
  110.  
  111. std::cout << "\nFin du bloc local, sortie de portée imminente...\n";
  112. }
  113. std::cout << "Fin du bloc local atteinte (tous les destructeurs ont été exécutés).\n";
  114.  
  115. return 0;
  116. }

Résultat de l'exécution :

=== TD 2 - Exercice 2 : Cycle de Vie & Masquage de Noms === --- 1. Observation de la cascade de construction et destruction --- Début du bloc local... Instanciation d'un ClavierRGB (pile) : [1. Composant] Constructeur (#4501) @0x7ffce4ec0210 [2. Peripherique] Constructeur (Interface: USB-C) @0x7ffce4ec0210 [3. ClavierRGB] Constructeur (RGB: Aura Rainbow) @0x7ffce4ec0210 Objet opérationnel en mémoire : Clavier #4501 | Bus: USB-C | Éclairage: Aura Rainbow Test des méthodes de configuration (grâce à 'using Composant::configurer') : -> Configuration générique du composant #4501 -> Configuration du périphérique #4501 sur bus USB-C bridé à 1000 Mbps -> Clavier #4501 basculé sur le profil RGB : Rouge Pulsant Fin du bloc local, sortie de portée imminente... [3. ClavierRGB] Destructeur (RGB: Rouge Pulsant) @0x7ffce4ec0210 [2. Peripherique] Destructeur (Interface: USB-C) @0x7ffce4ec0210 [1. Composant] Destructeur (#4501) @0x7ffce4ec0210 Fin du bloc local atteinte (tous les destructeurs ont été exécutés).
Exercice 3

Modes de Dérivation (Public, Protected, Private) & Rupture du « EST-UN »

★★★☆☆ Difficulté : Intermédiaire ⏱️ 25 min Concepts : public vs private inheritance, Upcasting, Héritage d'implémentation

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

  1. Créez la classe TableauFixe encapsulant un tableau statique int m_donnees[50] et std::size_t m_taille. Proposez les méthodes publiques insererFin(int), insererIndex(index, val), supprimerFin(), get(index) const, getTaille() const et afficher() const.
  2. Dérivez la classe TableauStatistique par héritage public : class TableauStatistique : public TableauFixe. Ajoutez-lui des méthodes de calcul statistique : moyenne() const, minimum() const, maximum() const.
  3. Écrivez une fonction libre : void inspecterConteneur(const TableauFixe& tab);. Vérifiez qu'on peut lui passer une instance de TableauStatistique sans la moindre erreur. Pourquoi est-ce possible ?
  4. Dérivez la classe PileEntiers par 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 ?
  5. Dans PileEntiers, implémentez l'interface d'une pile stricte : empiler(val), depiler(), sommet() const, estVide() const et afficherPile() const en appelant les méthodes internes de TableauFixe.
  6. Utilisez la clause using TableauFixe::getTaille; dans la section publique de PileEntiers pour réexposer uniquement la taille aux utilisateurs de la pile, sans réexposer les méthodes d'insertion indicée.
  7. Dans le main(), tentez d'appeler pile.insererIndex(1, 999); et observez l'erreur du compilateur. Tentez ensuite d'appeler inspecterConteneur(pile);. Expliquez le refus du compilateur concernant l'upcasting.
💥 Règle d'or : Quand utiliser l'héritage privé ?

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.

CPPtd2_ex3_modes_derivation.cpp
  1. /**
  2.  * TD 2 - Exercice 3 : Modes de Dérivation (public, protected, private)
  3.  * Rupture de la relation « EST-UN » et héritage d'implémentation
  4.  */
  5.  
  6. #include <iostream>
  7. #include <stdexcept>
  8. #include <numeric>
  9. #include <algorithm>
  10.  
  11. // 1. Classe de Base : Conteneur brut avec accès indicé libre
  12. class TableauFixe {
  13. protected:
  14. static constexpr std::size_t CAPACITE_MAX = 50;
  15. int m_donnees[CAPACITE_MAX];
  16. std::size_t m_taille;
  17.  
  18. public:
  19. TableauFixe() : m_taille(0) {}
  20.  
  21. std::size_t getTaille() const { return m_taille; }
  22. std::size_t getCapacite() const { return CAPACITE_MAX; }
  23.  
  24. void insererFin(int valeur) {
  25. if (m_taille >= CAPACITE_MAX) {
  26. throw std::overflow_error("TableauFixe plein : capacité maximale atteinte.");
  27. }
  28. m_donnees[m_taille++] = valeur;
  29. }
  30.  
  31. void insererIndex(std::size_t index, int valeur) {
  32. if (m_taille >= CAPACITE_MAX) {
  33. throw std::overflow_error("TableauFixe plein.");
  34. }
  35. if (index > m_taille) {
  36. throw std::out_of_range("Index hors limites lors de l'insertion.");
  37. }
  38. for (std::size_t i = m_taille; i > index; --i) {
  39. m_donnees[i] = m_donnees[i - 1];
  40. }
  41. m_donnees[index] = valeur;
  42. ++m_taille;
  43. }
  44.  
  45. void supprimerFin() {
  46. if (m_taille == 0) {
  47. throw std::underflow_error("Tableau vide, impossible de supprimer.");
  48. }
  49. --m_taille;
  50. }
  51.  
  52. int get(std::size_t index) const {
  53. if (index >= m_taille) {
  54. throw std::out_of_range("Index hors limites.");
  55. }
  56. return m_donnees[index];
  57. }
  58.  
  59. void afficher() const {
  60. std::cout << "[ ";
  61. for (std::size_t i = 0; i < m_taille; ++i) {
  62. std::cout << m_donnees[i] << " ";
  63. }
  64. std::cout << "] (taille = " << m_taille << ")\n";
  65. }
  66. };
  67.  
  68. // 2. Dérivation PUBLIQUE : "Un TableauStatistique EST-UN TableauFixe"
  69. // Tous les membres publics de TableauFixe restent publics pour les clients.
  70. class TableauStatistique : public TableauFixe {
  71. public:
  72. // Constructeur par défaut appelant le constructeur de la base
  73. TableauStatistique() : TableauFixe() {}
  74.  
  75. // Ajout de calculs statistiques
  76. double moyenne() const {
  77. if (m_taille == 0) return 0.0;
  78. double somme = 0.0;
  79. for (std::size_t i = 0; i < m_taille; ++i) {
  80. somme += m_donnees[i];
  81. }
  82. return somme / m_taille;
  83. }
  84.  
  85. int minimum() const {
  86. if (m_taille == 0) throw std::logic_error("Tableau vide");
  87. int minVal = m_donnees[0];
  88. for (std::size_t i = 1; i < m_taille; ++i) {
  89. if (m_donnees[i] < minVal) minVal = m_donnees[i];
  90. }
  91. return minVal;
  92. }
  93.  
  94. int maximum() const {
  95. if (m_taille == 0) throw std::logic_error("Tableau vide");
  96. int maxVal = m_donnees[0];
  97. for (std::size_t i = 1; i < m_taille; ++i) {
  98. if (m_donnees[i] > maxVal) maxVal = m_donnees[i];
  99. }
  100. return maxVal;
  101. }
  102. };
  103.  
  104. // 3. Dérivation PRIVÉE : "Une Pile N'EST PAS un TableauFixe, elle est implémentée avec"
  105. // L'accès direct aux indices intermédiaires violerait le principe fondamental LIFO.
  106. // Tous les membres hérités deviennent privés dans PileEntiers.
  107. class PileEntiers : private TableauFixe {
  108. public:
  109. PileEntiers() : TableauFixe() {}
  110.  
  111. // Exposition sélective et contrôlée de la méthode getTaille de la base
  112. using TableauFixe::getTaille;
  113.  
  114. // Interface stricte de pile LIFO
  115. void empiler(int val) {
  116. insererFin(val); // Appel autorisé en interne
  117. }
  118.  
  119. int depiler() {
  120. if (estVide()) {
  121. throw std::underflow_error("Dépilement impossible : la pile est vide !");
  122. }
  123. int val = m_donnees[m_taille - 1];
  124. supprimerFin();
  125. return val;
  126. }
  127.  
  128. int sommet() const {
  129. if (estVide()) {
  130. throw std::underflow_error("Pile vide, aucun sommet disponible.");
  131. }
  132. return m_donnees[m_taille - 1];
  133. }
  134.  
  135. bool estVide() const {
  136. return m_taille == 0;
  137. }
  138.  
  139. void afficherPile() const {
  140. std::cout << "Pile (sommet -> bas) : [ ";
  141. for (std::size_t i = m_taille; i > 0; --i) {
  142. std::cout << m_donnees[i - 1] << " ";
  143. }
  144. std::cout << "]\n";
  145. }
  146. };
  147.  
  148. // Fonction externe attendant un TableauFixe par référence
  149. void inspecterConteneur(const TableauFixe& tab) {
  150. std::cout << "Conteneur inspecté : taille=" << tab.getTaille() << " -> ";
  151. tab.afficher();
  152. }
  153.  
  154. int main() {
  155. std::cout << "=== TD 2 - Exercice 3 : Modes d'Héritage (public vs private) ===\n\n";
  156.  
  157. // 1. Dérivation Publique
  158. std::cout << "--- 1. TableauStatistique (Héritage public : IS-A TableauFixe) ---\n";
  159. TableauStatistique stats;
  160. stats.insererFin(12);
  161. stats.insererFin(45);
  162. stats.insererFin(8);
  163. stats.insererFin(27);
  164. stats.insererIndex(2, 99); // Méthode publique de la base accessible !
  165.  
  166. std::cout << "Données : ";
  167. stats.afficher();
  168. std::cout << "Moyenne : " << stats.moyenne() << "\n";
  169. std::cout << "Min : " << stats.minimum() << " | Max : " << stats.maximum() << "\n";
  170.  
  171. std::cout << "\nConversion implicite vers la base (Upcasting autorisé car public) :\n";
  172. inspecterConteneur(stats); // ✅ Valide : TableauStatistique EST un TableauFixe
  173.  
  174. // 2. Dérivation Privée
  175. std::cout << "\n--- 2. PileEntiers (Héritage privé : Implémentation masquée) ---\n";
  176. PileEntiers pile;
  177. pile.empiler(10);
  178. pile.empiler(20);
  179. pile.empiler(30);
  180. pile.empiler(40);
  181.  
  182. pile.afficherPile();
  183. std::cout << "Taille de la pile (via 'using TableauFixe::getTaille') : " << pile.getTaille() << "\n";
  184. std::cout << "Sommet actuel : " << pile.sommet() << "\n";
  185.  
  186. std::cout << "Dépilement : " << pile.depiler() << "\n";
  187. std::cout << "Dépilement : " << pile.depiler() << "\n";
  188. std::cout << "Après 2 dépilements : ";
  189. pile.afficherPile();
  190.  
  191. // DÉMONSTRATION DES SÉCURITÉS À LA COMPILATION :
  192. // pile.insererIndex(1, 999);
  193. // ❌ ERREUR COMPILATION : 'insererIndex' is a private member of 'TableauFixe'
  194. //
  195. // inspecterConteneur(pile);
  196. // ❌ ERREUR COMPILATION : 'TableauFixe' is an inaccessible base of 'PileEntiers'
  197. // Car la relation EST-UN n'existe pas pour l'extérieur !
  198.  
  199. std::cout << "\nProtection vérifiée : l'utilisateur externe ne peut pas corrompre la pile\n"
  200. << "ni forcer un upcasting vers TableauFixe.\n";
  201.  
  202. return 0;
  203. }

Résultat de l'exécution :

=== TD 2 - Exercice 3 : Modes d'Héritage (public vs private) === --- 1. TableauStatistique (Héritage public : IS-A TableauFixe) --- Données : [ 12 45 99 8 27 ] (taille = 5) Moyenne : 38.2 Min : 8 | Max : 99 Conversion implicite vers la base (Upcasting autorisé car public) : Conteneur inspecté : taille=5 -> [ 12 45 99 8 27 ] (taille = 5) --- 2. PileEntiers (Héritage privé : Implémentation masquée) --- Pile (sommet -> bas) : [ 40 30 20 10 ] Taille de la pile (via 'using TableauFixe::getTaille') : 4 Sommet actuel : 40 Dépilement : 40 Dépilement : 30 Après 2 dépilements : Pile (sommet -> bas) : [ 20 10 ] Protection vérifiée : l'utilisateur externe ne peut pas corrompre la pile ni forcer un upcasting vers TableauFixe.
Exercice 4

Héritage Multiple : Fusion de Comportements, Conflits de Noms & Décalages Mémoire

★★★★☆ Difficulté : Avancé ⏱️ 30 min Concepts : Héritage multiple, Conflit homonyme, Opérateur de portée, Offsets 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 :

  1. Le conflit de noms : si Imprimante et Scanner définissent toutes deux une méthode reinitialiser() ou afficherStatut(), l'appel direct copieur.afficherStatut(); est rejeté pour ambiguïté par le compilateur.
  2. 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

  1. Créez la classe Imprimante avec les attributs protégés int m_resolutionDPI et int m_pagesImprimees. Équipez-la des méthodes imprimer(const std::string& doc, int copies), reinitialiser() et afficherStatut() const.
  2. Créez la classe Scanner avec les attributs protégés int m_vitessePPM et int m_pagesScannees. Équipez-la des méthodes numeriser(const std::string& doc), reinitialiser() et afficherStatut() const.
  3. Concevez la classe CopieurMultifonction héritant publiquement à la fois de Imprimante et de Scanner. Ajoutez-lui un attribut propre std::string m_modele.
  4. 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 ?
  5. Implémentez une méthode combinée photocopier(const std::string& doc, int copies) qui appelle successivement numeriser() puis imprimer().
  6. 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();.
  7. Résolution de collision n°2 (Redéfinition unificatrice) :
    Redéfinissez la méthode afficherStatut() const et reinitialiser() dans CopieurMultifonction pour qu'elles fédèrent les actions des deux sous-systèmes sans ambiguïté.
  8. 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-objet Scanner.
💡 Observation sur la mémoire : Pourquoi un offset de pointeur ?

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

CPPtd2_ex4_heritage_multiple.cpp
  1. /**
  2.  * TD 2 - Exercice 4 : Héritage Multiple, Conflits de Noms & Agencement Mémoire
  3.  * Combinaison d'interfaces et désambiguïsation par l'opérateur de portée ::
  4.  */
  5.  
  6. #include <iostream>
  7. #include <string>
  8.  
  9. // 1. Première classe de base : Imprimante
  10. class Imprimante {
  11. protected:
  12. int m_resolutionDPI;
  13. int m_pagesImprimees;
  14.  
  15. public:
  16. Imprimante(int dpi) : m_resolutionDPI(dpi), m_pagesImprimees(0) {
  17. std::cout << " [+] Constructeur Imprimante @" << this
  18. << " (DPI: " << m_resolutionDPI << ")\n";
  19. }
  20.  
  21. ~Imprimante() {
  22. std::cout << " [-] Destructeur Imprimante @" << this << "\n";
  23. }
  24.  
  25. void imprimer(const std::string& doc, int copies = 1) {
  26. m_pagesImprimees += copies;
  27. std::cout << " [Imprimante] Impression de \"" << doc
  28. << "\" (" << copies << " copie(s) à " << m_resolutionDPI << " DPI)\n";
  29. }
  30.  
  31. // Méthode générant un conflit de nom avec Scanner
  32. void reinitialiser() {
  33. m_pagesImprimees = 0;
  34. std::cout << " [Imprimante] Compteur de pages imprimées remis à zéro.\n";
  35. }
  36.  
  37. // Deuxième méthode en conflit
  38. void afficherStatut() const {
  39. std::cout << " [Statut Imprimante @" << this << "] DPI: " << m_resolutionDPI
  40. << " | Pages imprimées: " << m_pagesImprimees << "\n";
  41. }
  42. };
  43.  
  44. // 2. Deuxième classe de base : Scanner
  45. class Scanner {
  46. protected:
  47. int m_vitessePPM; // Pages par minute
  48. int m_pagesScannees;
  49.  
  50. public:
  51. Scanner(int ppm) : m_vitessePPM(ppm), m_pagesScannees(0) {
  52. std::cout << " [+] Constructeur Scanner @" << this
  53. << " (Vitesse: " << m_vitessePPM << " PPM)\n";
  54. }
  55.  
  56. ~Scanner() {
  57. std::cout << " [-] Destructeur Scanner @" << this << "\n";
  58. }
  59.  
  60. void numeriser(const std::string& doc) {
  61. ++m_pagesScannees;
  62. std::cout << " [Scanner] Numérisation du document \"" << doc
  63. << "\" (débit " << m_vitessePPM << " PPM)\n";
  64. }
  65.  
  66. // Méthode homonyme en conflit
  67. void reinitialiser() {
  68. m_pagesScannees = 0;
  69. std::cout << " [Scanner] Compteur de pages numérisées remis à zéro.\n";
  70. }
  71.  
  72. // Deuxième méthode en conflit
  73. void afficherStatut() const {
  74. std::cout << " [Statut Scanner @" << this << "] Débit: " << m_vitessePPM
  75. << " PPM | Pages numérisées: " << m_pagesScannees << "\n";
  76. }
  77. };
  78.  
  79. // 3. Classe Dérivée par Héritage Multiple : CopieurMultifonction
  80. // Note : L'ordre de déclaration ci-dessous détermine l'ordre d'appel des constructeurs !
  81. class CopieurMultifonction : public Imprimante, public Scanner {
  82. private:
  83. std::string m_modele;
  84.  
  85. public:
  86. // Constructeur : invoque Imprimante puis Scanner
  87. CopieurMultifonction(const std::string& modele, int dpi, int ppm)
  88. : Imprimante(dpi), Scanner(ppm), m_modele(modele) {
  89. std::cout << "[+] CopieurMultifonction \"" << m_modele << "\" initialisé @" << this << "\n";
  90. }
  91.  
  92. ~CopieurMultifonction() {
  93. std::cout << "[-] Destructeur CopieurMultifonction \"" << m_modele << "\" @" << this << "\n";
  94. }
  95.  
  96. // Opération combinée utilisant les deux facettes héritées
  97. void photocopier(const std::string& docOriginal, int nbCopies) {
  98. std::cout << "\n>>> Démarrage d'une photocopie combinée (" << nbCopies << " copies) <<<\n";
  99. // Appel de la méthode héritée de Scanner
  100. numeriser(docOriginal);
  101. // Appel de la méthode héritée d'Imprimante
  102. imprimer("Copie de " + docOriginal, nbCopies);
  103. }
  104.  
  105. // Résolution de l'ambiguïté pour afficherStatut() :
  106. // On redéfinit la méthode dans la classe dérivée pour fédérer les deux statuts
  107. void afficherStatut() const {
  108. std::cout << "\n=== Statut Synthétique Copieur [" << m_modele << "] ===\n";
  109. Imprimante::afficherStatut();
  110. Scanner::afficherStatut();
  111. }
  112.  
  113. // Résolution de l'ambiguïté pour reinitialiser()
  114. void reinitialiser() {
  115. std::cout << "\n=== Réinitialisation globale de tous les modules ===\n";
  116. Imprimante::reinitialiser();
  117. Scanner::reinitialiser();
  118. }
  119. };
  120.  
  121. int main() {
  122. std::cout << "=== TD 2 - Exercice 4 : Héritage Multiple & Conflits de Noms ===\n\n";
  123.  
  124. std::cout << "--- 1. Instanciation et analyse de la disposition mémoire (Offsets) ---\n";
  125. {
  126. CopieurMultifonction copieur("OfficeJet Pro 9000", 1200, 35);
  127.  
  128. // Analyse avancée des pointeurs et décalage d'adresse (Offset)
  129. const CopieurMultifonction* ptrCopieur = &copieur;
  130. const Imprimante* ptrImprimante = &copieur;
  131. const Scanner* ptrScanner = &copieur;
  132.  
  133. std::cout << "\nAnalyse des adresses des sous-objets en mémoire :\n";
  134. std::cout << " Adresse de l'objet complet CopieurMultifonction : " << ptrCopieur << "\n";
  135. std::cout << " Adresse du sous-objet Imprimante (1ère base) : " << ptrImprimante << "\n";
  136. std::cout << " Adresse du sous-objet Scanner (2ème base) : " << ptrScanner << "\n";
  137. std::cout << " -> Décalage (offset) de Scanner : "
  138. << (reinterpret_cast<const char*>(ptrScanner) - reinterpret_cast<const char*>(ptrCopieur))
  139. << " octets.\n";
  140.  
  141. // Utilisation des services combinés
  142. copieur.photocopier("Contrat_Confidentialite.pdf", 3);
  143.  
  144. // Affichage synthétique via la méthode redéfinie unificatrice
  145. copieur.afficherStatut();
  146.  
  147. // 2. Levée d'ambiguïté explicite depuis l'extérieur sans redéfinition
  148. std::cout << "\n--- 2. Appel ciblé avec l'opérateur de portée (::) ---\n";
  149. std::cout << "Réinitialisation UNIQUEMENT du module scanner :\n";
  150. copieur.Scanner::reinitialiser();
  151.  
  152. copieur.afficherStatut();
  153.  
  154. std::cout << "\nSortie de portée : destruction en ordre inverse...\n";
  155. }
  156.  
  157. return 0;
  158. }

Résultat de l'exécution :

=== TD 2 - Exercice 4 : Héritage Multiple & Conflits de Noms === --- 1. Instanciation et analyse de la disposition mémoire (Offsets) --- [+] Constructeur Imprimante @0x7fffc376a150 (DPI: 1200) [+] Constructeur Scanner @0x7fffc376a158 (Vitesse: 35 PPM) [+] CopieurMultifonction "OfficeJet Pro 9000" initialisé @0x7fffc376a150 Analyse des adresses des sous-objets en mémoire : Adresse de l'objet complet CopieurMultifonction : 0x7fffc376a150 Adresse du sous-objet Imprimante (1ère base) : 0x7fffc376a150 Adresse du sous-objet Scanner (2ème base) : 0x7fffc376a158 -> Décalage (offset) de Scanner : 8 octets. >>> Démarrage d'une photocopie combinée (3 copies) <<< [Scanner] Numérisation du document "Contrat_Confidentialite.pdf" (débit 35 PPM) [Imprimante] Impression de "Copie de Contrat_Confidentialite.pdf" (3 copie(s) à 1200 DPI) === Statut Synthétique Copieur [OfficeJet Pro 9000] === [Statut Imprimante @0x7fffc376a150] DPI: 1200 | Pages imprimées: 3 [Statut Scanner @0x7fffc376a158] Débit: 35 PPM | Pages numérisées: 1 --- 2. Appel ciblé avec l'opérateur de portée (::) --- Réinitialisation UNIQUEMENT du module scanner : [Scanner] Compteur de pages numérisées remis à zéro. === Statut Synthétique Copieur [OfficeJet Pro 9000] === [Statut Imprimante @0x7fffc376a150] DPI: 1200 | Pages imprimées: 3 [Statut Scanner @0x7fffc376a158] Débit: 35 PPM | Pages numérisées: 0 Sortie de portée : destruction en ordre inverse... [-] Destructeur CopieurMultifonction "OfficeJet Pro 9000" @0x7fffc376a150 [-] Destructeur Scanner @0x7fffc376a158 [-] Destructeur Imprimante @0x7fffc376a150
Exercice 5

Le Problème du Diamant (Diamond of Death) & Résolution par Héritage Virtuel

★★★★★ Difficulté : Expert ⏱️ 35 min Concepts : Diamant, virtual public, Base unique, Responsabilité terminale

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 :

Structure du Diamant d'Héritage
🏛️ Sommet : Vehicule
m_immatriculation, m_puissanceKW
↙ virtual public ↘ virtual public
⛵ Bateau
m_tirantDEau
🚗 Automobile
m_nbPlaces
↘ public ↙ public
🚤🚗 VehiculeAmphibie
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é Bateau ne modifierait pas celle du côté Automobile !).
  • L'impossibilité d'effectuer un Upcasting vers Vehicule& sans ambiguïté.

2. Travail à réaliser

  1. Créez la classe de base sommet Vehicule contenant std::string m_immatriculation et int m_puissanceKW. Munissez-la d'un constructeur avec message de traçage, d'un destructeur virtuel virtual ~Vehicule(), d'accesseurs/mutateurs et d'une méthode afficherVehicule() const.
  2. Créez la classe intermédiaire Bateau avec son attribut double m_tirantDEau. Faites-la dériver virtuellement de Vehicule : class Bateau : virtual public Vehicule.
  3. Créez la classe intermédiaire Automobile avec son attribut int m_nbPlaces. Faites-la dériver également virtuellement de Vehicule : class Automobile : virtual public Vehicule.
  4. Créez la classe terminale VehiculeAmphibie : public Bateau, public Automobile. Ajoutez-lui les attributs std::string m_nomModele et bool m_heliceDeployee, ainsi qu'une méthode de bascule basculerModeNautique().
  5. Règle fondamentale de l'Héritage Virtuel :
    Dans le constructeur de VehiculeAmphibie, appelez explicitement le constructeur de Vehicule(immat, kw), en plus de ceux de Bateau et Automobile. 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 de Bateau et Automobile.
  6. 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.
  7. Écrivez une fonction externe void controleTechnique(const Vehicule& v);. Passez-lui directement votre instance de VehiculeAmphibie. 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 ?
  8. Affichez les adresses mémoire des sous-objets ptrAmph, ptrBateau, ptrAuto et ptrVehicule. Que remarquez-vous sur la position de l'ancêtre virtuel ?

⚡ Les 3 Principes Clés de l'Héritage Virtuel en C++

1
Unicité Mémoire

virtual public Base

Le sous-objet ancêtre n'existe qu'en un seul et unique exemplaire.

2
Init Terminale

Most Derived Class

La classe la plus basse appelle le constructeur virtuel ; les appels intermédiaires sont ignorés.

3
Upcasting Sûr

Base& ref = diamant;

La conversion polymorphique vers le sommet du diamant est totale et sans ambiguïté.

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.

CPPtd2_ex5_diamant_amphibie.cpp
  1. /**
  2.  * TD 2 - Exercice 5 : Le Problème du Diamant & L'Héritage Virtuel
  3.  * Résolution du "Diamond of Death", unicité de l'ancêtre commun
  4.  * et responsabilité d'initialisation de la classe la plus dérivée.
  5.  */
  6.  
  7. #include <iostream>
  8. #include <string>
  9.  
  10. // 1. Sommet du Diamant : Classe de Base Commune
  11. class Vehicule {
  12. protected:
  13. std::string m_immatriculation;
  14. int m_puissanceKW;
  15.  
  16. public:
  17. Vehicule(const std::string& immat, int kw)
  18. : m_immatriculation(immat), m_puissanceKW(kw) {
  19. std::cout << " [1. SOMMET Vehicule] Constructeur @" << this
  20. << " | Immat: " << m_immatriculation
  21. << ", Puissance: " << m_puissanceKW << " kW\n";
  22. }
  23.  
  24. virtual ~Vehicule() {
  25. std::cout << " [1. SOMMET Vehicule] Destructeur @" << this
  26. << " (Immat: " << m_immatriculation << ")\n";
  27. }
  28.  
  29. std::string getImmatriculation() const { return m_immatriculation; }
  30. int getPuissanceKW() const { return m_puissanceKW; }
  31.  
  32. void setPuissanceKW(int kw) {
  33. m_puissanceKW = kw;
  34. }
  35.  
  36. void afficherVehicule() const {
  37. std::cout << " [Base Vehicule @" << this << "] Immat: "
  38. << m_immatriculation << " (" << m_puissanceKW << " kW)\n";
  39. }
  40. };
  41.  
  42. // 2. Branche Gauche : Bateau avec Héritage VIRTUEL
  43. class Bateau : virtual public Vehicule {
  44. protected:
  45. double m_tirantDEau; // en mètres
  46.  
  47. public:
  48. Bateau(const std::string& immat, int kw, double tirantDEau)
  49. : Vehicule(immat, kw), m_tirantDEau(tirantDEau) {
  50. std::cout << " [2. BRANCHE GAUCHE Bateau] Constructeur @" << this
  51. << " | Tirant d'eau: " << m_tirantDEau << " m\n";
  52. }
  53.  
  54. ~Bateau() override {
  55. std::cout << " [2. BRANCHE GAUCHE Bateau] Destructeur @" << this << "\n";
  56. }
  57.  
  58. void naviguer() const {
  59. std::cout << " -> Navigation maritime : " << m_immatriculation
  60. << " navigue avec " << m_tirantDEau << " m de tirant d'eau.\n";
  61. }
  62. };
  63.  
  64. // 3. Branche Droite : Automobile avec Héritage VIRTUEL
  65. class Automobile : virtual public Vehicule {
  66. protected:
  67. int m_nbPlaces;
  68.  
  69. public:
  70. Automobile(const std::string& immat, int kw, int nbPlaces)
  71. : Vehicule(immat, kw), m_nbPlaces(nbPlaces) {
  72. std::cout << " [2. BRANCHE DROITE Automobile] Constructeur @" << this
  73. << " | Places: " << m_nbPlaces << "\n";
  74. }
  75.  
  76. ~Automobile() override {
  77. std::cout << " [2. BRANCHE DROITE Automobile] Destructeur @" << this << "\n";
  78. }
  79.  
  80. void rouler() const {
  81. std::cout << " -> Conduite sur route : " << m_immatriculation
  82. << " roule (" << m_nbPlaces << " places à bord).\n";
  83. }
  84. };
  85.  
  86. // 4. Pointe du Diamant : Classe Terminale VehiculeAmphibie
  87. class VehiculeAmphibie : public Bateau, public Automobile {
  88. private:
  89. std::string m_nomModele;
  90. bool m_heliceDeployee;
  91.  
  92. public:
  93. // ⚡ RÈGLE FONDAMENTALE DE L'HÉRITAGE VIRTUEL :
  94. // C'est obligatoirement la classe LA PLUS DÉRIVÉE (ici VehiculeAmphibie)
  95. // qui doit invoquer le constructeur de la base virtuelle commune Vehicule(immat, kw).
  96. // Les appels Vehicule(...) situés dans Bateau et Automobile sont purement ignorés !
  97. VehiculeAmphibie(const std::string& immat, int kw, double tirantDEau, int nbPlaces, const std::string& modele)
  98. : Vehicule(immat, kw), // 1. Initialisation de la base virtuelle unique
  99. Bateau(immat, kw, tirantDEau), // 2. Initialisation branche Bateau
  100. Automobile(immat, kw, nbPlaces), // 3. Initialisation branche Automobile
  101. m_nomModele(modele),
  102. m_heliceDeployee(false) {
  103. std::cout << "[3. POINTE VehiculeAmphibie] Prêt : \"" << m_nomModele << "\" @" << this << "\n";
  104. }
  105.  
  106. ~VehiculeAmphibie() override {
  107. std::cout << "[-] Destructeur VehiculeAmphibie \"" << m_nomModele << "\" @" << this << "\n";
  108. }
  109.  
  110. void basculerModeNautique() {
  111. m_heliceDeployee = !m_heliceDeployee;
  112. std::cout << " -> Mode nautique " << (m_heliceDeployee ? "ACTIVÉ (Hélice déployée)" : "DÉSACTIVÉ (Roues motrices)") << "\n";
  113. }
  114.  
  115. void afficherFicheComplete() const {
  116. std::cout << "\n================ Fiche Technique Amphibie ================\n";
  117. std::cout << "Modèle : " << m_nomModele << "\n";
  118. // ✅ Aucun conflit d'accès ! getImmatriculation() et m_puissanceKW sont uniques !
  119. std::cout << "Immat : " << getImmatriculation() << "\n";
  120. std::cout << "Puissance : " << getPuissanceKW() << " kW (" << m_puissanceKW << ")\n";
  121. std::cout << "Tirant d'eau : " << m_tirantDEau << " m\n";
  122. std::cout << "Capacité : " << m_nbPlaces << " passagers\n";
  123. std::cout << "Hélice active : " << (m_heliceDeployee ? "Oui" : "Non") << "\n";
  124. std::cout << "==========================================================\n";
  125. }
  126. };
  127.  
  128. // Fonction démontrant le polymorphisme ascendant (Upcasting) vers le sommet du diamant
  129. void controleTechnique(const Vehicule& v) {
  130. std::cout << ">> [Contrôle Technique] Validation du véhicule Immat : "
  131. << v.getImmatriculation() << " (" << v.getPuissanceKW() << " kW)\n";
  132. }
  133.  
  134. int main() {
  135. std::cout << "=== TD 2 - Exercice 5 : Le Diamant d'Héritage et sa Résolution Virtuelle ===\n\n";
  136.  
  137. std::cout << "--- 1. Instanciation du véhicule amphibie (construction de la hiérarchie) ---\n";
  138. {
  139. VehiculeAmphibie amph("FR-AMPH-2026", 180, 0.85, 6, "AquaTerra Cruiser");
  140.  
  141. std::cout << "\n--- 2. Analyse de l'unicité du sous-objet virtuel Vehicule ---\n";
  142. // Vérification des adresses :
  143. const VehiculeAmphibie* ptrAmph = &amph;
  144. const Bateau* ptrBat = &amph;
  145. const Automobile* ptrAuto = &amph;
  146. const Vehicule* ptrVehicule = &amph; // ✅ Possible UNIQUEMENT grâce à virtual !
  147.  
  148. std::cout << "Adresse globale VehiculeAmphibie : " << ptrAmph << "\n";
  149. std::cout << "Adresse sous-objet Bateau : " << ptrBat << "\n";
  150. std::cout << "Adresse sous-objet Automobile : " << ptrAuto << "\n";
  151. std::cout << "Adresse sous-objet commun Vehicule: " << ptrVehicule << "\n";
  152.  
  153. amph.afficherFicheComplete();
  154.  
  155. std::cout << "\n--- 3. Démonstration de cohérence d'état unique ---\n";
  156. std::cout << "Modification de la puissance à 220 kW...\n";
  157. amph.setPuissanceKW(220); // Modifie l'unique exemplaire de m_puissanceKW !
  158.  
  159. // Actions combinées des deux branches
  160. amph.rouler();
  161. amph.basculerModeNautique();
  162. amph.naviguer();
  163.  
  164. std::cout << "\n--- 4. Upcasting vers la base commune Vehicule ---\n";
  165. // Sans héritage virtuel, l'instruction suivante provoquerait une erreur :
  166. // "Vehicule is an ambiguous base of VehiculeAmphibie"
  167. controleTechnique(amph);
  168.  
  169. std::cout << "\n--- 5. Destruction automatique (LIFO) en sortie de bloc ---\n";
  170. }
  171.  
  172. return 0;
  173. }

Résultat de l'exécution :

=== TD 2 - Exercice 5 : Le Diamant d'Héritage et sa Résolution Virtuelle === --- 1. Instanciation du véhicule amphibie (construction de la hiérarchie) --- [1. SOMMET Vehicule] Constructeur @0x7ffcdbb35838 | Immat: FR-AMPH-2026, Puissance: 180 kW [2. BRANCHE GAUCHE Bateau] Constructeur @0x7ffcdbb357f0 | Tirant d'eau: 0.85 m [2. BRANCHE DROITE Automobile] Constructeur @0x7ffcdbb35800 | Places: 6 [3. POINTE VehiculeAmphibie] Prêt : "AquaTerra Cruiser" @0x7ffcdbb357f0 --- 2. Analyse de l'unicité du sous-objet virtuel Vehicule --- Adresse globale VehiculeAmphibie : 0x7ffcdbb357f0 Adresse sous-objet Bateau : 0x7ffcdbb357f0 Adresse sous-objet Automobile : 0x7ffcdbb35800 Adresse sous-objet commun Vehicule: 0x7ffcdbb35838 ================ Fiche Technique Amphibie ================ Modèle : AquaTerra Cruiser Immat : FR-AMPH-2026 Puissance : 180 kW (180) Tirant d'eau : 0.85 m Capacité : 6 passagers Hélice active : Non ========================================================== --- 3. Démonstration de cohérence d'état unique --- Modification de la puissance à 220 kW... -> Conduite sur route : FR-AMPH-2026 roule (6 places à bord). -> Mode nautique ACTIVÉ (Hélice déployée) -> Navigation maritime : FR-AMPH-2026 navigue avec 0.85 m de tirant d'eau. --- 4. Upcasting vers la base commune Vehicule --- >> [Contrôle Technique] Validation du véhicule Immat : FR-AMPH-2026 (220 kW) --- 5. Destruction automatique (LIFO) en sortie de bloc --- [-] Destructeur VehiculeAmphibie "AquaTerra Cruiser" @0x7ffcdbb357f0 [2. BRANCHE DROITE Automobile] Destructeur @0x7ffcdbb35800 [2. BRANCHE GAUCHE Bateau] Destructeur @0x7ffcdbb357f0 [1. SOMMET Vehicule] Destructeur @0x7ffcdbb35838 (Immat: FR-AMPH-2026)

🎉 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.
← Revoir le Chapitre 2 (Héritage) ← Revoir le TD 1 (Concepts & Copie) Chapitre 3 : La Généricité ➔ Passer au TD 3 : La Généricité C++ ➔