manipulation de fractions
Ces travaux pratiques sont à effectuer seul.
Les réponses aux exercices sont à envoyer à mon adresse mail avant la séance suivante.
Nous nous proposons de réaliser une classe Fraction
, de manipulation de fractions.
Une fraction est représentée par un couple d'entiers n et d tels que la fraction s'exprime par le quotien .
n est appelé numérateur et d dénominateur.
Cette classe devra être capable d'additionner, soustraine, multiplier et diviser deux fractions. Bien entendu, les précautions d'usage sur le dénominateur doivent être prises et si ce dernier est nul à un moment ou à un autre, ça doit être détecté.
Il devra également être possible de comparer deux fractions (savoir si elles
sont égales, différentes, inférieures, supérieures, inférieures ou égales ou
supérieures ou égales). Ces opérations de comparaison retournent une valeur booléenne
(de type bool
).
Remarque : la comparaison n'est possible que si les fractions sont réduites.
Enfin, un affichage de la fraction sous la forme n/d devra être possible via une méthode dédiée. L'affichage devra s'adapter quand le dénominateur est égal à 1.
Vous écrirez une classe Fraction
qui contiendra au moins deux attributs : le numérateur et le dénominateurs de la fraction.
Ces deux champs doivent être privés et il ne doit pas y avoir de méthode pour les modifier.
Le numérateur et le dénominateur doivent donc être des paramètres du constructeur de votre classe Fraction
.
Il n'y a a priori qu'un constructeur qui prend en paramètre les deux entiers représentants le numérateur et le dénominateur. Cependant, en C++, si un constructeur est défini et qu'il prend des paramètres, il est obligatoire de redéfinir le constructeur par défaut, dit de recopie.
Il peut ne pas y avoir de destructeur, vu que rien n'est alloué dynamiquement, celui par défaut fera très bien l'affaire.
Pour qu'il soit possible de faire les opérations
mentionnées dans la section précédente, vous devrez surcharger les
opérateurs +
, -
, *
, /
et =
.
Vous devrez également surcharger les opérateurs de comparaison
==
, !=
, <
, >
, <=
,
>=
. Mais, comme nous l'avons mentionné plus haut, ces opérateurs ne donnerons des résultats valides que si les fractions sont sous leur forme réduite. Il faudra donc trouver les décompositions des numérateurs et dénominateurs en facteurs premiers, puis réduire la fraction en conséquence.
Il est, de toutes manières, intéressant de disposer d'une méthode de décomposition d'un nombre en facteurs premiers. C'est ce que nous proposons d'écrire en retranscrivant l'algorithme ci-dessous.
La méthode de réduction de fraction utilisée précédemment n'est sans doute pas la plus efficace. Il serait plus intéressant de pouvoir calculer le PGCD du numérateur et du dénominateur pour ensuite les diviser tous les deux par cette valeur.
Il faudra donc écrire une méthode int pgcd(int m, int n)
qui calcule le PGCD de deux entiers pour l'appliquer à notre fraction. Un algorithme de calcul du PGCD de deux nombres est donné ci-dessous :
Fraction
qui possède deux attributs privés représentant le numérateur et le dénominateur. Vous ajouterez les constructeurs évoqués plus haut ainsi qu'une méthode void afficher()
permettant d'afficher les valeurs des attributs.+
, -
, *
et /
. Pour le moment nous ne parlons pas de réduction de fractions.string toString()
qui retournera une chaîne de caractères représentant votre fraction et pouvant être utilisée comme argument de cout
par exemple.vector<int>
et qui vont recevoir les décompositions du numérateur et du dénominateur de la fraction.void premiers(vector<int>&, int)
qui décompose en facteurs premiers le nombre passé en second argument et qui stocke cette décomposition dans le vecteur passé en premier argument.void reduireFacteurs()
qui utilise la décomposition en facteurs premiers pour réduire la fraction. Elle devra donc parcourir les deux vecteurs obtenus par décomposition du numérateur et du dénominateur, et retirer les termes communs. <
, >
, <=
, >=
, ==
et !=
.int pgcd(int, int)
et ajoutez une méthode void reduit(int&, int&)
qui réduit la fraction en utilisant le PGCD.Jusqu'ici nous ne nous sommes pas posé la question de la validité de notre fraction. Typiquement, si le dénominateur était nul, cela ne poserait pas de problème. Or, nous savons que ça pose effectivement un problème. Il faut donc détecter ce cas et le traiter en conséquence : le signaler et ne pas procéder à l'instanciation de la classe.
Une méthode pratique pour cela est l'utilisation des exceptions. Une exception peut être vue comme un message envoyé par une méthode lorsqu'un problème apparaît. Le destinataire de ce message est en général la méthode appellante, mais cette dernière peut vouloir simplement retransmettre ce message sans le traiter. Cela peut continuer jusqu'à ce que ce message arrive tout en haut de la pile d'appels, auquel cas, le programme se terminera.
En C++, une exception peut être n'importe quoi, du simple entier jusqu'à une classe dédiée. Il existe en effet une classe exception
dont les spécifications sont données plus bas et dont le rôle est d'aider à la gestion des erreurs.
Les étapes de la vie d'une exception sont les suivantes :
La déclaration se fait en général par héritage de la classe exception
en redéfinissant la méthode virtuelle virtual const char* what() const throw() ;
pour qu'elle afficher un message en rapport avec notre type d'erreur.
La levée, c'est à dire le fait d'envoyer le message, se fait au moment où l'on détecte l'erreur (pour nous, typiquement au moment de la création de la fraction, si le dénominateur est nul) via l'utilisation du mot clé throw
. Un exemple est donné ci-dessous.
Le traitement peut se faire ou ne pas se faire. Dans le dernier cas, le programme se terminera simplement en affichant, s'il existe, le message de la méthode what()
.
Si on désire procéder au traitement, alors il faut utiliser la (lourde) construction try { ... } catch (exception &e) { ... }
. Le code susceptible de générer une erreur est positionné dans le bloc try { ... }
et la gestion en elle-même, dans le bloc catch (exception &e) { ... }
où exception &e
représente l'objet exception à traiter.
Un exemple est donné ci-dessous, il reprend ce qui a été présenté plus haut.
Inspirez-vous de la classe d'exception ci-dessus pour ajouter la gestion d'un dénominateur nul lors de la création d'une fraction.
A priori les méthodes pgcd
et premiers
ne sont pas publiques et sont utilisées automatiquement par la classe à chaque fois que cela s'avère nécessaire. Pourtant, nous pouvons tout à fait enviseager de décomposer un nombre en facteur premiers sans que celui-ci ne soit si le numéateur ou le dénominateur d'une fraction et de même, nous pouvons imaginer vouloir connaître le PGCD de deux nombres hors du cadre d'une fraction.
Il est possible de réaliser de telles méthodes utilitaires pour qu'elles soient utilisables sans instance de la classe en question. Ce sont des méthodes dites statiques.
Une méthode statique se définie en ajoutant le mot clé static
devant sa déclaration (c'est à dire dans le corps de la classe) :
#include <vector> class Fraction { private : int n, d ; public : // ... static void premiers(vector<int> &V, int N) ; static int pgcd(int m, int n) ; // ... } ;
À partir de ce moment, il sera possible de l'invoquer directement, sans avoir à instancier un objet de la classe, comme ci-dessous :
#include <iostream> #include <Fraction> int main(int argc, char *argv[]) { vector<int> facteurs ; Fraction::premiers(facteurs, 123456) ; cout << "PGCD(254,122) = " << Fraction::pgcd(254,122) << endl ; return 0 ; }
Attention cependant, une méthode statique ne peut faire référence qu'à d'autres méthodes ou attributs statiques de sa classe. C'est pour cela que dans notre cas, ces méthodes prennent des paramètres et n'utilisent pas directement les attributs n
et d
de la classe Fraction (ils ne sont pas statiques).
Fraction
pour que les deux méthodes précédentes soient statiques.