Module 02 : Surcharge d'opérateurs et Forme Canonique Orthodoxe
Concepts clés :
- Forme Canonique Orthodoxe (OCF)
- Constructeur de copie
- Opérateur d’assignation par copie
- Surcharge d’opérateurs
- Nombres à virgule fixe
Pourquoi c’est important
Section intitulée « Pourquoi c’est important »Ce module introduit deux concepts critiques : l’OCF (Forme Canonique Orthodoxe) et la surcharge d’opérateurs.
Pourquoi l’OCF ?
Section intitulée « Pourquoi l’OCF ? »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.
Pourquoi la surcharge d’opérateurs ?
Section intitulée « Pourquoi la surcharge d’opérateurs ? »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.
Pourquoi la surcharge de fonctions est importante
Section intitulée « Pourquoi la surcharge de fonctions est importante »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érentsint 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 bonneint 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.
Comprendre les opérateurs de ce module
Section intitulée « Comprendre les opérateurs de ce module »Ce module vous apprend à définir un comportement personnalisé pour les opérateurs. Voici ce que la nouvelle syntaxe signifie :
L’opérateur = (Assignation)
Section intitulée « L’opérateur = (Assignation) »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&otherobtient l’adresse mémoire de l’objetotherthisest un pointeur vers l’objet courantthis != &othercompare 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 !)*thisdéréférence le pointeurthispour obtenir l’objet réel- Les opérateurs d’assignation retournent
*thispour pouvoir chaîner les assignations :a = b = c; - Sans le
*, vous retourneriez un pointeur, ce qui ne permettrait pas le chaînage
Surcharge d’opérateurs : La syntaxe
Section intitulée « Surcharge d’opérateurs : La syntaxe »// 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 eststd::ostream/std::istream(pas votre classe)
Les opérateurs ++ et --
Section intitulée « Les opérateurs ++ et -- »// Pré-incrémentation : ++aFixed& 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 !)<< 8multiplie par 2^8 (256) - décale les bits vers la gauche>> 8divise 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 !
Constructeur de copie avec & (Référence const)
Section intitulée « Constructeur de copie avec & (Référence const) »Fixed(const Fixed& other); // Constructeur de copie&après le type signifie “référence vers”constsignifie “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 !)
1. Forme Canonique Orthodoxe (OCF)
Section intitulée « 1. Forme Canonique Orthodoxe (OCF) »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.
Les quatre membres requis
Section intitulée « Les quatre membres requis »À 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};Diagramme du pattern OCF
Section intitulée « Diagramme du pattern OCF »┌─────────────────────────────────────────────────────────────────┐│ 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.
Pourquoi l’OCF est important
Section intitulée « Pourquoi l’OCF est important »Sans OCF correct, les classes avec mémoire dynamique auront des bugs :
// MAUVAIS : Pas de constructeur de copie ou opérateur d'assignationclass 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 !2. Constructeur de copie
Section intitulée « 2. Constructeur de copie »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); }};Quand il est appelé
Section intitulée « Quand il est appelé »Sample a;Sample b(a); // Constructeur de copieSample 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 vs profonde
Section intitulée « Copie superficielle vs profonde »// 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 }};3. Opérateur d’assignation par copie
Section intitulée « 3. Opérateur d’assignation par copie »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; }};Quand il est appelé
Section intitulée « Quand il est appelé »Sample a;Sample b;b = a; // Opérateur d'assignation (b existe déjà)La vérification d’auto-assignation
Section intitulée « La vérification d’auto-assignation »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é !4. Exemple OCF complet
Section intitulée « 4. Exemple OCF complet »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; }};5. Surcharge d’opérateurs
Section intitulée « 5. Surcharge d’opérateurs »Qu’est-ce que la surcharge d’opérateurs ?
Section intitulée « Qu’est-ce que la surcharge d’opérateurs ? »Définir un comportement personnalisé pour les opérateurs (+, -, *, /, ==, <, <<, etc.) quand ils sont utilisés avec votre classe.
// Comme fonction membreReturnType operator@(parameters);
// Comme fonction non-membre (friend ou utilise l'interface publique)ReturnType operator@(LeftType, RightType);Opérateurs de comparaison
Section intitulée « Opérateurs de comparaison »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; }};Opérateurs arithmétiques
Section intitulée « Opérateurs arithmétiques »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; }};Opérateurs d’incrémentation/décrémentation
Section intitulée « Opérateurs d’incrémentation/décrémentation »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; }};Types de retour pré vs post incrémentation
Section intitulée « Types de retour pré vs post incrémentation »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.
Opérateur d’insertion de flux (<<)
Section intitulée « Opérateur d’insertion de flux (<<) »// Doit être une fonction non-membre (ostream est à gauche)std::ostream& operator<<(std::ostream& os, const Fixed& fixed) { os << fixed.toFloat(); return os;}
// UtilisationFixed a(42.42f);std::cout << a << std::endl; // Affiche : 42.42196. Nombres à virgule fixe
Section intitulée « 6. Nombres à virgule fixe »Que sont les nombres à virgule fixe ?
Section intitulée « Que sont les nombres à virgule fixe ? »Une façon de représenter des nombres décimaux en utilisant des entiers, avec un nombre fixe de bits pour la partie fractionnaire.
Exemple : 8 bits fractionnaires
Section intitulée « Exemple : 8 bits fractionnaires »Entier : 42Binaire : 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.0fImplémentation
Section intitulée « Implémentation »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); }};Pourquoi la virgule fixe ?
Section intitulée « Pourquoi la virgule fixe ? »- 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
7. Fonctions membres statiques
Section intitulée « 7. Fonctions membres statiques »Fonctions min et max
Section intitulée « Fonctions min et max »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; }};
// UtilisationFixed a(2.0f), b(3.0f);std::cout << Fixed::max(a, b) << std::endl; // 3Exercice 00 : Forme Canonique Orthodoxe
Section intitulée « Exercice 00 : Forme Canonique Orthodoxe »Analyse du sujet
Section intitulée « Analyse du sujet »Créez une classe Fixed qui représente des nombres à virgule fixe avec ces quatre membres requis (OCF) :
- Constructeur par défaut - Crée un Fixed avec valeur 0
- Constructeur de copie - Crée un Fixed depuis un autre Fixed
- Opérateur d’assignation par copie - Assigne un Fixed à un autre
- Destructeur - Nettoie (affiche un message pour cet exercice)
Plus des getters/setters basiques pour la valeur raw bits.
Stratégie d’approche
Section intitulée « Stratégie d’approche »É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.
Construction progressive du code
Section intitulée « Construction progressive du code »Étape 1 - Déclaration de la classe :
#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 :
#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;}Explication ligne par ligne
Section intitulée « Explication ligne par ligne »| Ligne | Code | Pourquoi |
|---|---|---|
Fixed(const Fixed& other) | Signature constructeur copie | Prend référence const vers la source |
: _rawValue(other._rawValue) | Liste d’initialisation | Copie la valeur directement |
Fixed& operator=(...) | Assignation retourne référence | Active le chaînage : a = b = c |
if (this != &other) | Vérif auto-assignation | Évite que a = a casse |
return *this | Retourne cet objet | Active le chaînage |
static const int | Constante de classe | Partagée par toutes les instances |
Pièges courants
Section intitulée « Pièges courants »1. Confondre constructeur de copie et assignation
Fixed a;Fixed b(a); // Constructeur de copie - b est en cours de CRÉATIONFixed 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 = aFixed& operator=(const Fixed& other) { _rawValue = other._rawValue; return *this;}
// JUSTE - sûr pour l'auto-assignationFixed& 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 assignationsvoid operator=(const Fixed& other) { _rawValue = other._rawValue;}
// JUSTE - active a = b = cFixed& operator=(const Fixed& other) { // ... return *this;}Conseils de test
Section intitulée « Conseils de test »# Compiler et exécuterc++ -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)Code final
Section intitulée « Code final »#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);};
#endifExercice 01 : Vers une virgule fixe plus utile
Section intitulée « Exercice 01 : Vers une virgule fixe plus utile »Analyse du sujet
Section intitulée « Analyse du sujet »É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
Stratégie d’approche
Section intitulée « Stratégie d’approche »É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 fixe42.42 * 256 = 10859.52 → arrondi à 1086010860 / 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.
Construction progressive du code
Section intitulée « Construction progressive du code »Étape 1 - Constructeurs de conversion :
// Depuis int : décale à gauche par fractional bitsFixed::Fixed(const int value) : _rawValue(value << _fractionalBits) { std::cout << "Constructeur Int appelé" << std::endl;}
// Depuis float : multiplie par 2^8 et arronditFixed::Fixed(const float value) : _rawValue(roundf(value * (1 << _fractionalBits))) { std::cout << "Constructeur Float appelé" << std::endl;}Étape 2 - Fonctions de conversion :
// Vers int : décale à droite (perd la partie fractionnaire)int Fixed::toInt() const { return _rawValue >> _fractionalBits;}
// Vers float : divise par 2^8float Fixed::toFloat() const { return (float)_rawValue / (1 << _fractionalBits);}Étape 3 - Opérateur de flux :
// Fonction non-membrestd::ostream& operator<<(std::ostream& os, const Fixed& fixed) { os << fixed.toFloat(); return os;}Explication ligne par ligne
Section intitulée « Explication ligne par ligne »| Ligne | Code | Pourquoi |
|---|---|---|
value << _fractionalBits | Décale gauche 8 bits | Équivalent à value * 256 |
roundf(value * ...) | Arrondit au plus proche | Conversion float nécessite arrondi |
1 << _fractionalBits | Égal à 256 | 2^8 = 256 |
_rawValue >> _fractionalBits | Décale droite 8 bits | Équivalent à division entière par 256 |
(float)_rawValue / ... | Cast en float d’abord | Évite division entière ! |
return os | Retourne le flux | Active le chaînage : cout << a << b |
Pièges courants
Section intitulée « Pièges courants »1. Oublier roundf() pour la conversion float
// FAUX - tronque, perd de la précisionFixed::Fixed(const float value) : _rawValue(value * (1 << _fractionalBits)) {}
// JUSTE - arrondit au plus procheFixed::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'abordfloat 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-membrestd::ostream& operator<<(std::ostream& os, const Fixed& fixed);Conseils de test
Section intitulée « Conseils de test »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;}Code final
Section intitulée « Code final »#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);
#endifExercice 02 : Maintenant on parle
Section intitulée « Exercice 02 : Maintenant on parle »Analyse du sujet
Section intitulée « Analyse du sujet »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
Stratégie d’approche
Section intitulée « Stratégie d’approche »É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.
Construction progressive du code
Section intitulée « Construction progressive du code »Étape 1 - Opérateurs de comparaison :
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 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 :
// Pré-incrémentation : ++aFixed& 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 : --aFixed& 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& 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;}Explication ligne par ligne
Section intitulée « Explication ligne par ligne »| Ligne | Code | Pourquoi |
|---|---|---|
operator++(int) | Signature post-incrémentation | Paramètre int distingue de pré |
Fixed temp(*this) | Sauve valeur courante | Post-incrémentation retourne ancienne valeur |
return *this | Retourne référence | Pré-incrémentation retourne objet modifié |
return temp | Retourne copie | Post-incrémentation retourne copie sauvée |
static Fixed& min(...) | Fonction statique | Appelée comme Fixed::min(a, b) |
| Deux surcharges min | Pour const/non-const | Préserve la const-correctness |
Pièges courants
Section intitulée « Pièges courants »1. Mauvais types de retour pour pré/post incrémentation
// FAUX - post devrait retourner copie, pas référenceFixed& operator++(int) { // ...}
// JUSTEFixed& operator++(); // Pré retourne référenceFixed operator++(int); // Post retourne copie2. 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-conststatic const Fixed& min(const Fixed& a, const Fixed& b); // Pour constConseils de test
Section intitulée « Conseils de test »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;}Code final
Section intitulée « Code final »// Comparaisonbool 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étiqueFixed 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émentationFixed& operator++();Fixed operator++(int);Fixed& operator--();Fixed operator--(int);
// min/max statiquesstatic 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);Exercice 03 : BSP (Binary Space Partitioning)
Section intitulée « Exercice 03 : BSP (Binary Space Partitioning) »Analyse du sujet
Section intitulée « Analyse du sujet »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
truesi le point est strictement à l’intérieur du triangle - Retourne
falsesi le point est sur un bord ou un sommet
Vous devez aussi créer une classe Point avec des attributs x et y const.
Stratégie d’approche
Section intitulée « Stratégie d’approche »Étape 1 - Comprendre l’algorithme (Méthode du produit vectoriel)
Pour chaque arête du triangle :
- Calculer le produit vectoriel de (vecteur arête) x (vecteur point-vers-sommet)
- Si les trois produits vectoriels ont le même signe, le point est à l’intérieur
- 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)Construction progressive du code
Section intitulée « Construction progressive du code »Étape 1 - Classe Point :
#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 :
#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 rienPoint& 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 :
#include "Point.hpp"
// Le produit vectoriel nous dit de quel côté d'une ligne un point se trouvestatic 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;}Explication ligne par ligne
Section intitulée « Explication ligne par ligne »| Ligne | Code | Pourquoi |
|---|---|---|
Fixed const _x | Membre const | Coordonnée X ne peut pas changer |
(void)other | Supprime warning | L’assignation ne peut rien faire d’utile |
crossProduct(a, b, point) | Arête AB au point | Le signe nous dit quel côté |
d1 == Fixed(0) | Vérif sur arête | Produit zéro = colinéaire = sur l’arête |
allNegative || allPositive | Vérif intérieur | Même signe = même côté de toutes les arêtes |
Pièges courants
Section intitulée « Pièges courants »1. Retourner true pour les points sur les arêtes
// FAUX - le sujet dit arête/sommet = falseif (d1 == Fixed(0)) return true;
// JUSTEif (d1 == Fixed(0)) return false;2. Essayer de modifier les membres const dans l’assignation
// FAUX - ne compilera pas, _x et _y sont constPoint& Point::operator=(const Point& other) { _x = other._x; // Erreur ! _y = other._y; // Erreur ! return *this;}
// JUSTE - reconnaître que ça ne fait rien d'utilePoint& 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())Conseils de test
Section intitulée « Conseils de test »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;}Code final
Section intitulée « Code final »#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;}Référence rapide
Section intitulée « Référence rapide »Template OCF
Section intitulée « Template OCF »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};Surcharge d’opérateurs
Section intitulée « Surcharge d’opérateurs »| Opérateur | Membre ? | Signature |
|---|---|---|
+, -, *, / | Oui | T operator+(const T&) const |
==, !=, <, > | Oui | bool operator==(const T&) const |
++, -- (pré) | Oui | T& operator++() |
++, -- (post) | Oui | T operator++(int) |
<< | Non | ostream& operator<<(ostream&, const T&) |
Conversions virgule fixe
Section intitulée « Conversions virgule fixe »// 8 bits fractionnairesint_vers_fixed : value << 8fixed_vers_int : value >> 8float_vers_fixed : roundf(value * 256)fixed_vers_float : value / 256.0fConcepts connexes
Section intitulée « Concepts connexes »Continuez votre parcours C++ :
- Précédent : Module 01 : Mémoire & Références - Revoir les pointeurs et références
- Suivant : Module 03 : Héritage - Apprendre à construire des hiérarchies de classes
Termes clés de ce module
Section intitulée « Termes clés de ce module »Visitez le Glossaire pour les définitions de :