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

Travaux Dirigés 1 : Concepts Fondamentaux, Encapsulation & Règle des Trois

Cette séance de travaux dirigés vous permet de mettre en pratique l'ensemble des notions abordées dans le Chapitre 1 : transition du procédural vers l'objet, protection des invariants de classe, sémantique du pointeur this, cycle de vie des objets en mémoire et mise en œuvre sans compromis de la Règle des Trois.

Objectifs pédagogiques de la séance

  • Exercice 1 (★☆☆☆☆) : Délaisser les struct procédurales au profit de classes encapsulées avec accesseurs const et constructeurs avec listes d'initialisation.
  • Exercice 2 (★★☆☆☆) : Garantir des invariants stricts et utiliser *this pour implémenter une interface fluide (chaînage d'appels).
  • Exercice 3 (★★★☆☆) : Suivre pas à pas l'ordre d'appel des constructeurs et destructeurs lors d'une composition d'objets, et mesurer l'impact du passage par valeur versus référence constante.
  • Exercice 4 (★★★★☆) : Mettre en évidence expérimentalement l'erreur fatale de la copie superficielle (Double Free) lors d'allocations dynamiques brutes et la résoudre par un constructeur de recopie profond.
  • Exercice 5 (★★★★★) : Concevoir de A à Z une classe gérant des ressources mémoire sur le tas selon la forme canonique complète (destructeur, constructeur par recopie, opérateur d'affectation sécurisé contre l'auto-affectation).
⚙️

Consignes de Travail & Outils de Compilation

Commandes de compilation recommandées

Activez systématiquement la norme C++20 et tous les avertissements du compilateur :

g++ -std=c++20 -Wall -Wextra -pedantic exercice.cpp -o exo
./exo

Vérification avec AddressSanitizer

Pour les exercices 4 et 5 impliquant des allocations mémoires dynamiques, compilez avec le sanitizer intégré :

g++ -std=c++20 -fsanitize=address -g exercice.cpp -o exo
./exo

AddressSanitizer signale instantanément toute fuite mémoire, dépassement de tampon (buffer overflow) ou double libération (double free).

Exercice 1

Transition Procédural → Objet : Modélisation d'un Point2D

★☆☆☆☆ Difficulté : Débutant ⏱️ 15-20 min Concepts : Encapsulation, const, Initialisation

1. Contexte

En C procédural classique, un point du plan cartésien était généralement représenté par une structure libre dont les champs étaient modifiables à tout moment depuis n'importe quelle fonction :

struct Point {
    double x;
    double y;
};
 
void translater(struct Point* p, double dx, double dy) {
    p->x += dx;
    p->y += dy;
}

Cette approche ne fournit aucune protection : n'importe quel code peut corrompre les données sans contrôle. En POO C++, nous souhaitons regrouper les données et les comportements au sein d'une classe hermétique.

2. Travail à réaliser

  1. Définissez une classe Point2D encapsulant deux attributs privés m_x et m_y de type double.
  2. Écrivez un constructeur par défaut qui positionne le point à l'origine $(0.0, 0.0)$, et un constructeur paramétré Point2D(double x, double y). Utilisez obligatoirement la liste d'initialisation.
  3. Déclarez les accesseurs (getters) getX() et getY(). Pourquoi doivent-ils impérativement être qualifiés de const ?
  4. Écrivez les mutateurs (setters) setX(double x) et setY(double y).
  5. Ajoutez une méthode void translater(double dx, double dy) modifiant l'état du point.
  6. Ajoutez une méthode double distance(const Point2D& autre) const calculant la distance euclidienne $√{(x_1 - x_2)^2 + (y_1 - y_2)^2}$ (vous pouvez utiliser std::hypot de la bibliothèque <cmath>). Expliquez pourquoi le paramètre autre est passé par référence constante.
  7. Ajoutez une méthode void afficher() const affichant les coordonnées sous la forme (x, y) où $x$ et $y$ sont remplacés par leurs valeurs.
  8. Dans un main(), instanciez deux points sur la pile, translatez l'un d'eux, affichez leur distance, et testez une fonction externe prenant un const Point2D&.
💡 Indice pédagogique : Le contrat const

Si une fonction externe reçoit un point par référence constante : void afficherInfo(const Point2D& pt), le compilateur refusera formellement d'appeler toute méthode sur pt (comme pt.getX() ou pt.afficher()) à moins que ces méthodes n'aient été déclarées avec le mot-clé const à la fin de leur prototype !

Correction détaillée & Analyse de conception

Voici l'implémentation complète en C++20. Notez l'usage strict de la liste d'initialisation : m_x(x), m_y(y) et la transmission par const Point2D& qui évite de recopier inutilement les 16 octets de la structure en mémoire :

CPPtd1_ex1_point2d.cpp
  1. /**
  2.  * TD 1 - Exercice 1 : Transition Procédural vers Objet & Encapsulation de base
  3.  * Modélisation d'un Point2D géométrique en C++20
  4.  */
  5.  
  6. #include <iostream>
  7. #include <cmath>
  8.  
  9. class Point2D {
  10. private:
  11. double m_x;
  12. double m_y;
  13.  
  14. public:
  15. // 1. Constructeur par défaut (initialise à l'origine)
  16. Point2D() : m_x(0.0), m_y(0.0) {}
  17.  
  18. // 2. Constructeur avec coordonnées (liste d'initialisation)
  19. Point2D(double x, double y) : m_x(x), m_y(y) {}
  20.  
  21. // 3. Accesseurs (getters) - strictement 'const' car ils ne modifient pas l'objet
  22. double getX() const { return m_x; }
  23. double getY() const { return m_y; }
  24.  
  25. // 4. Mutateurs (setters)
  26. void setX(double x) { m_x = x; }
  27. void setY(double y) { m_y = y; }
  28.  
  29. // 5. Opération de translation modifiant l'état de l'objet
  30. void translater(double dx, double dy) {
  31. m_x += dx;
  32. m_y += dy;
  33. }
  34.  
  35. // 6. Calcul de distance euclidienne entre *this et un autre point
  36. // Passage par référence constante pour éviter toute copie inutile
  37. double distance(const Point2D& autre) const {
  38. return std::hypot(m_x - autre.m_x, m_y - autre.m_y);
  39. }
  40.  
  41. // 7. Affichage formaté
  42. void afficher() const {
  43. std::cout << "(" << m_x << ", " << m_y << ")";
  44. }
  45. };
  46.  
  47. // Fonction externe illustrant le passage par référence constante
  48. void afficherInfosPoint(const Point2D& p) {
  49. std::cout << "Point aux coordonnées ";
  50. p.afficher(); // Possible uniquement car afficher() est const !
  51. std::cout << " (distance origine = " << p.distance(Point2D()) << ")\n";
  52. }
  53.  
  54. int main() {
  55. std::cout << "=== TD 1 - Exercice 1 : Point2D ===\n";
  56.  
  57. Point2D p1; // Origine (0, 0)
  58. Point2D p2(3.0, 4.0); // Point (3, 4)
  59.  
  60. std::cout << "p1 initial : ";
  61. p1.afficher();
  62. std::cout << "\n";
  63.  
  64. std::cout << "p2 initial : ";
  65. p2.afficher();
  66. std::cout << "\n";
  67.  
  68. std::cout << "\nDistance entre p1 et p2 : " << p1.distance(p2) << "\n";
  69.  
  70. std::cout << "\nTranslation de p1 de (+1.5, -2.0)...\n";
  71. p1.translater(1.5, -2.0);
  72. std::cout << "p1 après translation : ";
  73. p1.afficher();
  74. std::cout << "\n";
  75.  
  76. std::cout << "\nAppel de fonction externe avec passage par const ref :\n";
  77. afficherInfosPoint(p2);
  78.  
  79. return 0;
  80. }

Résultat de l'exécution :

=== TD 1 - Exercice 1 : Point2D === p1 initial : (0, 0) p2 initial : (3, 4) Distance entre p1 et p2 : 5 Translation de p1 de (+1.5, -2.0)... p1 après translation : (1.5, -2) Appel de fonction externe avec passage par const ref : Point aux coordonnées (3, 4) (distance origine = 5)
Exercice 2

Préservation d'un Invariant de Classe & Chaînage via this

★★☆☆☆ Difficulté : Facile à Intermédiaire ⏱️ 20-25 min Concepts : Invariant, private helper, Fluent Interface, this

1. Contexte

Un invariant de classe est une condition logique qui doit rester vraie à chaque instant de la vie d'un objet (dès la fin de son constructeur et à la sortie de chaque méthode publique). Nous souhaitons modéliser une classe Duree représentant un temps sous forme d'heures, minutes et secondes. L'invariant stipule que :

  • Les secondes doivent être comprises dans $[0, 59]$.
  • Les minutes doivent être comprises dans $[0, 59]$.
  • Les heures doivent être positives ou nulles ($≥ 0$).

Une durée comme "1h 75m 130s" est non normalisée et viole l'invariant. Elle doit automatiquement être transformée en "2h 17m 10s".

2. Travail à réaliser

  1. Déclarez la classe Duree avec trois attributs privés entiers : m_heures, m_minutes, m_secondes.
  2. Écrivez une méthode privée void normaliser() chargée de rétablir l'invariant. Pourquoi cette méthode doit-elle impérativement être privée et non publique ?
  3. Proposez trois constructeurs :
    • Un constructeur par défaut (durée nulle 0h 00m 00s).
    • Un constructeur explicit Duree(int secondesTotales) qui convertit un nombre global de secondes. Pourquoi utilise-t-on ici le mot-clé explicit ?
    • Un constructeur complet Duree(int h, int m, int s) faisant appel à normaliser().
  4. Définissez les accesseurs getHeures(), getMinutes(), getSecondes() et enSecondes() const.
  5. Interface fluide (Fluent Interface) :
    Implémentez les méthodes modificatrices ajouterSecondes(int s), ajouterMinutes(int m) et ajouterHeures(int h). Faites en sorte que ces méthodes renvoient une référence sur l'objet courant (Duree&) en utilisant le pointeur this. Démontrez que cette syntaxe permet d'enchaîner les appels :
    duree.ajouterHeures(1).ajouterMinutes(45).ajouterSecondes(30);
  6. Implémentez une méthode bool estSuperieure(const Duree& autre) const comparant deux durées.
⚠️ Piège fréquent : Retour par référence vs par valeur

Si la méthode ajouterSecondes renvoyait Duree (par valeur) au lieu de Duree& (par référence), l'instruction d.ajouterMinutes(10).ajouterSecondes(30); créerait un objet temporaire anonyme sur lequel s'exécuterait le deuxième appel, laissant l'objet initial d inchangé pour la deuxième opération !

Correction détaillée & Analyse de conception

La méthode privée normaliser() convertit l'ensemble des grandeurs en secondes totales avant de redistribuer en base 60. Le renvoi de *this par référence permet le chaînage d'opérations fluide très prisé dans les bibliothèques C++ modernes (comme std::stringstream ou les constructeurs de requêtes) :

CPPtd1_ex2_duree.cpp
  1. /**
  2.  * TD 1 - Exercice 2 : Invariants de classe, Pointeur 'this' et Chaînage de méthodes
  3.  * Classe Duree normalisée avec interface fluide (Fluent Interface) en C++20
  4.  */
  5.  
  6. #include <iostream>
  7. #include <iomanip>
  8.  
  9. class Duree {
  10. private:
  11. int m_heures;
  12. int m_minutes;
  13. int m_secondes;
  14.  
  15. // Méthode privée préservant l'invariant de classe :
  16. // - 0 <= m_secondes < 60
  17. // - 0 <= m_minutes < 60
  18. // - m_heures >= 0
  19. void normaliser() {
  20. // Conversion globale en secondes
  21. long long totalSec = static_cast<long long>(m_heures) * 3600 + m_minutes * 60 + m_secondes;
  22. if (totalSec < 0) {
  23. totalSec = 0; // Seuil plancher (pas de durée négative)
  24. }
  25.  
  26. m_heures = static_cast<int>(totalSec / 3600);
  27. long long reste = totalSec % 3600;
  28. m_minutes = static_cast<int>(reste / 60);
  29. m_secondes = static_cast<int>(reste % 60);
  30. }
  31.  
  32. public:
  33. // 1. Constructeur par défaut (durée nulle)
  34. Duree() : m_heures(0), m_minutes(0), m_secondes(0) {}
  35.  
  36. // 2. Constructeur à partir d'un nombre total de secondes (mot-clé explicit pour éviter conversions implicites surprises)
  37. explicit Duree(int secondesTotales) : m_heures(0), m_minutes(0), m_secondes(secondesTotales) {
  38. normaliser();
  39. }
  40.  
  41. // 3. Constructeur complet
  42. Duree(int h, int m, int s) : m_heures(h), m_minutes(m), m_secondes(s) {
  43. normaliser();
  44. }
  45.  
  46. // 4. Accesseurs const
  47. int getHeures() const { return m_heures; }
  48. int getMinutes() const { return m_minutes; }
  49. int getSecondes() const { return m_secondes; }
  50.  
  51. int enSecondes() const {
  52. return m_heures * 3600 + m_minutes * 60 + m_secondes;
  53. }
  54.  
  55. // 5. Méthodes chaînables (Fluent Interface) grâce au renvoi de *this par référence
  56. Duree& ajouterSecondes(int s) {
  57. m_secondes += s;
  58. normaliser();
  59. return *this; // Renvoie l'objet courant pour permettre le chaînage
  60. }
  61.  
  62. Duree& ajouterMinutes(int m) {
  63. m_minutes += m;
  64. normaliser();
  65. return *this;
  66. }
  67.  
  68. Duree& ajouterHeures(int h) {
  69. m_heures += h;
  70. normaliser();
  71. return *this;
  72. }
  73.  
  74. // 6. Comparaison
  75. bool estSuperieure(const Duree& autre) const {
  76. return this->enSecondes() > autre.enSecondes();
  77. }
  78.  
  79. // 7. Affichage formaté 00h 00m 00s
  80. void afficher() const {
  81. std::cout << std::setfill('0')
  82. << std::setw(2) << m_heures << "h "
  83. << std::setw(2) << m_minutes << "m "
  84. << std::setw(2) << m_secondes << "s";
  85. }
  86. };
  87.  
  88. int main() {
  89. std::cout << "=== TD 1 - Exercice 2 : Invariants et Pointeur this (Duree) ===\n";
  90.  
  91. // Test 1 : Invariant de classe et normalisation
  92. std::cout << "Création d'une durée brute (1h, 75m, 130s) :\n";
  93. Duree d1(1, 75, 130);
  94. std::cout << "-> Résultat après normalisation : ";
  95. d1.afficher();
  96. std::cout << " (" << d1.enSecondes() << " secondes)\n\n";
  97.  
  98. // Test 2 : Chaînage de méthodes (Fluent Interface) via *this
  99. std::cout << "Chaînage sur d1: .ajouterHeures(2).ajouterMinutes(40).ajouterSecondes(50)\n";
  100. d1.ajouterHeures(2)
  101. .ajouterMinutes(40)
  102. .ajouterSecondes(50);
  103.  
  104. std::cout << "-> Résultat : ";
  105. d1.afficher();
  106. std::cout << "\n\n";
  107.  
  108. // Test 3 : Comparaison
  109. Duree marathon(3, 15, 0); // 3h 15m 00s
  110. std::cout << "Durée marathon : ";
  111. marathon.afficher();
  112. std::cout << "\n";
  113.  
  114. if (d1.estSuperieure(marathon)) {
  115. std::cout << "d1 est plus longue que le marathon.\n";
  116. } else {
  117. std::cout << "d1 est plus courte ou égale au marathon.\n";
  118. }
  119.  
  120. return 0;
  121. }

Résultat de l'exécution :

=== TD 1 - Exercice 2 : Invariants et Pointeur this (Duree) === Création d'une durée brute (1h, 75m, 130s) : -> Résultat après normalisation : 02h 17m 10s (8230 secondes) Chaînage sur d1: .ajouterHeures(2).ajouterMinutes(40).ajouterSecondes(50) -> Résultat : 04h 58m 00s Durée marathon : 03h 15m 00s d1 est plus longue que le marathon.
Exercice 3

Composition d'Objets & Traçage du Cycle de Vie Mémoire

★★★☆☆ Difficulté : Intermédiaire ⏱️ 25 min Concepts : Composition, Constructeurs/Destructeurs, Copies cachées

1. Contexte

En C++, la composition d'objets (relation "A-UN") obéit à des règles d'ordonnancement strictes pour l'initialisation et la libération de la mémoire. Il est crucial pour un développeur de savoir prédire l'instant exact où chaque constructeur et chaque destructeur est appelé, notamment pour mesurer le surcoût des copies d'objets.

2. Travail à réaliser

  1. Créez une classe Capteur ayant deux attributs : un identifiant entier m_id et un type std::string m_type. Équipez-la d'un constructeur paramétré, d'un constructeur de copie et d'un destructeur. Chacune de ces méthodes doit afficher un message sur la console précisant l'adresse mémoire de l'objet (this) pour suivre son existence.
  2. Créez une classe StationMeteo qui contient :
    • Un nom std::string m_nom.
    • Deux objets capteurs membres : Capteur m_capteurTemp; et Capteur m_capteurPression;.
    Ajoutez également des messages de traçage dans son constructeur, son constructeur de copie et son destructeur.
  3. Expérimentation n°1 (Ordre d'exécution) :
    Instanciez une StationMeteo dans un bloc délimité par des accolades { ... }. Avant d'exécuter, prédisez sur papier l'ordre exact dans lequel s'afficheront les messages de construction et de destruction. Vérifiez ensuite la règle C++ : les membres sont construits dans l'ordre de leur déclaration dans la classe, puis détruits dans l'ordre strictement inverse.
  4. Expérimentation n°2 (Copies cachées) :
    Écrivez deux fonctions de test :
    • void inspecterParValeur(StationMeteo s);
    • void inspecterParReference(const StationMeteo& s);
    Appelez ces deux fonctions et comptez le nombre d'appels aux constructeurs de copie et destructeurs. Que constatez-vous ?
💥 Piège d'examen classique : Ordre d'initialisation

En C++, l'ordre d'initialisation des attributs est exclusivement déterminé par l'ordre dans lequel ils sont déclarés dans la définition de la classe, et NON par l'ordre dans lequel vous les écrivez dans la liste d'initialisation du constructeur ! Les compilateurs modernes émettent d'ailleurs un avertissement (-Wreorder) si les deux ordres ne correspondent pas.

Correction détaillée & Analyse de l'ordre d'exécution

Cet exercice met en lumière deux principes majeurs :
1. La destruction suit une structure en pile (LIFO - Last In, First Out) : le dernier attribut construit est toujours le premier détruit.
2. L'appel par valeur inspecterParValeur(s) provoque la duplication de la station ET de ses deux capteurs internes, ce qui génère 3 constructeurs de copie et 3 destructeurs supplémentaires ! Le passage par référence constante const StationMeteo& supprime totalement ce gâchis.

CPPtd1_ex3_composition.cpp
  1. /**
  2.  * TD 1 - Exercice 3 : Composition d'Objets & Cycle de Vie
  3.  * Observation de l'ordre d'appel des constructeurs/destructeurs
  4.  * et impact du passage par valeur vs référence constante en C++20.
  5.  */
  6.  
  7. #include <iostream>
  8. #include <string>
  9.  
  10. class Capteur {
  11. private:
  12. int m_id;
  13. std::string m_type;
  14.  
  15. public:
  16. Capteur(int id, const std::string& type) : m_id(id), m_type(type) {
  17. std::cout << " [+] Capteur #" << m_id << " (" << m_type
  18. << ") construit @" << this << "\n";
  19. }
  20.  
  21. // Constructeur par recopie explicite pour tracer les duplications
  22. Capteur(const Capteur& source) : m_id(source.m_id), m_type(source.m_type + " [copie]") {
  23. std::cout << " [C] Capteur COPIÉ depuis @" << &source
  24. << " vers @" << this << "\n";
  25. }
  26.  
  27. ~Capteur() {
  28. std::cout << " [-] Capteur #" << m_id << " détruit @" << this << "\n";
  29. }
  30.  
  31. void mesurer() const {
  32. std::cout << " Mesure acquise par " << m_type << " #" << m_id << "\n";
  33. }
  34. };
  35.  
  36. class StationMeteo {
  37. private:
  38. std::string m_nom;
  39. // Les membres sont TOUJOURS initialisés dans leur ordre de déclaration ici :
  40. Capteur m_capteurTemp;
  41. Capteur m_capteurPression;
  42.  
  43. public:
  44. // Remarquez : même si on inversait l'ordre dans la liste d'initialisation,
  45. // C++ construirait m_capteurTemp puis m_capteurPression selon la déclaration ci-dessus.
  46. StationMeteo(const std::string& nom, int idT, int idP)
  47. : m_nom(nom),
  48. m_capteurTemp(idT, "Thermomètre"),
  49. m_capteurPression(idP, "Baromètre") {
  50. std::cout << "[+] StationMeteo \"" << m_nom << "\" entièrement initialisée @" << this << "\n";
  51. }
  52.  
  53. // Constructeur de copie
  54. StationMeteo(const StationMeteo& source)
  55. : m_nom(source.m_nom + " (copie)"),
  56. m_capteurTemp(source.m_capteurTemp),
  57. m_capteurPression(source.m_capteurPression) {
  58. std::cout << "[C] StationMeteo COPIÉE depuis @" << &source << " vers @" << this << "\n";
  59. }
  60.  
  61. ~StationMeteo() {
  62. std::cout << "[-] StationMeteo \"" << m_nom << "\" commence sa destruction @" << this << "\n";
  63. // Après le corps de ce destructeur, les membres m_capteurPression puis
  64. // m_capteurTemp puis m_nom sont détruits dans l'ordre INVERSE de leur déclaration !
  65. }
  66.  
  67. void releverDonnees() const {
  68. std::cout << "--- Relevé de " << m_nom << " ---\n";
  69. m_capteurTemp.mesurer();
  70. m_capteurPression.mesurer();
  71. }
  72. };
  73.  
  74. // Fonction recevant l'objet par VALEUR (provoque une copie complète en cascade)
  75. void inspecterParValeur(StationMeteo s) {
  76. std::cout << ">> Dans inspecterParValeur() :\n";
  77. s.releverDonnees();
  78. std::cout << ">> Fin de inspecterParValeur(). Destruction imminente de la copie :\n";
  79. }
  80.  
  81. // Fonction recevant l'objet par RÉFÉRENCE CONSTANTE (zéro copie, coût nul)
  82. void inspecterParReference(const StationMeteo& s) {
  83. std::cout << ">> Dans inspecterParReference() (aucune copie créée) :\n";
  84. s.releverDonnees();
  85. std::cout << ">> Fin de inspecterParReference().\n";
  86. }
  87.  
  88. int main() {
  89. std::cout << "=== TD 1 - Exercice 3 : Composition & Cycle de Vie ===\n\n";
  90.  
  91. std::cout << "--- Étape 1 : Construction locale dans un bloc ---\n";
  92. {
  93. StationMeteo stationAlpha("Pic du Midi", 101, 102);
  94. std::cout << "-> Station construite avec succès.\n";
  95. std::cout << "-> Sortie du bloc local :\n";
  96. }
  97. std::cout << "-> Bloc local terminé.\n\n";
  98.  
  99. std::cout << "--- Étape 2 : Impact du passage par valeur vs par référence ---\n";
  100. StationMeteo stationBeta("Mont Blanc", 201, 202);
  101.  
  102. std::cout << "\nAppel 1 : inspecterParValeur (observez la cascade de copies) :\n";
  103. inspecterParValeur(stationBeta);
  104.  
  105. std::cout << "\nAppel 2 : inspecterParReference (recommandé en C++) :\n";
  106. inspecterParReference(stationBeta);
  107.  
  108. std::cout << "\n--- Fin du main(). Destruction de stationBeta : ---\n";
  109. return 0;
  110. }

Résultat de l'exécution :

=== TD 1 - Exercice 3 : Composition & Cycle de Vie === --- Étape 1 : Construction locale dans un bloc --- [+] Capteur #101 (Thermomètre) construit @0x7ffcc6158fb0 [+] Capteur #102 (Baromètre) construit @0x7ffcc6158fd8 [+] StationMeteo "Pic du Midi" entièrement initialisée @0x7ffcc6158f90 -> Station construite avec succès. -> Sortie du bloc local : [-] StationMeteo "Pic du Midi" commence sa destruction @0x7ffcc6158f90 [-] Capteur #102 détruit @0x7ffcc6158fd8 [-] Capteur #101 détruit @0x7ffcc6158fb0 -> Bloc local terminé. --- Étape 2 : Impact du passage par valeur vs par référence --- [+] Capteur #201 (Thermomètre) construit @0x7ffcc6158f40 [+] Capteur #202 (Baromètre) construit @0x7ffcc6158f68 [+] StationMeteo "Mont Blanc" entièrement initialisée @0x7ffcc6158f20 Appel 1 : inspecterParValeur (observez la cascade de copies) : [C] Capteur COPIÉ depuis @0x7ffcc6158f40 vers @0x7ffcc6158fb0 [C] Capteur COPIÉ depuis @0x7ffcc6158f68 vers @0x7ffcc6158fd8 [C] StationMeteo COPIÉE depuis @0x7ffcc6158f20 vers @0x7ffcc6158f90 >> Dans inspecterParValeur() : --- Relevé de Mont Blanc (copie) --- Mesure acquise par Thermomètre [copie] #201 Mesure acquise par Baromètre [copie] #202 >> Fin de inspecterParValeur(). Destruction imminente de la copie : [-] StationMeteo "Mont Blanc (copie)" commence sa destruction @0x7ffcc6158f90 [-] Capteur #202 détruit @0x7ffcc6158fd8 [-] Capteur #201 détruit @0x7ffcc6158fb0 Appel 2 : inspecterParReference (recommandé en C++) : >> Dans inspecterParReference() (aucune copie créée) : --- Relevé de Mont Blanc --- Mesure acquise par Thermomètre #201 Mesure acquise par Baromètre #202 >> Fin de inspecterParReference().
Exercice 4

Allocation Dynamique, Copie Superficielle & Copie Profonde

★★★★☆ Difficulté : Avancé ⏱️ 30 min Concepts : new[], delete[], Shallow Copy, Double Free, Deep Copy

1. Contexte

Dès lors qu'une classe encapsule un pointeur vers de la mémoire allouée dynamiquement sur le tas avec new[], la copie automatique générée par le compilateur C++ devient un piège mortel : elle réalise une copie superficielle (Shallow Copy), copiant simplement la valeur brute de l'adresse mémoire.

2. Travail à réaliser

  1. Définissez une classe VecteurDynamique contenant deux attributs privés : std::size_t m_capacite et un pointeur double* m_donnees.
  2. Écrivez un constructeur paramétré explicit VecteurDynamique(std::size_t capacite, double valInit = 0.0) qui alloue dynamiquement sur le tas un tableau de capacite éléments réels via new double[capacite].
  3. Écrivez le destructeur ~VecteurDynamique() qui libère la ressource avec delete[] m_donnees.
  4. Ajoutez les méthodes getCapacite(), get(std::size_t index) const et set(std::size_t index, double valeur) avec vérification d'indice hors bornes (en lançant std::out_of_range).
  5. Mise en évidence du bogue fatal (sans constructeur de copie) :
    Considérez le code suivant :
    VecteurDynamique v1(4, 10.0);
    VecteurDynamique v2 = v1; // Copie par défaut synthétisée par le compilateur
    v2.set(0, 999.0);
    • Dessinez le schéma de la mémoire (pile et tas) avant et après la copie.
    • Pourquoi la modification de v2[0] modifie-t-elle également v1[0] ?
    • Que se produit-il à la sortie de portée lorsque v2 puis v1 sont détruits ? Quel est le nom précis de cette erreur d'exécution ?
  6. Résolution par la Copie Profonde (Deep Copy) :
    Implémentez le constructeur par recopie canonique :
    VecteurDynamique(const VecteurDynamique& source);
    Veillez à allouer un nouveau bloc mémoire distinct sur le tas et à dupliquer les valeurs une à une.
  7. Vérifiez avec -fsanitize=address que l'exécution se termine désormais sans aucune erreur de libération.
💥 Schéma mémoire : Le mécanisme du Double Free

En copie superficielle, v1.m_donnees et v2.m_donnees pointent vers la même adresse sur le tas (ex: 0x6000020).
1. En fin de bloc, v2 est détruit → delete[] 0x6000020 (mémoire libérée).
2. Ensuite, v1 est détruit → delete[] 0x6000020 sur une zone DÉJÀ libérée → SIGABRT / double free or corruption !

Correction détaillée & Analyse mémoire

Le constructeur de copie alloue une nouvelle case mémoire propre à v2, garantissant que chaque instance est propriétaire exclusive de son bloc mémoire :

CPPtd1_ex4_vecteur_dynamique.cpp
  1. /**
  2.  * TD 1 - Exercice 4 : Allocation Dynamique, Détection de Bogue et Copie Profonde
  3.  * Classe VecteurDynamique gérant manuellement sa mémoire sur le tas en C++20.
  4.  */
  5.  
  6. #include <iostream>
  7. #include <algorithm> // std::copy
  8. #include <stdexcept> // std::out_of_range
  9.  
  10. class VecteurDynamique {
  11. private:
  12. std::size_t m_capacite;
  13. double* m_donnees;
  14.  
  15. public:
  16. // 1. Constructeur paramétré : allocation dynamique sur le tas
  17. explicit VecteurDynamique(std::size_t capacite, double valInit = 0.0)
  18. : m_capacite(capacite),
  19. m_donnees(capacite > 0 ? new double[capacite] : nullptr) {
  20. for (std::size_t i = 0; i < m_capacite; ++i) {
  21. m_donnees[i] = valInit;
  22. }
  23. std::cout << "[+] VecteurDynamique (taille " << m_capacite
  24. << ") alloué sur le tas à l'adresse : " << static_cast<void*>(m_donnees) << "\n";
  25. }
  26.  
  27. // 2. Destructeur : libération de la ressource allouée
  28. ~VecteurDynamique() {
  29. std::cout << "[-] Libération du tableau à l'adresse : "
  30. << static_cast<void*>(m_donnees) << "\n";
  31. delete[] m_donnees;
  32. m_donnees = nullptr;
  33. }
  34.  
  35. // 3. Constructeur par Recopie PROFONDE (Deep Copy)
  36. // INDISPENSABLE : sans lui, le compilateur génère une copie bit-à-bit (Shallow Copy)
  37. // où les deux objets partagent le même pointeur m_donnees -> crash "double free" !
  38. VecteurDynamique(const VecteurDynamique& source)
  39. : m_capacite(source.m_capacite),
  40. m_donnees(source.m_capacite > 0 ? new double[source.m_capacite] : nullptr) {
  41.  
  42. // Copie des éléments un par un dans notre nouvelle zone mémoire distincte
  43. for (std::size_t i = 0; i < m_capacite; ++i) {
  44. m_donnees[i] = source.m_donnees[i];
  45. }
  46.  
  47. std::cout << "[C] COPIE PROFONDE créée (nouvelle adresse tas : "
  48. << static_cast<void*>(m_donnees)
  49. << " distincte de la source : " << static_cast<void*>(source.m_donnees) << ")\n";
  50. }
  51.  
  52. // Accesseurs avec contrôle de bornes
  53. std::size_t getCapacite() const { return m_capacite; }
  54.  
  55. double get(std::size_t index) const {
  56. if (index >= m_capacite) {
  57. throw std::out_of_range("Indice de vecteur hors bornes !");
  58. }
  59. return m_donnees[index];
  60. }
  61.  
  62. void set(std::size_t index, double valeur) {
  63. if (index >= m_capacite) {
  64. throw std::out_of_range("Indice de vecteur hors bornes !");
  65. }
  66. m_donnees[index] = valeur;
  67. }
  68.  
  69. void afficher() const {
  70. std::cout << "[ ";
  71. for (std::size_t i = 0; i < m_capacite; ++i) {
  72. std::cout << m_donnees[i] << (i + 1 < m_capacite ? ", " : " ");
  73. }
  74. std::cout << "] (adresse tas: " << static_cast<void*>(m_donnees) << ")\n";
  75. }
  76. };
  77.  
  78. int main() {
  79. std::cout << "=== TD 1 - Exercice 4 : Allocation Dynamique & Copie Profonde ===\n\n";
  80.  
  81. std::cout << "1. Création de v1 de taille 4 initialisé avec 10.0 :\n";
  82. VecteurDynamique v1(4, 10.0);
  83. std::cout << "v1 : ";
  84. v1.afficher();
  85.  
  86. std::cout << "\n2. Copie de v1 vers v2 via le constructeur de copie :\n";
  87. VecteurDynamique v2 = v1; // Appelle VecteurDynamique(const VecteurDynamique&)
  88. std::cout << "v2 : ";
  89. v2.afficher();
  90.  
  91. std::cout << "\n3. Modification de v2[0] = 999.0 :\n";
  92. v2.set(0, 999.0);
  93.  
  94. std::cout << "v1 après modif de v2 : ";
  95. v1.afficher(); // Doit être INCHANGÉ (prouve l'indépendance mémoire)
  96. std::cout << "v2 après modif de v2 : ";
  97. v2.afficher();
  98.  
  99. std::cout << "\n4. Sortie de fonction (destructions ordonnées sans conflit) :\n";
  100. return 0;
  101. }

Résultat de l'exécution :

=== TD 1 - Exercice 4 : Allocation Dynamique & Copie Profonde === 1. Création de v1 de taille 4 initialisé avec 10.0 : [+] VecteurDynamique (taille 4) alloué sur le tas à l'adresse : 0x6ccddf7e0040 v1 : [ 10, 10, 10, 10 ] (adresse tas: 0x6ccddf7e0040) 2. Copie de v1 vers v2 via le constructeur de copie : [C] COPIE PROFONDE créée (nouvelle adresse tas : 0x6ccddf7e0070 distincte de la source : 0x6ccddf7e0040) v2 : [ 10, 10, 10, 10 ] (adresse tas: 0x6ccddf7e0070) 3. Modification de v2[0] = 999.0 : v1 après modif de v2 : [ 10, 10, 10, 10 ] (adresse tas: 0x6ccddf7e0040) v2 après modif de v2 : [ 999, 10, 10, 10 ] (adresse tas: 0x6ccddf7e0070) 4. Sortie de fonction (destructions ordonnées sans conflit) : [-] Libération du tableau à l'adresse : 0x6ccddf7e0070 [-] Libération du tableau à l'adresse : 0x6ccddf7e0040
Exercice 5

La Forme Canonique Complète : Implémentation de la Classe Chaine

★★★★★ Difficulté : Expert / Synthèse ⏱️ 35-40 min Concepts : Règle des Trois, operator=, Auto-affectation, RAII

1. Contexte

Cet exercice de synthèse est le couronnement du premier chapitre de POO. Il s'agit de recréer les rouages internes de la classe standard std::string en concevant notre propre classe Chaine gérant une chaîne de caractères C dynamique (char* terminé par '\0'). Pour que cette classe soit robuste et réutilisable dans des conteneurs, elle doit respecter à la lettre la Règle des Trois (The Rule of Three).

2. Travail à réaliser

  1. Définissez la classe Chaine avec deux attributs privés :
    • char* m_tampon : pointeur brut vers la zone allouée sur le tas.
    • std::size_t m_taille : nombre de caractères utiles (hors '\0').
  2. Écrivez les constructeurs :
    • Constructeur par défaut créant une chaîne vide "" (allouant 1 octet pour le terminateur '\0').
    • Constructeur à partir d'un littéral C Chaine(const char* str), sécurisé si str == nullptr.
  3. 1ère règle : Le Destructeur (~Chaine())
    Libérez proprement le tableau dynamique avec delete[] m_tampon.
  4. 2ème règle : Le Constructeur par Recopie (Chaine(const Chaine&))
    Allouez un nouveau tampon de taille source.m_taille + 1 et copiez-y la chaîne source avec std::strcpy.
  5. 3ème règle : L'Opérateur d'Affectation (Chaine& operator=(const Chaine&))
    Appliquez rigoureusement les 4 étapes canoniques :
    1. Test d'auto-affectation : if (this == &source) return *this;. Pourquoi cette vérification est-elle vitale ? Que se passerait-il si on exécutait ch = ch; sans ce test ?
    2. Libération de l'ancienne mémoire allouée par this->m_tampon.
    3. Réallocation et copie en profondeur des données de la source.
    4. Renvoi de *this par référence pour autoriser les affectations en chaîne (a = b = c;).
  6. Ajoutez des méthodes utilitaires :
    • std::size_t longueur() const et const char* c_str() const.
    • char at(std::size_t index) const avec vérification de bornes.
    • void afficher() const.
    • Une méthode de concaténation Chaine& concatener(const Chaine& autre) et la surcharge associée operator+=(const Chaine&).
  7. Dans le main(), soumettez votre classe à un banc d'essai exhaustif : construction, recopie, affectation ordinaire, auto-affectation (s1 = s1), affectation chaînée (s3 = s2 = s1) et concaténation.

⚡ Récapitulatif : Les 3 Piliers de la Forme Canonique

1
Destructeur

~Chaine()

Libère la mémoire quand l'objet disparaît.

2
Constructeur Copie

Chaine(const Chaine&)

Clone en profondeur lors de l'initialisation.

3
Opérateur d'Affectation

operator=(const Chaine&)

Nettoie l'ancien état puis duplique le nouveau.

Correction complète & Explication architecturale

Remarquez le test fondamental if (this == &source) return *this; au tout début de operator=. Si on écrivait s = s; sans ce test, la ligne suivante delete[] m_tampon; détruirait immédiatement le texte à copier, transformant la copie qui suit en une lecture de mémoire libérée (Use-After-Free) qui corromprait irréversiblement l'objet !

CPPtd1_ex5_chaine.cpp
  1. /**
  2.  * TD 1 - Exercice 5 : La Forme Canonique Complète (Règle des Trois)
  3.  * Conception intégrale d'une classe Chaine (gestion manuelle de char*) en C++20.
  4.  */
  5.  
  6. #include <iostream>
  7. #include <cstring> // strlen, strcpy, strcat
  8. #include <stdexcept> // out_of_range
  9.  
  10. class Chaine {
  11. private:
  12. char* m_tampon; // Pointeur brut vers la chaîne stockée sur le tas
  13. std::size_t m_taille; // Longueur effective (hors caractère nul '\0')
  14.  
  15. public:
  16. // 1. Constructeur par défaut (chaîne vide "")
  17. Chaine() : m_tampon(new char[1]{'\0'}), m_taille(0) {
  18. std::cout << "[+] Chaine() vide créée @" << this << "\n";
  19. }
  20.  
  21. // 2. Constructeur à partir d'un littéral C (const char*)
  22. Chaine(const char* str) {
  23. if (str == nullptr) {
  24. m_taille = 0;
  25. m_tampon = new char[1]{'\0'};
  26. } else {
  27. m_taille = std::strlen(str);
  28. m_tampon = new char[m_taille + 1];
  29. std::strcpy(m_tampon, str);
  30. }
  31. std::cout << "[+] Chaine(\"" << m_tampon << "\") construite @" << this << "\n";
  32. }
  33.  
  34. // -------------------------------------------------------------
  35. // LA RÈGLE DES TROIS (FORME CANONIQUE DE COPIE)
  36. // -------------------------------------------------------------
  37.  
  38. // 1ère règle : LE DESTRUCTEUR
  39. ~Chaine() {
  40. std::cout << "[-] ~Chaine() détruite @" << this
  41. << " (libère \"" << (m_tampon ? m_tampon : "") << "\")\n";
  42. delete[] m_tampon;
  43. m_tampon = nullptr;
  44. }
  45.  
  46. // 2ème règle : LE CONSTRUCTEUR PAR RECOPIE (Deep Copy)
  47. Chaine(const Chaine& source) : m_taille(source.m_taille) {
  48. m_tampon = new char[m_taille + 1];
  49. std::strcpy(m_tampon, source.m_tampon);
  50. std::cout << "[C] Chaine COPIÉE depuis @" << &source << " vers @" << this
  51. << " (valeur: \"" << m_tampon << "\")\n";
  52. }
  53.  
  54. // 3ème règle : L'OPÉRATEUR D'AFFECTATION (operator=)
  55. Chaine& operator=(const Chaine& source) {
  56. std::cout << "[=] operator= invoqué sur @" << this
  57. << " depuis source @" << &source << "\n";
  58.  
  59. // Étape 1 : Protection vitale contre l'auto-affectation (a = a)
  60. // Sans ce test, on détruirait notre propre mémoire avant de pouvoir la copier !
  61. if (this == &source) {
  62. std::cout << " -> Auto-affectation détectée, aucun travail nécessaire.\n";
  63. return *this;
  64. }
  65.  
  66. // Étape 2 : Libération de l'ancienne mémoire détenue par this
  67. delete[] m_tampon;
  68.  
  69. // Étape 3 : Allocation de la nouvelle taille et copie profonde
  70. m_taille = source.m_taille;
  71. m_tampon = new char[m_taille + 1];
  72. std::strcpy(m_tampon, source.m_tampon);
  73.  
  74. // Étape 4 : Renvoyer une référence vers *this pour permettre l'enchaînement (a = b = c)
  75. return *this;
  76. }
  77.  
  78. // -------------------------------------------------------------
  79. // MÉTHODES UTILITAIRES
  80. // -------------------------------------------------------------
  81.  
  82. std::size_t longueur() const {
  83. return m_taille;
  84. }
  85.  
  86. const char* c_str() const {
  87. return m_tampon;
  88. }
  89.  
  90. char at(std::size_t index) const {
  91. if (index >= m_taille) {
  92. throw std::out_of_range("Indice hors bornes dans Chaine !");
  93. }
  94. return m_tampon[index];
  95. }
  96.  
  97. void afficher() const {
  98. std::cout << "\"" << m_tampon << "\" (longueur: " << m_taille << ")";
  99. }
  100.  
  101. // Concaténation en place
  102. Chaine& concatener(const Chaine& autre) {
  103. std::size_t nouvelleTaille = m_taille + autre.m_taille;
  104. char* nouveauTampon = new char[nouvelleTaille + 1];
  105.  
  106. std::strcpy(nouveauTampon, m_tampon);
  107. std::strcat(nouveauTampon, autre.m_tampon);
  108.  
  109. delete[] m_tampon;
  110. m_tampon = nouveauTampon;
  111. m_taille = nouvelleTaille;
  112.  
  113. return *this;
  114. }
  115.  
  116. Chaine& operator+=(const Chaine& autre) {
  117. return concatener(autre);
  118. }
  119. };
  120.  
  121. int main() {
  122. std::cout << "=== TD 1 - Exercice 5 : La Forme Canonique Complète (Chaine) ===\n\n";
  123.  
  124. std::cout << "--- 1. Constructions de base ---\n";
  125. Chaine s1("Bonjour");
  126. Chaine s2(" Monde");
  127. std::cout << "s1 = "; s1.afficher(); std::cout << "\n";
  128. std::cout << "s2 = "; s2.afficher(); std::cout << "\n\n";
  129.  
  130. std::cout << "--- 2. Constructeur par recopie (initialisation) ---\n";
  131. Chaine s3 = s1; // Appelle le constructeur de recopie
  132. std::cout << "s3 = "; s3.afficher(); std::cout << "\n\n";
  133.  
  134. std::cout << "--- 3. Affectation d'un objet existant (operator=) ---\n";
  135. Chaine s4("Temporaire");
  136. s4 = s2; // Appelle operator=
  137. std::cout << "s4 = "; s4.afficher(); std::cout << "\n\n";
  138.  
  139. std::cout << "--- 4. Test critique de l'auto-affectation (s1 = s1) ---\n";
  140. s1 = s1;
  141. std::cout << "s1 après s1 = s1 : "; s1.afficher(); std::cout << "\n\n";
  142.  
  143. std::cout << "--- 5. Test des affectations enchaînées (s4 = s3 = s2) ---\n";
  144. s4 = s3 = s2;
  145. std::cout << "s4 après chaîne : "; s4.afficher(); std::cout << "\n\n";
  146.  
  147. std::cout << "--- 6. Concaténation ---\n";
  148. Chaine message("Programmation ");
  149. message += Chaine("Orientée Objet C++ !");
  150. std::cout << "message : "; message.afficher(); std::cout << "\n\n";
  151.  
  152. std::cout << "--- 7. Sortie du programme et destructions RAII automatiques : ---\n";
  153. return 0;
  154. }

Résultat de l'exécution :

=== TD 1 - Exercice 5 : La Forme Canonique Complète (Chaine) === --- 1. Constructions de base --- [+] Chaine("Bonjour") construite @0x759dbdaf0020 [+] Chaine(" Monde") construite @0x759dbdaf0040 s1 = "Bonjour" (longueur: 7) s2 = " Monde" (longueur: 6) --- 2. Constructeur par recopie (initialisation) --- [C] Chaine COPIÉE depuis @0x759dbdaf0020 vers @0x759dbdaf0060 (valeur: "Bonjour") s3 = "Bonjour" (longueur: 7) --- 3. Affectation d'un objet existant (operator=) --- [+] Chaine("Temporaire") construite @0x759dbdaf0080 [=] operator= invoqué sur @0x759dbdaf0080 depuis source @0x759dbdaf0040 s4 = " Monde" (longueur: 6) --- 4. Test critique de l'auto-affectation (s1 = s1) --- [=] operator= invoqué sur @0x759dbdaf0020 depuis source @0x759dbdaf0020 -> Auto-affectation détectée, aucun travail nécessaire. s1 après s1 = s1 : "Bonjour" (longueur: 7) --- 5. Test des affectations enchaînées (s4 = s3 = s2) --- [=] operator= invoqué sur @0x759dbdaf0060 depuis source @0x759dbdaf0040 [=] operator= invoqué sur @0x759dbdaf0080 depuis source @0x759dbdaf0060 s4 après chaîne : " Monde" (longueur: 6) --- 6. Concaténation --- [+] Chaine("Programmation ") construite @0x759dbdaf00a0 [+] Chaine("Orientée Objet C++ !") construite @0x759dbdaf00c0 [-] ~Chaine() détruite @0x759dbdaf00c0 (libère "Orientée Objet C++ !") message : "Programmation Orientée Objet C++ !" (longueur: 35) --- 7. Sortie du programme et destructions RAII automatiques : --- [-] ~Chaine() détruite @0x759dbdaf00a0 (libère "Programmation Orientée Objet C++ !") [-] ~Chaine() détruite @0x759dbdaf0080 (libère " Monde") [-] ~Chaine() détruite @0x759dbdaf0060 (libère " Monde") [-] ~Chaine() détruite @0x759dbdaf0040 (libère " Monde") [-] ~Chaine() détruite @0x759dbdaf0020 (libère "Bonjour")

🎉 Félicitations : Vous maîtrisez les concepts fondamentaux de la POO !

Au terme de ce TD 1, vous avez acquis les compétences fondamentales indispensables pour aborder sereinement les chapitres suivants :

  • Vous savez concevoir des classes C++ étanches grâce à l'encapsulation et aux méthodes const.
  • Vous maîtrisez la gestion de la mémoire sur la pile et sur le tas sans provoquer de fuite ni d'accès invalide.
  • Vous appliquez spontanément la Règle des Trois dès qu'un objet alloue une ressource dynamique.
← Revoir le Chapitre 1 Chapitre 2 : L'Héritage ➔ Passer au TD 2 : L'Héritage C++ ➔