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érarchiestd::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 avecstd::shared_ptret rupture de cycles avecstd::weak_ptr. - Sémantique de déplacement (Move Semantics) : Références rvalue
T&&, conversionstd::moveet transition de la Règle des 3 vers la Règle des 5. - L'Amitié (friend) & Opérateurs de flux : Surcharge de
operator<<etoperator>>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 avectypeid.
Gestion des Exceptions et Sécurité des Constructeurs
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.
throw expression;: Interrompt immédiatement la fonction courante et propage l'exception dans la pile d'appels.try { ... } catch (const std::exception& e) { ... }: Isole le bloc à risque et intercepte les anomalies pour les traiter.
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.
/** * Annexe - Exemple 1 : Gestion des Exceptions et Sécurité dans les Constructeurs * Exceptions personnalisées, try/catch, et garantie noexcept sur les destructeurs en C++20. */ #include <iostream> #include <string> #include <stdexcept> // 1. Définition d'une exception personnalisée dérivée de std::runtime_error class SoldeInsuffisantException : public std::runtime_error { private: double m_soldeActuel; double m_montantDemande; public: SoldeInsuffisantException(double solde, double montant) : std::runtime_error("Opération refusée : solde bancaire insuffisant"), m_soldeActuel(solde), m_montantDemande(montant) {} double getSolde() const noexcept { return m_soldeActuel; } double getMontant() const noexcept { return m_montantDemande; } }; // 2. Classe protégeant ses invariants via des exceptions class CompteBancaireSecurise { private: std::string m_titulaire; double m_solde; public: // Une exception lancée dans un constructeur ANNULE la création de l'objet // La mémoire allouée sur la pile est automatiquement libérée (déroulement de pile / stack unwinding) CompteBancaireSecurise(const std::string& titulaire, double soldeInitial) : m_titulaire(titulaire), m_solde(soldeInitial) { if (soldeInitial < 0.0) { throw std::invalid_argument("Le solde initial ne peut pas être négatif !"); } std::cout << "[+] Compte de " << m_titulaire << " créé avec " << m_solde << " €\n"; } // ⚡ RÈGLE ABSOLUE : Un destructeur DOIT TOUJOURS être noexcept (ne JAMAIS lancer d'exception) ! // Si une exception est lancée pendant un déroulement de pile, std::terminate() est appelé immédiatement. ~CompteBancaireSecurise() noexcept { std::cout << "[-] Compte de " << m_titulaire << " détruit proprement.\n"; } void retirer(double montant) { if (montant <= 0.0) { throw std::invalid_argument("Le montant retiré doit être strictement positif."); } if (montant > m_solde) { throw SoldeInsuffisantException(m_solde, montant); } m_solde -= montant; std::cout << "Retrait de " << montant << " € effectué. Reste : " << m_solde << " €\n"; } double getSolde() const noexcept { return m_solde; } }; int main() { std::cout << "=== Annexe : Exceptions & Sécurité en C++ ===\n\n"; // Test 1 : Exception dans le constructeur (déroulement de pile) std::cout << "--- 1. Exception dans le constructeur ---\n"; try { std::cout << "Tentative d'instanciation avec solde négatif (-100 €)...\n"; CompteBancaireSecurise compteInvalide("Client Erroné", -100.0); std::cout << "Cette ligne ne sera jamais atteinte !\n"; } catch (const std::invalid_argument& e) { std::cout << ">> Exception capturée : " << e.what() << "\n"; } // Test 2 : Opération normale et exception métier personnalisée std::cout << "\n--- 2. Retrait et exception personnalisée ---\n"; try { CompteBancaireSecurise c1("Jean Martin", 200.0); c1.retirer(50.0); // OK : reste 150 € std::cout << "Tentative de retrait de 300 €...\n"; c1.retirer(300.0); // Déclenche SoldeInsuffisantException } catch (const SoldeInsuffisantException& e) { std::cout << ">> Exception métier : " << e.what() << "\n"; std::cout << " Détails : Solde=" << e.getSolde() << " €, Demandé=" << e.getMontant() << " €\n"; } catch (const std::exception& e) { std::cout << ">> Autre exception : " << e.what() << "\n"; } return 0; }
Les Pointeurs Intelligents (Smart Pointers)
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 |
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).
/** * Annexe - Exemple 2 : Les Pointeurs Intelligents (Smart Pointers en C++ Moderne) * std::unique_ptr, std::shared_ptr, std::weak_ptr et std::make_unique / make_shared */ #include <iostream> #include <memory> #include <string> class Document { private: std::string m_titre; public: Document(const std::string& titre) : m_titre(titre) { std::cout << " [+] Création Document : \"" << m_titre << "\"\n"; } ~Document() { std::cout << " [-] Destruction Document : \"" << m_titre << "\"\n"; } void afficher() const { std::cout << " -> Document : \"" << m_titre << "\"\n"; } }; // Exemple de dépendance cyclique évitée avec std::weak_ptr struct Noeud { std::string nom; std::shared_ptr<Noeud> suivant; // Utiliser weak_ptr pour le pointeur arrière afin de casser le cycle de vie circulaire ! std::weak_ptr<Noeud> precedent; Noeud(const std::string& n) : nom(n) { std::cout << " [+] Noeud '" << nom << "' alloué\n"; } ~Noeud() { std::cout << " [-] Noeud '" << nom << "' libéré\n"; } }; int main() { std::cout << "=== Annexe : Pointeurs Intelligents (Modern C++) ===\n\n"; // 1. std::unique_ptr : Possession exclusive (Zéro surcoût par rapport à un pointeur brut) std::cout << "--- 1. std::unique_ptr (possession exclusive & transfert) ---\n"; { // Création recommandée avec std::make_unique (sécurisé contre les fuites en cas d'exception) auto doc1 = std::make_unique<Document>("Rapport_Annuel_2026.pdf"); doc1->afficher(); // ❌ doc1 n'est PAS copiable : auto doc2 = doc1; (Erreur de compilation !) // ✅ Mais il est déplaçable avec std::move : std::cout << "Transfert de propriété vers doc2 via std::move...\n"; std::unique_ptr<Document> doc2 = std::move(doc1); if (!doc1) { std::cout << "doc1 est maintenant vide (nullptr).\n"; } doc2->afficher(); std::cout << "Sortie du bloc de portée locale...\n"; } // doc2 est détruit automatiquement ici ! Zéro fuite de mémoire. // 2. std::shared_ptr : Possession partagée avec compteur de références std::cout << "\n--- 2. std::shared_ptr (possession partagée) ---\n"; { auto part1 = std::make_shared<Document>("Cahier_des_charges.docx"); std::cout << "Compteur de références part1 : " << part1.use_count() << "\n"; { auto part2 = part1; // Copie autorisée : incrémente le compteur std::cout << "Après copie part2 = part1 -> Compteur : " << part1.use_count() << "\n"; part2->afficher(); std::cout << "Fin du sous-bloc (part2 va être détruit)...\n"; } std::cout << "Après destruction de part2 -> Compteur : " << part1.use_count() << "\n"; } // Le document est détruit ici quand le dernier shared_ptr meurt. // 3. std::weak_ptr : Observateur non-propriétaire (brise les cycles) std::cout << "\n--- 3. std::weak_ptr (rupture de cycle circulaire) ---\n"; { auto n1 = std::make_shared<Noeud>("A"); auto n2 = std::make_shared<Noeud>("B"); n1->suivant = n2; n2->precedent = n1; // weak_ptr n'augmente pas le use_count() ! std::cout << "n1 use_count : " << n1.use_count() << " (grâce à weak_ptr au lieu de shared_ptr)\n"; // Accès à un weak_ptr via lock() if (auto p = n2->precedent.lock()) { std::cout << "Accès valide depuis n2 vers précédent : " << p->nom << "\n"; } } // n1 et n2 sont parfaitement détruits tous les deux ! return 0; }
La Sémantique de Déplacement & la Règle des Cinq
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
- Lvalue (
T&) : Objet persistant ayant un nom et une adresse mémoire identifiables. - Rvalue (
T&&) : Valeur temporaire éphémère (ex: résultat d'un calcul ou retour direct de fonction). std::move(objet): Ne déplace rien physiquement ! C'est un simple transtypage (cast) qui convertit une lvalue en rvalue pour autoriser le vol de ses ressources.
- 1. Nouvelle allocation sur le tas
- 2. Copie octet par octet des $N$ éléments
- 3. Deux tampons indépendants
- 1. Vol du pointeur interne brut
- 2. Mise à
nullptrdu 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 :
- Le Destructeur :
~MaClasse() noexcept; - Le Constructeur de recopie :
MaClasse(const MaClasse&); - L'Opérateur d'affectation par recopie :
MaClasse& operator=(const MaClasse&); - Le Constructeur par déplacement :
MaClasse(MaClasse&&) noexcept; - L'Opérateur d'affectation par déplacement :
MaClasse& operator=(MaClasse&&) noexcept;
/** * Annexe - Exemple 3 : Sémantique de Déplacement & Règle des Cinq (Rule of Five) * Références rvalue (T&&), std::move, constructeur et affectation par déplacement. */ #include <iostream> #include <utility> #include <cstring> class TamponDynamique { private: std::size_t m_taille; char* m_donnees; public: // Constructeur standard TamponDynamique(std::size_t taille, char caractere) : m_taille(taille), m_donnees(new char[taille + 1]) { std::memset(m_donnees, caractere, taille); m_donnees[taille] = '\0'; std::cout << " [Constructeur] Allocation de " << m_taille << " octets à " << static_cast<void*>(m_donnees) << "\n"; } // 1/5 : Destructeur ~TamponDynamique() noexcept { delete[] m_donnees; std::cout << " [Destructeur] Libération de la mémoire\n"; } // 2/5 : Constructeur de recopie (Copie Profonde - Coûteux) TamponDynamique(const TamponDynamique& autre) : m_taille(autre.m_taille), m_donnees(new char[autre.m_taille + 1]) { std::memcpy(m_donnees, autre.m_donnees, autre.m_taille + 1); std::cout << " [Copie] COPIE PROFONDE de " << m_taille << " octets (nouvelle allocation)\n"; } // 3/5 : Opérateur d'affectation par recopie TamponDynamique& operator=(const TamponDynamique& autre) { if (this != &autre) { delete[] m_donnees; m_taille = autre.m_taille; m_donnees = new char[m_taille + 1]; std::memcpy(m_donnees, autre.m_donnees, m_taille + 1); std::cout << " [Affectation Copie] Réallocation et copie de " << m_taille << " octets\n"; } return *this; } // 4/5 : Constructeur par déplacement (Vol de ressources en O(1) - Gratuit !) // Toujours marquer 'noexcept' pour permettre aux conteneurs STL de l'utiliser TamponDynamique(TamponDynamique&& autre) noexcept : m_taille(autre.m_taille), m_donnees(autre.m_donnees) { // Neutralisation de la source pour éviter une double libération autre.m_taille = 0; autre.m_donnees = nullptr; std::cout << " [Déplacement] VOL DE RESSOURCE en O(1) sans aucune allocation !\n"; } // 5/5 : Opérateur d'affectation par déplacement TamponDynamique& operator=(TamponDynamique&& autre) noexcept { if (this != &autre) { delete[] m_donnees; // Libère nos anciennes ressources // Vol des ressources de l'autre m_taille = autre.m_taille; m_donnees = autre.m_donnees; // Neutralisation autre.m_taille = 0; autre.m_donnees = nullptr; std::cout << " [Affectation Déplacement] Transfert instantané de pointeur en O(1)\n"; } return *this; } void afficher(const char* label) const { std::cout << " " << label << " -> Taille: " << m_taille << " | Pointeur: " << static_cast<void*>(m_donnees) << "\n"; } }; int main() { std::cout << "=== Annexe : Sémantique de Déplacement & Règle des Cinq ===\n\n"; std::cout << "--- 1. Copie Profonde classique (Lente) ---\n"; TamponDynamique original(10000, 'A'); original.afficher("original"); TamponDynamique copie = original; // Appelle le constructeur de recopie copie.afficher("copie"); std::cout << "\n--- 2. Déplacement avec std::move (Ultra-rapide, O(1)) ---\n"; // std::move effectue un cast vers une rvalue reference (TamponDynamique&&) // Il signale au compilateur : "Je n'ai plus besoin du contenu d'original, volez-le !" TamponDynamique deplace = std::move(original); deplace.afficher("deplace"); original.afficher("original (après vol)"); std::cout << "\n--- 3. Fin du programme (destructions) ---\n"; return 0; }
L'Amitié (friend) & Surcharge des Opérateurs de Flux
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 :
/** * Annexe - Exemple 4 : Amitié (friend) & Surcharge des Opérateurs de Flux * Surcharge de operator<< et operator>> pour affichage et saisie d'objets personnalisés. */ #include <iostream> #include <sstream> #include <string> class Fraction { private: int m_numerateur; int m_denominateur; void simplifier() { // Calcul du PGCD simple int a = (m_numerateur < 0) ? -m_numerateur : m_numerateur; int b = (m_denominateur < 0) ? -m_denominateur : m_denominateur; while (b != 0) { int t = b; b = a % b; a = t; } int pgcd = (a == 0) ? 1 : a; m_numerateur /= pgcd; m_denominateur /= pgcd; if (m_denominateur < 0) { m_numerateur = -m_numerateur; m_denominateur = -m_denominateur; } } public: Fraction(int num = 0, int den = 1) : m_numerateur(num), m_denominateur(den) { if (m_denominateur == 0) { throw std::invalid_argument("Le dénominateur ne peut pas être nul !"); } simplifier(); } // ⚡ DÉCLARATION D'AMITIÉ (friend) : // operator<< ne peut PAS être une méthode membre de Fraction, car son opérande de GAUCHE // est un flux (std::ostream&), et non un objet Fraction ! // Déclarer la fonction 'friend' lui donne un accès direct aux champs privés m_numerateur et m_denominateur. friend std::ostream& operator<<(std::ostream& os, const Fraction& f); friend std::istream& operator>>(std::istream& is, Fraction& f); }; // Implémentation externe de l'opérateur d'insertion dans un flux std::ostream& operator<<(std::ostream& os, const Fraction& f) { if (f.m_denominateur == 1) { os << f.m_numerateur; } else { os << f.m_numerateur << "/" << f.m_denominateur; } return os; // Renvoi par référence pour permettre le chaînage cout << a << b } // Implémentation de l'opérateur d'extraction depuis un flux std::istream& operator>>(std::istream& is, Fraction& f) { char slash = '\0'; int num = 0, den = 1; if (is >> num) { if (is >> slash && slash == '/') { is >> den; } f = Fraction(num, den); } return is; } int main() { std::cout << "=== Annexe : L'Amitié (friend) & Surcharge de << / >> ===\n\n"; Fraction f1(3, 4); Fraction f2(10, 15); // Simplifié automatiquement en 2/3 Fraction f3(8, 2); // Simplifié en 4 // Utilisation ultra-naturelle avec std::cout grâce à operator<< std::cout << "f1 = " << f1 << "\n"; std::cout << "f2 = " << f2 << "\n"; std::cout << "f3 = " << f3 << "\n"; // Chaînage std::cout << "Chaînage naturel : " << f1 << " et " << f2 << "\n\n"; // Test de lecture via flux (ici simulé avec std::stringstream) std::stringstream fluxSimulation("7/9"); Fraction f4; fluxSimulation >> f4; std::cout << "Fraction lue depuis un flux string : " << f4 << "\n"; return 0; }
Les Membres Statiques (Attributs & Méthodes static)
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
thisimplicite. - Ne peut manipuler que d'autres variables ou méthodes statiques de la classe.
/** * Annexe - Exemple 5 : Membres Statiques (Attributs & Méthodes static) * Variables de classe partagées, méthodes de classe et patron Singleton / Compteur d'instances. */ #include <iostream> #include <string> class Utilisateur { private: std::string m_pseudo; int m_idUnique; // 1. Attribut statique : partagé par TOUTES les instances de la classe // Il existe en un seul exemplaire en mémoire (dans le segment de données statiques, pas sur la pile) static int s_prochainId; static int s_nbUtilisateursActifs; public: Utilisateur(const std::string& pseudo) : m_pseudo(pseudo), m_idUnique(s_prochainId++) { ++s_nbUtilisateursActifs; std::cout << "[+] Utilisateur créé : " << m_pseudo << " (ID: " << m_idUnique << ")\n"; } ~Utilisateur() { --s_nbUtilisateursActifs; std::cout << "[-] Utilisateur détruit : " << m_pseudo << " (ID: " << m_idUnique << ")\n"; } // 2. Méthode statique : n'a PAS de pointeur 'this', peut être appelée sans objet ! // Elle ne peut accéder qu'aux membres statiques de la classe. static int getNombreActifs() { return s_nbUtilisateursActifs; } static int getProchainId() { return s_prochainId; } void afficher() const { std::cout << " -> Utilisateur #" << m_idUnique << " : " << m_pseudo << "\n"; } }; // 3. Définition et initialisation hors de la classe (dans le fichier source d'implémentation) int Utilisateur::s_prochainId = 1001; int Utilisateur::s_nbUtilisateursActifs = 0; int main() { std::cout << "=== Annexe : Membres Statiques en C++ ===\n\n"; // Appel direct sans aucune instance créée ! std::cout << "Utilisateurs actifs au démarrage : " << Utilisateur::getNombreActifs() << "\n"; std::cout << "Prochain ID disponible : " << Utilisateur::getProchainId() << "\n\n"; // Création d'instances Utilisateur u1("Alice"); Utilisateur u2("Bob"); std::cout << "Utilisateurs actifs actuels : " << Utilisateur::getNombreActifs() << "\n\n"; { Utilisateur temporaire("Charlie_Invite"); temporaire.afficher(); std::cout << "Actifs pendant la session invité : " << Utilisateur::getNombreActifs() << "\n"; } // destruction de temporaire std::cout << "\nActifs après la déconnexion de l'invité : " << Utilisateur::getNombreActifs() << "\n"; std::cout << "Prochain ID incrémenté : " << Utilisateur::getProchainId() << "\n"; return 0; }
Le Transtypage Sécurisé et le RTTI (Run-Time Type Information)
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 :
- Sur un pointeur : renvoie un pointeur valide si l'objet réel est bien du type demandé (ou d'un sous-type), sinon renvoie
nullptr. - Sur une référence : comme une référence ne peut jamais être nulle,
dynamic_cast<T&>lève une exceptionstd::bad_casten cas d'incompatibilité.
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.
/** * Annexe - Exemple 6 : Transtypage Sécurisé & RTTI (Run-Time Type Information) * dynamic_cast, static_cast, typeid et gestion des hiérarchies polymorphiques en C++20. */ #include <iostream> #include <string> #include <typeinfo> #include <memory> class Animal { public: // ⚡ Pour activer le RTTI et la vtable, la classe DOIT avoir au moins une méthode virtuelle ! virtual ~Animal() = default; virtual void emettreSon() const = 0; }; class Chien : public Animal { public: void emettreSon() const override { std::cout << "Ouaf !\n"; } void rapporterBalle() const { std::cout << "Le chien court chercher la balle avec enthousiasme !\n"; } }; class Chat : public Animal { public: void emettreSon() const override { std::cout << "Miaou !\n"; } void ronronner() const { std::cout << "Le chat ronronne sur le canapé...\n"; } }; // Fonction démontrant le transtypage sécurisé vers le bas (Downcasting) void interagir(Animal* animal) { if (!animal) return; std::cout << "\n[Inspection RTTI] typeid réel : " << typeid(*animal).name() << "\n"; animal->emettreSon(); // 1. dynamic_cast avec pointeur : Renvoie nullptr en cas d'échec if (Chien* pChien = dynamic_cast<Chien*>(animal)) { std::cout << ">> Détection réussie : C'est un Chien !\n"; pChien->rapporterBalle(); } else if (Chat* pChat = dynamic_cast<Chat*>(animal)) { std::cout << ">> Détection réussie : C'est un Chat !\n"; pChat->ronronner(); } else { std::cout << ">> Animal de type inconnu.\n"; } } int main() { std::cout << "=== Annexe : Transtypage Sécurisé & RTTI en C++ ===\n\n"; std::unique_ptr<Animal> a1 = std::make_unique<Chien>(); std::unique_ptr<Animal> a2 = std::make_unique<Chat>(); std::cout << "--- 1. Downcasting avec dynamic_cast sur pointeur ---"; interagir(a1.get()); interagir(a2.get()); // 2. dynamic_cast avec référence : Lève std::bad_cast en cas d'échec std::cout << "\n--- 2. dynamic_cast sur référence & std::bad_cast ---\n"; try { Animal& refAnimal = *a1; // refAnimal est en réalité un Chien std::cout << "Tentative risquée de caster refAnimal vers Chat&...\n"; Chat& refChat = dynamic_cast<Chat&>(refAnimal); // Échoue car refAnimal est un Chien ! refChat.ronronner(); } catch (const std::bad_cast& e) { std::cout << ">> Exception std::bad_cast interceptée avec succès : " << e.what() << "\n"; std::cout << " (Impossible de convertir une référence de Chien en Chat !)\n"; } return 0; }
Synthèse Générale & Auto-évaluation
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
noexcept ?
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 !
dynamic_cast<Chien*>(pAnimal) si pAnimal pointe en réalité vers une instance de Chat ?
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.
std::unique_ptr et std::shared_ptr ?
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 :