Aller au contenu

Module 02 : Surcharge d'opérateurs et Forme Canonique Orthodoxe

Télécharger le PDF du sujet officiel

Concepts clés :

  • Forme Canonique Orthodoxe (OCF)
  • Constructeur de copie
  • Opérateur d’assignation par copie
  • Surcharge d’opérateurs
  • Nombres à virgule fixe

Ce module introduit deux concepts critiques : l’OCF (Forme Canonique Orthodoxe) et la surcharge d’opérateurs.

Sans sémantique de copie correcte, vos classes auront des bugs. Si votre classe gère de la mémoire dynamique et que vous ne définissez pas de constructeur de copie, C++ créera une “copie superficielle” qui partage les pointeurs—menant à des crashes de double suppression.

La surcharge d’opérateurs permet à vos types personnalisés de fonctionner naturellement avec les opérateurs. Au lieu de point1.add(point2), vous pouvez écrire point1 + point2. Cela rend le code plus lisible et intuitif.


En C, si vous vouliez des fonctions qui fonctionnent avec différents types, vous deviez leur donner des noms différents :

// Approche C - noms différents pour types différents
int max_int(int a, int b);
double max_double(double a, double b);
float max_float(float a, float b);

C++ a introduit la surcharge de fonctions : des fonctions avec le même nom mais des types de paramètres différents. Le compilateur choisit la bonne en fonction des arguments :

// Approche C++ - même nom, le compilateur choisit la bonne
int max(int a, int b);
double max(double a, double b);
float max(float a, float b);
max(3, 5); // Appelle max(int, int)
max(3.14, 2.71); // Appelle max(double, double)

C’est une forme de polymorphisme ad-hoc : le même nom de fonction s’adapte à différents types. La surcharge d’opérateurs étend cette idée aux opérateurs comme +, -, <<, etc.


Ce module vous apprend à définir un comportement personnalisé pour les opérateurs. Voici ce que la nouvelle syntaxe signifie :

Fixed a;
Fixed b;
a = b; // Appelle l'opérateur d'assignation
  • = assigne la valeur d’un objet à un autre objet EXISTANT
  • Vous pouvez le personnaliser en définissant operator= dans votre classe
  • Le compilateur en fournit un par défaut, mais pour les classes avec pointeurs vous DEVEZ écrire le vôtre
  • Contrairement au constructeur de copie (qui crée un NOUVEL objet), l’assignation fonctionne sur des objets EXISTANTS

L’opérateur & (Adresse de dans l’auto-vérification)

Section intitulée « L’opérateur & (Adresse de dans l’auto-vérification) »
if (this != &other) { // Vérifie l'auto-assignation
  • &other obtient l’adresse mémoire de l’objet other
  • this est un pointeur vers l’objet courant
  • this != &other compare deux adresses pour vérifier si c’est le même objet
  • Cela évite les bugs quand quelqu’un écrit a = a;

L’opérateur * (Déréférencement pour le retour)

Section intitulée « L’opérateur * (Déréférencement pour le retour) »
return *this; // Retourne l'objet courant (pas le pointeur !)
  • *this déréférence le pointeur this pour obtenir l’objet réel
  • Les opérateurs d’assignation retournent *this pour pouvoir chaîner les assignations : a = b = c;
  • Sans le *, vous retourneriez un pointeur, ce qui ne permettrait pas le chaînage
// Comme fonction membre :
bool operator==(const Fixed& other) const {
return _rawValue == other._rawValue;
}
// Comme fonction non-membre (doit être friend ou utiliser l'interface publique) :
std::ostream& operator<<(std::ostream& os, const Fixed& fixed) {
os << fixed.toFloat();
return os;
}
  • operator@ (où @ est n’importe quel opérateur) définit un comportement personnalisé pour cet opérateur
  • Les opérateurs membres ont un paramètre implicite : this (l’opérande gauche)
  • Les opérateurs non-membres ont besoin des deux opérandes comme paramètres explicites
  • << et >> sont généralement non-membres car le côté gauche est std::ostream/std::istream (pas votre classe)
// Pré-incrémentation : ++a
Fixed& operator++() {
_rawValue++; // Incrémente d'abord
return *this; // Retourne l'objet incrémenté
}
// Post-incrémentation : a++
Fixed operator++(int) { // Le (int) est juste un marqueur !
Fixed temp(*this); // Sauve la valeur courante
_rawValue++; // Incrémente
return temp; // Retourne l'ANCIENNE valeur
}
  • ++ et -- peuvent être préfixe (++a) ou postfixe (a++)
  • Le paramètre (int) distingue postfixe de préfixe (il n’est pas utilisé, juste un marqueur)
  • La pré-incrémentation retourne par référence (plus rapide, retourne l’objet lui-même)
  • La post-incrémentation retourne par valeur (plus lent, retourne une copie de l’ancienne valeur)

L’opérateur << (Décalage de bits - Math à virgule fixe)

Section intitulée « L’opérateur << (Décalage de bits - Math à virgule fixe) »
_rawValue = (_rawValue << 8); // Décale à gauche de 8 bits
_rawValue = (_rawValue >> 8); // Décale à droite de 8 bits
  • << et >> effectuent aussi des décalages de bits (sans rapport avec les opérateurs de flux !)
  • << 8 multiplie par 2^8 (256) - décale les bits vers la gauche
  • >> 8 divise par 2^8 (256) - décale les bits vers la droite
  • Utilisé en arithmétique à virgule fixe pour les calculs fractionnaires
  • Les mêmes symboles font des choses complètement différentes selon le contexte !
Fixed(const Fixed& other); // Constructeur de copie
  • & après le type signifie “référence vers”
  • const signifie “ne pas modifier l’original”
  • const Fixed& est une “référence const” - efficace (pas de copie) et sûr (lecture seule)
  • Le constructeur de copie prend une référence const vers l’objet source
  • Sans &, vous créeriez une récursion infinie (constructeur de copie s’appelant lui-même !)

Philosophie de conception : L’OCF n’est pas juste une règle - c’est un pattern de conception qui garantit que toutes vos classes ont une interface cohérente et prévisible. Quand chaque classe suit l’OCF :

  • Les autres programmeurs savent à quoi s’attendre de n’importe quelle classe
  • Le comportement de copie/assignation est explicite et documenté
  • La gestion des ressources est correctement gérée
  • Votre code s’intègre harmonieusement avec les conteneurs et algorithmes STL

Pensez à l’OCF comme établissant un “contrat” que chaque classe respecte.

À partir du Module 02, TOUTES les classes doivent implémenter :

class Sample {
public:
Sample(); // 1. Constructeur par défaut
Sample(const Sample& other); // 2. Constructeur de copie
Sample& operator=(const Sample& other); // 3. Opérateur d'assignation par copie
~Sample(); // 4. Destructeur
};
┌─────────────────────────────────────────────────────────────────┐
│ Forme Canonique Orthodoxe │
│ (La Règle des Quatre) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ │
│ │ Constructeur │───→ Crée l'objet avec valeurs par défaut │
│ │ par défaut │ MyClass a; │
│ └─────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Constructeur │───→ Crée NOUVEL objet depuis un existant │
│ │ de copie │ MyClass b(a); // ou MyClass b = a; │
│ └─────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Opérateur │───→ Assigne à un objet EXISTANT │
│ │ d'assignation │ a = b; // les deux existent déjà │
│ └─────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Destructeur │───→ Nettoie quand l'objet est détruit │
│ │ │ (appelé automatiquement) │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘

Pourquoi les quatre comptent : Si votre classe gère de la mémoire dynamique (pointeurs), les valeurs par défaut générées par le compilateur causeront des bugs de double suppression. Vous devez implémenter les quatre pour gérer correctement le cycle de vie des ressources.

Sans OCF correct, les classes avec mémoire dynamique auront des bugs :

// MAUVAIS : Pas de constructeur de copie ou opérateur d'assignation
class Bad {
private:
int* _data;
public:
Bad() { _data = new int(42); }
~Bad() { delete _data; }
};
Bad a;
Bad b = a; // Copie par défaut : b._data = a._data (même pointeur !)
// Quand a et b sont détruits : double delete ! CRASH !

Crée un NOUVEL objet comme copie d’un objet existant.

class Sample {
private:
int _value;
int* _data;
public:
// Constructeur de copie
Sample(const Sample& other) : _value(other._value) {
std::cout << "Constructeur de copie appelé" << std::endl;
// Copie profonde : alloue nouvelle mémoire et copie le contenu
_data = new int(*other._data);
}
};
Sample a;
Sample b(a); // Constructeur de copie
Sample c = a; // Constructeur de copie (PAS assignation !)
func(a); // Constructeur de copie (passage par valeur)
return a; // Constructeur de copie (retour par valeur)
// COPIE SUPERFICIELLE (par défaut, dangereux avec pointeurs)
class Shallow {
int* _ptr;
public:
Shallow(const Shallow& other) {
_ptr = other._ptr; // Les deux pointent vers la même mémoire !
}
};
// COPIE PROFONDE (correct pour les pointeurs)
class Deep {
int* _ptr;
public:
Deep(const Deep& other) {
_ptr = new int(*other._ptr); // Nouvelle mémoire, copie valeur
}
};

Assigne la valeur d’un objet EXISTANT à un autre objet EXISTANT.

class Sample {
public:
Sample& operator=(const Sample& other) {
std::cout << "Opérateur d'assignation appelé" << std::endl;
// 1. Vérifier l'auto-assignation
if (this != &other) {
// 2. Nettoyer les ressources existantes
delete _data;
// 3. Copier les valeurs
_value = other._value;
_data = new int(*other._data);
}
// 4. Retourner *this pour le chaînage : a = b = c;
return *this;
}
};
Sample a;
Sample b;
b = a; // Opérateur d'assignation (b existe déjà)
Sample a;
a = a; // Auto-assignation - serait un bug sans vérification !
// Sans vérification :
// 1. delete _data; // Libère la mémoire
// 2. _data = new int(*other._data); // other._data est déjà supprimé !

class Fixed {
private:
int _rawValue;
static const int _fractionalBits = 8;
public:
// Constructeur par défaut
Fixed() : _rawValue(0) {
std::cout << "Constructeur par défaut appelé" << std::endl;
}
// Constructeur de copie
Fixed(const Fixed& other) : _rawValue(other._rawValue) {
std::cout << "Constructeur de copie appelé" << std::endl;
}
// Opérateur d'assignation par copie
Fixed& operator=(const Fixed& other) {
std::cout << "Opérateur d'assignation appelé" << std::endl;
if (this != &other)
_rawValue = other._rawValue;
return *this;
}
// Destructeur
~Fixed() {
std::cout << "Destructeur appelé" << std::endl;
}
// Getters/Setters
int getRawBits() const {
std::cout << "Fonction membre getRawBits appelée" << std::endl;
return _rawValue;
}
void setRawBits(int const raw) {
_rawValue = raw;
}
};

Définir un comportement personnalisé pour les opérateurs (+, -, *, /, ==, <, <<, etc.) quand ils sont utilisés avec votre classe.

// Comme fonction membre
ReturnType operator@(parameters);
// Comme fonction non-membre (friend ou utilise l'interface publique)
ReturnType operator@(LeftType, RightType);
class Fixed {
public:
bool operator>(const Fixed& other) const {
return _rawValue > other._rawValue;
}
bool operator<(const Fixed& other) const {
return _rawValue < other._rawValue;
}
bool operator>=(const Fixed& other) const {
return _rawValue >= other._rawValue;
}
bool operator<=(const Fixed& other) const {
return _rawValue <= other._rawValue;
}
bool operator==(const Fixed& other) const {
return _rawValue == other._rawValue;
}
bool operator!=(const Fixed& other) const {
return _rawValue != other._rawValue;
}
};
class Fixed {
public:
Fixed operator+(const Fixed& other) const {
Fixed result;
result._rawValue = _rawValue + other._rawValue;
return result;
}
Fixed operator-(const Fixed& other) const {
Fixed result;
result._rawValue = _rawValue - other._rawValue;
return result;
}
Fixed operator*(const Fixed& other) const {
Fixed result;
// Pour virgule fixe : (a * b) >> fractionalBits
result._rawValue = (_rawValue * other._rawValue) >> _fractionalBits;
return result;
}
Fixed operator/(const Fixed& other) const {
Fixed result;
// Pour virgule fixe : (a << fractionalBits) / b
result._rawValue = (_rawValue << _fractionalBits) / other._rawValue;
return result;
}
};
class Fixed {
public:
// Pré-incrémentation : ++a
Fixed& operator++() {
_rawValue++;
return *this;
}
// Post-incrémentation : a++
Fixed operator++(int) { // Le paramètre int est juste un marqueur
Fixed temp(*this); // Sauvegarde la valeur courante
_rawValue++; // Incrémente
return temp; // Retourne l'ancienne valeur
}
// Pré-décrémentation : --a
Fixed& operator--() {
_rawValue--;
return *this;
}
// Post-décrémentation : a--
Fixed operator--(int) {
Fixed temp(*this);
_rawValue--;
return temp;
}
};

Comprendre pourquoi la pré-incrémentation retourne par référence et la post-incrémentation par valeur :

┌─────────────────────────────────────────────────────────────────┐
│ Pré-incrémentation (++a) vs Post-incrémentation (a++) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ PRÉ-INCRÉMENTATION (++a) : │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. Incrémente la valeur │ │
│ │ 2. Retourne *this (l'objet lui-même) │ │
│ │ │ │
│ │ Fixed& operator++() { ┌─────┐ │ │
│ │ _rawValue++; ────→ │ 6 │ a (après) │ │
│ │ return *this; └─────┘ │ │
│ │ } ↑ │ │
│ │ Retour par référence │ │
│ │ (retourne l'objet) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ POST-INCRÉMENTATION (a++) : │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. Sauve l'ancienne valeur dans temp │ │
│ │ 2. Incrémente la valeur │ │
│ │ 3. Retourne temp (l'ANCIENNE valeur) │ │
│ │ │ │
│ │ Fixed operator++(int) { ┌─────┐ │ │
│ │ Fixed temp(*this); │ 5 │ temp (ancien) │ │
│ │ _rawValue++; └─────┘ │ │
│ │ return temp; ────→ ↑ │ │
│ │ } Retour par valeur │ │
│ │ (retourne une COPIE) │ │
│ │ ┌─────┐ │ │
│ │ │ 6 │ a (après) │ │
│ │ └─────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ DIFFÉRENCES CLÉS : │
│ ┌────────────────┬──────────────────┬──────────────────┐ │
│ │ │ Pré (++a) │ Post (a++) │ │
│ ├────────────────┼──────────────────┼──────────────────┤ │
│ │ Type retour │ Fixed& (ref) │ Fixed (valeur) │ │
│ │ Retourne │ L'objet │ Une copie │ │
│ │ Chaînable ? │ Oui : ++++a │ Non : a++++ │ │
│ │ Performance │ Plus rapide │ Plus lent (copie)│ │
│ │ Usage │ Préféré │ Quand ancienne │ │
│ │ │ │ valeur nécessaire│ │
│ └────────────────┴──────────────────┴──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘

Bonne pratique : Utilisez la pré-incrémentation (++a) quand vous n’avez pas besoin de l’ancienne valeur—c’est plus efficace car ça évite de créer une copie temporaire.

// Doit être une fonction non-membre (ostream est à gauche)
std::ostream& operator<<(std::ostream& os, const Fixed& fixed) {
os << fixed.toFloat();
return os;
}
// Utilisation
Fixed a(42.42f);
std::cout << a << std::endl; // Affiche : 42.4219

Une façon de représenter des nombres décimaux en utilisant des entiers, avec un nombre fixe de bits pour la partie fractionnaire.

Entier : 42
Binaire : 00101010.00000000
^^^^^^^^ ^^^^^^^^
Entier Fraction
Pour convertir :
- Int vers Fixed : int << 8
- Fixed vers Int : fixed >> 8
- Float vers Fixed : float * 256 (puis arrondir)
- Fixed vers Float : fixed / 256.0f
class Fixed {
private:
int _rawValue;
static const int _fractionalBits = 8;
public:
// Depuis int
Fixed(const int value) : _rawValue(value << _fractionalBits) {}
// Depuis float
Fixed(const float value)
: _rawValue(roundf(value * (1 << _fractionalBits))) {}
// Vers int
int toInt() const {
return _rawValue >> _fractionalBits;
}
// Vers float
float toFloat() const {
return (float)_rawValue / (1 << _fractionalBits);
}
};
  • Plus rapide que la virgule flottante sur certains matériels
  • Précision prévisible
  • Pas d’erreurs d’arrondi flottant pour les valeurs exactes
  • Utilisé dans : graphiques, audio, systèmes embarqués

class Fixed {
public:
// Version non-const
static Fixed& min(Fixed& a, Fixed& b) {
return (a < b) ? a : b;
}
// Version const
static const Fixed& min(const Fixed& a, const Fixed& b) {
return (a < b) ? a : b;
}
// Version non-const
static Fixed& max(Fixed& a, Fixed& b) {
return (a > b) ? a : b;
}
// Version const
static const Fixed& max(const Fixed& a, const Fixed& b) {
return (a > b) ? a : b;
}
};
// Utilisation
Fixed a(2.0f), b(3.0f);
std::cout << Fixed::max(a, b) << std::endl; // 3

Créez une classe Fixed qui représente des nombres à virgule fixe avec ces quatre membres requis (OCF) :

  1. Constructeur par défaut - Crée un Fixed avec valeur 0
  2. Constructeur de copie - Crée un Fixed depuis un autre Fixed
  3. Opérateur d’assignation par copie - Assigne un Fixed à un autre
  4. Destructeur - Nettoie (affiche un message pour cet exercice)

Plus des getters/setters basiques pour la valeur raw bits.

Étape 1 - Comprendre ce que signifie l’OCF

Ces quatre fonctions contrôlent comment les objets sont :

  • Créés à partir de rien (constructeur par défaut)
  • Créés depuis un autre objet (constructeur de copie)
  • Assignés depuis un autre objet (opérateur d’assignation)
  • Détruits (destructeur)

Étape 2 - Comprendre la représentation à virgule fixe

Un nombre à virgule fixe stocke des valeurs fractionnaires en utilisant des entiers :

  • 8 bits pour la partie fractionnaire (à droite de la décimale)
  • Bits restants pour la partie entière (à gauche de la décimale)
  • Valeur = _rawValue / 256 (puisque 2^8 = 256)

Étape 3 - Afficher des messages pour montrer quand chacun est appelé

Le sujet exige d’afficher des messages pour que vous puissiez voir l’ordre des appels.

Étape 1 - Déclaration de la classe :

Fixed.hpp
#ifndef FIXED_HPP
#define FIXED_HPP
class Fixed {
private:
int _rawValue;
static const int _fractionalBits = 8;
public:
Fixed(); // Constructeur par défaut
Fixed(const Fixed& other); // Constructeur de copie
Fixed& operator=(const Fixed& other); // Assignation par copie
~Fixed(); // Destructeur
int getRawBits() const;
void setRawBits(int const raw);
};
#endif

Étape 2 - Implémentation :

Fixed.cpp
#include "Fixed.hpp"
#include <iostream>
Fixed::Fixed() : _rawValue(0) {
std::cout << "Constructeur par défaut appelé" << std::endl;
}
Fixed::Fixed(const Fixed& other) : _rawValue(other._rawValue) {
std::cout << "Constructeur de copie appelé" << std::endl;
}
Fixed& Fixed::operator=(const Fixed& other) {
std::cout << "Opérateur d'assignation appelé" << std::endl;
if (this != &other)
_rawValue = other._rawValue;
return *this;
}
Fixed::~Fixed() {
std::cout << "Destructeur appelé" << std::endl;
}
int Fixed::getRawBits() const {
std::cout << "Fonction membre getRawBits appelée" << std::endl;
return _rawValue;
}
void Fixed::setRawBits(int const raw) {
_rawValue = raw;
}
LigneCodePourquoi
Fixed(const Fixed& other)Signature constructeur copiePrend référence const vers la source
: _rawValue(other._rawValue)Liste d’initialisationCopie la valeur directement
Fixed& operator=(...)Assignation retourne référenceActive le chaînage : a = b = c
if (this != &other)Vérif auto-assignationÉvite que a = a casse
return *thisRetourne cet objetActive le chaînage
static const intConstante de classePartagée par toutes les instances

1. Confondre constructeur de copie et assignation

Fixed a;
Fixed b(a); // Constructeur de copie - b est en cours de CRÉATION
Fixed c = a; // AUSSI constructeur de copie - c est en cours de CRÉATION (syntaxe d'initialisation)
Fixed d;
d = a; // Opérateur d'assignation - d EXISTE déjà

2. Oublier la vérification d’auto-assignation

// FAUX - casse si a = a
Fixed& operator=(const Fixed& other) {
_rawValue = other._rawValue;
return *this;
}
// JUSTE - sûr pour l'auto-assignation
Fixed& operator=(const Fixed& other) {
if (this != &other)
_rawValue = other._rawValue;
return *this;
}

3. Ne pas retourner *this depuis l’assignation

// FAUX - ne peut pas chaîner les assignations
void operator=(const Fixed& other) {
_rawValue = other._rawValue;
}
// JUSTE - active a = b = c
Fixed& operator=(const Fixed& other) {
// ...
return *this;
}
Fenêtre de terminal
# Compiler et exécuter
c++ -Wall -Wextra -Werror *.cpp -o fixed
# Sortie attendue pour un test basique :
# Constructeur par défaut appelé
# Constructeur de copie appelé
# Opérateur d'assignation appelé
# (suivi des appels de destructeur dans l'ordre inverse)
Fixed.hpp
#ifndef FIXED_HPP
#define FIXED_HPP
class Fixed {
private:
int _rawValue;
static const int _fractionalBits = 8;
public:
Fixed();
Fixed(const Fixed& other);
Fixed& operator=(const Fixed& other);
~Fixed();
int getRawBits() const;
void setRawBits(int const raw);
};
#endif

Étendre la classe Fixed avec :

  • Constructeur depuis int : Convertit un entier en virgule fixe
  • Constructeur depuis float : Convertit un float en virgule fixe
  • toInt() : Convertit la virgule fixe en entier
  • toFloat() : Convertit la virgule fixe en float
  • operator<< : Insertion de flux pour l’affichage

Étape 1 - Comprendre les maths

Avec 8 bits fractionnaires :

  • int → fixed : Décaler à gauche de 8 (multiplier par 256)
  • fixed → int : Décaler à droite de 8 (diviser par 256)
  • float → fixed : Multiplier par 256, puis arrondir
  • fixed → float : Diviser par 256.0

Étape 2 - Pourquoi ces conversions ?

Exemple : 42.42 en virgule fixe
42.42 * 256 = 10859.52 → arrondi à 10860
10860 / 256 = 42.421875 (proche de l'original)

Étape 3 - L’opérateur de flux N’EST PAS un membre

std::cout << fixed signifie que ostream est à gauche. On ne peut pas modifier ostream, donc operator<< doit être une fonction non-membre.

Étape 1 - Constructeurs de conversion :

Fixed.cpp
// Depuis int : décale à gauche par fractional bits
Fixed::Fixed(const int value)
: _rawValue(value << _fractionalBits) {
std::cout << "Constructeur Int appelé" << std::endl;
}
// Depuis float : multiplie par 2^8 et arrondit
Fixed::Fixed(const float value)
: _rawValue(roundf(value * (1 << _fractionalBits))) {
std::cout << "Constructeur Float appelé" << std::endl;
}

Étape 2 - Fonctions de conversion :

Fixed.cpp
// Vers int : décale à droite (perd la partie fractionnaire)
int Fixed::toInt() const {
return _rawValue >> _fractionalBits;
}
// Vers float : divise par 2^8
float Fixed::toFloat() const {
return (float)_rawValue / (1 << _fractionalBits);
}

Étape 3 - Opérateur de flux :

Fixed.cpp
// Fonction non-membre
std::ostream& operator<<(std::ostream& os, const Fixed& fixed) {
os << fixed.toFloat();
return os;
}
LigneCodePourquoi
value << _fractionalBitsDécale gauche 8 bitsÉquivalent à value * 256
roundf(value * ...)Arrondit au plus procheConversion float nécessite arrondi
1 << _fractionalBitsÉgal à 2562^8 = 256
_rawValue >> _fractionalBitsDécale droite 8 bitsÉquivalent à division entière par 256
(float)_rawValue / ...Cast en float d’abordÉvite division entière !
return osRetourne le fluxActive le chaînage : cout << a << b

1. Oublier roundf() pour la conversion float

// FAUX - tronque, perd de la précision
Fixed::Fixed(const float value)
: _rawValue(value * (1 << _fractionalBits)) {}
// JUSTE - arrondit au plus proche
Fixed::Fixed(const float value)
: _rawValue(roundf(value * (1 << _fractionalBits))) {}

2. Division entière dans toFloat()

// FAUX - division entière donne 0 pour petites valeurs !
float Fixed::toFloat() const {
return _rawValue / (1 << _fractionalBits);
}
// JUSTE - cast en float d'abord
float Fixed::toFloat() const {
return (float)_rawValue / (1 << _fractionalBits);
}

3. Faire de operator<< une fonction membre

// FAUX - ne peut pas être membre (ostream côté gauche)
class Fixed {
std::ostream& operator<<(std::ostream& os) const;
};
// JUSTE - fonction non-membre
std::ostream& operator<<(std::ostream& os, const Fixed& fixed);
int main() {
Fixed a;
Fixed const b(10); // Depuis int
Fixed const c(42.42f); // Depuis float
Fixed const d(b); // Copie
std::cout << "a est " << a << std::endl; // 0
std::cout << "b est " << b << std::endl; // 10
std::cout << "c est " << c << std::endl; // 42.4219 (proche de 42.42)
std::cout << "d est " << d << std::endl; // 10
std::cout << "b en int : " << b.toInt() << std::endl; // 10
std::cout << "c en int : " << c.toInt() << std::endl; // 42
return 0;
}
Fixed.hpp
#ifndef FIXED_HPP
#define FIXED_HPP
#include <iostream>
class Fixed {
private:
int _rawValue;
static const int _fractionalBits = 8;
public:
Fixed();
Fixed(const int value);
Fixed(const float value);
Fixed(const Fixed& other);
Fixed& operator=(const Fixed& other);
~Fixed();
int getRawBits() const;
void setRawBits(int const raw);
float toFloat() const;
int toInt() const;
};
std::ostream& operator<<(std::ostream& os, const Fixed& fixed);
#endif

Ajouter la surcharge d’opérateurs à la classe Fixed :

  • 6 opérateurs de comparaison : >, <, >=, <=, ==, !=
  • 4 opérateurs arithmétiques : +, -, *, /
  • 4 incrémentation/décrémentation : pré/post ++, pré/post --
  • 4 fonctions statiques min/max : pour références Fixed et références const

Étape 1 - Opérateurs de comparaison

Comparer les raw bits directement - c’est la beauté de la virgule fixe !

Étape 2 - Opérateurs arithmétiques

Retourner un nouvel objet Fixed (ne pas modifier l’existant).

Étape 3 - Incrémentation : Pré vs Post

  • Pré-incrémentation ++a : Modifie et retourne référence
  • Post-incrémentation a++ : Sauve copie, modifie, retourne ancienne copie

Étape 4 - La plus petite incrémentation

Ajouter 1 aux raw bits = ajouter 1/256 = 0.00390625 à la valeur.

Étape 1 - Opérateurs de comparaison :

Fixed.cpp
bool Fixed::operator>(const Fixed& other) const {
return _rawValue > other._rawValue;
}
bool Fixed::operator<(const Fixed& other) const {
return _rawValue < other._rawValue;
}
bool Fixed::operator>=(const Fixed& other) const {
return _rawValue >= other._rawValue;
}
bool Fixed::operator<=(const Fixed& other) const {
return _rawValue <= other._rawValue;
}
bool Fixed::operator==(const Fixed& other) const {
return _rawValue == other._rawValue;
}
bool Fixed::operator!=(const Fixed& other) const {
return _rawValue != other._rawValue;
}

Étape 2 - Opérateurs arithmétiques :

Fixed.cpp
Fixed Fixed::operator+(const Fixed& other) const {
return Fixed(toFloat() + other.toFloat());
}
Fixed Fixed::operator-(const Fixed& other) const {
return Fixed(toFloat() - other.toFloat());
}
Fixed Fixed::operator*(const Fixed& other) const {
return Fixed(toFloat() * other.toFloat());
}
Fixed Fixed::operator/(const Fixed& other) const {
return Fixed(toFloat() / other.toFloat());
}

Étape 3 - Incrémentation/décrémentation :

Fixed.cpp
// Pré-incrémentation : ++a
Fixed& Fixed::operator++() {
_rawValue++;
return *this;
}
// Post-incrémentation : a++
Fixed Fixed::operator++(int) {
Fixed temp(*this); // Sauvegarde valeur courante
_rawValue++; // Incrémente
return temp; // Retourne ANCIENNE valeur
}
// Pré-décrémentation : --a
Fixed& Fixed::operator--() {
_rawValue--;
return *this;
}
// Post-décrémentation : a--
Fixed Fixed::operator--(int) {
Fixed temp(*this);
_rawValue--;
return temp;
}

Étape 4 - min/max statiques :

Fixed.cpp
Fixed& Fixed::min(Fixed& a, Fixed& b) {
return (a < b) ? a : b;
}
const Fixed& Fixed::min(const Fixed& a, const Fixed& b) {
return (a < b) ? a : b;
}
Fixed& Fixed::max(Fixed& a, Fixed& b) {
return (a > b) ? a : b;
}
const Fixed& Fixed::max(const Fixed& a, const Fixed& b) {
return (a > b) ? a : b;
}
LigneCodePourquoi
operator++(int)Signature post-incrémentationParamètre int distingue de pré
Fixed temp(*this)Sauve valeur courantePost-incrémentation retourne ancienne valeur
return *thisRetourne référencePré-incrémentation retourne objet modifié
return tempRetourne copiePost-incrémentation retourne copie sauvée
static Fixed& min(...)Fonction statiqueAppelée comme Fixed::min(a, b)
Deux surcharges minPour const/non-constPréserve la const-correctness

1. Mauvais types de retour pour pré/post incrémentation

// FAUX - post devrait retourner copie, pas référence
Fixed& operator++(int) {
// ...
}
// JUSTE
Fixed& operator++(); // Pré retourne référence
Fixed operator++(int); // Post retourne copie

2. Ne pas comprendre la plus petite incrémentation

Fixed a(0);
a++; // a est maintenant 0.00390625 (1/256)
// PAS 1.0 !

3. Oublier les versions const et non-const de min/max

// Besoin des deux :
static Fixed& min(Fixed& a, Fixed& b); // Pour non-const
static const Fixed& min(const Fixed& a, const Fixed& b); // Pour const
int main() {
Fixed a;
Fixed const b(Fixed(5.05f) * Fixed(2));
std::cout << a << std::endl; // 0
std::cout << ++a << std::endl; // 0.00390625
std::cout << a << std::endl; // 0.00390625
std::cout << a++ << std::endl; // 0.00390625
std::cout << a << std::endl; // 0.0078125
std::cout << b << std::endl; // 10.1016
std::cout << Fixed::max(a, b) << std::endl; // 10.1016
return 0;
}
Fixed.hpp (section opérateurs)
// Comparaison
bool operator>(const Fixed& other) const;
bool operator<(const Fixed& other) const;
bool operator>=(const Fixed& other) const;
bool operator<=(const Fixed& other) const;
bool operator==(const Fixed& other) const;
bool operator!=(const Fixed& other) const;
// Arithmétique
Fixed operator+(const Fixed& other) const;
Fixed operator-(const Fixed& other) const;
Fixed operator*(const Fixed& other) const;
Fixed operator/(const Fixed& other) const;
// Incrémentation/Décrémentation
Fixed& operator++();
Fixed operator++(int);
Fixed& operator--();
Fixed operator--(int);
// min/max statiques
static Fixed& min(Fixed& a, Fixed& b);
static const Fixed& min(const Fixed& a, const Fixed& b);
static Fixed& max(Fixed& a, Fixed& b);
static const Fixed& max(const Fixed& a, const Fixed& b);

Implémenter une fonction pour tester si un point est à l’intérieur d’un triangle :

bool bsp(Point const a, Point const b, Point const c, Point const point);
  • Retourne true si le point est strictement à l’intérieur du triangle
  • Retourne false si le point est sur un bord ou un sommet

Vous devez aussi créer une classe Point avec des attributs x et y const.

Étape 1 - Comprendre l’algorithme (Méthode du produit vectoriel)

Pour chaque arête du triangle :

  1. Calculer le produit vectoriel de (vecteur arête) x (vecteur point-vers-sommet)
  2. Si les trois produits vectoriels ont le même signe, le point est à l’intérieur
  3. Si un produit vectoriel est zéro, le point est sur une arête

Étape 2 - Concevoir la classe Point

Contrainte clé : x et y sont const - ne peuvent pas être changés après construction.

Étape 3 - La formule du produit vectoriel

Pour les vecteurs de P1 à P2 et P1 à P3 :

cross = (P2.x - P1.x) * (P3.y - P1.y) - (P2.y - P1.y) * (P3.x - P1.x)

Étape 1 - Classe Point :

Point.hpp
#ifndef POINT_HPP
#define POINT_HPP
#include "Fixed.hpp"
class Point {
private:
Fixed const _x;
Fixed const _y;
public:
Point();
Point(const float x, const float y);
Point(const Point& other);
Point& operator=(const Point& other);
~Point();
Fixed getX() const;
Fixed getY() const;
};
bool bsp(Point const a, Point const b, Point const c, Point const point);
#endif

Étape 2 - Implémentation Point :

Point.cpp
#include "Point.hpp"
Point::Point() : _x(0), _y(0) {}
Point::Point(const float x, const float y) : _x(x), _y(y) {}
Point::Point(const Point& other) : _x(other._x), _y(other._y) {}
// L'assignation ne peut pas modifier les membres const - ne fait effectivement rien
Point& Point::operator=(const Point& other) {
(void)other;
return *this;
}
Point::~Point() {}
Fixed Point::getX() const { return _x; }
Fixed Point::getY() const { return _y; }

Étape 3 - Algorithme BSP :

bsp.cpp
#include "Point.hpp"
// Le produit vectoriel nous dit de quel côté d'une ligne un point se trouve
static Fixed crossProduct(Point const& p1, Point const& p2, Point const& p3) {
return (p2.getX() - p1.getX()) * (p3.getY() - p1.getY())
- (p2.getY() - p1.getY()) * (p3.getX() - p1.getX());
}
bool bsp(Point const a, Point const b, Point const c, Point const point) {
// Calculer les produits vectoriels pour chaque arête
Fixed d1 = crossProduct(a, b, point);
Fixed d2 = crossProduct(b, c, point);
Fixed d3 = crossProduct(c, a, point);
// Vérifier si sur une arête (produit vectoriel = 0)
if (d1 == Fixed(0) || d2 == Fixed(0) || d3 == Fixed(0))
return false;
// Vérifier si tous du même signe (tous positifs OU tous négatifs)
bool allNegative = (d1 < Fixed(0)) && (d2 < Fixed(0)) && (d3 < Fixed(0));
bool allPositive = (d1 > Fixed(0)) && (d2 > Fixed(0)) && (d3 > Fixed(0));
return allNegative || allPositive;
}
LigneCodePourquoi
Fixed const _xMembre constCoordonnée X ne peut pas changer
(void)otherSupprime warningL’assignation ne peut rien faire d’utile
crossProduct(a, b, point)Arête AB au pointLe signe nous dit quel côté
d1 == Fixed(0)Vérif sur arêteProduit zéro = colinéaire = sur l’arête
allNegative || allPositiveVérif intérieurMême signe = même côté de toutes les arêtes

1. Retourner true pour les points sur les arêtes

// FAUX - le sujet dit arête/sommet = false
if (d1 == Fixed(0))
return true;
// JUSTE
if (d1 == Fixed(0))
return false;

2. Essayer de modifier les membres const dans l’assignation

// FAUX - ne compilera pas, _x et _y sont const
Point& Point::operator=(const Point& other) {
_x = other._x; // Erreur !
_y = other._y; // Erreur !
return *this;
}
// JUSTE - reconnaître que ça ne fait rien d'utile
Point& Point::operator=(const Point& other) {
(void)other;
return *this;
}

3. Mauvaise formule de produit vectoriel

// FAUX - mauvais ordre des opérations
(p2.getX() - p1.getX()) * (p2.getY() - p1.getY())
// JUSTE
(p2.getX() - p1.getX()) * (p3.getY() - p1.getY())
- (p2.getY() - p1.getY()) * (p3.getX() - p1.getX())
int main() {
// Triangle avec sommets à (0,0), (10,0), (5,10)
Point a(0.0f, 0.0f);
Point b(10.0f, 0.0f);
Point c(5.0f, 10.0f);
// Cas de test
Point inside(5.0f, 3.0f); // À l'intérieur
Point onEdge(5.0f, 0.0f); // Sur l'arête AB
Point onVertex(0.0f, 0.0f); // Sur le sommet A
Point outside(15.0f, 5.0f); // À l'extérieur
std::cout << "Intérieur : " << bsp(a, b, c, inside) << std::endl; // 1
std::cout << "Sur arête : " << bsp(a, b, c, onEdge) << std::endl; // 0
std::cout << "Sur sommet : " << bsp(a, b, c, onVertex) << std::endl; // 0
std::cout << "Extérieur : " << bsp(a, b, c, outside) << std::endl; // 0
return 0;
}
bsp.cpp
#include "Point.hpp"
static Fixed crossProduct(Point const& p1, Point const& p2, Point const& p3) {
return (p2.getX() - p1.getX()) * (p3.getY() - p1.getY())
- (p2.getY() - p1.getY()) * (p3.getX() - p1.getX());
}
bool bsp(Point const a, Point const b, Point const c, Point const point) {
Fixed d1 = crossProduct(a, b, point);
Fixed d2 = crossProduct(b, c, point);
Fixed d3 = crossProduct(c, a, point);
if (d1 == Fixed(0) || d2 == Fixed(0) || d3 == Fixed(0))
return false;
bool allNegative = (d1 < Fixed(0)) && (d2 < Fixed(0)) && (d3 < Fixed(0));
bool allPositive = (d1 > Fixed(0)) && (d2 > Fixed(0)) && (d3 > Fixed(0));
return allNegative || allPositive;
}

class ClassName {
public:
ClassName(); // Constructeur par défaut
ClassName(const ClassName& other); // Constructeur de copie
ClassName& operator=(const ClassName& other); // Opérateur d'assignation
~ClassName(); // Destructeur
};
OpérateurMembre ?Signature
+, -, *, /OuiT operator+(const T&) const
==, !=, <, >Ouibool operator==(const T&) const
++, -- (pré)OuiT& operator++()
++, -- (post)OuiT operator++(int)
<<Nonostream& operator<<(ostream&, const T&)
// 8 bits fractionnaires
int_vers_fixed : value << 8
fixed_vers_int : value >> 8
float_vers_fixed : roundf(value * 256)
fixed_vers_float : value / 256.0f

Continuez votre parcours C++ :

Visitez le Glossaire pour les définitions de :