33
ENSEIGNEMENT DE PROMOTION SOCIALE —————————————————————— Cours de LANGAGE C++ - Opérateurs et surcharge - —————————————————————— H. Schyns Octobre 2010

LANGAGE C++ - Opérateurs et surchargeCPP - Operators 1 - Introduction H. Schyns 1.3 Nous illustrerons ensuite la surcharge des opérateurs de comparaison dans le cas d'une classe

  • Upload
    others

  • View
    2

  • Download
    0

Embed Size (px)

Citation preview

ENSEIGNEMENT DE PROMOTION SOCIALE

—————————————————————— Cours de

LANGAGE C++

- Opérateurs et surcharge -

——————————————————————

H. Schyns

Octobre 2010

CPP - Operators Sommaire

H. Schyns S.1

Sommaire

1. INTRODUCTION

2. LES OPERATEURS ARITHMETIQUES

2.1. Syntaxe 2.2. La classe Fraction 2.3. Arithmétique entre objets de la même classe

2.3.1. L'opérateur d'addition 2.3.2. Autres opérateurs arithmétiques 2.3.3. Les opérateurs combinés 2.3.4. Opérateurs arithmétiques unaires

2.4. Arithmétique entre objets de classes différentes 2.4.1. Opérateurs arithmétiques "inter-classes" 2.4.2. Opérateurs arithmétiques "hors classe" 2.4.3. "Cast" par constructeur 2.4.4. Operateur de "cast"

3. LES OPERATEURS D'ENTREE/SORTIE

4. LES OPERATEURS LOGIQUES

5. GENERALISATION

5.1. Une nouvelle algèbre ! 5.2. operator [ ] 5.3. Opérateur d'appel de fonction

6. CONFLITS POTENTIELS

7. ANNEXES

7.1. Simplification des fractions 7.2. Constructeur avec argument de type float

8. SOURCES

CPP - Operators 1 - Introduction

H. Schyns 1.1

1. Introduction

Les langages informatiques en général, et C et C++ en particulier, utilisent des types de variables pré-définis :

- char, short, int, long avec leurs variantes signed et unsigned , - float, double.

Ces variables interagissent grâce à des opérateurs répartis en plusieurs catégories :

affectation = arithmétiques +

+= ++

- -= --

* *=

/ /=

% %=

logiques > >= &&

< <= ||

!= !

==

manipulation de bits & &=

| |=

^ ^=

~ ~=

>> >>=

<< <<=

spéciaux () [] -> ->* ,

Le but premier de la surcharge des opérateurs est de permettre une grande simplification de la syntaxe.

Nous avons déjà rencontré l'opérateur d'affectation (operator=). Nous savons que nous devons le définir chaque fois que nous créons une classe qui fait appel à l'allocation dynamique de mémoire. Il permet de remplacer un routine telle que

copy(Bidule& destination, Bidule& source);

par l'expression plus habituelle

destination = source;

De même, pour additionner deux objets et mettre le résultat dans un troisième, nous pouvons toujours écrire une fonction telle que :

add(Bidule& a, Bidule& b, Bidule& resu);

ou, dans une philosophie objet, quelque chose comme

Resu.add(Bidule& a, Bidule& b);

mais la syntaxe la plus claire est évidemment

Resu = a + b;

Dans le même ordre d'idée, il serait intéressant de pouvoir redéfinir les opérateurs logiques de comparaison (>, <, >=, <=, ==, !=) afin de les utiliser directement dans des tests. Par exemple, si nous considérons une application qui gère une classe Bidule, nous pourrions avoir un code qui ressemble à ceci :

CPP - Operators 1 - Introduction

H. Schyns 1.2

class Bidule // créer une classe quelconque { char *nom; unsigned long dateanniversaire; double salaire; x // signifie que d'autres lignes de code suivent }; void main (void) { Bidule x, y; x if (x == y) // comparer certains data membres de la classe { x } x };

Sauf avis contraire de concepteur, pour l'évaluation du test

if (x == y)

le compilateur génère une simple comparaison byte pour byte. Ceci ,e correspond pas nécessairement aux vœux du concepteur si la classe contient plusieurs data membres ; a fortiori si certains d'entre eux font appel à de l'allocation dynamique. Nous devrons donc définir ce que nous entendons par "identité de deux objets de classe Bidule" : s'agit-il seulement de comparer les noms, les dates d'anniversaire ou faut-il que tous les data membres soient égaux ?

Par ailleurs, les mathématiques ont inventé (1) des tas d'objets tels que fractions, nombres complexes, polynômes qui suivent leurs propres règles arithmétiques, différentes de celles de leurs composants. Par exemple, deux fractions ne s'additionnent pas comme deux entiers. Ici, nous devrons redéfinir les notions d'addition, de soustraction, de multiplication, etc.

Dans le chapitre suivant, pour illustrer notre propos, nous définirons une classe Fraction et nous montrerons comment redéfinir les opérateurs arithmétiques.

Nous avons choisi cet exemple car il permet d'illustrer pratiquement tous les opérateurs. Il est clair que, dans la réalité du développement d'une application ou d'une classe quelconque, on ne développe que les opérateurs qui ont une utilité ou un sens pour l'utilisateur.

Il nous faut donc définir des opérateurs qui acceptent des classes différentes de part et d'autre.

Deux cas peuvent se présenter :

- la classe différente est à droite de l'opérateur Fraction + long ;

Ce cas de ne présente pas de difficulté majeure

- la classe différente est à gauche de l'opérateur long + Fraction ;

1 Il est communément admis par les philosophes que les mathématiciens n'inventent rien ; ils découvrent de

nouveaux territoires.

CPP - Operators 1 - Introduction

H. Schyns 1.3

Nous illustrerons ensuite la surcharge des opérateurs de comparaison dans le cas d'une classe quelconque.

Nous verrons ensuite la surcharge des opérateurs de saisie >> et <<.

La surcharge des opérateurs spéciaux () et [] est expliquée dans le document relatif aux conteneurs.

Nous verrons que les opérateurs ne nous permettent de faire face à toutes les situations. Il nous faudra introduire des fonctions "hors classe" et des fonctions "friend", ce qui fera l'objet d'un autre document.

CPP - Operators 2 - Les opérateurs arithmétiques

H. Schyns 2.1

2. Les opérateurs arithmétiques

2.1. Syntaxe

Nous avons vu que le but premier de la surcharge des opérateurs est de permettre une grande simplification de la syntaxe.

En réalité, lorsqu'il rencontre une expression telle que

a + b

le compilateur la remplace par un appel de fonction du type

a.operator+(b)

operator+ est le nom d'une fonction membre de la classe à laquelle appartient l'objet a. Elle prend un objet (ou une référence sur un objet) de classe b en paramètre (1). Il n'est absolument pas nécessaire que a et b appartiennent à la même classe.

Pour la bonne compréhension du mécanisme, il est important de noter que

l'opérateur appartient toujours à la classe de l'objet qui le précède !

En poursuivant le raisonnement, une instruction telle que

c = a + b

fait appel à deux opérateurs :

- l'opérateur d'affectation = de la classe à laquelle appartient c, - l'opérateur d'addition + de la classe à laquelle appartient a.

A nouveau, il n'est absolument pas nécessaire que a, b et c appartiennent à la même classe.

L'instruction ci-dessus est traduite sous la forme

c.operator=( a.operator+(b) )

C'est un peu déroutant au début mais on s'y fait assez vite.

2.2. La classe Fraction

Pour illustrer la démarche, créons une classe Fraction. Chacun se souvient – parfois vaguement – que les fractions ne s'additionnent pas comme les entiers : il faut passer par une mise au même dénominateur. Ce comportement quelque peu "anormal" est implémenté grâce à la surcharge des opérateurs arithmétiques.

1 + - * / etc. sont des caractères auxquels nous donnons une signification particulière. Pour le compilateur, il

s'agit de caractères (presque) comme tous les autres, et il tolère leur usage dans un nom de fonction… sous certaines conditions.

CPP - Operators 2 - Les opérateurs arithmétiques

H. Schyns 2.2

Commençons par définir la classe et ses fonctions canoniques (1) :

class Fraction //fraction.h { protected : long num; long den; public : Fraction (void); Fraction (long anum, long aden); Fraction (const Fraction& source); Fraction& operator= (const Fraction& source); void set (long anum, long aden); void get (long& anum, long& aden); void display (void); };

La fonction display() est provisoire. Elle nous permettra de voir ce qui se passe dans les tests.

Le mot-clé const indique que, bien que le paramètre soit passé par référence, la fonction n'a pas le droit de le modifier.

L'implémentation ne présente pas de difficulté majeure. Il faut simplement se souvenir qu'une fraction ne peut avoir de dénominateur nul. Le constructeur de base initialise la fraction à zéro avec un dénominateur unitaire.

Comme une fraction négative peut s'écrire

dn

dn

dn −=

−=−

nous décidons de faire systématiquement porter le signe par le numérateur.

//fraction.cpp //----------------- constructeurs ---------------- Fraction::Fraction(void) { num = 0l; // l pour long den = 1l; } Fraction::Fraction (long anum, long aden) { if (!aden) return; // denominateur nul ? num = anum; den = aden; if (den <0) // signe porte par le numerateur { num = -num; den = -den; } }

1 Pour éviter d'alourdir le code, nous ne noterons pas les #include, #ifndef, et autres using namespace.

Le lecteur saura les replacer là où ils sont nécessaires.

CPP - Operators 2 - Les opérateurs arithmétiques

H. Schyns 2.3

Fraction::Fraction (const Fraction& source) { if(&source == this) return; num = source.num; den = source.den; } //-------------------- operateurs ---------------- Fraction& Fraction::operator= (Fraction& source) { if(&source != this) { num = source.num; den = source.den; } return(*this); } //-------------------- accesseurs ---------------- void Fraction::set (long anum, long aden) { if (!aden) return; // denominateur nul ? num = anum; den = aden; if (den <0) { num = -num; den = -den; } } void Fraction::get (long& anum, long& aden) { anum = num; aden = den; } //-------------------- display ----------------- void Fraction::display(void) { cout << '[' << num << '/' << den << ']'; }

Testons ces fonctions de base :

void main (void) //textfrac.cpp { Fraction a(4,7), b; Fraction c=a; long n, d; a.display(); cout << endl; // [4/7] b.display(); cout << endl; // [0/1] c.display(); cout << endl; // [4/7] b.set(5, 9); b.display(); cout << endl; // [5/9] a.get(n, d); cout << n << endl << d << endl; // 4 7 }

2.3. Arithmétique entre objets de la même classe

2.3.1. L'opérateur d'addition

Pour ceux qui auraient oublié, rappelons que l'addition de deux fractions s'écrit

CPP - Operators 2 - Les opérateurs arithmétiques

H. Schyns 2.4

yb

xbyayx

ba

⋅⋅+⋅

=+

Repartons de l'instruction

c = a + b ;

dans laquelle a, b et c sont des objets de la classe Fraction. Elle fait appel à deux opérateurs :

- l'opérateur d'affectation = de l'objet c, - l'opérateur d'addition + de l'objet a,

Il est important de noter que

- les objets a et b ne sont pas modifiés par l'instruction, - l'opérateur = a besoin d'un paramètre qu'il doit récupérer sur la stack

Nous en concluons que l'opérateur d'addition de l'objet a doit stocker le résultat de l'addition dans un Fraction temporaire pour puis le mettre sur la stack où il sera récupéré par l'opérateur = de l'objet c.

class Fraction //fraction.h { x public : x Fraction operator+ (const Fraction& bfrac); x };

L'implémentation s'écrit

//fraction.cpp //----------------- operateurs ---------------- x Fraction Fraction::operator+ (const Fraction& bfrac) { Fraction resu; resu.num = num*bfrac.den + den*bfrac.num; resu.den = den*bfrac.den; return (resu); }

Notons bien que c'est un objet Fraction qui est renvoyé sur la stack et non une référence. En effet, l'objet resu est local à la fonction et est détruit dès la sortie, ce qui invaliderait une référence. En réalité, la stack contient une copie de resu. Nous pouvons le vérifier en traçant l'appel de operator=.

En utilisant le constructeur avec paramètres, nous rendons le code plus compact :

//fraction.cpp //----------------- operateurs ---------------- x Fraction Fraction::operator+ (const Fraction& bfrac) { Fraction resu(num*bfrac.den + den*bfrac.num , den*bfrac.den); return (resu); }

CPP - Operators 2 - Les opérateurs arithmétiques

H. Schyns 2.5

Testons cela :

void main (void) //textfrac.cpp { Fraction a(4,7), b(3,5), c; c = a + b; cout << "c = a + b : " << endl; a.display(); cout << endl; // [4/7] b.display(); cout << endl; // [3/5] c.display(); cout << endl; // [41/35] }

2.3.2. Autres opérateurs arithmétiques

Nous définissons les autres opérateurs arithmétiques selon le même principe :

class Fraction //fraction.h { x public : x Fraction operator+ (const Fraction& bfrac); Fraction operator- (const Fraction& bfrac); Fraction operator* (const Fraction& bfrac); Fraction operator/ (const Fraction& bfrac); x };

L'implémentation est similaire, si on se souvient que

yb

xbyayx

ba

⋅⋅−⋅

=− ybxa

yx

ba

⋅⋅=⋅

xbya

xy

ba

yx

ba

⋅⋅

=⋅=

Un peu de prudence s'impose avec la division dans le cas où le numérateur du paramètre est nul mais ce cas sera pris en compte par le constructeur :

//fraction.cpp //----------------- operateurs ---------------- x Fraction Fraction::operator- (const Fraction& bfrac) { Fraction resu(num*bfrac.den - den*bfrac.num , den*bfrac.den); return (resu); } Fraction Fraction::operator* (const Fraction& bfrac) { Fraction resu(num*bfrac.num , den*bfrac.den); return (resu); } Fraction Fraction::operator/ (const Fraction& bfrac) { Fraction resu(num*bfrac.den, den*bfrac.num); return (resu); }

Testons cela :

CPP - Operators 2 - Les opérateurs arithmétiques

H. Schyns 2.6

void main (void) //textfrac.cpp { Fraction a(4,7), b(3,5), c(1,4), d(2,3), r; r = a + b; r.display(); cout << endl; // [41/35] r = b - c; r.display(); cout << endl; // [7/20] r = a * d; r.display(); cout << endl; // [8/21] e = (a + b)*(b – c) / (a * d); e.display();cout << endl; // [6027/5600] }

Il serait utile de disposer d'une fonction de simplification des fractions. Nous verrons cela plus tard.

2.3.3. Les opérateurs combinés

A la différence des opérateurs arithmétiques + - * / qui laissent intacts les objets impliqués, les opérateurs combinés += -= *= /= modifient l'objet courant. Il n'y a donc pas création d'un objet temporaire intermédiaire.

Toutefois, comme les syntaxes du type

c = a += b ;

sont admises, il faut que l'opérateur += de l'objet a renvoient quelque chose sur la stack afin de satisfaire l'opérateur = de l'objet c. Dans le cas présent, nous renverrons la référence de l'objet a qui vient d'être modifié, qui existait déjà et qui existe toujours (1).

class Fraction //fraction.h { x public : x const Fraction& operator+= (const Fraction& bfrac); const Fraction& operator-= (const Fraction& bfrac); const Fraction& operator*= (const Fraction& bfrac); const Fraction& operator/= (const Fraction& bfrac); x };

Le mot-clé const au début de chaque déclaration exprime que, bien que la fonction renvoie une référence, la routine qui la récupère ne peut pas modifier l'objet.

//fraction.cpp //---------- operateurs combinés -------------- const Fraction& Fraction::operator+= (const Fraction& bfrac) { num = num*bfrac.den - den*bfrac.num; den *= bfrac.den; return (*this); }

1 Rappelons que les opérateurs de priorités identiques sont appelés à partir de la droite et en allant vers la

gauche. Ici += sera exécuté avant =.

CPP - Operators 2 - Les opérateurs arithmétiques

H. Schyns 2.7

const Fraction& Fraction::operator-= (const Fraction& bfrac) { num = num*bfrac.den - den*bfrac.num; den *= bfrac.den; return (*this); } const Fraction& Fraction::operator*= (const Fraction& bfrac) { num *= bfrac.num; den *= bfrac.den; return (*this); } const Fraction& Fraction::operator/= (const Fraction& bfrac) { if (bfrac.num) // numérateur de b non nul { num *= bfrac.den; den *= bfrac.num; } return (*this); }

Testons cela :

void main (void) //textfrac.cpp { Fraction a(4,7), b(3,5), c(1,4), d(2,3); a += b; a.display(); cout << endl; // [41/35] b -= c; b.display(); cout << endl; // [7/20] c *= d; c.display(); cout << endl; // [2/12] c /= d; c.display(); cout << endl; // [6/24] }

2.3.4. Opérateurs arithmétiques unaires

On appelle opérateur unaire (ang.: unary operator) un opérateur qui ne fait intervenir qu'une seule opérande. La norme C++ admet plusieurs opérateurs unaires :

Operator Usage Effet Retour - b = -a renvoie l'opposé de a mais ne modifie pas a Objet + b = +a renvoie la valeur de a mais ne la modifie pas Référence

++ (pré) b = ++a incrémente a et renvoie la valeur incrémentée Référence ++ (post) b = a++ renvoie valeur actuelle de a puis incrémente a Objet -- (pré) b = --a décrémente a et renvoie la valeur décrémentée Référence -- (post) b = a-- renvoie valeur actuelle de a puis décrémente a Objet

La fonction renvoie une référence si l'état renvoyé est identique à celui de l'objet à la fin de l'opération; sinon elle renvoie un objet.

Les opérateurs unaires ne prennent pas de paramètres ce qui est une entorse au principe selon lequel l'opérateur se rapporte à l'objet qui le précède. Toutefois, les opérateurs de post-incrémentation (a++) et post-décrémentation (a--) prennent un paramètre (int) dont le rôle purement syntaxique sert à les distinguer des pré-incrémentation et pré-décrémentation.

CPP - Operators 2 - Les opérateurs arithmétiques

H. Schyns 2.8

class Fraction //fraction.h { x public : x Fraction operator- (void); // operateur - unaire Fraction& operator+ (void); // operateur + unaire Fraction& operator++ (void); // operateur ++ preincrement Fraction& operator-- (void); // operateur -- predecrement Fraction operator++ (int); // operateur ++ postincrement Fraction operator-- (int); // operateur -- postdecrement x };

Nous pouvons nous demander pourquoi il faut un operator+ unaire qui n'a finalement aucun effet. Tout simplement pour une raison de cohérence et de symétrie.

Voyons l'implémentation :

//fraction.cpp //---------- operateurs unaires -------------- Fraction Fraction::operator- (void) { Fraction resu(-num, den); // changement de signe return(resu); } Fraction& Fraction::operator+ (void) { return(*this); // pas de changement de signe } Fraction& Fraction::operator++ (void) { num += den; // pré incrément return(*this); } Fraction& Fraction::operator-- (void) { num -= den; // pré décrément return(*this); } Fraction Fraction::operator++ (int) { Fraction resu (num+den, den); // post incrément return(resu); } Fraction Fraction::operator-- (int) { Fraction resu (num-den, den); // post incrément return(resu); }

Testons ce nouvelles fonctions :

void main (void) //textfrac.cpp { Fraction a(4,7), b(3,5), c(2,9), d;

CPP - Operators 2 - Les opérateurs arithmétiques

H. Schyns 2.9

d = a++; d.display(); cout << endl; // [4/7] a.display(); cout << endl; // [11/7] d = -b; d.display(); cout << endl; // [-3/5] b.display(); cout << endl; // [3/5] d = ++b; d.display(); cout << endl; // [8/5] b.display(); cout << endl; // [8/5] d = --c; d.display(); cout << endl; // [-7/9] c.display(); cout << endl; // [-7/9] d = ++(a+b); d.display(); cout << endl; // [146/35] a.display(); cout << endl; // [11/7] b.display(); cout << endl; // [8/5] }

Le dernier paragraphe est particulièrement intéressant :

d = ++(a+b);

L'operator+ de la parenthèse crée un objet temporaire (resu) dont une copie est placée sur la stack. C'est cet objet temporaire qui est incrémenté avant d'être passé à l'operator= de d. Ensuite, il est détruit.

En utilisant la post-incrémentation, l'instruction donnerait un tout autre résultat :

d = (a+b)++;

L'objet intermédiaire issu de l'addition est recopié dans d puis incrémenté puis détruit. Cette incrémentation n'a aucun impact sur a, b ou d ; elle est inutile.

2.4. Arithmétique entre objets de classes différentes

Tout ceci est bien joli et, en fin de compte, pas trop compliqué. Les routines se ressemblent (presque) toutes et il suffit d'un copier/coller "intelligent" pour les implémenter.

Pourtant, malgré tous nos efforts une expression telle que

c = a + 1 ;

dans laquelle a et c appartiennent à la classe Fraction provoquera l'ire du compilateur. Pourquoi ?

Le problème vient du fait que l'opérateur appartient à la classe de l'opérande de gauche [ a ] qui est une Fraction et que le paramètre est l'opérande de droite [ 1 ] qui est de type int ou long !

Le compilateur – qui ignore totalement ce qu'est une fraction du point de vue mathématique – parcourt donc la déclaration de la classe à la recherche d'une fonction qui aurait pour prototype quelque chose comme :

Fraction Fraction::operator+ (long x) ;

CPP - Operators 2 - Les opérateurs arithmétiques

H. Schyns 2.10

Hélas, les seules fonctions qu'il trouve sont celles qui prennent une Fraction en paramètre… Il nous faut donc définir des opérateurs qui acceptent des classes différentes de part et d'autre.

Deux cas peuvent se présenter :

- la classe différente est à droite de l'opérateur Fraction + long ; Ce cas ne présente pas de difficulté majeure, il suffit de définir de nouveaux

opérateurs pour la classe, ce qui sera fait dans le paragraphe suivant.

- la classe différente est à gauche de l'opérateur long + Fraction ; Ce cas est un peu plus subtil, il faudra écrire des opérateurs "hors classe", ce

qui sera fait au paragraphe 2.4.2.

2.4.1. Opérateurs arithmétiques "inter-classes"

Nous voici donc repartis pour un tour, à redéfinir tous les opérateurs et opérateurs combinés pour qu'ils acceptent un long en paramètre :

class Fraction //fraction.h { x public : x Fraction operator+ (long x); Fraction operator- (long x); Fraction operator* (long x); Fraction operator/ (long x); const Fraction& operator+= (long x); const Fraction& operator-= (long x); const Fraction& operator*= (long x); const Fraction& operator/= (long x); x };

Rappelons que le mot-clé const au début de chaque déclaration exprime que, bien que la fonction renvoie une référence, la routine qui la récupère ne peut pas modifier l'objet.

Devrons-nous ensuite répéter l'opération pour les char, double et float ?

Non, car le compilateur "sait" comment convertir ("caster") un char, double ou float en long, quitte à perdre de la précision dans l'opération (d'où un "warning" éventuel à la compilation).

//fraction.cpp //------------- operateurs inter-classe -------------- Fraction Fraction::operator+ (long x) { Fraction resu(num + den*x , den); return (resu); } Fraction Fraction::operator- (long x) { Fraction resu(num - den*x , den); return (resu); } x

CPP - Operators 2 - Les opérateurs arithmétiques

H. Schyns 2.11

//------ operateurs inter-classe combines ----------- const Fraction& Fraction::operator+= (long x) { num += den*x; return (*this); } const Fraction& Fraction::operator-= (long x) { num -= den*x return (*this); } x

Testons cela :

void main (void) //textfrac.cpp { Fraction a(4,7), b(3,5), c; long x = 3; float y = 9.81; c = a + x; c.display(); cout << endl; // [25/7] b -= y; b.display(); cout << endl; // [-42/5] c *= 2; c.display(); cout << endl; // [50/7] c = a / x; c.display(); cout << endl; // [4/21] }

Ouf ! nous sommes tirés d'affaire !

2.4.2. Opérateurs arithmétiques "hors classe"

L'idée est bonne, mais non, nous ne sommes par encore au bout de nos peines. En effet, si l'instruction

c = a + 1 ;

dans laquelle a et c appartiennent à la classe Fraction passe bien. Mais si nous permutons les opérandes, plus rien ne va !

c = 1 + a ;

Pourtant, du point de vue mathématique, c'est la même chose.

Hélas, le compilateur ne comprend rien aux maths et la commutativité de l'addition est quelque chose qui lui échappe totalement.

Le problème vient à nouveau du fait que l'opérateur appartient à la classe de l'opérande de gauche [ 1 ] qui est de type int ou long alors que le paramètre est l'opérande de droite [ a ] qui est une Fraction !

Cette fois, le problème est de taille car il nous est impossible d'ajouter une fonction membre à la classe long. Elle fait partie d'une bibliothèque complètement fermée. Certes, nous pourrions dériver une classe personnelle mylong à partir de la classe long et ajouter les fonctions membres voulues. Mais ce ne sera sûrement pas au goût des développeurs !

CPP - Operators 2 - Les opérateurs arithmétiques

H. Schyns 2.12

Le truc consiste à créer un opérateur "hors classe" (1) :

- d'un point de vue syntaxique, il s'utilise comme tout autre opérateur; - d'un point de vue fonctionnel, tout se passe comme si on ajoutait un opérateur à

une classe "fermée" (2); - d'un point de vue organisationnel, la fonction est déclarée après notre classe.

class Fraction //fraction.h { x }; // fin de la déclaration de classe //------------ fonctions hors classe ------------- Fraction operator+(long a, Fraction& bfrac); Fraction operator-(long a, Fraction& bfrac); x

Ces opérateurs prennent deux paramètres :

- le premier donne le type de l'opérande "de gauche" (la classe fermée); - le second donne le type de l'opérande "de droite" (notre classe); - le type de la valeur en retour peut être quelconque.

Ces fonctions nous permettront donc d'écrire des instructions telles que

Fraction = long + Fraction ;

Implémentons-les à la suite des autres fonctions. Attention, comme il s'agit de fonctions "hors classe", elle ne sont pas préfixées par Fraction::.

//fraction.cpp //------------ fonctions hors classe ------------- Fraction operator+(long a, Fraction& bfrac) { long bnum, bden; bfrac.get(bnum, bden); Fraction resu (a*bden + bnum, bden); return (resu); } Fraction operator-(long a, Fraction& bfrac) { long bnum, bden; bfrac.get(bnum, bden); Fraction resu (a*bden - bnum, bden); return (resu); } x

Comme il s'agit de fonctions "hors classe" elles ne peuvent pas accéder directement aux data membres num et den de la Fraction. Elles doivent obligatoirement passer par l'accesseur get() ce qui est plus lent (3).

Remarquons aussi que le paramètre Fraction n'est pas marqué const. Si nous le faisons, le compilateur nous envoie un message d'erreur.

1 L'opérateur "hors classe" n'est finalement qu'une fonction "C" indépendante 2 Par classe "fermée", on entend une classe écrite par quelqu'un d'autre, à laquelle il est interdit de toucher

et donc d'ailleurs, nous ne possédons pas les sources. 3 Nous verrons dans un autre chapitre que cette contrainte peut être contournée par une déclaration

"friend".

CPP - Operators 2 - Les opérateurs arithmétiques

H. Schyns 2.13

En effet le préfixe const interdit à la routine de modifier le paramètre bfrac. Or, la routine doit utiliser la fonction get() dont elle ignore absolument si elle modifie ou non bfrac. Elle ne peut donc pas garantir le fait que bfrac ne sera pas modifié (1).

Les autres opérateurs s'écrivent de manière similaire.

Testons ces operateurs :

void main (void) //textfrac.cpp { Fraction a(4,7), b(3,5), c; long x = 3; float y = 9.81; c = x + a; c.display(); cout << endl; // [25/7] c = y - b; c.display(); cout << endl; // [42/5] }

2.4.3. "Cast" par constructeur

Une autre manière de nous en tirer serait de transformer un type numérique quelconque en fraction grâce à une sorte de "cast". En d'autres mots, il nous faut construire une fraction à partir d'un type numérique ; il nous faut donc un constructeur !

class Fraction //fraction.h { public : x Fraction (long anum, long aden); Fraction (long a); // nouveau constructeur x };

Implémentons-le à la suite des autres constructeurs :

//fraction.cpp //----------------- constructeurs ---------------- x Fraction::Fraction(long a) { num = a; den = 1l; // l pour long } x

Remarquons que le constructeur que nous venons de créer ressemble à celui qui existait déjà. Il suffirait de passer un dénominateur unitaire. Or C++ nous permet de créer des fonctions avec paramètres optionels. Pour que le constructeur à deux paramètres puisse aussi servir avec un seul, il suffit d'écrire :

1 Pour "rassurer" la routine, nous devons certifier que get() ne modifie par l'objet courant en changeant sa

définition dans la déclaration de classe et lors de l'implémentation : void get (long& anum, long& aden) const;

CPP - Operators 2 - Les opérateurs arithmétiques

H. Schyns 2.14

class Fraction //fraction.h { public : x Fraction (long anum, long aden = 1l); x };

Attention : il suffit de modifier l'en-tête dans la déclaration de classe; le code, lui, doit rester inchangé.

Testons cela :

void main (void) //textfrac.cpp { Fraction a(4,7), b(3,5), c; long x = 3; float y = 9.81; c = a + Fraction(x); c.display(); cout << endl; // [25/7] b -= Fraction(y); b.display(); cout << endl; // [-42/5] }

Notons que dans la représentation binaire d'un float ou d'un double est précisément une fraction que nous pouvons reconstruire en explorant le motif binaire (1). Cette fonction est présentée dans l'annexe.

2.4.4. Operateur de "cast"

Dans les exemples précédents, nous avons vu comment transformer une variable de type prédéfini (p.ex.: long) ou autre en une Fraction.

Inversement, nous pouvons définir une opérateur, appelé opérateur de cast, qui transforme une Fraction en un type prédéfini (p.ex.: double) ou autre.

class Fraction //fraction.h { public : x operator double (void); // transforme une Fraction en double x };

Plusieurs choses sont à remarquer au niveau de la syntaxe :

- il y a un espace obligatoire entre le mot-clé operator et le type, - l'opérateur ne prend pas de paramètre, - l'opérateur ne renvoie rien, pas même un void.

Dans le cas qui nous intéresse, la fonction s'implémente sous la forme

Fraction::operator double (void) //fraction.cpp { return ((double)num / (double)den); }

1 Voir les chapitres du cours de "Structure des Ordinateurs" intitulés "Binaire et Hexadécimal" et "Codage de

l'information".

CPP - Operators 2 - Les opérateurs arithmétiques

H. Schyns 2.15

Attention : il faut bien "caster" le numérateur et dénominateur de la fraction avant d'effectuer la division sinon nous nous retrouverons avec un résultat entier.

A présent, le compilateur peut se tirer de toutes les situations embarrassantes :

void main (void) //textfrac.cpp { Fraction a(4,7), b(3,5), c; float y; // cast de Fraction vers double puis float y = (9.81 + a + b)/2. ; cout << y << endl; }

En fait, la situation n'est pas si simple… devant l'abondance de fonction, le compilateur n'arrive plus toujours à savoir laquelle choisir (voir chap 6).

CPP - Operators 3 - Les opérateurs d'entrée/sortie

H. Schyns 3.1

3. Les opérateurs d'entrée/sortie

Jusqu'à présent nous avons utilisé la fonction display() pour afficher le contenu d'une fonction.

Cette fonction présente deux inconvénients :

- elle ne peut diriger sa sortie que vers les flux standard cin et cout, - elle ne permet pas de chaîner les sorties comme on le fait pour les autres types.

De plus, nous aimerions pouvoir écrire les instructions d'entrée/sortie sous la forme classique

cout << afrac; // afrac est une Fraction cin >> afrac;

L'analyse de ces instructions révèle que c'est le paramètre de l'opérateur qui est du type Fraction. Par contre, les opérateurs >> et << appartiennent aux classes de cout, soit ostream; et cin, soit istream. Ces deux classes sont des classes "fermées" auxquelles nous ne pouvons pas ajouter de fonction.

Nous devrons donc appliquer ce qui a été vu au point 2.4.2 et construire deux opérateurs "hors classe".

class Fraction //fraction.h { x }; // fin de la déclaration de classe //------------ fonctions hors classe ------------- ostream& operator<<(ostream& sout, Fraction& afrac); istream& operator>>(istream& sin, Fraction& afrac); x

Notons que le premier paramètre peut être n'importe que stream de sortie (ou d'entrée) et pas seulement cout (ou cin).

ostream& operator<<(ostream& sout, Fraction& afrac) { long anum, aden; afrac.get(anum, aden); if (aden==1) // afficher comme un nombre entier sout << anum; else sout << '[' << anum << '/' << aden << ']'; return (sout); } istream& operator>>(istream& sin, Fraction& afrac) { long anum, aden; sin >> anum >> aden; afrac.set(anum, aden); // fait les tests de validation return (sin); }

Le premier opérateur n'affiche que le numérateur lorsque le dénominateur est unitaire. Sinon, la fraction est affichée entre crochets.

Les fonctions renvoient la référence du stream sur la stack, ce qui peut sembler contradictoire avec ce que nous avons vu pour les opérateurs arithmétiques.

CPP - Operators 3 - Les opérateurs d'entrée/sortie

H. Schyns 3.2

Ceci vient du fait qu'une expression chaînée telle que

cout << "Fraction : " << afrac << endl;

est explorée à partir de la gauche non à partir de la droite comme les expressions arithmétiques.

Dans le même ordre d'idées, nous pourrions définir une classe Formulaire, chargée de la saisie et de l'affichage des données d'une classe Bidule et définir les fonctions :

Formulaire& Formulaire::operator<<(Bidule& abidule); Formulaire& Formulaire::operator>>(Bidule& abidule);

CPP - Operators 4 - Les opérateurs logiques

H. Schyns 4.1

4. Les opérateurs logiques

Il y a six opérateurs de comparaison (>, >=, <, <=, ==, !=) mais, en réalité, un seul est strictement nécessaire : soit operator< , soit operator> ; les autres s'obtiennent par combinaison. En effet :

Si Alors a > b est vrai b < a est vrai

a >= b est vrai a < b est faux a <= b est vrai b < a est faux a == b est vrai a < b est faux et b < a est faux a != b est vrai a < b est vrai ou b < a est vrai

Nous voyons que la colonne de droite n'utilise que différentes combinaisons de l'opérateur <. Cette manière de faire peut sembler barbare mais elle certifie la cohérence des opérateurs dès que le critère de comparaison devient un peu complexe. Dès lors, elle est beaucoup plus sûre.

Nous savons que :

si dc

ba > alors cbda ⋅>⋅

à condition que [ b ] et [ d ] soient positifs, ce qui est le cas dans une Fraction puisque nous avons décidé que le signe éventuel est porté par le numérateur.

class Fraction //fraction.h { x bool operator> (const Fraction& bfrac) const; bool operator< (const Fraction& bfrac) const; bool operator>= (const Fraction& bfrac) const; bool operator<= (const Fraction& bfrac) const; bool operator== (const Fraction& bfrac) const; bool operator!= (const Fraction& bfrac) const; x }

Les opérateurs renvoient une valeur booléenne. Le paramètre bfrac est marqué const pour indiquer qu'il n'est pas modifié par la fonction : de même, la fonction est marquée par le suffixe const pour signaler qu'elle ne modifie pas l'objet courant *this.

Implémentons les fonctions :

//fraction.cpp bool Fraction::operator> (const Fraction& bfrac) const { return (num*bfrac.den > den*bfrac.num); } bool Fraction::operator< (const Fraction& bfrac) const { return (bfrac > *this); }

CPP - Operators 4 - Les opérateurs logiques

H. Schyns 4.2

bool Fraction::operator<= (const Fraction& bfrac) const { return !(*this > bfrac); } bool Fraction::operator>= (const Fraction& bfrac) const { return !(bfrac > *this); } bool Fraction::operator!= (const Fraction& bfrac) const { return ((bfrac > *this) || (*this > bfrac)); } bool Fraction::operator== (const Fraction& bfrac) const { return (!(bfrac > *this) && !(*this > bfrac)); }

Bien entendu, nous pouvons aussi définir les opérateurs qui comparent une fraction à un entrier ou à un réel.

Un petit test qui crée NFRAC fractions aléatoires et les classe en ordre décroissant :

//textfrac.cpp x // autres #define et #include #define NFRAC 10 void main (void) { Fraction *vfrac, ftrav; // un pointeur et une fraction int i, j; // deux int pour les boucles time_t t; // un temps pour le gener. aleat. vfrac = new Fraction[NFRAC]; // allouer le vecteur srand((unsigned) time(&t)); // amorcer le gener. aleat. for (i=0; i<NFRAC; i++) { // remplir les fractions vfrac[i].set(rand()%49, rand()%49); cout << vfrac[i] << " "; } cout << endl; // trier en ordre décroissant for (i=0; i<NFRAC-1; i++) // simple tri bulle for (j=i+1; j<NFRAC; j++) { // ICI : test de operator< if (vfrac[i] < vfrac [j]) { ftrav = vfrac[i]; vfrac[i] = vfrac[j]; vfrac[j] = ftrav; } } for (i=0; i<NFRAC; i++) // afficher la liste cout << vfrac[i] << " "; cout << endl; delete [] vfrac; }

CPP - Operators 5- Généralisation

H. Schyns 5.1

5. Généralisation

Les opérateurs ne s'appliquent pas uniquement à des objets mathématiques. Nous pouvons les utiliser chaque fois qu'ils permettent une simplification de la syntaxe.

Par exemple, reprenons la classe Pile définie dans un autre chapitre et adaptons-la pour en faire une pile de Fractions (1).

class PileFrac //pilefrac.h { protected : Fraction *vfrac; // base de la pile int nelem; // nombre de places prévues int nstore; // nombre d'elements stockes int idx; // index sur l'element courant int incr; // increment public : PileFrac (); // constructeurs PileFrac (const PileFrac& source); ~PileFrac (); // destructeurs x }

5.1. Une nouvelle algèbre !

Grâce aux opérateurs, nous pouvons définir une "algèbre de piles". A nous de définir le rôle de chaque fonction. Par exemple :

PileFrac& PileFrac::operator+= (const PileFrac& apile) PileFrac PileFrac::operator+ (const PileFrac& apile)

La première fonction ajoute tout le contenu d'une PileFrac apile à la PileFrac courante.

La seconde crée une nouvelle PileFrac qui combine les éléments de la Pilefrac courante et d'une PileFrac apile passée en paramètre.

PileFrac& PileFrac::operator-= (const PileFrac& apile) PileFrac PileFrac::operator- (const PileFrac& apile)

La première fonction retire (détruit) de la PileFrac courante tous les éléments déjà présents dans la PileFrac apile passée en paramètre.

La seconde crée une nouvelle PileFrac qui ne contient que les éléments venant de la première PileFrac et non repris dans la PileFrac apile passée en paramètre.

PileFrac& PileFrac::operator++ (void) PileFrac& PileFrac::operator-- (void)

Les fonctions déplacent l'index idx vers l'élément précédent ou l'élément suivant de la PileFrac courante.

1 Nous verrons plus tard comment les patrons de classe <Template> permettent de résoudre élégamment ce

problème.

CPP - Operators 5- Généralisation

H. Schyns 5.2

Dans le même ordre d'idées, nous pouvons définir une algèbre entre les PileFrac et les Fractions :

PileFrac& PileFrac::operator>> (Fraction& afrac) PileFrac& PileFrac::operator<< (const Fraction& afrac)

Ces fonctions ajoutent ou retirent une Fraction de la PileFrac. A nous de décider si c'est "par le haut" ou "par le bas". Notons qu'il s'agit de fonction "de classe" et non de fonctions "hors classe" puisqu'elle appartiennent à la classe que nous sommes en train d'écrire.

Nous pouvons aussi donner un autre sens aux opérateurs

PileFrac& PileFrac::operator+= (const Fraction& afrac) PileFrac& PileFrac::operator-= (const Fraction& afrac)

L'une, semblable à operator<<, ajoute une Fraction à la PileFrac courante; l'autre retire la Fraction afrac de la PileFrac courante si elle s'y trouve déjà.

Et ainsi de suite…

5.2. operator [ ]

Appliqué à la PileFrac, operator[] nous permet d'introduire un accesseur à l'un des éléments du tableau pointé par vfrac qui soit plus facile à manipuler que les traditionnels set et get.

class PileFrac //pilefrac.h { x public : Fraction& operator[] (const aidx); x }

Dans l'implémentation, nous pouvons ajouter un contrôle d'erreur élémentaire :

Fraction& PileFrac::operator[] (const aidx) //pilefrac.cpp { if (aidx < 0) return (vfrac[0]); //ca va faire des dégats c'est sûr else if (aidx >= nstore) return (vfrac[nstore-1]); //ca aussi ! else return (vfrac[aidx]); // ca c'est correct }

Notons que, ni dans la déclaration, ni dans l'implémentation, le paramètre n'est pas entre les crochets !

Comme l'opérateur renvoie une référence sur une fraction, on peut l'utiliser aussi bien comme rvalue que comme lvalue … moyennant quelques précautions (1) :

1 Pour faire simple, rappelons qu'une lvalue est une expression qui peut se trouver à gauche d'un signe

d'affectation (variable, pointeur ou référence) tandis que la rvalue est une expression qui peut se trouver à droite de l'affectation (valeur, référence ou adresse).

CPP - Operators 5- Généralisation

H. Schyns 5.3

void main (void) //testpile.cpp { PileFrac pilef; Fraction frac; int i; // remplir la pile avant! for (i=0; i<10; i++) pilef << Fraction(i+1, i+2); // maintenant, on peut utiliser for (i=1; i<9; i++) // l'operateur pilef[i] = pilef[i-1] + pilef[i+1]; }

Notons que, lors de l'utilisation, le paramètre est entre les crochets !

5.3. Opérateur d'appel de fonction

C++ définit aussi un opérateur un peu spécial operator() appelé opérateur d'appel de fonction que nous pouvons aussi surcharger.

Cet opérateur permet à une classe d'effectuer une fonction - que l'on pourrait appeler "fonction signature" - définie par le concepteur de la classe.

Nous pouvons faire une version de cette opérateur qui renvoie le nombre d'éléments stockés :

class PileFrac //pilefrac.h { x public : int operator() (void); x }

Implémentons-la :

int PileFrac::operator() (void) //pilefrac.cpp { return (nstore); }

Comme pour la plupart des fonctions membres, operator() peut prendre un nombre quelconque d'arguments de type quelconque.

void main (void) //testpile.cpp { PileFrac pilef; Fraction frac; int i; for (i=0; i<10; i++) pilef << Fraction(i+1, i+2); for (i=0; i<pilef(); i++) // utilisation de operator() cout << pilef[i]; }

Dans une classe, on peut créer autant d'operator() que l'on veut tant que le type du ou des paramètres sont différents (règle habituelle de surcharge).

Une classe qui implémente l'operator() est appelée classe de fonction ou functor.

CPP - Operators 5- Généralisation

H. Schyns 5.4

Attention : bien que, selon l'implémentation, l'expression puisse ressembler à l'appel d'un constructeur avec paramètres, ce n'en est pas un :

myfrac = pilef(aparam); // appel d'un operator() à 1 param

Alors que l'appel d'un hypothétique constructeur avec un paramètre s'écrirait

PileFrac pilef(aparam); // hypothétique constructeur à 1 param

Dans le chapitre consacré aux containers, nous verrons d'autres utilisations intéressantes de cet opérateur.

CPP - Operators 6 - Conflits potentiels

H. Schyns 6.1

6. Conflits potentiels

A force de surcharger des fonctions, les possibilités de la classe deviennent tellement vaste qu'il peut arriver que le compilateur ne sache plus quelle fonction choisir pour réaliser une opération :

void main (void) //testpile.cpp { Fraction a(3.14) // (1) int i; i = a + 9.81; // (2) cout << i << a << endl; }

1 - il peut - soit "caster" le float en long et utiliser le constructeur Fraction(long, long=1) - soit utiliser le constructeur Fraction::Fraction(float) 2 - il peut - soit "caster" le float en long et utiliser Fraction::operator+(long) - soit "caster" la Fraction en double et faire une addition "normale" puis "caster" le résultat en int.

Nous pouvons imaginer de nombreuses autres situations ambiguës.

Il est donc important de tester les fonctions au fur et à mesure afin de détecter le moment où une ambiguïté apparaît et, le cas échéant, prévoir une alternative.

CPP - Operators 7 - Annexes

H. Schyns 7.1

7. Annexes

7.1. Simplification des fractions

Au fil des opérations, les fractions deviennent de plus en plus volumineuses. Il est intéressant de les réduire à leur forme irréductible après chaque opération ou chaque série d'opération à l'aide d'une fonction simplify().

Cette fonction est protected. Elle n'est pas accessible de l'extérieur

class Fraction //fraction.h { x protected : void simplify (void); };

La fonction utilise l'algorithme d'Euclide de recherche du PGCD entre deux nombres (un Grand et un Petit).

void Fraction::simplify (void) { long Grand, Petit, Reste; // on prend les valeurs absolues Grand = (num >= 0) ? num : -num; Petit = den; if (Grand < Petit) // vérifier que Grand > Petit { Reste = Grand; Grand = Petit; Petit = Reste; } while (Reste = (Grand % Petit)) // c'est bien une affectation { // et non un test == Grand = Petit; Petit = Reste; } if (Petit > 1) // Récuperer le PGCD dans petit { num /= Petit; den /= Petit; } }

Le test dans l'instruction, avec = au lieu de ==

while (Reste = (Grand % Petit))

n'est pas une erreur. Elle se lit :

- calculer le reste de la division du Grand par le Petit (modulo), - stocker le résultat dans Reste, - vérifier si Reste est non nul.

Elle permet une écriture plus compacte du corps de la boucle.

CPP - Operators 7 - Annexes

H. Schyns 7.2

7.2. Constructeur avec argument de type float

Ce constructeur permet de convertir un float en fraction en exploitant le motif binaire du float.

Nous savons qu'en C, un float est représenté par un motif sur 32 bits

seeeeeee e[1]mmmmmmm mmmmmmmm mmmmmmmm où :

- s est le bit de signe, - e sont les 8 bits d'exposant, - [1] est le bit 1 implicite ; ce bit n'est pas repris dans le motif mais nous savons qu'il doit être là car le

premier bit significatif d'un nombre binaire est forcément un 1 ; - m sont les bits de mantisse.

Ceci représente le nombre réel mis sous la forme scientifique :

±1mmmmmmm mmmmmmmm mmmmmmmm * 2^(eeeeeeee-150)

Tout le problème consiste à extraire les motifs binaires pour reconstituer la fraction.

Pour extraire les bits, nous aurons besoins de "masques de bits" que nous utiliserons avec less opérateurs bit-à-bit & et |. Or, ces opérateurs ne fonctionnent qu'avec des variables de type entier (char, int, long).

Nous ne pouvons pas simplement "caster" le float dans un entier car cette opération engendre une recalcul et un recodage du motif binaire.

Nous donc devons arriver à lire les bits du motif binaire du float comme s'il s'agitssait d'un long, sans changer le motif. Cette opération est réalisée grâce à une union.

Rappelons qu'une union exprime que la même séquence de bytes peut être utilisée de différentes manières.

Fraction::Fraction(float aFloat) { long lexpos; union { float f; // utilisation sous forme de float long l; // utilisation sous forme de long } utrav; utrav.f = aFloat; // copier le float en tant que float // extraire les bits de la mantisse (23 bits à droite) // masque = 00000000 01111111 11111111 11111111 = 0x007FFFFF num = utrav.l & 0x007FFFFF; // ajouter le bit 1 implicite devant la série extraite // masque = 00000000 10000000 00000000 00000000 = 0x00800000 num |= 0x00800000; // extraire les bits de l'exposant // masque = 01111111 10000000 00000000 00000000 = 0x7F800000 lexpos = utrav.l & 0x7F800000;

CPP - Operators 7 - Annexes

H. Schyns 7.3

lexpos >>= 23; // cadrer le motif à gauche. // Il commence au bit de poids faible lexpos -= 150; // corriger le biais 150 // génération du dénominateur den = 1; // denominateur a priori unitaire if (lexpos > 0) while (lexpos--) num <<= 1; else if (lexpos < 0) while (lexpos++) den <<= 1; if (!den) den = 1; // extraire le bit de signe // masque = 10000000 00000000 00000000 00000000 = 0x80000000 if (utrav.l & 0x80000000) num = -num; }

La mantisse occupe les 23 bits de droite. Pour extraire ces bits et uniquement ceux-là, nous réalisons un AND bit à bit avec la forme long du motif binaire. Ensuite, nous ajoutons le 1 implicite à la bonne place grâce à un OR bit à bit (nous aurions pu utilier une addition mais le OR est plus rapide). Le résultat est le numérateur de la fraction.

Nous procédons de la même manière pour extraire les bits de l'exposant. Toutefois, pour pouvoir utiliser ce motif en tant que nombre, nous devons d'abord le cadrer à gauche. Nous pouvons alors soustraire le biais standard qui vaut 150.

Si l'exposant est positif, il faut multiplier le numérateur par une certaine puissance de 2, c'est à dire décaler le motif binaire du numérateur vers la gauche d'un certain nombre de rangs. Toutefois, ceci peut amener un débordement du numérateur.

A l'inverse, si l'exposant est négatif, il faut diviser le numérateur par une certaine puissance de 2, autrement dit, décaler le bit du dénominateur vers la gauche d'un certain nombre de rangs, ce qui peut aussi amener un débordement d'où le test final pour limiter les dégâts.

Enfin, si le bit de signe est non nul, nous changeons le signe du numérateur.

Un petit test :

void main (void) { Fraction a(-0.75), b(10.24), c(16.875); a.display(); cout << endl; //[-12582912/16777216] = [-3/4] b.display(); cout << endl; //[10737418/1048576] = [5368709/524288] c.display(); cout << endl; //[8847360/524288] = [135/8] }

CPP - Operators 8 - Sources

H. Schyns 8.1

8. Sources

- Professional C++ Nicolas A. Solter, Scott J. Kleper Collection WROX (www.wrox.com) Wiley Publishing, Inc. ISBN 0-7645-7484-1 Excellent ouvrage de référence

- Pont entre C et C++ Pierre-nicolas Lapointe Addison-Wesley ISBN 2-87908-094-0

- La surcharge d'opérateurs Marshall Cline www.developpez.com http://cpp.developpez.com/faq/cpp/?page=surcharge