# Cours 9 - Classes & Programmation orientée objet

Où est Charlie le Bug ? (15 minutes)

#include <iostream>
using namespace std;
 
int main() {
    int scores[5] = {90, 85, 78, 92, 88};
    for (int i = 0; i <= 5; i++) {
        cout << scores[i] << endl;
    }
 
    vector<int> nums;
    cout << nums[0];
 
    string name = "Charlie";
    char first = name[100];
    cout << first;
 
    vector<int> data {1, 2, 3};
    for (int i = 0; i < data.size(); i++) {
        cout << data[i] << endl;
        if (data[i] == 1)
            data.erase(data.begin() + i);
    }
    return 0;
}

Solution

#includd <iostream>
using namespace std;
 
int main() {
    int scores[5] = {90, 85, 78, 92, 88};
    for (int i = 0; i <= 5; i++) {     // Bug 1: off-by-one! i <= 5 goes to index 5 (out of bounds)
        cout << scores[i] << endl;
    }
 
    vector<int> nums;
    //cout << nums[0];                    // Bug 2: accessing empty vector - undefined behavior!
 
    string name = "Charlie";
    char first = name[100];             // Bug 3: out of bounds - undefined behavior!
    cout << first;                      // Bug 4: Impression d'une valeur UB !
 
    vector<int> data {1, 2, 3};
    for (int i = 0; i < data.size(); i++) {
        cout << data[i] << endl;
        if (data[i] == 1)
            data.erase(data.begin() + i);  // Bug 5: modifying container while iterating - skips elements
    }
    return 0;
}

struct vs class

struct Point {
    double x;      // public by default in struct
    double y;
};
 
class Point {
    double x;      // private by default in class
    double y;
public:
    // ... public interface
};

Règle générale : Utiliser struct pour les simples conteneurs de données, class pour les types avec comportement.

Fondamentaux des classes

Classes et Objets en C++ En programmation orientée objet, les classes et les objets sont des concepts fondamentaux qui permettent de représenter des entités et des concepts du monde réel.

  • Une classe est un modèle (ou gabarit) servant à créer des objets ayant des propriétés et des comportements similaires. On peut dire qu’une classe est le plan de construction d’un objet.
  • Un objet est une instance d’une classe. Par exemple, le type animal Chien est une classe, tandis qu’un chien particulier nommé Tommy est un objet de la classe Chien.

Les Classes en C++ Une classe est un type de données défini par l'utilisateur, qui regroupe ses propres membres de données (data members) et fonctions membres (member functions), accessibles en créant une instance de cette classe. Une classe C++ est comme le plan de construction d'un objet.

Créer une classe : Une classe doit être définie avant d’être utilisée. En C++, on la définit avec le mot-clé class.

#include <iostream>
using namespace std;
 
// Définition de la classe
class GfG
{
  public:
    // Membre de données
    int val;
 
    // Fonction membre
    void show()
    {
        cout << "Valeur : " << val << endl;
    }
};
 
int main()
{
    // Les objets seront déclarés ici
    GfG myGfG;
    
    myGfG.val = 2;
    myGfG.show();
    
    return 0;
}

Dans l’exemple ci-dessus, la classe GfG est créée avec un membre de données val et une fonction membre show(). La fonction membre est ici définie à l’intérieur de la classe, mais elle peut aussi être simplement déclarée dans la classe puis définie à l’extérieur à l’aide de l’opérateur de résolution de portée ::.

Ce qui précède s’appelle une définition de classe ou gabarit de classe.

Les Objets en C++ Quand une classe est définie, seules les spécifications (attributs et comportements) de l'objet sont établies. Aucune mémoire n'est allouée à la définition de la classe. Pour utiliser les données et accéder aux fonctions définies dans la classe, il faut créer des objets.

Les objets sont les entités réelles créées en tant qu’instances d’une classe. On peut créer autant d’objets d’une classe que l’on souhaite.

Créer un objet : Une fois la classe définie, on peut créer un objet de la même façon qu’on déclare une variable d’un type intégré :

NomClasse nomObjet;

Accès aux membres :

  • À l’intérieur de la classe, on accède aux membres directement par leur nom.
  • À l’extérieur, on utilise l’opérateur point (.) avec l’objet de la classe.
  • Les membres de données s’accèdent via objet.membre et les fonctions via objet.membre().

Classe Locale Les classes sont généralement déclarées dans la portée globale et sont accessibles à toutes les fonctions ou autres classes une fois définies. Cependant, C++ permet aussi de définir une classe à l'intérieur d'une fonction. On parle alors de classe locale, qui n'est accessible qu'au sein de cette fonction.

Classe Imbriquée (Nested Class) Une classe imbriquée est une classe définie à l'intérieur d'une autre classe (la classe englobante). En tant que membre de la classe englobante, elle dispose des mêmes droits d'accès que n'importe quel autre membre. Les membres de la classe englobante n'ont pas d'accès spécial aux membres de la classe imbriquée ; les règles d'accès standard s'appliquent.

#include <iostream>
using namespace std;
 
class Outer
{
  public:
    // Classe imbriquée à l'intérieur de Outer
    class Inner
    {
      public:
        void display()
        {
            cout << "Ceci est la classe Inner" << endl;
        }
    };
 
    void show()
    {
        cout << "Ceci est la classe Outer" << endl;
    }
};
 
int main()
{
    // Création d'un objet de la classe Outer
    Outer outerObj;
    outerObj.show();
 
    // Création d'un objet de la classe Inner via Outer
    Outer::Inner innerObj;
    innerObj.display();
 
    return 0;
}

Résultat :

Ceci est la classe Outer
Ceci est la classe Inner

Classe Enum (Enum Class) Les classes enum en C++ offrent une façon plus sûre et plus organisée d'utiliser les énumérations. Elles permettent de regrouper des constantes liées tout en évitant les conflits de noms et en garantissant une meilleure sécurité de type.

Classe vs Objet Le tableau suivant résume les principales différences entre classes et objets en C++ :

ClasseObjet
Un plan ou gabarit pour créer des objets.Une instance d’une classe avec des valeurs réelles.
Aucune mémoire n’est allouée tant qu’un objet n’est pas créé.La mémoire est allouée lors de la création d’un objet.
Entité conceptuelle décrivant structure et comportement.Entité réelle créée à partir de la classe.
Définit les propriétés et fonctions communes à tous les objets du type.Stocke des données spécifiques et les manipule via les fonctions de la classe.
Représente un concept ou un type général.Représente une instance spécifique de la classe.

Définir une classe avec un constructeur

// Defining a class constructor
#include <iostream>
#include <format>
 
class Box
{
public:
  // Constructor
  Box(double length, double width, double height)
  {
    std::cout << std::format("Box constructor called.") << std::endl;
    m_length = length;
    m_width = width;
    m_height = height;
  }
 
  // Function to calculate the volume of a box
  double volume()
  {
    return m_length * m_width * m_height;
  }
 
private:
  double m_length {1.0};
  double m_width {1.0};
  double m_height {1.0};
};
 
int main()
{
  Box firstBox {80.0, 50.0, 40.0};            // Create a box
  double firstBoxVolume {firstBox.volume()};   // Calculate the box volume
  std::cout << std::format("Volume of Box object is {}", firstBoxVolume) << std::endl;
 
  // Box secondBox;        // ERROR: no default constructor!
} 

Que se passe-t-il s'il n'y a pas de constructeur défini dans une classe C++ ? Si aucun constructeur n'est défini, le compilateur en génère un automagiquement : le constructeur par défaut implicite.

class Chat {
public:
    std::string nom;
    int age;
    float poids;
};
 
// Fonctionne : le compilateur a genere un constructeur par defaut
Chat c; // Les membres sont initialises par defaut

C’est ici que se cache le piege. Le comportement depend de l’endroit ou l’objet est cree et du type du membre.

Type de membreStack (Chat c;)Heap (new Chat)Possède son propre constructeur
int, float, etc.Valeur indéterminéeValeur indéterminéeNon
std::string, std::vector, etc.Initialise correctementInitialise correctementOui
Classe ou struct imbriquéeDepend de cette classeDepend de cette classeVariable
Chat c;
std::cout << c.age;  // Comportement indéfini : valeur garbage
std::cout << c.nom;  // Chaine vide : std::string a son propre constructeur

Le compilateur ne génère pas de constructeur par défaut si vous avez défini n’importe quel autre constructeur.

class Chien {
public:
    // Un constructeur custom est defini : plus de constructeur par defaut
    Chien(std::string nom, int age) { ... }
};
 
Chien d;             // Erreur de compilation : pas de constructeur par defaut
Chien d("Rex", 3);   // Correct

Vous pouvez le restaurer explicitement avec = default :

class Chien {
public:
    Chien() = default;                     // Restaure le constructeur implicite
    Chien(std::string nom, int age) { ... }
};

Ou l’interdire explicitement avec = delete :

class Singleton {
public:
    Singleton() = delete; // Aucune construction par defaut autorisee
};

Si votre classe gère une ressource (pointeur brut, descripteur de fichier…), le constructeur généré automagiquement ne sera pas suffisant. C’est là que s’applique la Règle de Trois / Cinq : destructeur, constructeur de copie, opérateur d’affectation par copie, et leurs variantes de déplacement (move) introduites en C++11. Ce sujet mérite son propre cours.

Le compilateur vous offre un constructeur gratuitement, mais les membres de types primitifs (int, float, etc.) contiennent des valeurs indéterminées.

☝🏼

Bref: Initialisez toujours vos membres explicitement.

Spécificateurs d'accès

class Character
{
public:              // Anyone can access
    std::string getName() const { return m_name; }
    int getHealth() const { return m_health; }
    void takeDamage(int amount) { m_health -= amount; }
 
protected:           // Only this class and derived classes
    int m_level {1};
 
private:             // Only this class
    std::string m_name;
    int m_health {100};
};

Listes d'initialisation (à privilégier !)

class Box
{
public:
    // Better: use initialization list
    Box(double length, double width, double height) : m_length{length}, m_width{width}, m_height{height}
    {
        // Constructor body (can be empty!)
        // m_length = length;
        // m_width = width;
        // m_height = height;
    }
 
private:
    double m_length;
    double m_width;
    double m_height;
};

Ce que tu vois s’appelle un member initializer list (ou simplement initializer list) en anglais.

En C++, les membres d’une classe sont construits avant que le corps du constructeur ne s’execute. Si tu fais ca :

Box(double length, double width, double height) {
    m_length = length; // Ce n'est PAS une initialisation, c'est une affectation
}
  1. m_length, m_width, m_height sont d’abord construits par défaut (valeur garbage pour un double)
  2. Puis ecrases par l’affectation dans le corps

C’est un travail en double. Avec l’initializer list :

Box(double length, double width, double height)
    : m_length{length}, m_width{width}, m_height{height}
{
}

Les membres sont directement construits avec la bonne valeur, en une seule étape.

NomDuConstructeur(params) : membre1{val1}, membre2{val2}, membre3{val3}
{
    // corps optionnel
}

Le : introduit la liste, et les membres sont séparés par des virgules.

class Moteur {
public:
    const int cylindres;     // const : ne peut etre affecté, seulement initialisé
    int& code_moteur;        // reference : doit etre liée a la construction
    Turbo turbo;             // classe sans constructeur par defaut
 
    Moteur(int cyl, int& code, Turbo t)
        : cylindres{cyl}, code_moteur{code}, turbo{t}  // obligatoire ici
    {}
};

Un membre const ou une référence ne peut pas être affecté après construction. L’initializer list est le seul endroit ou tu peux les initialiser.

L’ordre dans lequel les membres sont initialises est l’ordre de declaration dans la classe, pas l’ordre dans la liste. C’est un piège classique :

class Probleme {
    int a;
    int b;
public:
    Probleme(int val) : b{val}, a{b}  // a est initialisé AVANT b malgré l'ordre écrit
    {}                                 // a reçoit une valeur garbage
};

Bonne pratique : écrire la liste dans le même ordre que les déclarations de membres.

Préférer systématiquement l’initializer list au corps du constructeur. C’est plus efficace, c’est parfois obligatoire, et cela reflète mieux ce qui se passe réellement à l’exécution.

Fonctions membres const

class Circle
{
public:
    Circle(double radius) : m_radius{radius} {}
 
    double area() const           // const = this function doesn't modify the object
    {
        return std::numbers::pi * m_radius * m_radius;
    }
 
    void setRadius(double r)       // NOT const - it modifies the object
    {
        m_radius = r;
    }
 
private:
    double m_radius;
};

Règle : Marquez toute fonction membre qui ne modifie pas l’objet comme const.

Valeurs par défaut dans les constructeurs C++ Quand tu écris :

Box(double length, double width, double height)
    : m_length{length}, m_width{width}, m_height{height}
{}

Tu es obligé de passer les trois valeurs à la construction :

Box b{10.0, 5.0, 3.0}; // obligatoire
Box b;                 // erreur de compilation

Valeurs par défaut : deux approches

Le destructeur en C++ (Destructor) Le ~ désigne le destructeur d'une classe. C'est une méthode spéciale appelée automagiquement quand un objet est détruit. C'est le pendant exact du constructeur : le constructeur est appelé à la naissance de l'objet, le destructeur à sa mort.

Enemy() { ++s_count; }   // appele quand l'objet est cree
~Enemy() { --s_count; }  // appele quand l'objet est detruit

Quand est-ce qu'un objet est détruit

void maFonction() {
    Enemy e1;  // constructeur appelé : s_count = 1
    Enemy e2;  // constructeur appelé : s_count = 2
 
} // e1 et e2 sortent du scope, les deux destructeurs sont appelés : s_count = 0

Les objets sur le stack sont détruits automatiquement quand ils sortent de leur scope. Pour les objets sur le heap, le destructeur est appelé au moment du delete.

Enemy* e = new Enemy();  // constructeur : s_count = 1
delete e;                // destructeur  : s_count = 0

Sa signature Le destructeur a toujours la meme forme : le ~ suivi du nom de la classe, sans paramètres et sans type de retour. Il ne peut y en avoir qu'un seul par classe.

~Enemy() { /* code de nettoyage */ }

Quand doit-on en écrire un explicitement Le compilateur génère un destructeur vide par défaut, ce qui est suffisant dans la majorité des cas. Tu dois en écrire un quand ta classe gère une ressource manuellement : un pointeur brut alloué avec new, un fichier ouvert, une connexion réseau. C'est la troisième pièce de la Règle de Trois mentionnée dans le section sur les constructeurs implicites.

Stack (pile) vs Heap (tas) Le stack est une zone mémoire gérée automatiquement par le programme. Quand tu déclares une variable locale dans une fonction, elle va sur le stack. Quand la fonction se termine et que la variable sort de son scope, la memoire est libérée automatiquement. C'est rapide, c'est propre, mais la taille est limitée et la durée de vie est liée au scope.

void maFonction() {
    Enemy e1;       // sur le stack, géré automatiquement
}                   // e1 est détruit ici, destructeur appelé automatiquement

Le heap est une zone mémoire que tu gères toi-même. Tu alloues avec new, et tu dois libérer avec delete. La durée de vie n’est pas liée au scope, ce qui est utile quand tu veux qu’un objet survive a la fonction qui l’a créé. Mais si tu oublies le delete, la mémoire reste occupée jusqu’a la fin du programme : c’est une fuite memoire (memory leak).

void maFonction() {
    Enemy* e = new Enemy();  // sur le heap, tu gères la mémoire
    delete e;                // destructeur appelé, mémoire libérée
}

En C++ moderne, on évite le plus possible les new et delete manuels. On utilise des smart pointers comme std::unique_ptr ou std::shared_ptr qui gèrent le heap automatiquement pour toi, avec la garantie de ne pas oublier le delete. Mais ça, c’est un sujet à part entière.

Encapsulation & conception L'encapsulation est le principe qui consiste a regrouper les données et les fonctions qui les manipulent dans une même unité (la classe), tout en contrôlant ce qui est accessible de l'extérieur.

En pratique, ca veut dire que les membres d’une classe sont divisés en deux categories : ce qui est privé (les donnÉes internes) et ce qui est public (l’interface que tu exposes au reste du programme).

// Pas de classe, tout est accessible directement
double sante = 100.0;
sante = -9999.0; // Rien ne t'en empeche, meme si c'est absurde
class Personnage {
public:
    void setSante(double valeur) {
        if (valeur < 0.0) valeur = 0.0;     // on valide avant d'accepter
        if (valeur > 100.0) valeur = 100.0;
        m_sante = valeur;
    }
 
    double getSante() const {
        return m_sante;
    }
 
private:
    double m_sante{100.0}; // inaccessible directement de l'exterieur
};
 
Personnage p;
p.m_sante = -9999.0;    // erreur de compilation : membre privé
p.setSante(-9999.0);    // accepte, mais corrige a 0.0 automatiquement

Les trois niveaux d’accès

class MaClasse {
public:
    // accessible par tout le monde
 
protected:
    // accessible par la classe elle-meme et ses classes derivees (heritage)
 
private:
    // accessible uniquement par la classe elle-meme
};

La validation des données. Comme dans l’exemple ci-dessus, tu peux imposer des règles metier directement dans le setter. Une santé négative n’a pas de sens, le setter le garantit.

La liberté de modifier l’implementation. Si tu changes la façon dont m_sante est stockée en interne (par exemple tu passes d’un double a un int), le reste du programme n’a pas besoin de changer tant que l’interface publique reste la meme.

La lisibilité et la maintenance. En lisant une classe bien encapsulée, tu sais immédiatement ce qu’elle expose et ce qu’elle cache. Tu n’as pas besoin de comprendre ses entrailles pour l’utiliser.

Tu as probablement remarqué le préfixe m_ sur les membres prives. C’est une convention très répandue en C++ pour distinguer visuellement les membres de la classe des variables locales. Ce n’est pas une obligation du langage, mais c’est une bonne habitude à prendre.

Membres static

class Enemy
{
public:
    Enemy() { ++s_count; }
    ~Enemy() { --s_count; }
 
    static int getCount() { return s_count; }  // No object needed to call
 
private:
    static int s_count;  // Shared by ALL Enemy objects
};
 
int Enemy::s_count {0};  // Must be defined outside the class
 
// Usage:
Enemy e1, e2, e3;
std::cout << std::format("Total enemies: {}", Enemy::getCount()) << std::endl;  // 3

Lab :Concevoir une classe Vector2D Le fichier Vector2D.h (La déclaration de la classe et ses membres)

#ifndef VECTOR2D_VECTOR2D_H
#define VECTOR2D_VECTOR2D_H
 
class Vector2D
{
public:
    Vector2D(double x=0.0, double y=0.0) : m_x(x), m_y(y) {}
 
    // Getters
    double getX() const;
    double getY() const;
 
    // Setters
    void setX(double x);
    void setY(double y);
 
    double magnitude() const;
 
    Vector2D operator+(const Vector2D& other) const;
 
private:
    double m_x, m_y;
};
 
#endif //VECTOR2D_VECTOR2D_H
 

Le fichier Vector2D.cpp (la définition des méthodes)

#include "Vector2D.h"
#include <cmath>
 
// Getters
double Vector2D::getX() const { return m_x; }
double Vector2D::getY() const { return m_y; }
 
// Setters
void Vector2D::setX(double x) { m_x = x; }
void Vector2D::setY(double y) { m_y = y; }
 
double Vector2D::magnitude() const
{
    return std::sqrt(m_x * m_x + m_y * m_y);
}
 
Vector2D Vector2D::operator+(const Vector2D& other) const
{
    return Vector2D(m_x + other.m_x, m_y + other.m_y);
}

Le fichier main.cpp (l’utilisation de la class Vector2D)

#include <iostream>
#include <format>
#include "Vector2D.h"
 
int main()
{
    Vector2D a {3.0, 4.0};
    Vector2D b {1.0, 2.0};
 
    std::cout << std::format("Magnitude of vector a is : {}", a.magnitude()) << std::endl;
    
    Vector2D c = a + b;
    std::cout << std::format("Vector c is [{},{}] of magnitude {:.2f}", c.getX(), c.getY(), c.magnitude()) << std::endl;
    
    return 0;
}

Travail personnel

Lecture

  • Lire Beginning C++23 Chapitres 8-9

  • Lire Tour of C++ Chapitres 4-5

Défi 1 - Surcharge d'opérateurs Ajoutez deux opérateurs à la classe Color :

  • **operator==** - Compare deux couleurs par leurs composantes R, G, B (le nom n’est pas pris en compte).
  • **operator+** - Mélange deux couleurs en faisant la moyenne de leurs composantes et en combinant leurs noms avec " + ".
// Déclarations à ajouter dans la section public de Color (colors.h) :
bool operator==(const Color& other) const;
Color operator+(const Color& other) const;

Testez dans main.cpp :

Color a {"Rouge", 255, 0, 0};
Color b {"Bleu",  0, 0, 255};
 
if (a == b) { std::cout << "Identiques !\\n"; }
else        { std::cout << "Différentes !\\n"; }
 
Color somme = a + b;
somme.display();   // "Rouge + Bleu" R:127 G:0 B:127

Défi 2 - Setters avec validation Ajoutez des setters qui démontrent la valeur de l'encapsulation :

// Déclarations à ajouter dans la section public de Color (colors.h) :
void setR(int value);
void setG(int value);
void setB(int value);

Chaque setter doit clamper la valeur dans [0, 255] avant de l’assigner - exactement comme le constructeur. C’est l’avantage de l’encapsulation : on ne peut jamais mettre une valeur invalide !

Testez dans main.cpp :

Color c {"Test", 100, 100, 100};
c.setR(999);       // clampé à 255
c.setG(-50);       // clampé à 0
c.display();        // R:255 G:0 B:100

Défi 3 - Couleur inversée (avancé) Ajoutez une méthode qui retourne la couleur complémentaire (l'inverse) :

// Déclaration à ajouter dans la section public de Color (colors.h) :
Color invert() const;

La couleur inversée se calcule comme : (255 - r, 255 - g, 255 - b). Le nom de la couleur inversée sera "Inv:" + m_name.

Testez en affichant des paires de couleurs avec leur inverse :

Color tomate {"Tomato", 255, 99, 71};
Color inv = tomate.invert();
 
std::cout << "--- Original et inverse ---\\n";
tomate.display();    // R:255 G:99  B:71
inv.display();       // R:0   G:156 B:184

Solutions des défis

⚠️ Essayez d’abord par vous-même avant de regarder !

Solution - Défi 1 Déclarations dans **colors.h** (section **public** de **Color**) :

// Compare deux couleurs par composantes RGB (le nom est ignoré).
// other est BY CONST REFERENCE - on ne modifie pas l'autre couleur.
// const - ne modifie pas l'objet courant.
bool operator==(const Color& other) const;
 
// Mélange deux couleurs (moyenne des composantes, concaténation des noms).
// other est BY CONST REFERENCE.
// const - ne modifie pas l'objet courant.
// Retourne un nouveau Color BY VALUE.
Color operator+(const Color& other) const;

Définitions dans **colors.cpp** :

// ------------------------------------------------------------
// operator== - compare les composantes RGB uniquement
// Le nom est intentionnellement ignoré : deux couleurs avec
// des noms différents mais les mêmes composantes sont "égales".
// ------------------------------------------------------------
bool Color::operator==(const Color& other) const {
    return m_r == other.m_r && m_g == other.m_g && m_b == other.m_b;
}
 
// ------------------------------------------------------------
// operator+ - mélange deux couleurs par moyenne
// Retourne un nouveau Color avec le nom combiné
// ------------------------------------------------------------
Color Color::operator+(const Color& other) const {
    return Color{m_name + " + " + other.m_name,
                 (m_r + other.m_r) / 2,
                 (m_g + other.m_g) / 2,
                 (m_b + other.m_b) / 2};
}

Utilisation dans **main.cpp** :

Color a {"Rouge", 255, 0, 0};
Color b {"Bleu",  0, 0, 255};
Color c {"Rouge2", 255, 0, 0};
 
std::cout << "--- Test operator== ---\\n";
if (a == b) { std::cout << "a et b sont identiques\\n"; }
else        { std::cout << "a et b sont différentes\\n"; }   // Différentes
 
if (a == c) { std::cout << "a et c sont identiques\\n"; }   // Identiques (mêmes RGB)
else        { std::cout << "a et c sont différentes\\n"; }
 
std::cout << "\\n--- Test operator+ ---\\n";
Color somme = a + b;
somme.display();   // "Rouge + Bleu" R:127 G:0 B:127

Solution - Défi 2 Déclarations dans **colors.h** (section **public** de **Color**) :

// Modifie la composante rouge avec clamping automatique.
// value est BY VALUE - petit int, copie triviale.
void setR(int value);
 
// Modifie la composante verte avec clamping automatique.
void setG(int value);
 
// Modifie la composante bleue avec clamping automatique.
void setB(int value);

Définitions dans **colors.cpp** :

// ------------------------------------------------------------
// Setters - chaque setter utilise clamp() pour garantir [0, 255]
// C'est l'avantage de l'encapsulation : la validation est centralisée
// ------------------------------------------------------------
void Color::setR(int value) { m_r = clamp(value); }
void Color::setG(int value) { m_g = clamp(value); }
void Color::setB(int value) { m_b = clamp(value); }

Utilisation dans **main.cpp** :

std::cout << "--- Test setters avec clamping ---\\n";
Color c {"Test", 100, 100, 100};
c.display();        // R:100 G:100 B:100
 
c.setR(999);        // clampé à 255
c.setG(-50);        // clampé à 0
c.setB(128);
c.display();        // R:255 G:0   B:128
 
std::cout << "Sans encapsulation, on aurait pu écrire c.m_r = -500 !\\n";
std::cout << "Avec les setters, les valeurs sont toujours valides.\\n";

Solution - Défi 3 Déclaration dans **colors.h** (section **public** de **Color**) :

// Retourne la couleur complémentaire (255 - chaque composante).
// const - ne modifie pas l'objet courant.
// Retourne un nouveau Color BY VALUE.
Color invert() const;

Définition dans **colors.cpp** :

// ------------------------------------------------------------
// invert - retourne la couleur complémentaire
// Calcul : (255 - r, 255 - g, 255 - b)
// Le nom est préfixé par "Inv:" pour identifier l'inverse
// ------------------------------------------------------------
Color Color::invert() const {
    return Color{"Inv:" + m_name, 255 - m_r, 255 - m_g, 255 - m_b};
}

Utilisation dans **main.cpp** :

std::cout << "--- Couleurs et leurs inverses ---\\n";
std::vector<Color> palette = getWebColors();
 
for (const Color& c : palette) {
    Color inv = c.invert();
    std::cout << std::format("  {:<20s} R:{:<3d} G:{:<3d} B:{:<3d}  |  {:<25s} R:{:<3d} G:{:<3d} B:{:<3d}",
                 c.name(), c.r(), c.g(), c.b(),
                 inv.name(), inv.r(), inv.g(), inv.b()) << "\\n";
}

Sortie attendue :

--- Couleurs et leurs inverses ---
  Teal                 R:0   G:128 B:128  |  Inv:Teal                  R:255 G:127 B:127
  Salmon               R:250 G:128 B:114  |  Inv:Salmon                R:5   G:127 B:141
  Coral                R:255 G:127 B:80   |  Inv:Coral                 R:0   G:128 B:175
  Goldenrod            R:218 G:165 B:32   |  Inv:Goldenrod             R:37  G:90  B:223
  Orchid               R:218 G:112 B:214  |  Inv:Orchid                R:37  G:143 B:41
  SteelBlue            R:70  G:130 B:180  |  Inv:SteelBlue             R:185 G:125 B:75
  Tomato               R:255 G:99  B:71   |  Inv:Tomato                R:0   G:156 B:184
  MediumAquamarine     R:102 G:205 B:170  |  Inv:MediumAquamarine      R:153 G:50  B:85
  SlateGray            R:112 G:128 B:144  |  Inv:SlateGray             R:143 G:127 B:111
  Crimson              R:220 G:20  B:60   |  Inv:Crimson               R:35  G:235 B:195
  LimeGreen            R:50  G:205 B:50   |  Inv:LimeGreen             R:205 G:50  B:205
  DarkOrange           R:255 G:140 B:0    |  Inv:DarkOrange            R:0   G:115 B:255

Point clé : La méthode invert() est const - elle ne modifie pas l’objet courant, elle retourne un nouvel objet. C’est un patron de conception courant : les opérations qui transforment un objet retournent une copie modifiée plutôt que de modifier l’original.