• 50 heures
  • Difficile

Ce cours est visible gratuitement en ligne.

Ce cours existe en livre papier.

course.header.alt.is_certifying

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Mis à jour le 02/08/2019

Surchargez un opérateur

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

On l'a vu, le langage C++ propose beaucoup de fonctionnalités qui peuvent se révéler très utiles si on arrive à s'en servir correctement.

Une des fonctionnalités les plus étonnantes est « la surcharge des opérateurs », que nous allons étudier dans ce chapitre. C'est une technique qui permet de réaliser des opérations mathématiques intelligentes entre vos objets, lorsque vous utilisez dans votre code des symboles tels que +, -, *, ==, <, etc.

Au final, vous allez voir que votre code sera plus court et plus clair, et qu'il gagnera donc en lisibilité.

Petits préparatifs

Qu'est-ce que c'est ?

Le principe est très simple. Supposons que vous ayez créé une classe pour stocker une durée (par exemple 4h21m) et que vous ayez deux objets de typeDureeque vous voulez additionner pour connaître la durée totale.

En temps normal, il faudrait créer une fonction :

Duree resultat, duree1, duree2;

resultat = additionner(duree1, duree2);

La fonctionadditionnerréaliserait ici la somme deduree1etduree2, et stockerait la valeur ainsi obtenue dansresultat.
Cela fonctionne mais ce n'est pas franchement lisible. Ce que je vous propose dans ce chapitre, c'est de modifier la classeDureede sorte à être capables d'écrire cela :

Duree resultat, duree1, duree2;

resultat = duree1 + duree2;

En clair, on fait ici comme si les objets étaient de simples nombres. Mais comme un objet est bien plus complexe qu'un nombre (vous avez eu l'occasion de vous en rendre compte), il faut expliquer à l'ordinateur comment effectuer l'opération.

La classeDureepour nos exemples

Toutes les classes ne sont pas forcément adaptées à la surcharge d'opérateurs. Ainsi, additionner des objets de typePersonnageserait pour le moins inutile.
Nous allons donc changer d'exemple, ce sera l'occasion de vous aérer un peu l'esprit, sinon vous allez finir par croire que le C++ ne sert qu'à créer des RPG.

Cette classe Dureesera capable de stocker des heures, des minutes et des secondes. Rassurez-vous, c'est une classe relativement facile à écrire (plus facile que Personnageen tout cas !), cela ne devrait vous poser aucun problème si vous avez compris les chapitres précédents.

Duree.h
#ifndef DEF_DUREE
#define DEF_DUREE
 
class Duree
{
    public:
 
    Duree(int heures = 0, int minutes = 0, int secondes = 0);
 
    private:
 
    int m_heures;
    int m_minutes;
    int m_secondes;
};
 
#endif

Chaque objet de type Dureestockera un certain nombre d'heures, de minutes et de secondes.

Vous noterez que j'ai utilisé des valeurs par défaut au cas où l'utilisateur aurait la flemme de les préciser.
On pourra donc créer un objet de plusieurs façons différentes :

Duree chrono; // Pour stocker 0 heure, 0 minute et 0 seconde
Duree chrono(5); // Pour stocker 5 heures, 0 minute et 0 seconde
Duree chrono(5, 30); // Pour stocker 5 heures, 30 minutes et 0 seconde
Duree chrono(0, 12, 55); // Pour stocker 0 heure, 12 minutes et 55 secondes
Duree.cpp

L'implémentation de notre constructeur est expédiée en 30 secondes, montre en main.

#include "Duree.h"
 
Duree::Duree(int heures, int minutes, int secondes) : m_heures(heures), m_minutes(minutes), m_secondes(secondes)
{
}
Et dansmain.cpp?

Pour l'instant notre main.cppne déclare que deux objets de type Duree, que j'initialise un peu au hasard :

int main()
{
    Duree duree1(0, 10, 28), duree2(0, 15, 2);
 
    return 0;
}

Voilà, nous sommes maintenant prêts à affronter les surcharges d'opérateurs !

Les opérateurs de comparaison

Nous allons commencer par voir comment ajouter les opérateurs de comparaison (==, !=, <, >=,...) à notre classe. Ce sont généralement les plus simples à implémenter.
Commençons par l'opérateur de test d'égalité, le fameux ==.

Pour être capables d'utiliser le symbole « == » entre deux objets, vous devez créer une fonction ayant précisément pour nom operator==et dotée du prototype :

bool operator==(Objet const& a, Objet const& b);

La fonction reçoit deux références sur les objets (références constantes, qu'on ne peut donc pas modifier) à comparer et va renvoyer un booléen indiquant si les deux objets sont identiques ou non.
À coté de notre classe Duree, on doit donc rajouter cette fonction (ici dans le.h) :

bool operator==(Duree const& a, Duree const& b);

C'est la première fois que vous utilisez des références constantes. Au chapitre 7, je vous avais expliqué que, lors d'un passage par référence, la variable (ou l'objet) n'est pas copiée. Notre classe Dureecontient trois entiers, utiliser une référence permet donc d'éviter la copie inutile de ces trois entiers. Ici, le gain est assez négligeable mais, si vous prenez un objet de type string, qui peut contenir un texte très long, la copie prendra alors un temps important. C'est pour cela que, lorsque l'on manipule des objets, on préfère utiliser des références. Cependant, on aimerait bien que les fonctions ou méthodes ne modifient pas l'objet reçu. C'est pour cela que l'on utilise une référence constante.
Quand on effectue le test mathématique  $\(a == b\)$  ,  $\(a\)$   et  $\(b\)$  ne doivent pas être modifiés. Le mot-clé const est donc essentiel ici.

Mode d'utilisation

Comment marche ce truc ?

Dès le moment où vous avez créé cette fonction operator==, vous pouvez comparer deux objets de typeDuree:

if(duree1 == duree2)
{
     std::cout << "Les deux durees sont egales !" << std::endl;
}

De la même manière que l'on aurait pu écrire pour des entiers:

int a(3), b(5);
if(a == b)
{
     std::cout << "Les deux entiers sont egaux !" << std::endl;
}

Ce n'est pas de la magie. En fait le compilateur « traduit » cela par :

if(operator==(duree1, duree2))
{
     std::cout << "Les deux durees sont egales !" << std::endl;
}

C'est bien plus classique et compréhensible pour lui. C'est la nature du mot-clé operator: un opérateur se transforme en appel de fonction.
Le compilateur appelle donc la fonction operator==en passant en paramètres duree1et duree2. La fonction, elle, renvoie un résultat de type bool.

L'implémentation de l'opérateur==

Pour l'instant, nous avons juste défini l'opérateur de comparaison. Il nous faut donc encore explique au compilateur comment cette comparaison s'effectue. Dans notre cas c'est simple, deux Durees sont égales si elles contiennent le même nombre d'heures, de minutes et de secondes. Mais il y a des cas plus compliqués ! Pensez à la classePersonnage. Deux personnages sont-ils égaux si ils ont le même nom ? Si ils ont le même nombre de points de vie ? Si ils possèdent la même arme ? C'est au concepteur de la classe de choisir et de créer son opérateur de comparaison en conséquence.
Revenons à nos durées et attaquons l'écriture de notre opérateur. La solution la plus simple est de vérifier si les différents attributs de la classe sont égaux. Nous pourrions donc écrire:

bool operator==(Duree const& a, Duree const& b)
{
    //Teste si a.m_heure == b.m_heure etc.  
    if (a.m_heures == b.m_heures && a.m_minutes == b.m_minutes && a.m_secondes == b.m_secondes)
        return true;
    else
        return false;
}

On compare à chaque fois un attribut de l'objet dans lequel on se trouve avec un attribut de l'objet de référence (les heures avec les heures, les minutes avec les minutes…). Si ces 3 valeurs sont identiques, alors on peut considérer que les objets sont identiques et renvoyer true.

Sauf qu'il y a un petit souci : il nous faudrait lire les attributs des objetsaetb. Comme le veut la règle, ils sont privés et donc inaccessibles depuis l'extérieur de la classe.
Il existe trois solutions à ce problème:

  • Vous créez des accesseurs comme on l'a vu (ces fameuses méthodes getHeures(),getMinutes(), …). Cela marche bien mais c'est un peu ennuyeux à écrire.

  • Vous utilisez le concept d'amitié, que nous verrons dans un prochain chapitre.

  • Ou bien vous utilisez la technique que je vais vous montrer.

On opte ici pour la troisième solution (non, sans blague ?).

Le problème est que l'opérateur == est situé en-dehors de la classe (Ce n'est pas une méthode) et ne peut donc accéder aux attributs privés. Qu'à cela ne tienne, créons une méthode dans la classe qui fera la comparaison et demandons à notre opérateur d'appeler cette fonction.
On commence par créer une méthode estEgal()qui renvoie truesibest égal à l'objet dont on a appelé la méthode.

bool Duree::estEgal(Duree const& b) const
{
    //Teste si a.m_heure == b.m_heure etc.  
    if (m_heures == b.m_heures && m_minutes == b.m_minutes && m_secondes == b.m_secondes)
        return true;
    else
        return false;
}

ou (mieux) en version courte:

bool Duree::estEgal(Duree const& b) const
{
    return (m_heures == b.m_heures && m_minutes == b.m_minutes && m_secondes == b.m_secondes);     //Teste si a.m_heure == b.m_heure etc.  
}

Et on utilise cette méthode dans l'opérateur de comparaison :

bool operator==(Duree const& a, Duree const& b)
{
    return a.estEgal(b);
}

Dans le main(), on peut faire un simple test de comparaison pour vérifier que l'on a fait les choses correctement :

int main()
{
    Duree duree1(0, 10, 28), duree2(0, 10, 28);

    if (duree1 == duree2)
        cout << "Les durees sont identiques";
    else
        cout << "Les durees sont differentes";

    return 0;
}

Résultat :

Les durees sont identiques

Nous avons donc réussi ! Il nous est maintenant possible de comparer nos objets de type Duree de la même manière que nous comparions les entiers dans les chapitres précédents.
Ne nous arrêtons pas en si bon chemin. Il existe encore beaucoup d'autres opérateurs.

L'opérateur!=

Tester l'égalité, c'est bien mais parfois, on aime savoir si deux objets sont différents. On écrit alors un opérateur!=. Celui-là, il est très simple à écrire. Pour tester si deux objets sont différents, il suffit de tester s'ils ne sont pas égaux !

bool operator!=(Duree const& a, Duree const& b)
{
    if(a == b) //On utilise l'opérateur == qu'on a défini précédemment !
        return false; //S'ils sont égaux, alors ils ne sont pas différents
    else
        return true; //Et s'ils ne sont pas égaux, c'est qu'ils sont différents
}

Ou en version courte:

bool operator!=(Duree const& a, Duree const& b)
{
    return !(a==b); //On utilise l'opérateur == qu'on a défini précédemment !
}

Je vous avais dit que ce serait facile. Vous pouvez d'ailleurs noter un élément intéressant ici. L'opérateur != fait appel à l'opérateur ==. En réfléchissant une seule fois à la manière de comparer deux Duree, nous avons pu écrire deux opérateurs différents. Je vous invite d'ailleurs à le tester:

int main()
{
    Duree duree1(0, 10, 28), duree2(0, 10, 28);

    if (duree1 != duree2)
        cout << "Les durees sont differentes";
    else
        cout << "Les durees sont identiques";

    return 0;
}

Résultat :

Les durees sont identiques

Parfait, exactement ce que nous voulions.

L'opérateur<

Si l'opérateur==peut s'appliquer à la plupart des objets, il n'est pas certain que l'on puisse dire de tous nos objets lequel est le plus grand. Tous n'ont pas forcément une notion de grandeur. Prenez par exemple notre classePersonnage, il serait assez peu judicieux de vérifier si un Personnageest « inférieur » ou non à un autre (à moins que vous ne compariez les vies… à vous de voir).

En tout cas, avec la classe Duree, on a de la chance : il est facile et « logique » de vérifier si une Dureeest inférieure à une autre. Vous l'aurez certainement deviné, l'opérateur "est plus petit que" possède le prototype suivant:

bool operator<(Duree const &a, Duree const& b)

Comme dans les deux cas précédents, on a la séquence "type de retour - mot-clé operator - symbole de l'opérateur - arguments". Rien de sorcier.
A nouveau, nous sommes confrontés au problème d'accès aux attributs des objets. Et la solution est bien sûr identique: créer une méthode publique dans la classe et appeler cette méthode dans le corps de l'opérateur.

Voici mon implémentation pour l'opérateur<(« est strictement inférieur à ») :

bool operator<(Duree const &a, Duree const& b)
{
    return a.estPlusPetitQue(b);
}

Et la méthode estPlusPetitQue()de la classe Duree:

bool Duree::estPlusPetitQue(Duree const& b) const
{
    if (m_heures < b.m_heures)   // Si les heures sont différentes
        return true; 
    else if (m_heures == b.m_heures && m_minutes < b.m_minutes) //Si elles sont égales, on compare les minutes
        return true;
    else if (m_heures == b.m_heures && m_minutes == b.m_minutes && m_secondes < b.m_secondes) // Et si elles sont aussi égales, on compare les secondes
        return true;
    else              //Si tout est égal, alors l'objet n'est pas plus petit que b
        return false;
}

Avec un peu de réflexion, on finit par trouver cet algorithme ; il suffit d'activer un peu ses méninges.
Vous noterez que la méthode renvoie falsesi les durées sont identiques : c'est normal car il s'agit de l'opérateur<. En revanche, si ç'avait été la méthode de l'opérateur <= (« inférieur ou égal à »), il aurait fallu renvoyer true.

Je vous laisse le soin de tester dans le main()si cela fonctionne correctement:

int main()
{
    Duree duree1(0, 10, 28), duree2(4, 2, 13);

    if (duree1 < duree2)
        cout << "La premiere duree est plus petite";
    else
        cout << "La premiere duree n'est pas plus petite";

    return 0;
}

Faites des tests pour vérifier que ça fonctionne bien dans tous les cas.

Les autres opérateurs de comparaison

Il nous reste encore trois autres opérateurs de comparaison à écrire: >, <= et >=. Je ne vais pas tous les écrire ici, cela surchargerait inutilement. Mais comme pour!=et==, il suffit d'utiliser correctement<pour tous les implémenter. Je vous invite à essayer de les implémenter pour notre classe Duree, cela constituera un bon exercice sur le sujet. Pour vous aider, je vous donne quand même les prototypes:

  • bool operator>(Duree const &a, Duree const& b);

  • bool operator<=(Duree const &a, Duree const& b);

  • bool operator>=(Duree const &a, Duree const& b).

En réfléchissant un peu vous devriez être capable d'écrire tous ces opérateurs rien qu'en utilisant l'opérateur < que nous avons écris ensemble. Souvenez-vous par exemple que si $$a\leq b$$, alors "$$a$$ n'est pas plus grand que $$b$$". :p

Si vous avez un peu du mal à vous repérer dans le code, ce que je peux comprendre, je mets à votre disposition le projet complet avec les six opérateurs que nous venons de créer, dans unzip.

Télécharger les sources (2 Ko)

Les opérateurs arithmétiques

Passons maintenant aux opérateurs arithmétiques classiques, à savoir l'addition (+), la soustraction (-), la multiplication (*), la division (/) et le modulo (%).
Une fois que vous aurez appris à vous servir de l'un d'entre eux, vous verrez que vous saurez vous servir de tous les autres. Attaquons donc avec l'addition.

Appliquons les vieilles recettes

Au vu de ce que vous avez appris sur les opérateurs de comparaison, vous ne serez pas surpris de voir que l'opérateur d'addition se définit comme ceci:

Duree operator+(Duree const& a, Duree const& b);

Une fois que nous aurons écrit cette fonction, nous pourrons effectuer des additions de Duree comme si de rien était.

int main()
{
    Duree resultat, duree1, duree2;

    resultat = duree1 + duree2;

    return 0;
}

Réfléchissons un instant à ce que cet opérateur doit effectuer. Il doit créer un nouvel objet de type Dureedont la valeur des attributs est la somme des valeurs des attributs de aetb. C'est en tout cas ce que nous dit le prototype. Écrivons donc ceci dans le corps de l'opérateur.

Duree operator+(Duree const& a, Duree const& b)
{
    Duree resultat;
    // Calcul des valeurs des attributs de resultat
    return resultat;
}

Jusque-là tout va bien. Il ne nous reste plus qu'à effectuer l'addition proprement dite. C'est-à-dire prendre la valeur de m_heuresdes objets aet bpuis d'en effectuer la somme. Et le problème usuel se pose à nouveau: cet opérateur ne peut pas accéder aux attributs de aet deb.
Je vous vois déjà venir et proposer la même solution que précédemment: créer une méthode qui additionne deux Dureeet utiliser cette méthode dans notre opérateur, comme dans le cas de == ou de <. On aurait par exemple:

Duree operator+(Duree const& a, Duree const& b)
{
    Duree resultat;
    resultat = a.calculeAddition(b);   //Utilise une méthode de Duree pour effectuer l'addition
    return resultat;
}

Si vous avez pensé à faire ça, c'est bien. Cela veut dire que vous commencez à penser "orienté objet" et que vous commencez à maîtriser les notions d'encapsulation et de méthodes. :D
Cette manière de faire est correcte, mais n'est pas la meilleure manière de faire. :euh:
Ce que je vais vous présenter est un poil plus complexe mais est la manière de faire optimale, c'est-à-dire la plus efficace, qui garantit que vos opérateurs soient cohérents entre eux et aient la bonne sémantique.

Les opérateurs raccourcis

Avant de nous attaquer à nos problèmes d'accès aux attributs, revenons en arrière et jetons un œil aux différents opérateurs du C++. Si vous relisez le chapitre 5, vous verrez que nous y avions vu une autre série d'opérateurs que nous avions appelés "opérateurs raccourcis". Ainsi en parallèle de l'opérateur +, il y avait l'opérateur += qui effectue lui aussi des additions. Pour un entier, on pouvait écrire:

int a(2);
a += 4;
// a vaut maintenant 6

La différence est que cet opérateur change l'entier auquel il est appliqué, ce qui n'est pas le cas du + "normal" qui lui ne modifie pas ses deux opérandes. Imaginons un instant que vous vouliez écrire un tel opérateur pour votre classe Duree. Cet opérateur va devoir modifier l'objet qui l'utilise. Pour respecter l'encapsulation, il va donc falloir déclarer cet opérateur comme étant une méthode de la classe Dureeet pas comme une fonction externe.
Le prototype sera donc:

class Duree
{
// Le reste...
   void operator+=(Duree const& a);
};

Cette méthode ne renvoie rien et elle prend une autre Dureeen argument. Elle n'est pas déclarée constante puisqu'elle va modifier l'objet qui l'appelle.

D'ailleurs, comment on l'utilise cette méthode ?

Eh bien, comme dans le cas de == ou de +, le compilateur fera le remplacement nécessaire. On écrit donc

Duree a(2,12,43), b(0,34,17);
a += b;

ce que le compilateur traduira en

Duree a(2,12,43), b(0,34,17);
a.operator+=(b);

Magique, je vous dis ! :magicien:

Essayons d'écrire le corps de cette méthode. L'implémentation n'est pas vraiment compliquée mais il va quand même falloir réfléchir un peu. En effet, ajouter des secondes, minutes et heures cela va, mais il faut faire attention à la retenue si les secondes ou minutes dépassent $$60$$.
Je vous recommande d'essayer d'écrire la méthode vous-mêmes, c'est un excellent exercice algorithmique, cela entretient le cerveau (comme manger des légumes, oui, oui) et cela vous rend meilleur programmeur.

Voici ce que donne mon implémentation pour ceux qui ont besoin de la solution :

void Duree::operator+=(const Duree& a)
{
    //1 : ajout des secondes
    m_secondes += a.m_secondes;
    //Si le nombre de secondes dépasse 60, on rajoute des minutes
    //Et on met un nombre de secondes inférieur à 60
    m_minutes += m_secondes / 60;
    m_secondes %= 60;

    //2 : ajout des minutes
    m_minutes += a.m_minutes;
    //Si le nombre de minutes dépasse 60, on rajoute des heures
    //Et on met un nombre de minutes inférieur à 60
    m_heures += m_minutes / 60;
    m_minutes %= 60;

    //3 : ajout des heures
    m_heures += a.m_heures;
}

Ce n'est pas un algorithme ultra-complexe mais, comme je vous avais dit, il faut réfléchir un tout petit peu quand même pour pouvoir l'écrire.

Comme nous sommes dans une méthode de la classe, nous pouvons directement modifier les attributs. On y ajoute les heures, minutes et secondes de l'objet reçu en paramètre, à savoir a. On a ici exceptionnellement le droit d'accéder directement aux attributs de cet objet car on se trouve dans une méthode de la même classe. C'est un peu tordu mais cela nous aide bien (sinon il aurait fallu créer des méthodes comme getHeures()).

Rajouter les secondes, c'est facile. Mais on doit ensuite ajouter un reste si on a dépassé $$60$$ secondes (donc ajouter des minutes). Je ne vous explique pas comment cela fonctionne dans le détail, je vous laisse un peu vous remuer les méninges. Ce n'est vraiment pas bien difficile (c'est du niveau des tous premiers chapitres du cours). Vous noterez que c'est un cas où l'opérateur modulo (%), c'est-à-dire le reste de la division entière, est très utile.

Bref, on fait de même avec les minutes ; quant aux heures, c'est encore plus facile vu qu'il n'y a pas de reste (on peut dépasser les $$24$$ heures donc pas de problème).

Quelques tests

Avant de continuer, effectuons quelques tests. Pour ce faire, j'ai dû ajouter une méthode afficher()à la classe Duree(elle fait tout bêtement un coutde la durée) :

#include <iostream>
#include "Duree.h"

using namespace std;

int main()
{
    Duree duree1(0, 10, 28), duree2(0, 15, 2);
    Duree resultat;

    duree1.afficher();
    cout << " et " << endl;
    duree2.afficher();

    duree1 += duree2;

    cout << " donne " << endl;
    duree1.afficher();

    return 0;
}

Et le résultat tant attendu :

0h10m28s
et 
0h15m2s
donne
0h25m30s

Cool, cela marche. :soleil:
Bon, mais c'était trop facile, il n'y avait pas de reste dans mon calcul. Corsons un peu les choses avec d'autres valeurs :

1h45m50s
et
1h15m50s
donne
3h1m40s

Yeahhh ! Cela marche !
J'ai bien entendu testé d'autres valeurs pour être bien sûr que cela fonctionnait mais, de toute évidence, cela marche très bien et mon algorithme est donc bon.

Il y a un petit détail technique que j'ai laissé de côté jusque-là. Pour être parfait, notre opérateur ne devrait pas retourner voidmais une référence sur une Duree. Les raisons sont complexes et au-delà du niveau de ce cours. Prenez-cela comme une règle à suivre si vous voulez continuer à briller parmi vos collègues programmeurs C++. La méthode devrait donc être:

Duree& Duree::operator+=(const Duree& a)
{
    // Comme avant... rien à changer ici
    return *this;
}

Vous ne savez pas encore ce qu'est ce return *this, nous le verrons plus tard. Sachez simplement qu'il va avec le Duree&et qu'il fait partie des règles à appliquer pour les opérateurs raccourcis. La même règle s'appliquera donc aux opérateurs *=, /=, etc.

Bon, on en viendrait presque à oublier l'essentiel dans tout cela. Tout ce qu'on a fait là, c'était pour pouvoir écrire cette ligne :

resultat = duree1 + duree2;

Retour à l'opérateur +

Revenons à nos moutons. Nous cherchions à appeler une méthode qui effectue une addition dans notre fonction.

operator+

Ce qui tombe bien, c'est qu'on vient d'en définir une de ces méthodes. Elle a un peu un nom barbare et elle modifie l'objet qui l'appelle, mais elle effectue une sorte d'addition. Essayons donc de l'utiliser dans notre opérateur +. Nous avions :

Duree operator+(Duree const& a, Duree const& b)
{
    Duree resultat;
    resultat = a.calculeAddition(b);   //Utilise une méthode de Duree pour effectuer l'addition
    return resultat;
}

Nous ne pouvons pas écrire directement a+=bpuisque cela modifierait a, ce que nous ne voulons pas. Et en plus, aest déclaré constant, donc le compilateur ne vous laissera pas faire. Par contre, nous pouvons créer une copie de aet ensuite utiliser notre opérateur raccourci sur cette copie.

Duree operator+(Duree const& a, Duree const& b)
{
    Duree copie(a);   //On utilise le constructeur de copie de la classe Duree !
    copie += b;       //On appelle la méthode d'addition qui modifie l'objet 'copie'
    return copie;     //Et on renvoie le résultat. Ni a ni b n'ont changé.
}

Ce morceau de code n'est pas très simple à comprendre. Je vous invite à relire ce chapitre plusieurs fois si nécessaire. L'essentiel n'est pas forcément de comprendre tous les détails mais plutôt de noter les règles importantes et de les appliquer dans vos projets.

Télécharger le projet

Pour ceux d'entre vous qui n'auraient pas bien suivi la procédure, je vous propose de télécharger le projet contenant :

  • main.cpp;

  • Duree.cpp;

  • Duree.h;

  • ainsi que le fichier.cbpde Code::Blocks (si vous utilisez cet IDE comme moi).

Télécharger le projet (2 Ko)

Bonus track #1

Ce qui est vraiment sympa dans tout cela c'est que, tel que ce système est conçu, on peut très bien additionner d'un seul coup plusieurs durées sans aucun problème.

Par exemple, je rajoute juste une troisième durée dans mon main()et je l'additionne aux autres :

int main()
{
    Duree duree1(1, 45, 50), duree2(1, 15, 50), duree3 (0, 8, 20);
    Duree resultat;

    duree1.afficher();
    cout << "+" << endl;
    duree2.afficher();
    cout << "+" << endl;
    duree3.afficher();

    resultat = duree1 + duree2 + duree3;

    cout << "=" << endl;
    resultat.afficher();

    return 0;
}
1h45m50s
+
1h15m50s
+
0h8m20s
=
3h10m0s

En fait, la ligne-clé ici est :

resultat = duree1 + duree2 + duree3;

Cela revient à écrire :

resultat = operator+(operator+(duree1, duree2), duree3);

Le tout s'imbrique dans une logique implacable et vient se placer finalement dans l'objet resultat.

Bonus track #2

Et pour notre second bonus track, sachez qu'on n'est pas obligé d'additionner des Dureeavec des Duree, du moment que cela reste logique et compatible.
Par exemple, on pourrait très bien additionner une Dureeet un int. On considérerait dans ce cas que le nombre intest un nombre de secondes à ajouter.

Cela nous permettra d'écrire par exemple :

resultat = duree + 30;

Vive la surcharge des fonctions et des méthodes! La fonction operator+se définit en utilisant le même modèle qu'avant :

Duree operator+(Duree const& duree, int secondes)
{
    Duree copie(duree);    //On utilise le constructeur de copie de la classe Duree !
    copie += secondes;     //On appelle la méthode d'addition qui modifie l'objet 'copie'
    return copie;          //Et on renvoie le résultat
}

Tous les calculs sont reportés dans la méthode operator+=, comme précédemment.

Duree& operator+=(int secondes);

Mais cette fois, je vous laisse l'écrire. ;)

Les autres opérateurs arithmétiques

Maintenant que vous avez vu assez en détail le cas d'un opérateur (celui d'addition, pour ceux qui ont la mémoire courte), vous allez voir que, pour la plupart des autres opérateurs, c'est très facile et qu'il n'y a pas de difficulté supplémentaire. Le tout est de s'en servir correctement pour la classe que l'on manipule.

Ces opérateurs sont du même type que l'addition. Vous les connaissez déjà :

  • la soustraction (-) ;

  • la multiplication (*) ;

  • la division (/) ;

  • le modulo (%), c'est-à-dire le reste de la division entière.

Pour surcharger ces opérateurs, rien de plus simple : créez une fonction dont le nom commence paroperatorsuivi de l'opérateur en question. Cela donne donc :

  • operator-();

  • operator*();

  • operator/();

  • operator%().

Nous devons bien sûr ajouter aussi les versions raccourcies correspondantes, sous forme de méthodes.

  • operator-=();

  • operator*=();

  • operator/=();

  • operator%=().

Pour notre classe Duree, il peut être intéressant de définir la soustraction (operator-).
Je vous laisse le soin de le faire en vous basant sur l'addition, cela ne devrait pas être trop compliqué.

En revanche, les autres opérateurs ne servent a priori à rien : en effet, on ne multiplie pas des durées entre elles, et on les divise encore moins. Comme quoi, tous les opérateurs ne sont pas utiles à toutes les classes : ne définissez donc que ceux qui vous seront vraiment utiles.

Les opérateurs de flux

Parmi les nombreuses choses qui ont dû vous choquer quand vous avez commencé le C++, dans la catégorie « oulah c'est bizarre cela mais on verra plus tard », il y a l'injection dans les flux d'entrée-sortie. Derrière ce nom barbare se cachent les symboles>>et<<.
Quand les utilise-t-on ? Allons allons, vous n'allez pas me faire croire que vous avez la mémoire si courte.

cout << "Coucou !";
cin >> variable;

Figurez-vous justement que<<et>>sont des opérateurs. Le code ci-dessus revient donc à écrire :

operator<<(cout, "Coucou !");
operator>>(cin, variable);

On a donc fait appel aux fonctionsoperator<<etoperator>>!

Définissez ses propres opérateurs pour cout

Nous allons ici nous intéresser plus particulièrement à l'opérateur<<utilisé avec cout.
Les opérateurs de flux sont définis par défaut pour les types de variables int,double,char, ainsi que pour les objets comme string. On peut en effet aussi bien écrire :

cout << "Coucou !";

… que :

cout << 15;

Bon, le problème c'est que coutne connaît pas votre classe flambant neuve Duree, et il ne possède donc pas de fonction surchargée pour les objets de ce type. Par conséquent, on ne peut pas écrire :

Duree chrono(0, 2, 30);
cout << chrono;
//Erreur : il n'existe pas de fonction operator<<(cout, Duree &duree)

Qu'à cela ne tienne, nous allons écrire cette fonction !

Quoi ?! Mais on ne peut pas modifier le code de la bibliothèque standard ?

Déjà, si vous vous êtes posés la question, bravo : c'est que vous commencez à bien vous repérer. En effet, c'est une fonction utilisant un objet de la classe ostream(dont coutest une instance) que l'on doit définir, et on n'a pas accès au code correspondant.

On ne peut pas modifier la classe ostreammais ce n'est pas grave puisque tout ce qu'on veut faire c'est écrire une fonction qui reçoit un de ces objets en argument. Voyons donc comment rédiger cette fameuse fonction.

Implémentation d'operator<<

Commencez par écrire la fonction :

ostream& operator<<( ostream &flux, Duree const& duree )
{
    //Affichage des attributs
    return flux;
}

Comme vous pouvez le voir, c'est similaire à operator+sauf qu'ici, le type de retour est une référence et non un objet.

Le premier paramètre (référence sur un objet de type ostream) qui vous sera automatiquement passé est en fait l'objet cout(que l'on appelle ici fluxdans la fonction pour éviter les conflits de nom). Le second paramètre est une référence constante vers l'objet de type Dureeque vous tentez d'afficher en utilisant l'opérateur<<.

La fonction doit récupérer les attributs qui l'intéressent dans l'objet et les envoyer à l'objet flux(qui n'est autre que cout). Ensuite, elle renvoie une référence sur cet objet, ce qui permet de faire une chaîne :

cout << duree1 << duree2;

A nouveau, prenez cela comme une règle à suivre. Les types de retour des opérateurs ne sont pas toujours facile à déduire, mieux vaut les apprendre par cœur.

La suite, vous la connaissez. Il nous faut définir une méthode dans la classe Dureepour pouvoir accéder aux attributs et les afficher.

ostream &operator<<( ostream &flux, Duree const& duree)
{
    duree.afficher(flux) ;
    return flux;
}

Et rajoutez dans le fichier Duree.hune méthode afficher()à la classe Duree:

void afficher(std::ostream &flux) const;

Voici l'implémentation de la méthode dans Duree.cpp:

void Duree::afficher(ostream &flux) const
{
    flux << m_heures << "h" << m_minutes << "m" << m_secondes << "s";
}

Comme chaque fois, on passe donc le relais à une méthode à l'intérieur de la classe qui, elle, a le droit d'accéder aux attributs. La méthode prend en paramètre la référence vers l'objet fluxpour pouvoir lui envoyer les valeurs qui nous intéressent. Ce que l'on n'a pas pu faire dans la fonction operator<<, on le donne à faire à une méthode de la classe Duree. Exactement comme pour operator+en somme ! On a délégué le travail à une méthode de la classe qui, elle, a accès aux attributs.

Ouf ! Maintenant dans le main(), que du bonheur !

Bon, c'était un peu de gymnastique mais maintenant, ce n'est que du bonheur.
Vous allez pouvoir afficher très simplement vos objets de type Dureedans votre main():

int main()
{
    Duree duree1(2, 25, 28), duree2(0, 16, 33);
    
    cout << duree1 << " et " << duree2 << endl;

    return 0;
}

Résultat :

2h25m28s et 0h16m33s

Enfantin !
Comme quoi, on prend un peu de temps pour écrire la classe mais ensuite, quand on doit l'utiliser, c'est extrêmement simple !

Et l'on peut même combiner les opérateurs dans une seule expression. Faire une addition et afficher le résultat directement :

int main()
{
    Duree duree1(2, 25, 28), duree2(0, 16, 33);
    
    cout << duree1 + duree2 << endl;

    return 0;
}

Comme pour les int,double, etc. nos objets deviennent réellement simples à utiliser.

En résumé

  • Le C++ permet de surcharger les opérateurs, c'est-à-dire de créer des méthodes pour modifier le comportement des symboles +, -, *, /, >=, etc.

  • Pour surcharger un opérateur, on doit donner un nom précis à sa méthode (operator+pour le symbole + par exemple).

  • Ces méthodes doivent prendre des paramètres et renvoyer parfois des valeurs d'un certain type précis. Cela dépend de l'opérateur que l'on surcharge.

  • Il ne faut pas abuser de la surcharge d'opérateur car elle peut avoir l'effet inverse du but recherché, qui est de rendre la lecture du code plus simple.

Exemple de certificat de réussite
Exemple de certificat de réussite