Travaux Pratiques n°2

manipulation de fractions

Instructions

Ces travaux pratiques sont à effectuer seul.

Les réponses aux exercices sont à envoyer à mon adresse mail avant la séance suivante.

Représentation de fractions

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 nd.

n est appelé numérateur et d dénominateur.

Opérations sur les fractions

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.

Principes généraux de réalisation

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.

N ← le nombre positif à décomposer
pour P variant de 2 à racine carrée de N compris faire
	si la division entière de N par P a un reste nul alors
		P est un facteur premier de N
		N ← N / P
	sinon si P = 2 alors
		P ← 3
	sinon
		P ← P + 2
	finsi
fin pour
N contient à ce stade le dernier facteur
Algorithme de décomposition d'un nombre en facteurs premiers.

Pour aller plus loin

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 :

m ← le premier nombre
n ← le second nombre
tant que n non nul faire
	t ← reste de la division entière de m par n
	m ← n
	n ← t
fin tant que
m est le PGCD recherché
Algorithme de calcul du PGCD de deux nombres.

Réalisation pratique

Exercice

  1. Créez une classe publique 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.
  2. Dans cette classe ajoutez les différentes méthodes de surcharge des opérateurs +, -, * et /. Pour le moment nous ne parlons pas de réduction de fractions.
  3. Ajoutez à votre classe une méthode 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.
  4. Ajoutez deux champs privés à votre classe, qui sont de type vector<int> et qui vont recevoir les décompositions du numérateur et du dénominateur de la fraction.
  5. Écrivez une méthode 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.
  6. Ajoutez à votre classe une métode 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.
  7. Maintenant que vous savez réduire une fraction, vous pouvez ajouter les surcharges d'opérateurs pour les comparaisons : <, >, <=, >=, == et !=.
  8. Écrivez la méthode int pgcd(int, int) et ajoutez une méthode void reduit(int&, int&) qui réduit la fraction en utilisant le PGCD.

Gestion des erreurs — exceptions

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.

class exception
{
public:
	exception () throw() ;
	exception (const exception&) throw() ;
	exception& operator= (const exception&) throw() ;
	virtual ~exception() throw() ;
	virtual const char* what() const throw() ;
} ;
Spécifications de la classe exception.

Les étapes de la vie d'une exception sont les suivantes :

  1. déclaration (si nécessaire) ;
  2. levée ;
  3. traitement.

Déclaration

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.

class DivByZeroException : public exception
{
public:
	virtual const char* what() const throw()
	{
		return "Erreur : division par zéro !" ;
	}
} ;
Exemple de déclaration d'une exception

Levée

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.

float division(float a, float b)
{
	if ( b == 0 )
		throw DivByZeroException() ;
	return a/b ;
}
Exemple de levée d'une exception

Traitement

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) { ... }exception &e représente l'objet exception à traiter.

Un exemple est donné ci-dessous, il reprend ce qui a été présenté plus haut.

int main()
{
	int a, b ;
	a = 5.23 ;

	try {
		cout << division(a, b) << endl ;
	} catch (DivByZeroException &e) {
		// code de traitement de l'erreur si c'est possible
		cerr << e.what() << endl ;
	}
}
Exemple de traitement d'une exception

Exercice

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.

Classe à services

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).

Exercice

  1. Modifiez votre classe Fraction pour que les deux méthodes précédentes soient statiques.
  2. Ajoutez une troisième méthode statique qui calcule le PPCM de deux nombres.

  1. [↑] Plus Petit Commun Multiple. Il existe la relation suivante : a × b = PGCD(a,b) × PPCM(a,b).