Chapitre 1 : Programmation Objet — Concepts Fondamentaux
Comprendre la rupture avec l'approche procédurale, maîtriser la modélisation objet en C++ et assimiler les mécanismes vitaux de gestion de ressources (encapsulation, constructeurs, destructeur, affectation).
Objectifs pédagogiques de la séance
- Distinguer le paradigme procédural du paradigme orienté objet.
- Manipuler avec rigueur les notions de classe, instance, attributs et méthodes.
- Mettre en œuvre l'encapsulation (
public,private,protected) pour protéger les invariants. - Maîtriser le cycle de vie de l'objet en C++ et les constructeurs, destructeur, opérateur d'affectation).
De la Programmation Procédurale à la Programmation Objet
1.1. Le paradigme procédural et ses limites
En programmation procédurale (comme en langage C classique ou Pascal), le programme est structuré autour de
fonctions/procédures qui manipulent des données. Les données sont regroupées
dans des structures (struct), et des fonctions globales viennent lire et/ou modifier ces structures.
struct Compte {int numero;double solde;};- Données exposées à tout le programme
deposer(Compte* c, double m)retirer(Compte* c, double m)- Aucune garantie de cohérence globale
Lorsque la taille du projet grandit, cette séparation présente des faiblesses critiques :
- Absence de protection des champs de la structure de données : N'importe quelle partie du code peut modifier directement un champ d'une structure (par exemple, rendre un solde négatif ou insérer une date invalide comme le 31 février).
- Couplage fort et dispersion : Si la structure interne de la donnée change (par exemple remplacer un tableau statique par une liste chaînée), toutes les fonctions du projet qui y accèdent directement doivent être réécrites (=> c'est également le cas avec les objets).
- Maintenance complexe : Il est difficile de savoir quelles fonctions ont le droit d'altérer quelles données => les commentaires sont primordiaux. En un sens cela n'a pas d'importance est le nom des méthodes doit être assez explicite pour comprendre ce qui est réalisé.
1.2. La vision Orientée Objet (POO)
La Programmation Orientée Objet propose un changement fondamental :
- réunir les données et les opérations qui les manipulent au sein d'une même entité autonome appelée « Objet »
- protéger (encapsulation) les données pour éviter que l'objet soit dans un état instable ou incohérent
- procurer des moyens de modification ou de lecture ( méthodes ) des données de l'objet
Une machine à café moderne est un objet. Elle possède un état interne (niveau d'eau, grains de café, température de la chaudière) et une interface (boutons "Expresso", "Cappuccino"). En tant qu'utilisateur, vous appuyez sur un bouton. Vous n'allez pas ouvrir la machine avec un tournevis pour verser manuellement de l'eau dans le tuyau ! La machine garantit elle-même que la préparation se fait dans les règles et protège ses mécanismes internes.
La POO fonctionne par encapsulation : les données et les méthodes sont regroupées au sein d'objets, et
leur accès est contrôlé par sections spécifiques (public, private, protected) et seules les
méthodes associées à l'objet permettent de le modifier.
L'interface de l'objet est publique et permet d'interagir avec lui, tandis que son état interne (implémentation / implantation) est privée ou protégée.
État privé : numero, solde (inaccessibles de l'extérieur)
get_numero(), deposer(), retirer(), afficher_solde()
On peut donc comprendre l'approche objet comme une manière de structurer le code en regroupant les données et les méthodes qui les manipulent dans des entités autonomes, facilitant ainsi la gestion et la maintenance du programme.
On peut résumer les différences entre les deux approches comme suit :
- L'approche procédurale :
fonction(structure, paramètres) - L'approche objet :
structure.fonction(paramètres)
On parle généralement de couche objet ajoutée à un langage procédural (C++, C) car le code objet est traduit par le compilateur en code procédural.
1.3. Tableau comparatif synthétique
| Critère | Programmation Procédurale (ex: C) | Programmation Objet (ex: C++) |
|---|---|---|
| Unité de base | La fonction / la procédure | L'objet (données + méthodes) |
| Vision du système | Un ensemble d'actions séquentielles sur des données | Un réseau d'objets autonomes qui collaborent |
| Protection des données | Faible (données accessibles à quiconque a le pointeur) | Forte (données protégées par l'encapsulation) |
| Évolution du code | Difficile : modifier une structure impacte tout le code | Facile : l'implémentation interne peut changer sans casser l'interface |
| Modularité | Centrée sur les modules de code source (.c / .h) | Centrée sur les classes et types de données abstraits |
1.4. Exemple comparatif en C++ : Gestion d'un Compte Bancaire
Approche Procédurale (Fragile)
#include <iostream> // Structure de données représentant un compte bancaire struct Compte { int numero; double solde; double decouvert_maximum; }; // Initialisation de la structure de données void initialiser(Compte* c, int numero, double solde_initial, double decouvert_maximum) { c->numero = numero; c->solde = solde_initial; c->decouvert_maximum = decouvert_maximum; } // Retrait d'un montant du compte, sans protection de l'invariant void retirer(Compte* c, double montant) { if (c->solde - montant >= c->decouvert_maximum) { c->solde -= montant; } } int main() { Compte c; initialiser(&c, 1, 3600.0, -200.0); retirer(&c, 50.0); // Utilisation normale // ⚠️ DANGER PROCÉDURAL : N'importe qui peut violer l'invariant directement ! c.solde = -999999.0; std::cout << "Solde corrompu : " << c.solde << " €\n"; return 0; }
Approche Orientée Objet (Sécurisée)
C'est le compilateur qui garantit la sécurité des données et interdit d'écrire du code qui modifierait directement les données privées.
#include <iostream> // Classe Orientée Objet encapsulée class Compte { // données membres privées pour protéger l'invariant private: int m_numero; double m_solde; double m_decouvert_maximum; public: // Constructeur garantissant un état initial valide // Utilisation de la liste d'initialisation pour initialiser les membres Compte(int numero, double solde_initial, double decouvert_maximum); // Méthode protégeant l'invariant du compte // Retourne true si le retrait a été effectué, false sinon bool retirer(double montant); // Méthode pour accéder au solde, sans permettre de le modifier directement double solde() const { return m_solde; } }; Compte::Compte(int numero, double solde_initial, double decouvert_maximum) : m_numero(numero), m_solde(solde_initial), m_decouvert_maximum(decouvert_maximum) {} bool Compte::retirer(double montant) { if (montant > 0 && (m_solde - montant >= m_decouvert_maximum)) { m_solde -= montant; return true; } return false; } int main() { Compte c(1, 3600.0, -200.0); c.retirer(50.0); // c.m_solde = -999999.0; // ❌ Erreur de compilation : m_solde est privé ! std::cout << "Solde protégé : " << c.solde() << " €\n"; return 0; }
Notions Fondamentales : Classe, Instance, Attributs & Méthodes
La POO repose sur quatre piliers terminologiques qu'il est indispensable de distinguer clairement dès la Licence 3.
Description abstraite du concept dans le code source.
class Point2D { double x, y; };mais il ne faut pas oublier que la classe contient également les méthodes (fonctions membres) qui définissent le comportement de l'objet.
Cases mémoires allouées à l'exécution avec leur propre état.
- p1 : (x = 2.0, y = 3.5) @0x7ffd...1f0
- p2 : (x = -1.0, y = 0.0) @0x7ffd...2a0
2.1. Définitions clés
- Classe : C'est un type de données abstrait défini par le développeur qui définit à la fois la structure des données et les opérations qui lui sont applicables.
- Instance / Objet : C'est un exemplaire concret d'une classe occupant un emplacement mémoire précis lors de l'exécution du programme. On dit que l'objet est une instanciation de la classe. C'est également une variable du type de la classe.
- Attributs (ou Données Membres / Variables d'Instance) : Ce sont les variables définies dans la classe qui constituent l'état de chaque objet. Chaque instance possède sa propre copie de ces variables.
- Méthodes (ou Fonctions Membres) : Ce sont les fonctions déclarées au sein de la classe. Elles représentent le comportement de l'objet et peuvent accéder directement aux attributs de l'instance sur laquelle elles sont invoquées.
Pour distinguer immédiatement un attribut d'une variable locale ou d'un paramètre, la convention C++ standard recommande de préfixer les données membres par m_ (pour member) ou par un underscore de fin (ex: valeur_). Exemple : m_solde, m_largeur.
2.2. Le pointeur this et les méthodes constantes (const)
Au sein de toute méthode membre non-statique, C++ fournit un pointeur implicite nommé this. Il pointe sur l'instance exacte qui a exécuté la méthode.
En d'autres termes c'est l'adresse de l'objet courant. Cela permet de lever les ambiguïtés entre un paramètre et un attribut, et de renvoyer l'objet courant pour permettre
le chaînage d'appels.
Le pointeur this
Sert à :
- Lever les ambiguïtés entre paramètre et attribut :
this->nom = nom; - Renvoyer l'objet courant pour permettre le chaînage d'appels :
return *this; - Transmettre l'adresse de l'objet courant à d'autres fonctions.
Les méthodes const
Une méthode marquée const s'engage à ne modifier aucun attribut de l'objet :
double solde() const { // m_solde = 0; // ❌ Erreur compilation return m_solde; }
Obligatoire pour pouvoir être appelée sur des références ou pointeurs constants (const Compte&).
En résumé :
this: Pointeur implicite vers l'instance courante (son adresse en mémoire).*this: l'objet courant.const: Garantit que la méthode ne modifie pas les attributs/champs de l'objet.
2.3. Exemple C++ complet : Modélisation d'un Rectangle
#include <iostream> class Rectangle { private: double m_largeur; // Attribut / Donnée membre double m_hauteur; // Attribut / Donnée membre public: // Constructeur avec liste d'initialisation Rectangle(double largeur, double hauteur) : m_largeur(largeur), m_hauteur(hauteur) {} // Méthode de calcul (const car ne modifie pas l'état) double calculer_aire() const { return m_largeur * m_hauteur; } // Méthode mutatrice (modifie l'état de l'objet) void redimensionner(double facteur) { if (facteur > 0) { m_largeur *= facteur; m_hauteur *= facteur; } } void afficher() const { std::cout << "Rectangle [" << m_largeur << " x " << m_hauteur << "] - Aire = " << calculer_aire() << std::endl; } }; int main() { Rectangle r1(4.0, 5.0); Rectangle r2(2.5, 10.0); r1.afficher(); // Rectangle [4 x 5] - Aire = 20 r1.redimensionner(2.0); // Modifie uniquement r1 r1.afficher(); // Rectangle [8 x 10] - Aire = 80 r2.afficher(); // Inchangé : Rectangle [2.5 x 10] - Aire = 25 return 0; }
L'Encapsulation et le Contrôle d'Accès
3.1. Qu'est-ce que l'Encapsulation ?
L'encapsulation est le mécanisme fondamental qui consiste à :
- Regrouper les données et les méthodes qui les traitent au sein d'une même structure.
- Masquer les détails d'implémentation internes à l'extérieur de la classe.
- Garantir les invariants de classe : Un invariant est une propriété logique qui doit toujours demeurer vraie tout au long de la vie de l'objet (ex:
largeur > 0,date.jour ≤ 31,pointeur != nullptr); c'est à l'utilisateur de garantir ces invariants lors de codage et de l'utilisation de la classe.
Si on désirait répondre pleinement à l'encapsulation, on devrait :
- Créer un fichier d'en-tête (.h) pour déclarer les attributs et méthodes (interface).
- Créer un fichier source (.cpp) pour implémenter les méthodes (implémentation).
#pragma once #include <iostream> // =================================================================== // PARTIE INTERFACE DE LA CLASSE PileEntiers // =================================================================== /** * @class Pile d'entiers * @brief Classe représentant une pile d'entiers avec redimensionnement * dynamique. */ class PileEntiers { // Attributs de la structure de données (private ou protected si héritage) // Inaccessibles depuis l'extérieur de la classe pour garantir // l'encapsulation. private: int* m_elements; // Tableau dynamique d'entiers size_t m_capacite; // Capacité maximale allouée size_t m_sommet; // Nombre d'éléments empilés // Méthode privée pour redimensionner le tableau dynamique // Elle n'a pas besoin d'être publique car elle est utilisée // uniquement en interne par la classe. // C'est un détail d'implémentation qui ne doit pas être exposé // à l'utilisateur de la classe. // Mais elle doit être déclarée dans la classe pour que le compilateur // puisse la trouver. void redimensionner(size_t nouvelle_capacite); // Partie publique de la classe (interface) // Indique les fonctionnalités accessibles depuis l'extérieur de la classe. public: // 1. Constructeur usuel explicit PileEntiers(size_t capacite_initiale = 4); // 2. Destructeur ~PileEntiers(); // 3. Méthodes de manipulation de la pile void empiler(int element); int depiler(); };
#include "pile_entiers.hpp" // =================================================================== // PARTIE IMPLEMENTATION DE LA CLASSE PileEntiers // =================================================================== PileEntiers::PileEntiers(size_t capacite_initiale) : m_elements(new int[capacite_initiale]), m_capacite(capacite_initiale), m_sommet(0) {} PileEntiers::~PileEntiers() { delete[] m_elements; } void PileEntiers::redimensionner(size_t nouvelle_capacite) { int* nouv = new int[nouvelle_capacite]; for (size_t i = 0; i < m_sommet; ++i) { nouv[i] = m_elements[i]; } delete[] m_elements; m_elements = nouv; m_capacite = nouvelle_capacite; } void PileEntiers::empiler(int element) { if (m_sommet == m_capacite) { redimensionner(m_capacite * 2); } m_elements[m_sommet++] = element; } int PileEntiers::depiler() { if (m_sommet == 0) { throw std::out_of_range("Pile vide"); } return m_elements[--m_sommet]; }
3.2. Les Modificateurs d'Accès en C++
En C++, le contrôle d'accès s'exprime à l'aide de trois mots-clés :
| Modificateur | Accessibilité | Rôle & Usage typique |
|---|---|---|
public |
Accessible depuis n'importe où dans le programme. | Définit l'interface publique de la classe (les services rendus par l'objet). |
private |
Accessible uniquement par les méthodes de la classe elle-même. | Protège l'état interne et les fonctions auxiliaires de calcul. C'est le choix par défaut en POO pour les attributs. |
protected |
Accessible par la classe et par ses classes dérivées (sous-classes). | Permet à la descendance d'accéder à certains mécanismes internes lors de l'héritage tout en restant caché au reste du monde. |
struct et class en C++
En C++, la seule et unique différence technique entre un struct et une class est la visibilité par défaut :
• Dans une struct, tout est public par défaut.
• Dans une class, tout est private par défaut.
3.3. Getters et Setters
Parmi les méthodes fondamentales en programmation orientée objet, les getters (accesseurs) et les setters (mutateurs) sont particulièrement importants. Ils servent à protéger les données internes d'un objet. Au lieu de laisser les variables (attributs) d'une classe accessibles à tout le monde en les rendant publiques, on les rend privées (ou protégées). On utilise ensuite ces méthodes spécifiques pour interagir avec elles depuis l'extérieur de la classe :
- Le Getter (Accesseur) : C'est une méthode qui permet de lire (récupérer) la valeur d'un attribut privé. Elle se contente de généralement de renvoyer la donnée sans la modifier.
- Le Setter (Mutateur) : C'est une méthode qui permet de modifier la valeur d'un attribut privé. Son grand avantage est qu'il permet d'ajouter des règles et des vérifications et de sécurité (par exemple, s'assurer qu'un âge n'est pas négatif, ou qu'un mot de passe est assez long) avant d'appliquer la modification.
Créer un getter et un setter public pour chaque attribut privé détruit l'intérêt de l'encapsulation en exposant indirectement
toute la structure interne. Privilégiez des méthodes à sémantique métier (ex: compte.crediter(100)
plutôt que compte.set_solde(compte.get_solde() + 100)).
#include <iostream> #include <string> class Personne { private: // L'attribut est privé : il ne peut pas être modifié directement depuis // main() int m_age; public: // Constructeur Personne(int ageInitial) { // On utilise le setter dès la construction pour bénéficier de sa // validation set_age(ageInitial); } // --- GETTER --- // Permet de lire l'âge. Le "const" garantit que la méthode ne modifie pas // l'objet. A la manière de Java int get_age() const { return m_age; } // --- SETTER --- // Permet de modifier l'âge en imposant une règle de validation. // A la manière de Java void set_age(int age) { if (age >= 0) { m_age = age; } else { std::cout << "Erreur : l'âge ne peut pas être négatif !" << std::endl; } } // --- Variante plus personnelle du getter et du setter --- // On peut aussi nommer les méthodes de manière plus naturelle, comme si // elles étaient des propriétés de l'objet. Cela peut rendre le code plus // lisible. Exemple : int age() const { return m_age; } void age(int age) { if (age >= 0) { m_age = age; } else { std::cout << "Erreur : l'âge ne peut pas être négatif !" << std::endl; } } }; int main() { // Création d'un objet avec l'âge initial de 25 Personne jean(25); // Utilisation du GETTER pour afficher la valeur std::cout << "Jean a " << jean.get_age() << " ans." << std::endl; // Utilisation du SETTER pour modifier la valeur de manière valide jean.set_age(26); std::cout << "Jean a maintenant " << jean.get_age() << " ans." << std::endl; // Tentative de modification invalide : le SETTER la bloque jean.set_age(-5); return 0; }
3.4 Exemple : Préservation d'un Invariant de Classe (Une Heure Valide)
#include <iostream> #include <stdexcept> class Heure { private: int m_heures; // Invariant : 0 <= m_heures <= 23 int m_minutes; // Invariant : 0 <= m_minutes <= 59 bool est_valide(int h, int m) const { return (h >= 0 && h < 24) && (m >= 0 && m < 60); } public: Heure(int h, int m) { if (!est_valide(h, m)) { throw std::invalid_argument("Heure ou minutes invalides !"); } m_heures = h; m_minutes = m; } void avancer(int minutes) { if (minutes < 0) return; int total_minutes = m_heures * 60 + m_minutes + minutes; m_heures = (total_minutes / 60) % 24; m_minutes = total_minutes % 60; } void afficher() const { std::cout << (m_heures < 10 ? "0" : "") << m_heures << ":" << (m_minutes < 10 ? "0" : "") << m_minutes << std::endl; } }; int main() { Heure h(23, 45); h.afficher(); // 23:45 h.avancer(30); // Passage automatique au jour suivant h.afficher(); // 00:15 (Invariant garanti !) // h.m_heures = 99; // ❌ Interdit par le compilateur return 0; }
3.5 Polymorphisme
Le polymorphisme en programmation orientée objet est le fait que l'on peut disposer de plusieurs méthodes
ayant le même nom (dans l'exemple précédent age) mais qui se différencient par leur valeur de retour et/ou
leurs paramètres. Dans l'exemple de la classe Personne, on a :
-
int age() constpour obtenir l'âge d'une personne -
void age(int age)pour fixer l'âge
Ces deux méthodes sont bien distinctes.
Cycle de Vie de l'Objet & Forme Canonique (Règle des Trois)
En C++, la gestion de la mémoire et des ressources est intimement liée au cycle de vie de l'objet : Naissance (Construction) → Utilisation → Mort (Destruction).
4.1. Les Constructeurs : Naissance de l'Objet
Un constructeur est une méthode spéciale portant le même nom que la classe, sans aucun type de retour (pas même void).
Constructeur par défaut
Ne prend aucun paramètre (ou tous ses paramètres ont des valeurs par défaut). Permet d'écrire :
Vecteur v1; // Appelle Vecteur() par défaut
Si aucun constructeur n'est défini, le compilateur en génère un par défaut. Dès que vous écrivez un constructeur paramétré, le compilateur ne génère plus le constructeur par défaut automatique.
Liste d'Initialisation
En C++, il faut toujours privilégier la liste d'initialisation plutôt que l'assignation dans le corps :
// Recommandé et performant : Point(int x, int y) : m_x(x), m_y(y) {} // À éviter (construction puis réassignation) : Point(int x, int y) { m_x = x; m_y = y; }
4.2. Le Destructeur (~Classe())
Méthode unique par classe, préfixée par un tilde ~, sans paramètre ni retour.
Elle est invoquée automatiquement lorsque l'objet quitte sa portée (sur la pile) ou est détruit via delete (sur le tas).
C'est le cœur de l'idiome RAII (Resource Acquisition Is Initialization).
RAII signifie que la durée de vie d'une ressource est liée à la durée de vie d'un objet. On acquiert la ressource dans le constructeur, on la libère dans le destructeur. Comme le langage garantit l'appel du destructeur quand l'objet sort de sa portée, la libération devient automatique.
4.3. Le Constructeur par Recopie (Copy Constructor)
Appelé lorsqu'un objet est créé à partir d'un objet existant du même type :
- Initialisation explicite :
Vecteur v2 = v1;ouVecteur v2(v1); - Passage d'un objet par valeur à une fonction :
void traiter(Vecteur v); - Retour d'un objet par valeur depuis une fonction.
Sa signature canonique est obligatoirement une référence constante :
Classe(const Classe& autre);
Par défaut, le compilateur génère une recopie membre à membre bit-à-bit. Si votre classe contient un pointeur vers de la mémoire dynamique (int* m_data), la copie par défaut ne duplique que l'adresse du pointeur !
Résultat : deux objets pointent vers la même zone mémoire sur le tas → crash lors de la destruction (Double Free Error).
Obj1.ptr ──┐
Obj2.ptr ──┴─> [ Mémoire Tas : 10, 20, 30 ]
🚨 Double libération à la fin de portée !
Obj1.ptr ────> [ Mémoire Tas 1 : 10, 20, 30 ]
Obj2.ptr ────> [ Mémoire Tas 2 : 10, 20, 30 ]
🎉 Chaque objet possède sa propre ressource.
4.4. L'Opérateur d'Affectation (operator=)
Appelé lorsqu'on affecte un objet existant à un autre objet déjà existant :
Vecteur v1(5); Vecteur v2(10); v2 = v1; // Appel de v2.operator=(v1);
Pour écrire un operator= robuste en C++, 4 étapes sont indispensables :
- Tester l'auto-affectation (
if (this != &autre)) pour ne pas détruire sa propre ressource avant de la copier ! - Libérer l'ancienne ressource allouée par l'objet de destination.
- Allouer et copier la nouvelle ressource.
- Renvoyer
*thispar référence pour permettre les affectations chaînées (a = b = c;).
4.5. Conventions de codage
Plusieurs conventions peuvent être utilisées en C++.
Concernant les noms de classes on peut les écrire en majuscules ou en minuscules :
- Majuscule :
ListeEntiers - Minuscule :
liste_entiers
Cependant, la STL (Standard Template Library) utilise les minuscules.
Pour les attributs, on peut les préfixer par _, m_ ou les suffixer par _
-
_solde -
m_solde -
_solde_ -
solde_
Pour les méthodes :
- à la manière de Java :
debiterMontant(),getSolde(),setSolde() - à la manière du C :
debiter_montant(),get_solde(),set_solde()
En ce qui concerne les getters et setters, j'ai pour ma part fait le choix d'utiliser le nom de l'attribut :
double solde()void solde(double montant)
Dernier point, ne pas mixer anglais et français (nom de classe, attribut, méthode) : il faut écrire tout le code dans la même langue.
4.6. Exemple Complet C++ : La Forme Canonique d'un Tableau Dynamique
#include <iostream> #include <stdexcept> using namespace std; /** * @class array_of_int * @brief A class that represents a dynamically allocated array of integers. * * This class handles memory allocation, deep copying, and boundary checking * for an array of integers. * */ class array_of_int { protected: /** * @brief The number of elements in the array. */ int m_size; /** * @brief Pointer to the dynamically allocated memory block holding the * integers. */ int* m_data; public: /** * @brief Default constructor. * * Initializes an empty array with a size of 0 and a null data pointer. */ array_of_int() : m_size(0), m_data(nullptr) {} /** * @brief Parameterized constructor. * * Allocates an array of the specified size. * * @param size The number of elements to allocate for the array. * @throw std::runtime_error if the provided size is negative. */ array_of_int(int size) : m_size(size) { if (size == 0) { m_data = nullptr; } else if (m_size < 0) { throw std::runtime_error("size must be positive or null"); } else { m_data = new int[m_size]; } } /** * @brief Copy constructor. * * Performs a deep copy of another array_of_int object. * * @param object The array_of_int object to copy. */ array_of_int(const array_of_int& object) { m_size = object.m_size; if (m_size > 0) { m_data = new int[m_size]; std::copy(&object.m_data[0], &object.m_data[m_size], &m_data[0]); } else { m_data = nullptr; } } ~array_of_int() { if (m_data != nullptr) { delete[] m_data; } } /** * @brief Copy assignment operator. * * Safely releases existing resources and performs a deep copy of the * right-hand object. Protects against self-assignment. * * @param object The array_of_int object to assign from. * @return array_of_int& A reference to the updated current object. */ array_of_int& operator=(const array_of_int& object) { if (this != &object) { if (m_data != nullptr) delete[] m_data; m_size = object.m_size; if (m_size > 0) { m_data = new int[m_size]; std::copy(&object.m_data[0], &object.m_data[m_size], &m_data[0]); } else { m_data = nullptr; } } return *this; } /** * @brief Retrieves the current size of the array. * * @return int The number of elements in the array. */ int size() { return m_size; } /** * @brief Overloaded subscript operator for element access. * * Provides access to the element at the specified index. Includes bounds * checking. * * @param n The index of the element to access. * @return int& A reference to the element at the specified index. * @throw std::out_of_range if the index is less than 0 or greater * than/equal to the size. */ int& operator[](int n) { if ((n < 0) or (n >= m_size)) { throw std::out_of_range("index out of range"); } return m_data[n]; } /** * @brief Overloaded stream insertion operator. * * Formats the array contents as [val1 val2 val3 ] and outputs it to the * stream. * * @param out The output stream. * @param object The array_of_int object to output. * @return std::ostream& A reference to the output stream. */ friend std::ostream& operator<<(std::ostream& out, array_of_int& object) { out << '['; for (int i = 0; i < object.m_size; ++i) { out << object.m_data[i] << ' '; } out << ']'; return out; } }; // ================================================================== // Main function // ================================================================== int main() { // -------------------------------------------------------------- // create an array of 10 integers // -------------------------------------------------------------- array_of_int a1(10); // -------------------------------------------------------------- // fill the array with values 10, 11, ..., 19 // -------------------------------------------------------------- for (int i = 0; i < a1.size(); ++i) { a1[i] = 10 + i; } std::cout << "a1=" << a1 << std::endl; // -------------------------------------------------------------- // create a second array and assign the first array to it // -------------------------------------------------------------- array_of_int a2; a2 = a1; std::cout << "a2=" << a2 << std::endl; // -------------------------------------------------------------- // create a third array of 4 integers and assign values to it // -------------------------------------------------------------- array_of_int a3(4); a3[1] = 55; a3[3] = 99; std::cout << "a3=" << a3 << std::endl; // -------------------------------------------------------------- // assign the first array to the third array // -------------------------------------------------------------- a3 = a1; std::cout << "a3=" << a3 << std::endl; // -------------------------------------------------------------- // create a fourth array using the copy constructor // -------------------------------------------------------------- array_of_int a4(a1); a4[0] = 100; std::cout << "a4=" << a4 << std::endl; // -------------------------------------------------------------- // show that a1 is unchanged // -------------------------------------------------------------- std::cout << "a1=" << a1 << std::endl; return EXIT_SUCCESS; }
Synthèse & Auto-évaluation
5.1. Fiche Mémo des Formes Canoniques
| Méthode Spéciale | Signature Type (Classe T) |
Moment de déclenchement |
|---|---|---|
| Constructeur par défaut | T(); |
T obj; ou new T(); |
| Constructeur par recopie | T(const T& source); |
T copie = source; ou passage par valeur |
| Destructeur | ~T(); |
Sortie de bloc } ou delete ptr; |
| Opérateur d'affectation | T& operator=(const T& source); |
existant = source; |
5.2. QCM de Réflexion (Niveau L3)
int* data alloué dynamiquement avec new, et qu'on ne définit pas de constructeur par recopie ?
delete[]. Quand la deuxième instance est détruite à son tour, elle tente de libérer la même adresse mémoire, ce qui provoque une erreur fatale d'exécution (SIGABRT / double free).
Point p2 = p1; et p2 = p1; (en supposant p2 déjà déclaré plus haut) ?
Type a = b; est une initialisation par copie, qui appelle le constructeur par recopie sur une mémoire vierge. L'instruction a = b; sur des objets déjà existants est une affectation, qui doit nettoyer l'ancienne mémoire de a avant d'y dupliquer celle de b.
if (this == &autre) return *this; dans operator= ?
this->m_data avant de copier autre.m_data, or que autre est précisément *this, la source est détruite et la copie lira une mémoire corrompue (Use-After-Free / crash).
📝 Travaux Dirigés associés : TD n°1
Mettez en application immédiate les notions de ce chapitre à travers 5 exercices de difficulté croissante avec corrigés détaillés et dépliables :
- Exercice 1 (★☆☆☆☆) : Classe
Point2D, encapsulation, méthodesconstet passage par référence constante. - Exercice 2 (★★☆☆☆) : Invariants temporels de la classe
Duree, méthode privée de normalisation et chaînage d'appels via*this. - Exercice 3 (★★★☆☆) : Composition d'objets (
StationMeteo&Capteur), traçage du cycle de vie et surcoût des copies par valeur. - Exercice 4 (★★★★☆) : Classe
VecteurDynamique, diagnostic du crash par double libération (Double Free) et écriture du constructeur par recopie profonde. - Exercice 5 (★★★★★) : Implémentation intégrale de la classe
Chaine(String maison) selon la Règle des Trois (destructeur, constructeur de recopie,operator=sécurisé contre l'auto-affectation).