Niveau : Licence 3 Module : Programmation Orientée Objet Volume : 1h30 (CM/TD) Coloration : GeSHi

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

De la Programmation Procédurale à la Programmation Objet

⏱️ 20 minutes

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.

Approche Procédurale : Séparation Données / Traitements
📊 Données (struct)
  • struct Compte {
  • int numero;
  • double solde;
  • };
  • Données exposées à tout le programme
➔
⚙️ Fonctions Externes
  • 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 :

1.2. La vision Orientée Objet (POO)

La Programmation Orientée Objet propose un changement fondamental :

💡 Analogie : La machine à café

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.

Interface / Implémentation

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.

Approche Objet : L'Objet comme boîte noire autonome
📦 OBJET (Données + Comportements)

État privé : numero, solde (inaccessibles de l'extérieur)

Interface publique (Méthodes) : 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 :

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)

CPPcompte_procedural.cpp
#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.

CPPcompte_objet.cpp
#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;
}
2

Notions Fondamentales : Classe, Instance, Attributs & Méthodes

⏱️ 25 minutes

La POO repose sur quatre piliers terminologiques qu'il est indispensable de distinguer clairement dès la Licence 3.

Du modèle abstrait à l'objet concret en mémoire
📐 La CLASSE (Type / Patron)

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.

Instanciation ➔
📍 Les INSTANCES (Objets réels)

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

🎯 Convention de nommage en C++

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

2.3. Exemple C++ complet : Modélisation d'un Rectangle

CPPRectangle.cpp
#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;
}
3

L'Encapsulation et le Contrôle d'Accès

⏱️ 20 minutes

3.1. Qu'est-ce que l'Encapsulation ?

L'encapsulation est le mécanisme fondamental qui consiste à :

  1. Regrouper les données et les méthodes qui les traitent au sein d'une même structure.
  2. Masquer les détails d'implémentation internes à l'extérieur de la classe.
  3. 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 :

CPPpile_entiers.hpp
#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();
};
CPPpile_entiers.cpp
#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.
🎯 Différence fondamentale entre 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 :

⚠️ Attention à l'anti-pattern des « Getters/Setters aveugles »

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

CPPpersonne_getter_setter.cpp
#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)

CPPHorloge.cpp
#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 :

Ces deux méthodes sont bien distinctes.

4

Cycle de Vie de l'Objet & Forme Canonique (Règle des Trois)

⏱️ 25 minutes

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 :

Sa signature canonique est obligatoirement une référence constante : Classe(const Classe& autre);

💥 Le piège de la copie superficielle (Shallow Copy)

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

Copie Superficielle vs Copie Profonde en Mémoire
❌ Copie Superficielle (Défaut)

Obj1.ptr ──┐

Obj2.ptr ──┴─> [ Mémoire Tas : 10, 20, 30 ]

🚨 Double libération à la fin de portée !

vs
✅ Copie Profonde (Personnalisé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 :

  1. Tester l'auto-affectation (if (this != &autre)) pour ne pas détruire sa propre ressource avant de la copier !
  2. Libérer l'ancienne ressource allouée par l'objet de destination.
  3. Allouer et copier la nouvelle ressource.
  4. Renvoyer *this par référence pour permettre les affectations chaînées (a = b = c;).

⚡ La Règle des Trois (The Rule of Three)

En C++, dès lors qu'une classe gère une ressource dynamique (mémoire avec new, descripteur de fichier, socket réseau) et a besoin de définir explicitement l'un de ces trois éléments :

1
Le Destructeur

~Classe()

2
Le Constructeur par Recopie

Classe(const Classe&)

3
L'Opérateur d'Affectation

Classe& operator=(const Classe&)

👉 Elle DOIT IMPÉRATIVEMENT définir les TROIS !

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 :

Cependant, la STL (Standard Template Library) utilise les minuscules.

Pour les attributs, on peut les préfixer par _, m_ ou les suffixer par _

Pour les méthodes :

En ce qui concerne les getters et setters, j'ai pour ma part fait le choix d'utiliser le nom de l'attribut :

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

CPParray_of_int.cpp
#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;
}
5

Synthèse & Auto-évaluation

⏱️ 10 minutes

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)

❓ Question 1 : Que se passe-t-il si une classe possède un pointeur int* data alloué dynamiquement avec new, et qu'on ne définit pas de constructeur par recopie ?
A. Le code ne compile pas car le C++ exige toujours un constructeur de copie.
B. Le compilateur génère une copie membre à membre superficielle (copie du pointeur brut), ce qui conduit à un risque de double libération (double free) lors de la destruction.
C. Le compilateur alloue automatiquement un nouveau tableau et copie son contenu en profondeur.
Réponse B : Le compilateur synthétise une recopie superficielle (bitwise/shallow copy). Les deux instances partagent alors la même adresse de pointeur. Quand la première instance est détruite, le destructeur libère la mémoire avec 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).
❓ Question 2 : Quelle est la différence essentielle entre Point p2 = p1; et p2 = p1; (en supposant p2 déjà déclaré plus haut) ?
A. Aucune, les deux appellent l'opérateur operator=.
B. La première ligne initialise un nouvel objet via le constructeur par recopie, la seconde modifie un objet déjà existant via l'opérateur d'affectation.
C. La première ligne est invalide en C++.
Réponse B : L'écriture 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.
❓ Question 3 : Pourquoi est-il critique de vérifier l'auto-affectation if (this == &autre) return *this; dans operator= ?
A. Car si on écrit a = a; et qu'on commence par libérer notre mémoire (delete[] data), on détruit les données sources avant d'avoir pu les copier !
B. Pour des raisons purement esthétiques et de propreté de code.
C. Car le compilateur génère une erreur d'ambiguïté sinon.
Réponse A : Si l'opérateur libère la mémoire allouée par 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).
Séance Pratique

📝 Travaux Dirigés associés : TD n°1

Accéder au TD 1 complet (5 exercices) ➔

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éthodes const et 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).