Niveau : Licence 3 Module : Programmation Orientée Objet Volume : 2h00 (Approfondissement) Norme : C++20

Annexe : Notions Avancées de la POO en C++

Ce document de référence rassemble les concepts indispensables de la programmation orientée objet en C++ non abordés dans le tronc commun : la gestion robuste des exceptions et la garantie noexcept, l'éradication des fuites mémoire via les pointeurs intelligents (unique_ptr, shared_ptr, weak_ptr), l'optimisation critique par la sémantique de déplacement (Règle des Cinq), l'amitié contrôlée (friend) pour les flux d'E/S, les membres statiques et le transtypage sécurisé à l'exécution (RTTI).

Sujets approfondis dans cette annexe

  • Gestion des exceptions : try, catch, throw, hiérarchie std::exception, déroulement de pile et interdiction d'émettre dans les destructeurs (noexcept).
  • Pointeurs intelligents (Smart Pointers) : Propriété exclusive avec std::unique_ptr, partagée avec std::shared_ptr et rupture de cycles avec std::weak_ptr.
  • Sémantique de déplacement (Move Semantics) : Références rvalue T&&, conversion std::move et transition de la Règle des 3 vers la Règle des 5.
  • L'Amitié (friend) & Opérateurs de flux : Surcharge de operator<< et operator>> comme fonctions libres amies.
  • Membres Statiques : Attributs partagés à l'échelle de la classe, méthodes de classe statiques et initialisation hors-classe.
  • Transtypage & RTTI : Conversion sécurisée descendante avec dynamic_cast, identification dynamique de type avec typeid.
1

Gestion des Exceptions et Sécurité des Constructeurs

⏱️ 20 minutes

1.1. Philosophie des Exceptions en C++

En C classique, les erreurs sont souvent gérées par des codes de retour d'entiers (ex: renvoyer -1 ou NULL). En POO C++, cette approche est insuffisante car les constructeurs ne renvoient aucune valeur. Le mécanisme d'exceptions permet de séparer le flux normal d'exécution du traitement des cas d'échec.

🚨 RÈGLE D'OR : Un destructeur ne doit JAMAIS lancer d'exception !

Si une seconde exception est émise alors qu'une première exception est déjà en cours de propagation (lors du déroulement automatique de la pile — stack unwinding), le runtime C++ invoque immédiatement std::terminate(), ce qui interrompt brutalement le programme sans libérer la mémoire restante. En C++ moderne, tous les destructeurs sont implicitement marqués noexcept.

1.2. Exceptions dans les Constructeurs : Protection des Invariants

Le seul moyen légitime pour un constructeur de signaler qu'il est impossible de construire un objet valide (ex: argument incohérent, mémoire saturée, fichier introuvable) est de lancer une exception. Dans ce cas, l'objet est considéré comme n'ayant jamais existé : la mémoire allouée sur la pile est automatiquement nettoyée.

CPPannexe_exceptions.cpp
  1. /**
  2.  * Annexe - Exemple 1 : Gestion des Exceptions et Sécurité dans les Constructeurs
  3.  * Exceptions personnalisées, try/catch, et garantie noexcept sur les destructeurs en C++20.
  4.  */
  5.  
  6. #include <iostream>
  7. #include <string>
  8. #include <stdexcept>
  9.  
  10. // 1. Définition d'une exception personnalisée dérivée de std::runtime_error
  11. class SoldeInsuffisantException : public std::runtime_error {
  12. private:
  13. double m_soldeActuel;
  14. double m_montantDemande;
  15.  
  16. public:
  17. SoldeInsuffisantException(double solde, double montant)
  18. : std::runtime_error("Opération refusée : solde bancaire insuffisant"),
  19. m_soldeActuel(solde),
  20. m_montantDemande(montant) {}
  21.  
  22. double getSolde() const noexcept { return m_soldeActuel; }
  23. double getMontant() const noexcept { return m_montantDemande; }
  24. };
  25.  
  26. // 2. Classe protégeant ses invariants via des exceptions
  27. class CompteBancaireSecurise {
  28. private:
  29. std::string m_titulaire;
  30. double m_solde;
  31.  
  32. public:
  33. // Une exception lancée dans un constructeur ANNULE la création de l'objet
  34. // La mémoire allouée sur la pile est automatiquement libérée (déroulement de pile / stack unwinding)
  35. CompteBancaireSecurise(const std::string& titulaire, double soldeInitial)
  36. : m_titulaire(titulaire), m_solde(soldeInitial) {
  37. if (soldeInitial < 0.0) {
  38. throw std::invalid_argument("Le solde initial ne peut pas être négatif !");
  39. }
  40. std::cout << "[+] Compte de " << m_titulaire << " créé avec " << m_solde << " €\n";
  41. }
  42.  
  43. // ⚡ RÈGLE ABSOLUE : Un destructeur DOIT TOUJOURS être noexcept (ne JAMAIS lancer d'exception) !
  44. // Si une exception est lancée pendant un déroulement de pile, std::terminate() est appelé immédiatement.
  45. ~CompteBancaireSecurise() noexcept {
  46. std::cout << "[-] Compte de " << m_titulaire << " détruit proprement.\n";
  47. }
  48.  
  49. void retirer(double montant) {
  50. if (montant <= 0.0) {
  51. throw std::invalid_argument("Le montant retiré doit être strictement positif.");
  52. }
  53. if (montant > m_solde) {
  54. throw SoldeInsuffisantException(m_solde, montant);
  55. }
  56. m_solde -= montant;
  57. std::cout << "Retrait de " << montant << " € effectué. Reste : " << m_solde << " €\n";
  58. }
  59.  
  60. double getSolde() const noexcept { return m_solde; }
  61. };
  62.  
  63. int main() {
  64. std::cout << "=== Annexe : Exceptions & Sécurité en C++ ===\n\n";
  65.  
  66. // Test 1 : Exception dans le constructeur (déroulement de pile)
  67. std::cout << "--- 1. Exception dans le constructeur ---\n";
  68. try {
  69. std::cout << "Tentative d'instanciation avec solde négatif (-100 €)...\n";
  70. CompteBancaireSecurise compteInvalide("Client Erroné", -100.0);
  71. std::cout << "Cette ligne ne sera jamais atteinte !\n";
  72. } catch (const std::invalid_argument& e) {
  73. std::cout << ">> Exception capturée : " << e.what() << "\n";
  74. }
  75.  
  76. // Test 2 : Opération normale et exception métier personnalisée
  77. std::cout << "\n--- 2. Retrait et exception personnalisée ---\n";
  78. try {
  79. CompteBancaireSecurise c1("Jean Martin", 200.0);
  80. c1.retirer(50.0); // OK : reste 150 €
  81.  
  82. std::cout << "Tentative de retrait de 300 €...\n";
  83. c1.retirer(300.0); // Déclenche SoldeInsuffisantException
  84. } catch (const SoldeInsuffisantException& e) {
  85. std::cout << ">> Exception métier : " << e.what() << "\n";
  86. std::cout << " Détails : Solde=" << e.getSolde() << " €, Demandé=" << e.getMontant() << " €\n";
  87. } catch (const std::exception& e) {
  88. std::cout << ">> Autre exception : " << e.what() << "\n";
  89. }
  90.  
  91. return 0;
  92. }
2

Les Pointeurs Intelligents (Smart Pointers)

⏱️ 25 minutes

En C++ moderne (depuis C++11 et C++14), l'utilisation directe des opérateurs bruts new et delete est considérée comme une mauvaise pratique. Les pointeurs intelligents définis dans <memory> encapsulent un pointeur brut et garantissent la libération automatique et certaine de la mémoire via l'idiome RAII.

Type de Smart Pointer Modèle de Propriété Fonction de création recommandée Copiable ? Déplaçable ?
std::unique_ptr<T> Exclusive (un seul propriétaire) std::make_unique<T>(...) Non Oui (std::move)
std::shared_ptr<T> Partagée (compteur de références) std::make_shared<T>(...) Oui Oui
std::weak_ptr<T> Non-propriétaire (observateur) Assignation depuis shared_ptr Oui Oui
💡 Quand utiliser unique_ptr vs shared_ptr ?

Utilisez std::unique_ptr par défaut dans 95% des cas : il n'a strictement aucun surcoût en taille ni en vitesse par rapport à un pointeur brut classique. Réservez std::shared_ptr aux cas où plusieurs entités se partagent réellement la propriété conjointe d'une même ressource complexe (ex: graphe de dépendance).

CPPannexe_smart_pointers.cpp
  1. /**
  2.  * Annexe - Exemple 2 : Les Pointeurs Intelligents (Smart Pointers en C++ Moderne)
  3.  * std::unique_ptr, std::shared_ptr, std::weak_ptr et std::make_unique / make_shared
  4.  */
  5.  
  6. #include <iostream>
  7. #include <memory>
  8. #include <string>
  9.  
  10. class Document {
  11. private:
  12. std::string m_titre;
  13.  
  14. public:
  15. Document(const std::string& titre) : m_titre(titre) {
  16. std::cout << " [+] Création Document : \"" << m_titre << "\"\n";
  17. }
  18.  
  19. ~Document() {
  20. std::cout << " [-] Destruction Document : \"" << m_titre << "\"\n";
  21. }
  22.  
  23. void afficher() const {
  24. std::cout << " -> Document : \"" << m_titre << "\"\n";
  25. }
  26. };
  27.  
  28. // Exemple de dépendance cyclique évitée avec std::weak_ptr
  29. struct Noeud {
  30. std::string nom;
  31. std::shared_ptr<Noeud> suivant;
  32. // Utiliser weak_ptr pour le pointeur arrière afin de casser le cycle de vie circulaire !
  33. std::weak_ptr<Noeud> precedent;
  34.  
  35. Noeud(const std::string& n) : nom(n) {
  36. std::cout << " [+] Noeud '" << nom << "' alloué\n";
  37. }
  38. ~Noeud() {
  39. std::cout << " [-] Noeud '" << nom << "' libéré\n";
  40. }
  41. };
  42.  
  43. int main() {
  44. std::cout << "=== Annexe : Pointeurs Intelligents (Modern C++) ===\n\n";
  45.  
  46. // 1. std::unique_ptr : Possession exclusive (Zéro surcoût par rapport à un pointeur brut)
  47. std::cout << "--- 1. std::unique_ptr (possession exclusive & transfert) ---\n";
  48. {
  49. // Création recommandée avec std::make_unique (sécurisé contre les fuites en cas d'exception)
  50. auto doc1 = std::make_unique<Document>("Rapport_Annuel_2026.pdf");
  51. doc1->afficher();
  52.  
  53. // ❌ doc1 n'est PAS copiable : auto doc2 = doc1; (Erreur de compilation !)
  54. // ✅ Mais il est déplaçable avec std::move :
  55. std::cout << "Transfert de propriété vers doc2 via std::move...\n";
  56. std::unique_ptr<Document> doc2 = std::move(doc1);
  57.  
  58. if (!doc1) {
  59. std::cout << "doc1 est maintenant vide (nullptr).\n";
  60. }
  61. doc2->afficher();
  62. std::cout << "Sortie du bloc de portée locale...\n";
  63. } // doc2 est détruit automatiquement ici ! Zéro fuite de mémoire.
  64.  
  65. // 2. std::shared_ptr : Possession partagée avec compteur de références
  66. std::cout << "\n--- 2. std::shared_ptr (possession partagée) ---\n";
  67. {
  68. auto part1 = std::make_shared<Document>("Cahier_des_charges.docx");
  69. std::cout << "Compteur de références part1 : " << part1.use_count() << "\n";
  70.  
  71. {
  72. auto part2 = part1; // Copie autorisée : incrémente le compteur
  73. std::cout << "Après copie part2 = part1 -> Compteur : " << part1.use_count() << "\n";
  74. part2->afficher();
  75. std::cout << "Fin du sous-bloc (part2 va être détruit)...\n";
  76. }
  77.  
  78. std::cout << "Après destruction de part2 -> Compteur : " << part1.use_count() << "\n";
  79. } // Le document est détruit ici quand le dernier shared_ptr meurt.
  80.  
  81. // 3. std::weak_ptr : Observateur non-propriétaire (brise les cycles)
  82. std::cout << "\n--- 3. std::weak_ptr (rupture de cycle circulaire) ---\n";
  83. {
  84. auto n1 = std::make_shared<Noeud>("A");
  85. auto n2 = std::make_shared<Noeud>("B");
  86.  
  87. n1->suivant = n2;
  88. n2->precedent = n1; // weak_ptr n'augmente pas le use_count() !
  89.  
  90. std::cout << "n1 use_count : " << n1.use_count() << " (grâce à weak_ptr au lieu de shared_ptr)\n";
  91.  
  92. // Accès à un weak_ptr via lock()
  93. if (auto p = n2->precedent.lock()) {
  94. std::cout << "Accès valide depuis n2 vers précédent : " << p->nom << "\n";
  95. }
  96. } // n1 et n2 sont parfaitement détruits tous les deux !
  97.  
  98. return 0;
  99. }
3

La Sémantique de Déplacement & la Règle des Cinq

⏱️ 25 minutes

3.1. Le problème de la copie inutile

Au Chapitre 1, nous avons étudié la Règle des Trois : pour gérer un tableau dynamique sur le tas, il fallait écrire un destructeur, un constructeur de recopie et un opérateur d'affectation par recopie.

Cependant, lorsqu'un objet temporaire (sur le point de mourir) est assigné ou renvoyé par une fonction, copier l'intégralité de ses octets sur le tas puis détruire immédiatement la source est un gaspillage monumental de cycles CPU !

3.2. Références rvalue (T&&) et std::move

Copie Profonde vs Déplacement (Vol de Pointeur)
📋 Copie Profonde : O(N)
  • 1. Nouvelle allocation sur le tas
  • 2. Copie octet par octet des $N$ éléments
  • 3. Deux tampons indépendants
vs
⚡ Déplacement : O(1)
  • 1. Vol du pointeur interne brut
  • 2. Mise à nullptr du pointeur source
  • 3. Zéro allocation, instantané !

3.3. La Règle des Cinq (Rule of Five)

Dès qu'une classe gère manuellement une ressource dynamique, vous devez définir ou supprimer explicitement cinq fonctions spéciales :

  1. Le Destructeur : ~MaClasse() noexcept;
  2. Le Constructeur de recopie : MaClasse(const MaClasse&);
  3. L'Opérateur d'affectation par recopie : MaClasse& operator=(const MaClasse&);
  4. Le Constructeur par déplacement : MaClasse(MaClasse&&) noexcept;
  5. L'Opérateur d'affectation par déplacement : MaClasse& operator=(MaClasse&&) noexcept;
CPPannexe_move_semantics.cpp
  1. /**
  2.  * Annexe - Exemple 3 : Sémantique de Déplacement & Règle des Cinq (Rule of Five)
  3.  * Références rvalue (T&&), std::move, constructeur et affectation par déplacement.
  4.  */
  5.  
  6. #include <iostream>
  7. #include <utility>
  8. #include <cstring>
  9.  
  10. class TamponDynamique {
  11. private:
  12. std::size_t m_taille;
  13. char* m_donnees;
  14.  
  15. public:
  16. // Constructeur standard
  17. TamponDynamique(std::size_t taille, char caractere)
  18. : m_taille(taille), m_donnees(new char[taille + 1]) {
  19. std::memset(m_donnees, caractere, taille);
  20. m_donnees[taille] = '\0';
  21. std::cout << " [Constructeur] Allocation de " << m_taille << " octets à "
  22. << static_cast<void*>(m_donnees) << "\n";
  23. }
  24.  
  25. // 1/5 : Destructeur
  26. ~TamponDynamique() noexcept {
  27. delete[] m_donnees;
  28. std::cout << " [Destructeur] Libération de la mémoire\n";
  29. }
  30.  
  31. // 2/5 : Constructeur de recopie (Copie Profonde - Coûteux)
  32. TamponDynamique(const TamponDynamique& autre)
  33. : m_taille(autre.m_taille), m_donnees(new char[autre.m_taille + 1]) {
  34. std::memcpy(m_donnees, autre.m_donnees, autre.m_taille + 1);
  35. std::cout << " [Copie] COPIE PROFONDE de " << m_taille << " octets (nouvelle allocation)\n";
  36. }
  37.  
  38. // 3/5 : Opérateur d'affectation par recopie
  39. TamponDynamique& operator=(const TamponDynamique& autre) {
  40. if (this != &autre) {
  41. delete[] m_donnees;
  42. m_taille = autre.m_taille;
  43. m_donnees = new char[m_taille + 1];
  44. std::memcpy(m_donnees, autre.m_donnees, m_taille + 1);
  45. std::cout << " [Affectation Copie] Réallocation et copie de " << m_taille << " octets\n";
  46. }
  47. return *this;
  48. }
  49.  
  50. // 4/5 : Constructeur par déplacement (Vol de ressources en O(1) - Gratuit !)
  51. // Toujours marquer 'noexcept' pour permettre aux conteneurs STL de l'utiliser
  52. TamponDynamique(TamponDynamique&& autre) noexcept
  53. : m_taille(autre.m_taille), m_donnees(autre.m_donnees) {
  54. // Neutralisation de la source pour éviter une double libération
  55. autre.m_taille = 0;
  56. autre.m_donnees = nullptr;
  57. std::cout << " [Déplacement] VOL DE RESSOURCE en O(1) sans aucune allocation !\n";
  58. }
  59.  
  60. // 5/5 : Opérateur d'affectation par déplacement
  61. TamponDynamique& operator=(TamponDynamique&& autre) noexcept {
  62. if (this != &autre) {
  63. delete[] m_donnees; // Libère nos anciennes ressources
  64. // Vol des ressources de l'autre
  65. m_taille = autre.m_taille;
  66. m_donnees = autre.m_donnees;
  67. // Neutralisation
  68. autre.m_taille = 0;
  69. autre.m_donnees = nullptr;
  70. std::cout << " [Affectation Déplacement] Transfert instantané de pointeur en O(1)\n";
  71. }
  72. return *this;
  73. }
  74.  
  75. void afficher(const char* label) const {
  76. std::cout << " " << label << " -> Taille: " << m_taille
  77. << " | Pointeur: " << static_cast<void*>(m_donnees) << "\n";
  78. }
  79. };
  80.  
  81. int main() {
  82. std::cout << "=== Annexe : Sémantique de Déplacement & Règle des Cinq ===\n\n";
  83.  
  84. std::cout << "--- 1. Copie Profonde classique (Lente) ---\n";
  85. TamponDynamique original(10000, 'A');
  86. original.afficher("original");
  87.  
  88. TamponDynamique copie = original; // Appelle le constructeur de recopie
  89. copie.afficher("copie");
  90.  
  91. std::cout << "\n--- 2. Déplacement avec std::move (Ultra-rapide, O(1)) ---\n";
  92. // std::move effectue un cast vers une rvalue reference (TamponDynamique&&)
  93. // Il signale au compilateur : "Je n'ai plus besoin du contenu d'original, volez-le !"
  94. TamponDynamique deplace = std::move(original);
  95. deplace.afficher("deplace");
  96. original.afficher("original (après vol)");
  97.  
  98. std::cout << "\n--- 3. Fin du programme (destructions) ---\n";
  99. return 0;
  100. }
4

L'Amitié (friend) & Surcharge des Opérateurs de Flux

⏱️ 15 minutes

4.1. Pourquoi operator<< ne peut pas être une méthode membre ?

Considérons l'instruction classique : std::cout << monObjet;.
En C++, pour une méthode membre A.operator+(B), l'opérande de gauche est obligatoirement l'objet appelant (l'instance courante *this). Or, dans std::cout << monObjet, l'opérande de gauche est std::cout (de type std::ostream), et non pas monObjet !

L'opérateur doit donc être déclaré comme une fonction libre (externe) à deux paramètres :
std::ostream& operator<<(std::ostream& os, const MonObjet& obj);

4.2. Le mot-clé friend : L'encapsulation contrôlée

Pour que cette fonction libre puisse afficher directement les attributs privés de MonObjet sans devoir créer une cascade de getters publics, on la déclare amie (friend) à l'intérieur de la classe :

CPPannexe_friend_operateurs.cpp
  1. /**
  2.  * Annexe - Exemple 4 : Amitié (friend) & Surcharge des Opérateurs de Flux
  3.  * Surcharge de operator<< et operator>> pour affichage et saisie d'objets personnalisés.
  4.  */
  5.  
  6. #include <iostream>
  7. #include <sstream>
  8. #include <string>
  9.  
  10. class Fraction {
  11. private:
  12. int m_numerateur;
  13. int m_denominateur;
  14.  
  15. void simplifier() {
  16. // Calcul du PGCD simple
  17. int a = (m_numerateur < 0) ? -m_numerateur : m_numerateur;
  18. int b = (m_denominateur < 0) ? -m_denominateur : m_denominateur;
  19. while (b != 0) {
  20. int t = b;
  21. b = a % b;
  22. a = t;
  23. }
  24. int pgcd = (a == 0) ? 1 : a;
  25. m_numerateur /= pgcd;
  26. m_denominateur /= pgcd;
  27. if (m_denominateur < 0) {
  28. m_numerateur = -m_numerateur;
  29. m_denominateur = -m_denominateur;
  30. }
  31. }
  32.  
  33. public:
  34. Fraction(int num = 0, int den = 1) : m_numerateur(num), m_denominateur(den) {
  35. if (m_denominateur == 0) {
  36. throw std::invalid_argument("Le dénominateur ne peut pas être nul !");
  37. }
  38. simplifier();
  39. }
  40.  
  41. // ⚡ DÉCLARATION D'AMITIÉ (friend) :
  42. // operator<< ne peut PAS être une méthode membre de Fraction, car son opérande de GAUCHE
  43. // est un flux (std::ostream&), et non un objet Fraction !
  44. // Déclarer la fonction 'friend' lui donne un accès direct aux champs privés m_numerateur et m_denominateur.
  45. friend std::ostream& operator<<(std::ostream& os, const Fraction& f);
  46. friend std::istream& operator>>(std::istream& is, Fraction& f);
  47. };
  48.  
  49. // Implémentation externe de l'opérateur d'insertion dans un flux
  50. std::ostream& operator<<(std::ostream& os, const Fraction& f) {
  51. if (f.m_denominateur == 1) {
  52. os << f.m_numerateur;
  53. } else {
  54. os << f.m_numerateur << "/" << f.m_denominateur;
  55. }
  56. return os; // Renvoi par référence pour permettre le chaînage cout << a << b
  57. }
  58.  
  59. // Implémentation de l'opérateur d'extraction depuis un flux
  60. std::istream& operator>>(std::istream& is, Fraction& f) {
  61. char slash = '\0';
  62. int num = 0, den = 1;
  63. if (is >> num) {
  64. if (is >> slash && slash == '/') {
  65. is >> den;
  66. }
  67. f = Fraction(num, den);
  68. }
  69. return is;
  70. }
  71.  
  72. int main() {
  73. std::cout << "=== Annexe : L'Amitié (friend) & Surcharge de << / >> ===\n\n";
  74.  
  75. Fraction f1(3, 4);
  76. Fraction f2(10, 15); // Simplifié automatiquement en 2/3
  77. Fraction f3(8, 2); // Simplifié en 4
  78.  
  79. // Utilisation ultra-naturelle avec std::cout grâce à operator<<
  80. std::cout << "f1 = " << f1 << "\n";
  81. std::cout << "f2 = " << f2 << "\n";
  82. std::cout << "f3 = " << f3 << "\n";
  83.  
  84. // Chaînage
  85. std::cout << "Chaînage naturel : " << f1 << " et " << f2 << "\n\n";
  86.  
  87. // Test de lecture via flux (ici simulé avec std::stringstream)
  88. std::stringstream fluxSimulation("7/9");
  89. Fraction f4;
  90. fluxSimulation >> f4;
  91. std::cout << "Fraction lue depuis un flux string : " << f4 << "\n";
  92.  
  93. return 0;
  94. }
5

Les Membres Statiques (Attributs & Méthodes static)

⏱️ 15 minutes

Un membre déclaré static n'appartient pas à une instance particulière de la classe, mais à la classe elle-même dans son ensemble.

📦 Attribut Statique (Variable de classe)

  • Existe en un seul exemplaire unique en mémoire pour toutes les instances créées.
  • Ne consomme aucun octet dans l'empreinte mémoire individuelle de chaque objet.
  • Doit être obligatoirement défini et initialisé en dehors de la classe dans un fichier source .cpp.

⚙️ Méthode Statique (Fonction de classe)

  • Peut être invoquée directement via la classe : NomClasse::methode() sans avoir besoin de créer un objet.
  • N'a aucun pointeur this implicite.
  • Ne peut manipuler que d'autres variables ou méthodes statiques de la classe.
CPPannexe_membres_statiques.cpp
  1. /**
  2.  * Annexe - Exemple 5 : Membres Statiques (Attributs & Méthodes static)
  3.  * Variables de classe partagées, méthodes de classe et patron Singleton / Compteur d'instances.
  4.  */
  5.  
  6. #include <iostream>
  7. #include <string>
  8.  
  9. class Utilisateur {
  10. private:
  11. std::string m_pseudo;
  12. int m_idUnique;
  13.  
  14. // 1. Attribut statique : partagé par TOUTES les instances de la classe
  15. // Il existe en un seul exemplaire en mémoire (dans le segment de données statiques, pas sur la pile)
  16. static int s_prochainId;
  17. static int s_nbUtilisateursActifs;
  18.  
  19. public:
  20. Utilisateur(const std::string& pseudo)
  21. : m_pseudo(pseudo), m_idUnique(s_prochainId++) {
  22. ++s_nbUtilisateursActifs;
  23. std::cout << "[+] Utilisateur créé : " << m_pseudo << " (ID: " << m_idUnique << ")\n";
  24. }
  25.  
  26. ~Utilisateur() {
  27. --s_nbUtilisateursActifs;
  28. std::cout << "[-] Utilisateur détruit : " << m_pseudo << " (ID: " << m_idUnique << ")\n";
  29. }
  30.  
  31. // 2. Méthode statique : n'a PAS de pointeur 'this', peut être appelée sans objet !
  32. // Elle ne peut accéder qu'aux membres statiques de la classe.
  33. static int getNombreActifs() {
  34. return s_nbUtilisateursActifs;
  35. }
  36.  
  37. static int getProchainId() {
  38. return s_prochainId;
  39. }
  40.  
  41. void afficher() const {
  42. std::cout << " -> Utilisateur #" << m_idUnique << " : " << m_pseudo << "\n";
  43. }
  44. };
  45.  
  46. // 3. Définition et initialisation hors de la classe (dans le fichier source d'implémentation)
  47. int Utilisateur::s_prochainId = 1001;
  48. int Utilisateur::s_nbUtilisateursActifs = 0;
  49.  
  50. int main() {
  51. std::cout << "=== Annexe : Membres Statiques en C++ ===\n\n";
  52.  
  53. // Appel direct sans aucune instance créée !
  54. std::cout << "Utilisateurs actifs au démarrage : " << Utilisateur::getNombreActifs() << "\n";
  55. std::cout << "Prochain ID disponible : " << Utilisateur::getProchainId() << "\n\n";
  56.  
  57. // Création d'instances
  58. Utilisateur u1("Alice");
  59. Utilisateur u2("Bob");
  60. std::cout << "Utilisateurs actifs actuels : " << Utilisateur::getNombreActifs() << "\n\n";
  61.  
  62. {
  63. Utilisateur temporaire("Charlie_Invite");
  64. temporaire.afficher();
  65. std::cout << "Actifs pendant la session invité : " << Utilisateur::getNombreActifs() << "\n";
  66. } // destruction de temporaire
  67.  
  68. std::cout << "\nActifs après la déconnexion de l'invité : " << Utilisateur::getNombreActifs() << "\n";
  69. std::cout << "Prochain ID incrémenté : " << Utilisateur::getProchainId() << "\n";
  70.  
  71. return 0;
  72. }
6

Le Transtypage Sécurisé et le RTTI (Run-Time Type Information)

⏱️ 20 minutes

6.1. Le cast C-style et ses dangers

En C classique, la conversion de type brut (Chat*)animal force le compilateur à fermer les yeux sur la compatibilité des mémoires. Si l'adresse pointait en réalité vers un Chien, accéder aux champs spécifiques d'un Chat provoque un comportement indéfini critique.

6.2. dynamic_cast : La conversion sécurisée à l'exécution

L'opérateur dynamic_cast<T*> s'appuie sur les métadonnées de la vtable (RTTI) générées lors de l'utilisation de méthodes virtuelles :

💡 L'opérateur typeid

L'opérateur typeid(*pointeur).name() (défini dans <typeinfo>) permet d'inspecter dynamiquement à l'exécution le type réel de l'objet sous-jacent.

CPPannexe_transtypage_rtti.cpp
  1. /**
  2.  * Annexe - Exemple 6 : Transtypage Sécurisé & RTTI (Run-Time Type Information)
  3.  * dynamic_cast, static_cast, typeid et gestion des hiérarchies polymorphiques en C++20.
  4.  */
  5.  
  6. #include <iostream>
  7. #include <string>
  8. #include <typeinfo>
  9. #include <memory>
  10.  
  11. class Animal {
  12. public:
  13. // ⚡ Pour activer le RTTI et la vtable, la classe DOIT avoir au moins une méthode virtuelle !
  14. virtual ~Animal() = default;
  15. virtual void emettreSon() const = 0;
  16. };
  17.  
  18. class Chien : public Animal {
  19. public:
  20. void emettreSon() const override {
  21. std::cout << "Ouaf !\n";
  22. }
  23. void rapporterBalle() const {
  24. std::cout << "Le chien court chercher la balle avec enthousiasme !\n";
  25. }
  26. };
  27.  
  28. class Chat : public Animal {
  29. public:
  30. void emettreSon() const override {
  31. std::cout << "Miaou !\n";
  32. }
  33. void ronronner() const {
  34. std::cout << "Le chat ronronne sur le canapé...\n";
  35. }
  36. };
  37.  
  38. // Fonction démontrant le transtypage sécurisé vers le bas (Downcasting)
  39. void interagir(Animal* animal) {
  40. if (!animal) return;
  41.  
  42. std::cout << "\n[Inspection RTTI] typeid réel : " << typeid(*animal).name() << "\n";
  43. animal->emettreSon();
  44.  
  45. // 1. dynamic_cast avec pointeur : Renvoie nullptr en cas d'échec
  46. if (Chien* pChien = dynamic_cast<Chien*>(animal)) {
  47. std::cout << ">> Détection réussie : C'est un Chien !\n";
  48. pChien->rapporterBalle();
  49. } else if (Chat* pChat = dynamic_cast<Chat*>(animal)) {
  50. std::cout << ">> Détection réussie : C'est un Chat !\n";
  51. pChat->ronronner();
  52. } else {
  53. std::cout << ">> Animal de type inconnu.\n";
  54. }
  55. }
  56.  
  57. int main() {
  58. std::cout << "=== Annexe : Transtypage Sécurisé & RTTI en C++ ===\n\n";
  59.  
  60. std::unique_ptr<Animal> a1 = std::make_unique<Chien>();
  61. std::unique_ptr<Animal> a2 = std::make_unique<Chat>();
  62.  
  63. std::cout << "--- 1. Downcasting avec dynamic_cast sur pointeur ---";
  64. interagir(a1.get());
  65. interagir(a2.get());
  66.  
  67. // 2. dynamic_cast avec référence : Lève std::bad_cast en cas d'échec
  68. std::cout << "\n--- 2. dynamic_cast sur référence & std::bad_cast ---\n";
  69. try {
  70. Animal& refAnimal = *a1; // refAnimal est en réalité un Chien
  71. std::cout << "Tentative risquée de caster refAnimal vers Chat&...\n";
  72. Chat& refChat = dynamic_cast<Chat&>(refAnimal); // Échoue car refAnimal est un Chien !
  73. refChat.ronronner();
  74. } catch (const std::bad_cast& e) {
  75. std::cout << ">> Exception std::bad_cast interceptée avec succès : " << e.what() << "\n";
  76. std::cout << " (Impossible de convertir une référence de Chien en Chat !)\n";
  77. }
  78.  
  79. return 0;
  80. }
7

Synthèse Générale & Auto-évaluation

⏱️ 15 minutes

7.1. Fiche Mémo Récapitulative

Concept C++ Avancé Mots-clés / Types associés Bonne pratique recommandée
Exceptions try, catch, throw, noexcept Signaler l'échec dans les constructeurs ; bannir les exceptions dans les destructeurs.
Smart Pointers std::unique_ptr, std::shared_ptr Privilégier systématiquement std::make_unique aux pointeurs bruts.
Sémantique de Déplacement T&&, std::move, Règle des 5 Transférer les tampons volumineux en $O(1)$ au lieu de les dupliquer. Marquer noexcept.
Opérateurs de flux friend std::ostream& operator<< Fonctions libres amies à deux opérandes, avec renvoi de flux par référence pour chaînage.
Membres Statiques static, NomClasse:: Variables partagées uniques à initialiser hors-classe dans le .cpp.
Transtypage Polymorphique dynamic_cast<T*>, typeid Toujours tester si le pointeur renvoyé est non nul avant de l'utiliser.

7.2. QCM de Réflexion

❓ Question 1 : Pourquoi est-il indispensable que le constructeur par déplacement soit annoté avec le spécificateur noexcept ?
A. Parce que le compilateur refuse catégoriquement de compiler la classe sans ce mot-clé.
B. Pour permettre aux conteneurs de la STL (comme std::vector lors d'une réallocation) d'utiliser le déplacement au lieu de se replier par sécurité sur une copie coûteuse.
C. Parce que noexcept accélère la fréquence du processeur lors de l'exécution.
Réponse B : Si std::vector réalloue sa mémoire pour s'agrandir, il doit garantir la sécurité forte en cas d'exception. Si le constructeur de déplacement n'est pas garanti sans exception (noexcept), std::vector préfèrera effectuer une copie profonde pour éviter de corrompre les données en cas de panne à mi-parcours !
❓ Question 2 : Que renvoie dynamic_cast<Chien*>(pAnimal) si pAnimal pointe en réalité vers une instance de Chat ?
A. Une exception std::bad_cast est levée.
B. La valeur nullptr (pointeur nul).
C. Le programme plante immédiatement avec un crash mémoire.
Réponse B : Lorsqu'il s'applique à un pointeur, dynamic_cast échoue gracieusement en renvoyant nullptr. C'est uniquement lorsqu'il s'applique à une référence (qui ne peut pas être nulle) qu'il lève une exception std::bad_cast.
❓ Question 3 : Quelle est la différence fondamentale entre std::unique_ptr et std::shared_ptr ?
A. unique_ptr garantit une propriété exclusive (non copiable, zéro surcoût) alors que shared_ptr gère un compteur de références partagé.
B. unique_ptr est alloué sur la pile et shared_ptr est alloué sur le tas.
C. unique_ptr nécessite d'appeler explicitement delete à la fin.
Réponse A : std::unique_ptr ne peut avoir qu'un seul propriétaire à la fois (possession exclusive) et sa copie est interdite par le compilateur. std::shared_ptr maintient un bloc de contrôle avec un compteur atomique et ne libère la ressource que lorsque le dernier pointeur partagé disparaît.

Navigation dans le cours

Vous pouvez revenir au portail d'accueil ou consulter les chapitres du cours :

← Chapitre 5 (POO en Python) ← Chapitre 4 (La STL) Retour à l'Accueil du Cours ➔