Surcharge des opérateurs
Comment écrire z = z1 + z2 sur ses propres types : opérateurs arithmétiques, de comparaison, raccourcis, et le flux <<. Membre, non-membre, ami.
📄 Ressources du cours
🎥 Vidéos recommandées
-
🇫🇷
Cours de C++: Surcharge des operateurs - الدارجة المغربية
LearnToCode - الدارجة المغربية · 30:20 · 19k vues — ،السلام عليكم ورحمة الله تعالى وبركاتهDans cette vidéo, on va découvrir la notion de la surcharge des opérateurs qui est très utilisable dans la programmatio...
-
🇫🇷
CSharp #14 - surcharge d'opérateurs
FormationVidéo · 20:39 · 8k vues — Surcharger des opérateurs permet de les employer pour les objets des classes et structures que vous avez définies dans vos programmes en C#.➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖...
-
🇬🇧
C++ Operator Overloading beginner to advanced (in-depth explanation)
CodeBeauty · 35:19 · 252k vues — 📚 Learn how to solve problems and build projects with these Free E-Books ⬇️C++ Lambdas e-book - free download here: https://bit.ly/freeCppE-BookEntire Objec
1. Motivation — pourquoi surcharger ?
Soit la classe Complexe du cours précédent :
// complexe.h
class Complexe {
public:
Complexe();
Complexe(int r);
Complexe(int r, int i);
~Complexe();
private:
int reel{0};
int imaginaire{0};
};
On veut additionner deux complexes. Plusieurs façons d'écrire la même opération :
Complexe z, z1, z2;
z = additionner(z1, z2); // fonction libre — fonctionne mais peu lisible
z = z1.additionner(z2); // méthode — mieux, mais verbeux
z = operator+(z1, z2); // appel direct — équivalent à z1 + z2
z = z1.operator+(z2); // idem version méthode
z = z1 + z2; // ★ idéal — naturel comme pour les int
+, ==, <<…) pour ses propres types. Le compilateur traduit z1 + z2 par un appel à une fonction nommée operator+.
Exemple concret de cette traduction :
if (a == b) { // ce qu'on écrit
cout << "égaux !";
}
// ce que le compilateur appelle réellement :
if (operator==(a, b)) {
cout << "égaux !";
}
2. Catalogue des opérateurs surchargeables
| Famille | Opérateurs |
|---|---|
| Arithmétiques | + - * / |
| Comparaison | == != < > <= >= |
| Raccourcis (composés) | += -= *= /= |
| Flux | << >> |
| Affectation | = |
| Indexation, appel, accès | [] () -> |
| Incrément / décrément | ++ -- |
La surcharge sert à imiter le comportement attendu de l'opérateur, pas à le détourner.
+ doit additionner, == doit comparer. Sinon le code devient illisible.
3. Comparer deux complexes : trois façons
Pour utiliser z1 == z2, il faut une fonction nommée précisément operator==. Première tentative en fonction libre :
// fonction normale, en dehors de toute classe
bool operator==(const Complexe& a, const Complexe& b) {
if (a.reel == b.reel && a.imaginaire == b.imaginaire)
return true;
else
return false;
}
reel et imaginaire sont privés. Une fonction libre n'a pas le droit d'y accéder. Le code ci-dessus ne compile pas tel quel.
Le PDF présente quatre solutions à ce problème. On garde les trois principales.
Solution A — passer par des accesseurs
On expose getReel() et getImag() dans la classe, puis on les appelle depuis la fonction libre :
// complexe.h
class Complexe {
public:
Complexe(int r = 0, int i = 0);
int getReel() const;
int getImag() const;
private:
int reel, imaginaire;
};
// main.cpp — fonction libre
bool operator==(const Complexe& a, const Complexe& b) {
return a.getReel() == b.getReel() && a.getImag() == b.getImag();
}
La prof note : « ce n'est pas très élégant ». On multiplie les accesseurs juste pour faire passer l'opérateur.
Solution B — méthode de la classe
On déclare operator== à l'intérieur de la classe. Il devient une méthode, donc accède directement aux attributs privés via this.
// complexe.h
class Complexe {
public:
Complexe(int r = 0, int i = 0);
bool operator==(const Complexe& a) const; // un seul argument !
private:
int reel, imaginaire;
};
// complexe.cpp
bool Complexe::operator==(const Complexe& a) const {
return reel == a.reel && imaginaire == a.imaginaire;
}
// utilisation
Complexe z1, z2;
if (z1 == z2) ... // équivaut à z1.operator==(z2)
*this (implicite). Donc operator== ne prend qu'un argument (celui de droite). En fonction libre, il en prend deux.
Solution C — fonction amie (friend)
On garde la fonction libre mais on lui donne le droit d'accéder aux attributs privés en la déclarant amie dans la classe :
// complexe.h
class Complexe {
public:
Complexe(int r = 0, int i = 0);
friend bool operator==(const Complexe&, const Complexe&);
private:
int reel, imaginaire;
};
// main.cpp — implémentation en dehors de la classe
bool operator==(const Complexe& z1, const Complexe& z2) {
return z1.reel == z2.reel && z1.imaginaire == z2.imaginaire;
}
« Whenever you can avoid friend functions, you should, because, much as in real life, friends are often more trouble than they're worth. »
Traduction : si tu peux faire la méthode membre, fais-la membre.
friend en dernier recours.
4. Opérateurs raccourcis — *=, +=…
Les opérateurs raccourcis modifient l'objet courant. Naturellement une méthode membre : this est l'objet à modifier, l'argument est la valeur à combiner.
// fraction.h
class Fraction {
private:
int numerateur{0};
int denominateur{1};
public:
Fraction();
Fraction(int n);
Fraction(int n, int d);
~Fraction();
void operator*=(const Fraction& autre);
};
// fraction.cpp
void Fraction::operator*=(const Fraction& autre) {
numerateur *= autre.numerateur;
denominateur *= autre.denominateur;
}
Utilisation :
Fraction f1(4, 5);
Fraction f2(3, 11);
f1 *= f2; // équivaut à f1.operator*=(f2) — f1 vaut maintenant 12/55
f1 *= 2; // conversion implicite via Fraction(int) — f1 vaut 24/55
f1 *= 2 fonctionne car la classe a un constructeur Fraction(int n). Le compilateur construit une Fraction(2) temporaire pour appeler operator*=.
5. Membre vs non-membre — l'asymétrie
En méthode membre — opérande gauche obligatoirement du bon type
// méthode membre — opérande gauche = *this
Fraction Fraction::operator*(const Fraction& autre) const {
return Fraction(numerateur * autre.numerateur,
denominateur * autre.denominateur);
}
Fraction f1(1, 8), f2(1, 2);
Fraction res = f1 * f2; // OK — f1.operator*(f2)
res = f1 * 2; // OK — f1.operator*(Fraction(2))
res = 2 * f1; // ✗ ERREUR — il faudrait 2.operator*(f1)
2 * f1 ne compile pas ?
L'opérande gauche est un int. Le compilateur cherche int::operator*(Fraction) qui n'existe pas. La méthode membre Fraction::operator* ne peut pas être déclenchée car this serait l'int.
En fonction libre — symétrie restaurée
Solution : déclarer operator* hors de la classe (avec friend ou accesseurs). On parle de mixed-mode arithmetic.
// fonction libre (non-membre)
Fraction operator*(const Fraction& lho, const Fraction& rho) {
return Fraction(lho.getNumerateur() * rho.getNumerateur(),
lho.getDenominateur() * rho.getDenominateur());
}
res = f1 * 2; // OK — operator*(f1, Fraction(2))
res = 2 * f1; // OK — operator*(Fraction(2), f1)
- Opérateurs qui modifient l'objet courant (
=,+=,-=,++) → méthode membre - Opérateurs symétriques qui ne modifient rien (
+,-,*,==,!=) → fonction libre (avec friend si besoin) <<et>>toujours non-membres — l'opérande gauche est un fluxostreamqu'on ne contrôle pas
6. Surcharger << — afficher un objet
Pour écrire cout << f, on doit surcharger operator<<. Forcément non-membre (l'opérande gauche est un ostream). Et déclaré friend pour accéder aux attributs privés.
// fraction.h
class Fraction {
private:
int numerateur, denominateur;
public:
friend ostream& operator<<(ostream& out, const Fraction& f);
};
// fraction.cpp (ou main.cpp)
ostream& operator<<(ostream& out, const Fraction& f) {
out << "Fraction : " << f.numerateur << "/" << f.denominateur;
return out;
}
// utilisation
Fraction f1(5, 7), f2;
cout << f1 << endl << f2; // chaînage grâce au return out
return out; ?Pour permettre le chaînage.
cout << f1 << endl est évalué de gauche à droite : (cout << f1) doit renvoyer cout (par référence) pour que le second << puisse s'appliquer dessus.
Pourquoi ostream& et pas ostream ?
Un ostream ne se copie pas. Passer par référence est obligatoire à la fois pour l'argument et pour le retour. Si on essayait de copier cout, ce serait un objet déconnecté du terminal.
7. Cas pratique : classe Date (TP3)
Énoncé du TP : surcharger les six opérateurs de comparaison ==, !=, <, >, <=, >= sur une classe Date contenant jour, mois, annee.
class Date {
private:
short jour;
short mois;
int annee;
public:
Date(short j, short m, int a);
~Date();
bool operator<(const Date& autre) const;
bool operator==(const Date& autre) const;
bool operator!=(const Date& autre) const;
bool operator>(const Date& autre) const;
bool operator<=(const Date& autre) const;
bool operator>=(const Date& autre) const;
};
Implémentations — réutiliser < et ==
bool Date::operator<(const Date& a) const {
if (annee != a.annee) return annee < a.annee;
if (mois != a.mois) return mois < a.mois;
return jour < a.jour;
}
bool Date::operator==(const Date& a) const {
return jour == a.jour && mois == a.mois && annee == a.annee;
}
// les quatre autres s'expriment à partir de < et ==
bool Date::operator!=(const Date& a) const { return !(*this == a); }
bool Date::operator> (const Date& a) const { return a < *this; }
bool Date::operator<=(const Date& a) const { return !(a < *this); }
bool Date::operator>=(const Date& a) const { return !(*this < a); }
On écrit la vraie logique seulement dans
operator< et operator==. Les quatre autres sont des combinaisons booléennes. Une seule source de vérité.
Question 4.2 — version hors classe
Le TP demande : « que constatez-vous lorsque vous surchargez operator< hors de la classe ? »
bool operator<(const Date& d1, const Date& d2) {
// pas d'accès à d1.jour, d1.mois, d1.annee — ils sont private
}
On constate qu'on n'a pas accès aux attributs privés. Il faut soit déclarer la fonction friend, soit utiliser des accesseurs publics. C'est le même piège que pour operator== dans la section 3.
★ Réviser le chapitre
Pour vérifier ta compréhension
Qu'est-ce que la surcharge de fonction ?
Définir plusieurs fonctions de même nom mais avec des signatures différentes (nombre ou type des paramètres). Le compilateur choisit la bonne version à l'appel selon les arguments. Exemple : void afficher(int), void afficher(double), void afficher(string).
Quelle est la différence entre surcharger un opérateur en méthode et en fonction libre ?
Méthode membre : l'opérande gauche doit être du type de la classe. Fonction libre (souvent friend) : aucune contrainte sur l'opérande gauche, ce qui permet par exemple 5 + obj ou la surcharge de << avec ostream à gauche.
Pourquoi operator<< doit-il prendre un ostream& et le retourner ?
Pour permettre le chaînage : cout << a << b << endl;. Si on retournait par valeur, on copierait le flux (interdit). La référence permet de propager le même flux à toutes les opérations.
À quoi sert le mot-clé friend ?
Accorder à une fonction libre (ou à une autre classe) l'accès aux membres private et protected d'une classe. C'est utile pour la surcharge d'opérateurs comme << qui doit accéder aux attributs internes mais ne peut pas être une méthode membre.
Comment surcharger correctement l'opérateur = ?
Signature : NomClasse& operator=(const NomClasse& autre). Étapes : (1) vérifier l'auto-affectation (if (this == &autre) return *this;), (2) libérer les anciennes ressources, (3) copier les nouvelles, (4) retourner *this. Permet le chaînage a = b = c.
🃏 Flashcards
a == b ?operator==. Soit a.operator==(b) (méthode), soit operator==(a, b) (fonction libre).operator== en méthode membre ?*this (implicite). En fonction libre, il en prend deux.+ - * /) · ② Comparaison (== != < > <= >=) · ③ Raccourcis (+= -= *= /=) · ④ Flux (<< >>).friend ?friend à l'intérieur du class { }.operator doit-il être membre ?=, +=, -=, *=, /=, ++, --, [], ().operator doit-il être non-membre ?+, -, *, ==, !=, <…) ou quand l'opérande gauche n'est pas du type de la classe (<<, >>).2 * f1 ne compile pas si operator* est une méthode membre ?int. Le compilateur cherche int::operator*(Fraction) — inexistant. Une méthode membre impose que this soit du type de la classe.2 * f1 ET f1 * 2 compilent ?operator* en fonction libre (non-membre). La conversion implicite via Fraction(int) fonctionne alors des deux côtés. C'est le mixed-mode arithmetic.operator<< pour afficher ?ostream& operator<<(ostream& out, const T& obj). Toujours non-membre, souvent friend. Retour par référence pour permettre le chaînage.operator<< renvoie-t-il ostream& et non void ?cout << a << b. Le résultat de cout << a doit être cout pour que le second << ait un flux à utiliser.operator< et operator==. Les quatre autres (!=, >, <=, >=) se déduisent par combinaison booléenne.✎ Quiz éclair
operator+ en méthode membre ?*this, on ne passe que celui de droite.operator<< en fonction libre et pas en méthode ?this soit du type de la classe. Or pour cout << obj, le récepteur du << est cout (un ostream).operator* en méthode membre, laquelle de ces expressions ne compile pas ?int, le compilateur ne peut pas appeler Fraction::operator* dessus. Solution : surcharger en fonction libre.private sans déclaration spéciale ?friend est le mot-clé qui ouvre la porte ; sinon il faut passer par des getters publics.Date, le minimum d'opérateurs à écrire pour avoir les 6 comparaisons est :< et == on déduit !=, >, <=, >= par combinaison logique.ostream& operator<<(ostream& out, const T& x) renvoie out par référence pour :cout par référence permet à << b de s'appliquer sur le résultat de cout << a.operator*= sur une classe Fraction ?*= modifie l'objet courant : méthode membre, non const, prenant l'autre opérande en référence constante.Score : 0 / 8 ·