====== Partie I – Surcharge d’opérateurs ======
[[in204:tds:sujets:td5|TD5]]
===== Question n°1=====
Créer un projet dans lequel vous définissez une classe de nombre complexe qui aura typiquement la structure suivante :
#ifndef complexHPP
#define complexHPP
class Complex
{
private:
double mRealPart;
double mImaginaryPart;
public:
Complex();
~Complex();
...
};
#endif
==== Question n°1.1=====
Ajouter à cette classe un ou plusieurs opérateurs de conversion convertissant un nombre flottant en un nombre complexe.
Pour convertir un nombre en virgule flottante en un nombre complexe, il faut définir de [[in204:cpp:syntax:class:constructor|nouveaux constructeurs]] qui vont prendre un seul argument qui aura pour type le type d'un nombre à virgule flottante.
Sachant que dans le cas présent, nous souhaitons pouvoir transformer automatiquement un nombre à virgule flottante ''x'' en un nombre complexe si un nombre complexe est attendu, le constructeur ne doit pas être déclarée comme ''explicit''.
De fait, il est nécessaire d'ajouter les constructeurs suivant pour les types ''float'' et ''double''.
class Complex
{
private:
...
public:
Complex(double aReal) : mRealPart(aReal), mImaginaryPart(0.0) {}
Complex(float aReal) : mRealPart(aReal), mImaginaryPart(0.0) {}
...
};
Il n'est nullement souhaitable de se limiter aux nombres à virgule flottante, nous pouvons ajouter des opérations de conversions identiques pour les nombres entiers :
class Complex
{
private:
...
public:
Complex(double aReal) : mRealPart(aReal), mImaginaryPart(0.0) {}
Complex(float aReal) : mRealPart(aReal), mImaginaryPart(0.0) {}
Complex(int aReal) : mRealPart((double)aReal), mImaginaryPart(0.0) {}
Complex(unsigned in aReal) : mRealPart((double)aReal), mImaginaryPart(0.0) {}
...
};
==== Question n°1.2=====
Ajouter à cette classe une fonction qui affiche le nombre complexe sur la console.
Nous pouvons ajouter une fonction simple qui affiche le résultat sur la console. Cette fonction affiche la représentation minimale du nombre complexe.
class Complex
{
private:
...
public:
void print() const
{
if(mRealPart != 0.0)
{
std:cout << mRealPart;
if(mImaginaryPart != 0.0)
std::cout << " + " << mImaginaryPart << " I";
}
else if(mImaginaryPart != 0.0)
std::cout << " + " << mImaginaryPart << " I";
else
std::cout << "0.0";
}
...
};
==== Question n°1.3=====
Créer deux fonctions statiques créant des nombre complexes, l’une créant un nombre complexe à partir d’une paire correspondant à la partie réelle et imaginaire, l’autre créant un nombre complexe à partir de la paire (ρ,θ) correspondant à la notation polaire de ce nombre.
La difficulté, c'est qu'un nombre complexe admet deux représentations canoniques, une représentation sous la forme d'une partie réelle et une partie imaginaire ou au contraire une représentation sous la forme de coordonnées polaire.
Si nous souhaitons définir un constructeur pour construire un nombre complexe à partir de ses parties réelles et imaginaires, nous allons nous retrouver avec un constructeur ayant comme signature, la signature suivante :
Complex(double theRealPart, double theImaginaryPart);
Le constructeur qui construit un nombre complex à partie des coordonnées polaires aura la signature suivante :
Complex(double rho, double theta);
c'est à dire que les deux constructeurs auront la même signature ''Complex(double, double)'' et le compilateur se retrouvera avec deux définitions différentes du même constructeur, d'où une erreur au moment de la compilation.
Alors il existe plusieurs méthodes pour résoudre ce problème. Soit nous définissons des méthodes statiques, par exemple ''from_real_imaginary'' et ''from_polar''. Dans ce cas, nous devons définir le constructeur '' Complex(double theRealPart, double theImaginaryPart)'' comme étant un constructeur privé.
class Complex
{
private:
...
Complex(double theRealPart, double theImaginaryPart):
mRealPart(theRealPart), mImaginaryPart(theImaginaryPart) {}
...
public:
...
static Complex from_real_imaginary(double theRealPart, double theImaginaryPart)
{
return Complex(theRealPart, theImaginaryPart);
}
static Complex from_polar(double theRho, double theTheta)
{
return Complex(
theRho* cos(theTheta),
theRho* sin(theTheta));
}
};
Pour créer un nombre complexe à partir de ses parties réelles et imaginaires, resp. ses coordonnées polaires, il faudra l'une des deux méthodes statiques :
Complex complexA Complex::from_real_and_imaginary(1.0, 2.0);
Complex complexB = Complex::from_polar(1.0, 0,52);
Une autre approche consiste à ajouter des paramètres pour lever l'ambiguité au moment de la compilation.
class Complex
{
public:
...
class Polar {}; // Classe vide ne servant qu'à définir un
// type additionnel pour identifier
// une conversion polaire vers complexe.
private:
...
public:
...
Complex(double theReal, double theImaginary)
re(theReal), im(theImaginary) // Constructeur construisant un nombre complexe
{} // à partir de sa partie réelle et imaginaire.
Complex(Polar, double theRho, double thePhi)
re(theRho* cos(thePhi)), im(theRho* sin(thePhi)) // Constructeur construisant un nombre complexe
{} // à partir de ses coordonnées polaires.
...
};
Désormais, les deux constructeurs ont bien une signature de type différente :
Complex(double, double)
Complex(Polar, double, double)
Pour créer un nombre complexe à partir de ses parties réelles et imaginaires, resp. ses coordonnées polaires, il suffira d'écrire :
Complex complexA(1.0, 2.0);
Complex complexB(Polar(), 1.0, 0,52);
==== Question n°1.4 =====
Créer une variable globale ayant comme type la classe complexe et ayant pour valeur la valeur imaginaire 1.
Pour ce faire, il suffit de définir une variable globale :
const Complex I(0.0, 1.0);
ou sinon :
const Complex I = Complex::from_real_imaginary(0.0, 1.0);
en fonction de l'implantation que vous avez effectué aux questions précédentes.
Cependant, la difficulté, c'est qu'une variable globale ne peut être définie que dans une seule et unique unité de compilation. Dans ce cas, il suffit d'écrire dans le fichier ''complex.cpp''
// Fichier Complex.cpp
#include"Complex.hpp"
const Complex I(0.0, 1.0);
Cependant, dans ce cas, la variable n'est que visible dans l'unité de compilation ''Complex.cpp''. Pour la rendre visible dans les autres unités de compilation, il est nécessaire de déclarer son existence dans le fichier ''Complex.hpp'' pour que les unités de compilation faisant référence aux nombres complexes puissent faire référence à la variable ''I''.
Nous devons donc ajouter la déclaration suivante :
#ifndef ComplexHPP
#define ComplexHPP
...
class Complex
{
...
};
...
extern const Complex I;
...
#endif
Une approche permettant de simplifier l'écriture est de définir un identificateur permettant de savoir si le fichier ''Complex.hpp'' est chargé par le fichier ''Complex.cpp'', ie. si c'est l'unité ''Complex.cpp'' qui est en cours de compilation. Dans ce cas, il est possible d'effectuer l'ensemble de la déclaration dans le fichier ''Complex.h'', le fichier ''Complex.cpp'' se contentant de définir un symbole comme par exemple ''complexCPP''
Le fichier ''Complex.hpp'' a désormais la structure suivante :
#ifndef ComplexHPP
#define ComplexHPP
...
class Complex
{
...
};
...
#ifndef complexCPP
extern const Complex I;
#else
const Complex I(0.0, 1.0);
#endif
...
#endif
et le fichier ''Complex.cpp'' :
// Fichier Complex.cpp
#define complexHPP
#include"Complex.hpp"
==== Question n°1.5=====
Tester le bon comportement de votre classe sur des exemples simples. Notamment, est-il possible d’écrire désormais :
Complex complexValue = 3.3 + 5 * I;
En fait, il est nécessaire de déclarer un opérateur de multiplication par une valeur numérique avant de pouvoir procéder à cette écriture.
Pour ce faire vous ajouter la déclaration suivante :
class Complex
{
friend Complex operator * (int, const Complex&);
friend Complex operator * (double, const Complex&);
friend Complex operator * (const Complex&, int);
friend Complex operator * (const Complex&, double);
private:
...
};
inline Complex operator * (int theLeftValue, const Complex& theRightValue)
{
return Complex(theLeftValue * theRightValue.mRealPart,
theRightValue.mImaginaryPart);
}
inline Complex operator * (const Complex& theLeftValue, int theRightValue)
{
return Complex(theLeftValue.mRealPart * theRightValue,
theLeftValue.mImaginaryPart);
}
inline Complex operator * (double theLeftValue, const Complex& theRightValue)
{
return Complex(theLeftValue * theRightValue.mRealPart,
theRightValue.mImaginaryPart);
}
inline Complex operator * (const Complex& theLeftValue, double theRightValue)
{
return Complex(theLeftValue.mRealPart * theRightValue,
theLeftValue.mImaginaryPart);
}
===== Question n°2=====
Nous considérons les opérations de base simple que sont l’addition et la soustraction.
==== Question n°2.1====
Proposer une surcharge des opérations + et –. Implanter ces dernières et tester.
Complex operator + (const Complex& aRightValue) const;
Complex operator - (const Complex& aRightValue) const;
Complex operator + (const Complex& aRightValue) const
{
Complex result(this);
result.mRealPart += aRightValue.mRealPart;
result.mImaginaryPart += aRightValue.mImaginaryPart;
return result;
}
Complex operator - (const Complex& aRightValue) const;
{
Complex result(this);
result.mRealPart -= aRightValue.mRealPart;
result.mImaginaryPart -= aRightValue.mImaginaryPart;
return result;
}
==== Question n°2.2====
Proposer une surcharge des opérations + et –. Implanter ces dernières et tester.
Complex operator + (double aLeftValue, const Complex& aRightValue);
Complex operator – (double a aLeftValue, const Complex& aRightValue);
Expliquer la différence avec les opérations précédentes ?
class Complex
{
...
public:
Complex operator + (double aRightValue) const
{
Complex result(this);
result.mRealPart += aRightValue;
return result;
}
Complex operator - (double aRightValue) const;
{
Complex result(this);
result.mRealPart -= aRightValue;
return result;
}
Ces fonctions calculent l'addition d'un nombre à virgule flottante avec un nombre complexe, cependant, le nombre complexe est à gauche, le nombre à virgule flottante est à droite. Si nous voulons avoir un nombre à virgule flottante à gauche et un nombre complex à droite, il n'est pas possible de définir l'opérateur dans la classe mais en dehors de la classe. Les opérateurs définis dans la classe suppose que le premier argument de l'opéateur est l'objet lui-même.
class Complex
{
friend Complex operator +(double, const Complex&);
friend Complex operator -(double, const Complex&);
...
public:
Complex operator + (double aRightValue) const
{
Complex result(this);
result.mRealPart += aRightValue;
return result;
}
Complex operator - (double aRightValue) const;
{
Complex result(this);
result.mRealPart -= aRightValue;
return result;
}
...
};
Complex operator +(double aLeftValue, const Complex& aRightValue)
{
Complex result(aRightValue);
result.mRealPart += aRightValue;
return result;
}
Complex operator -(double aLeftValue, const Complex& aRightValue)
{
Complex leftValue(aLeftValue, 0.0);
return leftValue - aRightValue;
}
==== Question n°2.3 ====
Tester le bon comportement de votre classe sur des exemples simples. Notamment, est-il possible d’écrire désormais :
Complex complexValue = 3.3 + 5 * I;
Nous pouvons désormais définir des types complexes et effectué des calculs sur ces types complexes.
Ainsi il sera possible de définir :
Complex I(0, 1);
Complex complexValue = 3.3 + I;
Cependant, le code suivant est ambiguë :
Complex complexValue = 3.3 + 5 * I;
En effet, il faudrait être mesure de faire une multiplication entre un nombre entier ou un nombre à virgule flottante et un nombre complexe. Nous verrons comment faire pour effectuer ce type conversion à la fin de l'exercice.
==== Question n°2.4 ====
Proposer une surcharge des opérations += et –=. Implanter ces dernières et tester.
Complex& operator += (const Complex& aRightValue);
Complex& operator -= (const Complex& aRightValue);
Expliquer pourquoi les signatures des operations += et -= sont différentes de celles des operations + et - ?
===== Question n°3 =====
Nous considérons les opérations de base que sont la multiplication et la division.
==== Question n°3.1 ====
Proposer deux fonctions de conversion entre la représentation polaire et la représentation canonique des nombres complexes et implanter les.
==== Question n°3.2 ====
Proposer une surcharge des opérations * et /. Implanter ces dernières et tester.
Complex operator * (const Complex& aRightValue) const;
Complex operator / (const Complex& aRightValue) const;
Complex operator * (const Complex& aRightValue) const
{
return Complex(
mRealPart * aRightValue.mRealPart - mImaginaryPart * aRighValue.mImaginaryPart,
mRealPart * aRightValue.mImaginaryPart + mImaginaryPart * aRightValue.mRealPart);
}
Complex operator / (const Complex& aRightValue) const
{
double squareNorm = aRightValue.mRealPart * aRightValue.mRealPart
+ aRightValue.mImaginaryPart * aRightValue.mImaginaryPart;
return *this *
Complex(aRighValue.mRealPart / squareNorm,
- aRightValue.mImaginaryPart / squareNorm);
}
==== Question n°3.3 (optionnel) ====
Proposer une surcharge des opérations '*=' et '/='. Implanter ces dernières et tester.
Complex& operator *= (const Complex& aRightValue);
Complex& operator /= (const Complex& aRightValue);