Considérons une classe de nouveaux nombres comme par exemple la classe de rational
qui correspond à un nombre rationnel, composé d'un numérateur et d'un dénominateur.
#include<exception> #include<stdexcept> class rational { private: long m_numerator; long m_denominator; public: rational() : m_numerator(0), m_denominator(1) {} rational(long theNumerator) : m_numerator(theNumerator), m_denominator(1) {} rational(long theNumerator, long theDenominator) { if (theDenominator == 0) throw std::invalid_argument("denominator is 0"); if (theDenominator < 0) { theNumerator *= -1; theDenominator *= -1; } m_numerator = theNumerator; m_denominator = theDenominator; } rational(const rational& aRational): m_numerator(aRational.m_numerator), m_denominator(aRational.m_denominator) {} };
Nous souhaitons définir les opérations arithmétiques standards pour des nombres rationnels. Pour ce faire, nous devons ajouter les fonctions add
, sub
, mult
et div
à la classe précédente.
Par exemple, pour la méthode mult
, cela donnerait :
rational mult(const rational& aRational, const rational& anotherRational) { return rational(aRational.m_numerator * anotherRational.m_numerator, aRational.m_denominator * anotherRational.m_denominator); }
Il faudrait de plus que nous déclarions la fonction rational mult(const rational& aRational, const rational&)
comme une fonction amie de la classe rational
pour lui laisser accéder aux champs privés m_numerator
et m_denominator
.
class rational { friend rational mult(const rational& aRational, const rational& anotherRational); ... };
Ensuite, il suffit d'écrire pour effectuer les opérations arithmétiques :
rational a = {1, 2}; rational b = 2; rational c = mult(a, b);
Cependant, cela signifie que bien que nous ayons une classe qui représente un nombre et pour laquelle l'opération *
est définie, il est nécessaire d'appeller l'opération avec un appel fonctionnel mult
et non pas une opération *
.
Pour unifier, nous aimerions pouvoir écrire :
rational c = a * b;
C'est là qu'intervient la surcharge des opérateurs. L'idée de la surcharge des opérateurs est là-même que celle des fonctions. Nous pouvons déterminer l'opérateur à utiliser à partir du type des arguments. Si les arguments a
et b
sont de types int
nous savons que c'est l'addition sur les entiers 32 bits qui est utilisée, si a
et b
sont de type rational
, il suffirait de pouvoir définir une nouvelle opération qui accepte deux types rationnels en argument :
rational operator *(const rational& aRational, const rational& anotherRational) { return rational(aRational.m_numerator * anotherRational.m_numerator, aRational.m_denominator * anotherRational.m_denominator); }
La syntaxe est identique à celle d'une fonction à l'exception qu'au lieu de fournir un nom de fonction nous fournissons le nom de l'opérateur. Presque tous les opérateurs de C++ peuvent être surchargées. Cependant, le nombre d'arguments de l'opérateur est toujours le même. Un opérateur binaire (qui ne prende que deux arguments) prendra toujours deux arguments, il n'est pas possibles de le transformer en opérateur unaire. De même un opérateur unaire (qui ne prend qu'un argument) ne prendra toujours qu'un seul argument. Le nombre d'argument pour un opérateur donné est intangible.
Nous manipulons la surcharge d'opérateur comme celle d'une fonction. Ainsi, nous devons déclarer la surcharge de l'opérateur comme une fonction amie de la classe, ce qui se fait comme pour la fonction mult
sauf que nous remplaçons le nom de la fonction par operator *
:
class rational { friend rational operator *(const rational& aRational, const rational& anotherRational); ... };
Dans le cas précédent, l'opérateur *
est défini en dehors de la classe. C++ propose de définir un opérateur au sein de la classe. Dans le cas où un opérateur est défini au sein d'une classe, le premier argument correspond à l'instance courante de l'objet :
class rational { ... public: rational operator * (const rational& anotherRational) const { return rational(m_numerator * anotherRational.m_numerator, m_denominator * anotherRational.m_denominator); } ... };
Dans ce cas, l'opérateur *
prend un seul argument qui est l'argument droit de l'opérateur (right hand value), l'argument gauche de l'opérateur est l'instance courante de l'objet.
L'intérêt de procéder ainsi est de de grouper les déclarations au sein de l'objet et d'éviter d'avoir à déclarer les opérateurs définis à l'extérieur comme étant des opérateurs amis de la classe courante.
Certains opérateurs sont impérativement déclarés dans la classe à laquelle ils sont rattachés. Ces opérateurs sont les opérateurs suivants :
operator = (const B&) | l'opérateur d'affectation. Il affecte au contenu de l'instance de l'objet la valeur de l'objet passé en argument. |
operator ()(args…) | l'opérateur appel fonctionnel. Il expose une méthode par défaut qui est appelée pour la liste d'arguments donnée. |
operator [](args) | l'opérateur accès indexé. |
operator →() | l'opérateur pointeur qui retourne soit un pointeur soit au contraire un objet qui définit aussi un operator →() . |
class rational { ... public: rational& operator = (const rational& anotherRational) { m_numerator = anotherRational.m_numerator; m_denominator = anotherRational.m_denominator; return *this; } ... };
Il est impossible de surcharger un opérateur dans la classe quand c'est uniquement l'argument droit qui est un objet ou une référence à un objet du type de la classe et que l'argument gauche est un argument ayant un autre type.
Dans ce cas, nous nous retrouvons à avoir besoin de définir l'opérateur en dehors de la classe. Par exemple, si nous souhaitons définir l'addition d'un entier à un nombre rationnel, il faudra ajouter à la classe rational
le code suivant :
class rational { friend rational operator + (long, const rational&); ... }; rational operator + (long left, const rational& right) { return rational(left * anotherRational.m_denominator + anotherRational.m_numerator, m_denominator); }