• 50 heures
  • Difficile

Ce cours est visible gratuitement en ligne.

Ce cours existe en livre papier.

Vous pouvez obtenir un certificat de réussite à l'issue de ce cours.

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

J'ai tout compris !

Mis à jour le 19/02/2019

TP : la POO en pratique avec ZFraction

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

Vous avez appris dans les chapitres précédents à créer et manipuler des classes, il est donc grand temps de mettre tout cela en pratique avec un TP.

C'est le premier TP sur la POO, il porte donc sur les bases. C'est le bon moment pour arrêter un peu la lecture du cours, souffler et essayer de réaliser cet exercice par vous-mêmes. Vous aurez aussi l'occasion de vérifier vos connaissances et donc, si besoin, de retourner lire les chapitres sur les éléments qui vous ont manqués.

Dans ce TP, vous allez devoir écrire une classe représentant la notion de fraction. Le C++ permet d'utiliser des nombres entiers via le typeint, des nombres réels via le typedouble, mais il ne propose rien pour les nombre rationnels. À vous de palier ce manque !

Préparatifs et conseils

La classe que nous allons réaliser n'est pas très compliquée et il est assez aisé d'imaginer quelles méthodes et opérateurs nous allons utiliser. Cet exercice va en particulier tester vos connaissances sur :

  • les attributs et leurs droits d'accès ;

  • les constructeurs ;

  • la surcharge des opérateurs.

C'est donc le dernier moment pour réviser !

Description de la classeZFraction

Commençons par choisir un nom pour notre classe. Il serait judicieux qu'il contienne le mot « fraction » et, comme vous êtes en train de lire un Livre du Zéro, je vous propose d'ajouter un « Z » au début. Ce sera doncZFraction. Ce n'est pas super original mais, au moins, on sait directement à quoi on a affaire.

Utiliser desintou desdoubleest très simple. On les déclare, on les initialise et on utilise ensuite les opérateurs comme sur une calculatrice. Ce serait vraiment super de pouvoir faire la même chose avec des fractions. Ce qui serait encore mieux, ce serait de pouvoir comparer des fractions entre elles afin de déterminer laquelle est la plus grande.

On aimerait donc bien que lemain()suivant compile et fonctionne correctement :

#include <iostream>
#include "ZFraction.h"
using namespace std;

int main()
{
    ZFraction a(4,5);      //Déclare une fraction valant 4/5
    ZFraction b(2);        //Déclare une fraction valant 2/1 (ce qui vaut 2)
    ZFraction c,d;         //Déclare deux fractions valant 0

    c = a+b;               //Calcule 4/5 + 2/1 = 14/5

    d = a*b;               //Calcule 4/5 * 2/1 = 8/5

    cout << a << " + " << b << " = " << c << endl;

    cout << a << " * " << b << " = " << d << endl;

    if(a > b)
        cout << "a est plus grand que b." << endl;
    else if(a==b)
        cout << "a est egal a b." << endl;
    else
        cout << "a est plus petit que b." << endl;

    return 0;
}

Et voici le résultat espéré :

4/5 + 2 = 14/5
4/5 * 2 = 8/5
a est plus petit que b.

Pour arriver à cela, il nous faudra donc :

  • écrire la classe avec ses attributs ;

  • réfléchir aux constructeurs à implémenter ;

  • surcharger les opérateurs +, *, <<, < et == (au moins).

Je n'ai rien de plus à ajouter concernant la description du TP. Vous pourrez trouver, dans les cours de maths disponibles sur le Site du Zéro, certains rappels sur les calculs avec les nombres rationnels. Si vous vous sentez prêts, alors allez-y !

Rappel sur les fractions

Si par contre vous avez peur de vous lancer seuls, je vous propose de vous accompagner pour les premiers pas.

Créez un nouveau projet

Pour réaliser ce TP, vous allez devoir créer un nouveau projet. Utilisez l'IDE que vous voulez, pour ma part vous savez que j'utilise Code::Blocks. Ce projet sera constitué de trois fichiers que vous pouvez déjà créer :

  • main.cpp: ce fichier contiendra uniquement la fonctionmain(). Dans la fonctionmain(), nous créerons des objets basés sur notre classeZFractionpour tester son fonctionnement. À la fin, votre fonctionmain()devra ressembler à celle que je vous ai montré plus haut.

  • ZFraction.h: ce fichier contiendra le prototype de notre classeZFractionavec la liste de ses attributs et les prototypes de ses méthodes.

  • ZFraction.cpp: ce fichier contiendra l'implémentation des méthodes de la classeZFraction, c'est-à-dire le « code » à l'intérieur des méthodes.

Le code de base des fichiers

Nous allons écrire un peu de code dans chacun de ces fichiers, le strict minimum afin de pouvoir commencer.

main.cpp

Bon, celui-là, je vous l'ai déjà donné.
Mais pour commencer en douceur, je vous propose de simplifier l'intérieur de la fonctionmain()et d'y ajouter des instructions au fur et à mesure de l'avancement de votre classe.

#include <iostream>
#include "ZFraction.h"
using namespace std;
 
int main()
{
    ZFraction a(1,5); // Crée une fraction valant 1/5
    return 0;
}

Pour l'instant, on se contente d'un appel au constructeur deZFraction. Pour le reste, on verra plus tard.

ZFraction.h

Ce fichier contiendra la définition de la classeZFraction. Il inclut aussiiostreampour nos besoins futurs (nous aurons besoin de faire descoutdans la classe les premiers temps, ne serait-ce que pour débugger notre classe).

#ifndef DEF_FRACTION
#define DEF_FRACTION

#include <iostream>

class ZFraction
{
public:

private:

};

#endif

Pour l'instant, la classe est vide ; je ne vais pas non plus tout faire, ce n'est pas le but. J'y ai quand même mis une partie privée et une partie publique. Souvenez-vous de la règle principale de la POO qui veut que tous les attributs soient dans la partie privée. Je vous en voudrais beaucoup si vous ne la respectiez pas.

ZFraction.cpp

C'est le fichier qui va contenir les définitions des méthodes. Comme notre classe est encore vide, il n'y a rien à y écrire. Il faut simplement penser à inclure l'entêteZFraction.h.

#include "ZFraction.h"

Nous voilà enfin prêts à attaquer la programmation !

Choix des attributs de la classe

La première étape dans la création d'une classe est souvent le choix des attributs. Il faut se demander de quelles briques de base notre classe est constituée. Avez-vous une petite idée ?

Voyons cela ensemble. Un nombre rationnel est composé de deux nombres entiers appelés respectivement le numérateur (celui qui est au-dessus de la barre de fraction) et le dénominateur (celui du dessous). Cela nous fait donc deux constituants. En C++, les nombres entiers s'appellent desint. Ajoutons donc deuxintà notre classe :

#ifndef DEF_FRACTION
#define DEF_FRACTION

#include <iostream>

class ZFraction
{
public:

private:

    int m_numerateur;      //Le numérateur de la fraction
    int m_denominateur;    //Le dénominateur de la fraction

};

#endif

Nos attributs commencent toujours par le préfixe « m_ ». C'est une bonne habitude de programmation que je vous ai enseignée dans les chapitres précédents
Cela nous permettra, par la suite, de savoir si nous sommes en train de manipuler un attribut de la classe ou une simple variable « locale » à une méthode.

Les constructeurs

Je ne vais pas tout vous dire non plus mais, dans lemain()d'exemple que je vous ai présenté tout au début, nous utilisions trois constructeurs différents :

  • Le premier recevait comme arguments deux entiers. Ils représentaient respectivement le numérateur et le dénominateur de la fraction. C'est sans doute le plus intuitif des trois à écrire.

  • Le deuxième constructeur prend un seul entier en argument et construit une fraction égale à ce nombre entier. Cela veut dire que, dans ce cas, le dénominateur vaut 1.

  • Enfin, le dernier constructeur ne prend aucun argument (constructeur par défaut) et crée une fraction valant 0.

Je ne vais rien expliquer de plus. Je vous propose de commencer par écrire au moins le premier de ces trois constructeurs. Les autres suivront rapidement, j'en suis sûr.

Les opérateurs

La part la plus importante de ce TP sera l'implémentation des opérateurs. Il faut bien réfléchir à la manière de les écrire et vous pouvez bien sûr vous inspirer de ce qui a été fait pour la classeDureedu chapitre précédent, par exemple utiliser la méthodeoperator+=pour définir l'opérateur+ou écrire une méthodeestEgalA()pour l'opérateur d'égalité.

Une bonne chose à faire est de commencer par l'opérateur<<. Vous pourrez alors facilement tester vos autres opérateurs.

Simplifiez les fractions

L'important, avec les fractions, est de toujours manipuler des fractions simplifiées, c'est-à-dire que l'on préfère écrire

$\[\frac{2}{5}\]$

plutôt que

$\[\frac{4}{10}\]$

. Il serait bien que notre classe fasse de même et simplifie elle-même la fraction qu'elle représente.

Il nous faut donc un moyen mathématique de le faire puis traduire le tout en C++. Si l'on a une fraction

$\[\frac{a}{b}\]$

, il faut calculer le plus grand commun diviseur de a et b puis diviser a et b par ce nombre. Par exemple, le PGCD (Plus grand commun diviseur) de 4 et 10 est 2, ce qui veut dire que l'on peut diviser les numérateur et dénominateur de

$\[\frac{4}{10}\]$

par 2, et nous obtenons bien 

$\[\frac{2}{5}\]$

Calculer le PGCD n'est pas une opération facile. Aussi, je vous propose pour le faire une fonction que je vous invite à ajouter dans votre fichierZFraction.cpp:

int pgcd(int a, int b)
{
    while (b != 0)
    {
        const int t = b;
        b = a%b;
        a=t;
    }
    return a;
}

Il faut ajouter le prototype correspondant dansZFraction.h:

#ifndef DEF_FRACTION
#define DEF_FRACTION

#include <iostream>

class ZFraction
{
   //Contenu de la classe…
};

int pgcd(int a, int b);

#endif

Vous pourrez alors utiliser cette fonction dans les méthodes de la classe.

Allez au boulot !

Correction

Lâchez vos claviers, le temps imparti est écoulé !

Il est temps de passer à la phase de correction. Vous avez certainement passé pas mal de temps à réfléchir aux différentes méthodes, opérateurs et autres horreurs joyeusetés du C++. Si vous n'avez pas réussi à tout faire, ce n'est pas grave : lire la correction pour saisir les grands principes devrait vous aider. Et puis vous saurez peut-être vous rattraper avec les améliorations proposées en fin de chapitre.

Sans plus attendre, je vous propose de passer en revue les différentes étapes de création de la classe.

Les constructeurs

Je vous avais suggéré de commencer par le constructeur prenant en argument deux entiers, le numérateur et le dénominateur. Voici ma version.

ZFraction::ZFraction(int num, int den)
    :m_numerateur(num), m_denominateur(den)
{
}

On utilise la liste d'initialisation pour remplir les attributs m_numerateuret m_denominateurde la classe. Jusque-là, rien de sorcier.

En continuant sur cette lancée, on peut écrire les deux autres constructeurs :

ZFraction::ZFraction(int entier)
    :m_numerateur(entier), m_denominateur(1)
{
}

ZFraction::ZFraction()
    :m_numerateur(0), m_denominateur(1)
{
}

Il fallait se rappeler que le nombre 5 s'écrit, sous forme de fraction,

$\[\frac{5}{1}\]$

et 0,

$\[\frac{0}{1}\]$

.
Dans ce domaine, le cahier des charges est donc rempli. Avant de commencer à faire des choses compliquées, écrivons l'opérateur<<pour afficher notre fraction. En cas d'erreur, on pourra ainsi facilement voir ce qui se passe dans la classe.

Affichez une fraction

Comme nous l'avons vu au chapitre sur les opérateurs, la meilleure solution consiste à utiliser une méthode affiche()dans la classe et à appeler cette méthode dans la fonction<<. Je vous invite à réutiliser le code du chapitre précédent afin d'avoir directement le code de l'opérateur.

ostream& operator<<(ostream& flux, ZFraction const& fraction)
{
    fraction.affiche(flux);
    return flux;
}

Et pour la méthode affiche(), je vous propose cette version :

void ZFraction::affiche(ostream& flux) const
{
    if(m_denominateur == 1)
    {
        flux << m_numerateur;
    }
    else
    {
        flux << m_numerateur << '/' << m_denominateur;
    }
}

Vous avez certainement écrit quelque chose d'approchant. J'ai distingué le cas où le dénominateur vaut 1. Une fraction dont le dénominateur vaut 1 est un nombre entier, on n'a donc pas besoin d'afficher la barre de fraction et le dénominateur. Mais c'est juste une question d'esthétique.

L'opérateur d'addition

Comme pour<<, le mieux est d'employer la recette du chapitre précédent : définir une méthodeoperator+=()dans la classe et l'utiliser dans la fonctionoperator+().

ZFraction operator+(ZFraction const& a, ZFraction const& b)
{
    ZFraction copie(a);
    copie+=b;
    return copie;
}

La difficulté réside dans l'implémentation de l'opérateur d'addition raccourci.

En ressortant mes cahiers de maths, j'ai retrouvé la formule d'addition de deux fractions :

$\[\frac{a}{b} + \frac{c}{d} = \frac{a\cdot d + b\cdot c}{b\cdot d}\]$

Cela donne en C++ :

ZFraction& ZFraction::operator+=(ZFraction const& autre)
{
    m_numerateur = autre.m_denominateur * m_numerateur + m_denominateur * autre.m_numerateur;
    m_denominateur = m_denominateur * autre.m_denominateur;

    return *this;    
}

L'opérateur de multiplication

La formule de multiplication de deux fractions est encore plus simple que l'addition :

$\[\frac{a}{b} \cdot \frac{c}{d} = \frac{a \cdot c}{b \cdot d}\]$

Et je ne vais pas vous surprendre si je vous dis qu'il faut utiliser la méthodeoperator*=()et la fonctionoperator*(). Je pense que vous avez compris le truc.

ZFraction operator*(ZFraction const& a, ZFraction const& b)
{
    ZFraction copie(a);
    copie*=b;
    return copie;
}

ZFraction& ZFraction::operator*=(ZFraction const& autre)
{
    m_numerateur *= autre.m_numerateur;
    m_denominateur *= autre.m_denominateur;

    return *this;
}

Les opérateurs de comparaison

Comparer des fractions pour tester si elles sont égales revient à vérifier que leurs numérateurs d'une part, et leurs dénominateurs d'autre part, sont égaux. L'algorithme est donc à nouveau relativement simple. Je vous propose, comme toujours, de passer par une méthode de la classe puis d'utiliser cette méthode dans les opérateurs externes.

bool ZFraction::estEgal(ZFraction const& autre) const
{
    if(m_numerateur == autre.m_numerateur && m_denominateur == autre.m_denominateur)
        return true;
    else
        return false;
}

bool operator==(ZFraction const& a, ZFraction const& b)
{
    if(a.estEgal(b))
        return true;
    else
        return false;
}

bool operator!=(ZFraction const& a, ZFraction const& b)
{
    if(a.estEgal(b))
        return false;
    else
        return true;
}

ou en version plus courte (maintenant que vous avez l'habitude des opérations booléennes):

bool ZFraction::estEgal(ZFraction const& autre) const
{
    return (m_numerateur == autre.m_numerateur && m_denominateur == autre.m_denominateur);
}

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

bool operator!=(ZFraction const& a, ZFraction const& b)
{
   return !(a.estEgal(b));   //Souvenez-vous du ! qui veut dire "NON"
}

Une fois que la méthodeestEgal()est implémentée, on a deux opérateurs pour le prix d'un seul. Parfait, je n'avais pas envie de réfléchir deux fois.

Les opérateurs d'ordre

Il ne nous reste plus qu'à écrire un opérateur permettant de vérifier si une fraction est plus petite que l'autre. Il y a plusieurs moyens d'y parvenir. Toujours dans mes livres de maths, j'ai retrouvé une vieille relation intéressante :

$\[\frac{a}{b} < \frac{c}{d} \Longleftrightarrow a\cdot d < b \cdot c\]$

Cette relation peut être traduite en C++ pour obtenir le corps de la méthode estPlusPetitQue() :

bool ZFraction::estPlusPetitQue(ZFraction const& autre) const
{
    if(m_numerateur * autre.m_denominateur < m_denominateur * autre.m_numerateur)
        return true;
    else
        return false;
}

Et cette fois, ce n'est pas un pack "2 en 1", mais "4 en 1". Avec un peu de réflexion, on peut utiliser cette méthode pour les opérateurs <,>, <= et >=. :magicien:

bool operator<(ZFraction const& a, ZFraction const& b) //Vrai si a<b donc si a est plus petit que b
{
    if(a.estPlusPetitQue(b))
        return true;
    else
        return false;
}

bool operator>(ZFraction const& a, ZFraction const& b) //Vrai si a>b donc si b est plus petit que a
{
    if(b.estPlusPetitQue(a))
        return true;
    else
        return false;
}

bool operator<=(ZFraction const& a, ZFraction const& b) //Vrai si a<=b donc si b n'est pas plus petit que a
{
    if(b.estPlusPetitQue(a))
        return false;
    else
        return true;
}

bool operator>=(ZFraction const& a, ZFraction const& b) //Vrai si a>=b donc si a n'est pas plus petit que b
{
    if(a.estPlusPetitQue(b))
        return false;
    else
        return true;
}

ou mieux:

bool ZFraction::estPlusPetitQue(ZFraction const& autre) const
{
    return (m_numerateur * autre.m_denominateur < m_denominateur * autre.m_numerateur);
}

bool operator<(ZFraction const& a, ZFraction const& b) //Vrai si a<b donc si a est plus petit que b
{
    return a.estPlusPetitQue(b);
}

bool operator>(ZFraction const& a, ZFraction const& b) //Vrai si a>b donc si b est plus petit que a
{
    return b.estPlusPetitQue(a);
}

bool operator<=(ZFraction const& a, ZFraction const& b) //Vrai si a<=b donc si b n'est pas plus petit que a
{
    return !(b.estPlusPetitQue(a));
}

bool operator>=(ZFraction const& a, ZFraction const& b) //Vrai si a>=b donc si a n'est pas plus petit que b
{
    return !(a.estPlusPetitQue(b));
}

Avec ces quatre derniers opérateurs, nous avons fait le tour de ce qui était demandé. Ou presque. Il nous reste à voir la partie la plus difficile : le problème de la simplification des fractions.

Simplifiez les fractions

Je vous ai expliqué dans la présentation du problème quel algorithme utiliser pour simplifier une fraction. Il faut calculer le pgcd du numérateur et du dénominateur. Puis diviser les deux attributs de la fraction par ce nombre.

Comme c'est une opération qui doit être exécutée à différents endroits, je vous propose d'en faire une méthode de la classe. On aura ainsi pas besoin de récrire l'algorithme à différents endroits. Cette méthode n'a pas à être appelée par les utilisateurs de la classe. C'est de la mécanique interne. Elle va donc dans la partie privée de la classe.

void ZFraction::simplifie()
{
    int nombre=pgcd(m_numerateur, m_denominateur); //Calcul du pgcd

    m_numerateur /= nombre;     //Et on simplifie
    m_denominateur /= nombre;
}

Quand faut-il utiliser cette méthode ?

Bonne question ! Mais vous devriez avoir la réponse.
Il faut simplifier la fraction à chaque fois qu'un calcul est effectué. C'est-à-dire, dans les méthodes operator+=()etoperator*=():

ZFraction& ZFraction::operator+=(ZFraction const& autre)
{
    m_numerateur = autre.m_denominateur * m_numerateur + m_denominateur * autre.m_numerateur;
    m_denominateur = m_denominateur * autre.m_denominateur;

    simplifie();    //On simplifie la fraction
    return *this;    
}

ZFraction& ZFraction::operator*=(ZFraction const& autre)
{
    m_numerateur *= autre.m_numerateur;
    m_denominateur *= autre.m_denominateur;

    simplifie();    //On simplifie la fraction
    return *this;
}

Mais ce n'est pas tout ! Quand l'utilisateur construit une fraction, rien ne garantit qu'il le fait correctement. Il peut très bien initialiser sa ZFraction avec les valeurs

\[\frac{4}{8}\]

par exemple. Il faut donc aussi appeler la méthode dans le constructeur qui prend deux arguments.

ZFraction::ZFraction(int num, int den)
    :m_numerateur(num), m_denominateur(den)
{
    simplifie();
    //On simplifie au cas où l'utilisateur
    //Aurait entré de mauvaises informations
}

Et voilà ! En fait, si vous regardez bien, nous avons dû ajouter un appel à la méthode simplifie()dans toutes les méthodes qui ne sont pas déclarées constantes ! Chaque fois que l'objet est modifié, il faut simplifier la fraction. Nous aurions pu éviter de réfléchir et simplement analyser notre code à la recherche de ces méthodes. Utiliser constest donc un atout de sécurité. On voit tout de suite où il faut faire des vérifications (appeler simplifie()) et où c'est inutile.

Notre classe est maintenant fonctionnelle et respecte les critères que je vous ai imposés. Hip Hip Hip Hourra !

Allez plus loin

Notre classe est terminée. Disons qu'elle remplit les conditions posées en début de chapitre. Mais vous en conviendrez, on est encore loin d'avoir fait le tour du sujet. On peut faire beaucoup plus avec des fractions.

Je vous propose de télécharger le code source de ce TP, si vous le souhaitez, avant d'aller plus loin :

Télécharger le code source de zFraction

Voyons maintenant ce que l'on pourrait faire en plus :

    • Ajouter des méthodesnumerateur() etdenominateur()qui renvoient le numérateur et le dénominateur de la ZFractionsans la modifier.

    • Ajouter une méthode nombreReel() qui convertit notre fraction en un double.

    • Simplifier les constructeurs comme pour la classe Duree. En réfléchissant bien, on peut fusionner les trois constructeurs en un seul avec des valeurs par défaut.

    • Proposer plus d'opérateurs : nous avons implémenté l'addition et la multiplication, il nous manque la soustraction et la division.

    • Pour l'instant, notre classe ne gère que les fractions positives. C'est insuffisant ! Il faudrait permettre des fractions négatives.

Si vous vous lancez dans cette tâche, il va falloir faire des choix importants (sur la manière de gérer le signe, par exemple). Ce que je vous propose, c'est de toujours placer le signe de la fraction au numérateur. Ainsi,

$\[\frac{1}{-4}\]$

devra automatiquement être converti en

$\[\frac{-1}{4}\]$

. En plus de simplifier les fractions, vos opérateurs devront donc aussi veiller à placer le signe au bon endroit. À nouveau, je vous conseille d'utiliser une méthode privée.

    • Si vous autorisez les fractions négatives, alors il serait judicieux de proposer l'opérateur « moins unaire » (je ne vous ai pas parlé de cet opérateur). C'est l'opérateur qui transforme un nombre positif en nombre négatif comme dans b= -a;. Comme les autres opérateurs arithmétiques, il se déclare en dehors de la classe. Son prototype est :ZFraction operator-(ZFraction const& a);.

C'est nouveau mais pas très difficile si l'on utilise les bonnes méthodes de la classe.

  • Ajouter des fonctions mathématiques telles que abs(),sqrt()ou pow(), prenant en arguments des ZFraction. Pensez à inclure l'en-tête cmath.

Je pense que cela va vous demander pas mal de travail mais c'est tout bénéfice pour vous : il faut pas mal d'expérience avec les classes pour arriver à « penser objet » et il n'y a que la pratique qui peut vous aider.

Je ne vais pas vous fournir une correction détaillée pour chacun de ces points mais je peux vous proposer une solution possible :

Télécharger le code source de zFraction avec les améliorations proposées

Et si vous avez d'autres idées, n'hésitez pas à les ajouter à votre classe !

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