Le carnet On est
Séance 3 · encre bordeaux

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.

~ 50 min Source : Cours3 Surcharge + TP3 13 flashcards · 8 QCM

📄 Ressources du cours

📥 Télécharger les slides  ·  🔗 Ouvrir dans un nouvel onglet  ·  📝 Ouvrir le TD/TP

🎥 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
Idée Le C++ autorise à redéfinir le comportement des opérateurs (+, ==, <<…) 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

FamilleOpérateurs
Arithmétiques+   -   *   /
Comparaison==   !=   <   >   <=   >=
Raccourcis (composés)+=   -=   *=   /=
Flux<<   >>
Affectation=
Indexation, appel, accès[]   ()   ->
Incrément / décrément++   --
📌 Règle d'or
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;
}
⚠ Problème Les attributs 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)
Un seul argument en méthode Quand l'opérateur est une méthode, l'opérande gauche est *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;
}
🔑 Scott Meyers
« 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
Conversion implicite L'écriture 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)
⚠ Pourquoi 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)
Règle pratique
  • 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 flux ostream qu'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
🔑 Pourquoi 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); }
📌 DRY (don't repeat yourself)
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

Comment le compilateur traduit-il a == b ?
Par un appel de fonction nommée operator==. Soit a.operator==(b) (méthode), soit operator==(a, b) (fonction libre).
Combien d'arguments pour operator== en méthode membre ?
Un seul — l'opérande gauche est *this (implicite). En fonction libre, il en prend deux.
Quelles 4 familles d'opérateurs cite le cours ?
① Arithmétiques (+ - * /) · ② Comparaison (== != < > <= >=) · ③ Raccourcis (+= -= *= /=) · ④ Flux (<< >>).
Qu'est-ce qu'une fonction friend ?
Une fonction non-membre à qui la classe autorise l'accès à ses attributs privés et protégés. Déclarée avec friend à l'intérieur du class { }.
Citation de Scott Meyers sur les fonctions amies ?
« Whenever you can avoid friend functions, you should ». Préférer la méthode membre quand c'est possible.
Quand operator doit-il être membre ?
Quand il modifie l'objet courant : =, +=, -=, *=, /=, ++, --, [], ().
Quand operator doit-il être non-membre ?
Quand il est symétrique et ne modifie pas l'objet (+, -, *, ==, !=, <…) ou quand l'opérande gauche n'est pas du type de la classe (<<, >>).
Pourquoi 2 * f1 ne compile pas si operator* est une méthode membre ?
L'opérande gauche est int. Le compilateur cherche int::operator*(Fraction) — inexistant. Une méthode membre impose que this soit du type de la classe.
Solution pour que 2 * f1 ET f1 * 2 compilent ?
Déclarer 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.
Signature canonique de 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.
Pourquoi operator<< renvoie-t-il ostream& et non void ?
Pour permettre le chaînage cout << a << b. Le résultat de cout << a doit être cout pour que le second << ait un flux à utiliser.
L'amitié est-elle transitive ? héritée ? réciproque ?
Aucun des trois. Pas transitive (ami d'un ami ≠ ami), pas héritée (sous-classe d'un ami ≠ ami), pas réciproque (A ami de B n'implique pas B ami de A).
Pour 6 opérateurs de comparaison, combien d'algorithmes à écrire ?
Deux suffisent : operator< et operator==. Les quatre autres (!=, >, <=, >=) se déduisent par combinaison booléenne.

✎ Quiz éclair

1.Combien d'arguments pour operator+ en méthode membre ?
  • 0 — tout est implicite
  • 1 — l'opérande droite (le gauche est *this)
  • 2 — les deux opérandes
  • 3 — les deux opérandes plus le retour
En méthode membre, l'opérande gauche est *this, on ne passe que celui de droite.
2.Pourquoi écrire operator<< en fonction libre et pas en méthode ?
  • Performance — la fonction libre est plus rapide
  • Par convention seulement
  • L'opérande gauche est un ostream, pas notre classe — méthode impossible
  • Pour pouvoir utiliser endl
Une méthode membre exige que this soit du type de la classe. Or pour cout << obj, le récepteur du << est cout (un ostream).
3.Avec operator* en méthode membre, laquelle de ces expressions ne compile pas ?
  • f1 * f2
  • f1 * 2
  • f1.operator*(f2)
  • 2 * f1
L'opérande gauche est int, le compilateur ne peut pas appeler Fraction::operator* dessus. Solution : surcharger en fonction libre.
4.Une fonction libre peut-elle accéder aux private sans déclaration spéciale ?
  • Non — il faut soit des accesseurs publics, soit friend
  • Oui, toujours
  • Oui si elle est dans le même fichier
  • Oui si elle s'appelle operator…
L'encapsulation s'applique strictement. friend est le mot-clé qui ouvre la porte ; sinon il faut passer par des getters publics.
5.Pour la classe Date, le minimum d'opérateurs à écrire pour avoir les 6 comparaisons est :
  • 1 (par exemple compareTo)
  • 6 — un par opérateur
  • 2 — < et == suffisent, les autres se déduisent
  • 3 — <, >, ==
Avec < et == on déduit !=, >, <=, >= par combinaison logique.
6.ostream& operator<<(ostream& out, const T& x) renvoie out par référence pour :
  • Éviter une fuite mémoire
  • Permettre le chaînage cout << a << b
  • Économiser la pile
  • Initialiser endl
Retourner cout par référence permet à << b de s'appliquer sur le résultat de cout << a.
7.Quelle signature pour operator*= sur une classe Fraction ?
  • Fraction operator*=(Fraction a, Fraction b);
  • static void operator*=(Fraction& autre);
  • void Fraction::operator*=(const Fraction& autre);
  • Fraction Fraction::operator*=(const Fraction& autre) const;
*= modifie l'objet courant : méthode membre, non const, prenant l'autre opérande en référence constante.
8.L'amitié en C++ est :
  • Ni transitive, ni héritée, ni réciproque
  • Toujours réciproque
  • Héritée par les sous-classes
  • Transitive : les amis de mes amis sont mes amis
Le PDF résume : « Je ne fais pas confiance aux enfants de mes amis, ni aux amis de mes amis ; et mon amitié n'est pas réciproque ».

Score : 0 / 8 ·