Surcharge des opérateurs
Introduction :
Vous pouvez redéfinir ou surcharger la plupart des opérateurs intégrés disponibles en C++.
Cela signifie que C++ a la capacité de donner aux opérateurs une signification particulière
pour un type de données. Cette capacité est appelée surcharge d’opérateur. Par exemple,
nous pouvons surcharger l’opérateur « + » dans une classe telle que String afin de
concaténer deux chaînes en utilisant simplement +.
Le C++ dispose donc de deux méthodes différentes pour surcharger les opérateurs. Les
seuls opérateurs qui ne peuvent pas être surchargés sont les suivants :
:: (opérateur de résolution de portée) sizeof
. (opérateur point, pour accéder aux champs d’un objet) ?: (opérateur ternaire)
Tous les autres opérateurs sont surchargeables. Leur surcharge ne pose généralement pas
de problème et peut être réalisée soit dans la classe des objets sur lesquels ils s’appliquent,
soit à l’extérieur de cette classe.
Nous pouvons surcharger l’un des 38 opérateurs suivants : + - * / % ^ & |
~ ! = < > += -= *= /= %= ^= &= |= << >> >>= <<= == !
= <= >= <=> && || ++ -- , ->* -> ( ) [ ]
A.{Benderag,Benamrane} UCA/SMAS5/CPP 1
Surcharge des opérateurs
Le mécanisme de la surdéfinition d’opérateurs
Par exemple, si vous définissez une classe complexe destinée à représenter des nombres
complexes, il vous sera possible de donner une signification à des expressions telles que :
z1 + z2 z1 – z2 z1 * z2 z1/z2
Considérons une classe complexe :
class complexe{
double x, y ;
//.....
} ;
Dans un premier temps on va définir l’opérateur +
C++ surdéfinit cet opérateur + consiste à définir une fonction de nom : operator +
[Link] UCA/FPS/MIPS5/CPP 2
Surcharge des opérateurs
operator
Le mot clé operator est suivi de l’opérateur concerné (dans le cas présent, il ne serait
pas obligatoire de prévoir un espace).
Ici, notre fonction operator + doit disposer de deux arguments de type complexe et
fournir une valeur de retour du même type. En ce qui concerne sa nature ,elle peut être
une fonction membre de la classe concernée ou une fonction indépendante ;
dans ce dernier cas, il s’agira généralement d’une fonction amie, car elle devra
pouvoir accéder aux membres privés de la classe :
• Surdéfinition d’opérateur avec une fonction amie
• Surdéfinition d’opérateur avec une fonction membre
[Link] UCA/FPS/MIPS5/CPP 3
Surcharge des opérateurs
Surdéfinition d’opérateur avec une fonction amie
Le prototype de notre fonction operator+ sera : complexe operator + (complexe, complexe) ;
#include <iostream>
using namespace std;
class complexe{
double x, y ;
public :
complexe (double re=0, double im=0) {
x=re ; y=im ;
}
friend complexe operator+ (complexe, complexe) ;
void affiche () {
cout << x << " + i " << y << endl ;
}
} ;
complexe operator+ (complexe a, complexe b){
complexe p ;
p.x = a.x + b.x ; p.y = a.y + b.y ;
return p ;
}
int main(){
complexe z,z1(1,2),z2(3,4);
z=z1+z2;
[Link]();
z=z+z1+z2;
[Link]() ;
}
[Link] UCA/FPS/MIPS5/CPP 4
Surcharge des opérateurs #include<iostream>
using namespace std;
Surdéfinition d’opérateur avec class complexe{
double x, y ;
une fonction membre public :
Cette fois, le premier opérande de complexe (double re=0, double im=0) {
notre opérateur, correspondant au x=re ; y=im ;
}
premier argument de la fonction
complexe operator+ (complexe) ;
operator+ précédente, va se trouver void affiche () {
transmis implicitement : ce sera l’objet cout << x << " + i " << y << endl ;
ayant appelé la fonction membre. }
};
complexe complexe::operator+ (complexe a){
complexe p ;
p.x = a.x + x ;
p.y = a.y + y ;
return p ;
}
int main(){
complexe z,z1(1,2),z2(3,4);
z=z1+z2;
[Link]();
z=z+z1+z2;
[Link]() ;
}
[Link] UCA/FPS/MIPS5/CPP 5
Opérateurs et transmission par référence :
Dans les deux exemples précédents, la transmission des arguments et de la valeur de
retour de operator + se faisait par valeur. On peut envisager de faire appel au transfert par
référence, en particulier dans le cas d’objets de grande taille.
Le prototype de la fonction amie operator+:
complexe operator+ (complexe & a, complexe & b) ;
En revanche, la transmission par référence poserait un problème si on cherchait à l’appliquer
à la valeur de retour. En effet, le complexe p est créé localement dans la fonction ; il sera
donc détruit dès la fin de son exécution. Dans ces conditions, employer la transmission par
référence reviendrait à transmettre l’adresse d’un emplacement de mémoire libéré. Certes,
nous utilisons ici immédiatement la valeur de p, dès le retour dans la fonction main (ce qui
est généralement le cas avec un opérateur).
[Link] UCA/FPS/MIPS5/CPP 6
Surcharge des opérateurs
Si l’on cherche à protéger contre d’éventuelles modifications un argument transmis par
référence, on pourra toujours faire appel au mot clé const ; par exemple, l’en-tête de operator
+ pourrait être :
complexe operator+ (const complexe & a, const complexe & b) ;
Naturellement, si l’on utilise const dans le cas d’objets comportant des pointeurs sur des
parties dynamiques, seuls ces pointeurs seront « protégés » ; les parties dynamiques resteront
modifiables.
[Link] UCA/FPS/MIPS5/CPP 7
Surcharge des opérateurs
#include<iostream>
using namespace std;
class complexe{
double x, y ;
public :
complexe(double x=0, double y=0);
complexe(const complexe &);
double re(void) const;
double im(void) const;
complexe &operator=(const complexe &);
complexe &operator+(const complexe &);
complexe &operator-(const complexe &);
complexe &operator*(const complexe &);
complexe &operator/(const complexe &);
complexe &operator+=(const complexe &);
complexe &operator-=(const complexe &);
complexe &operator*=(const complexe &);
complexe &operator/=(const complexe &);
};
int main(){
return 0;
}
[Link] UCA/FPS/MIPS5/CPP
8
#include<iostream>
using namespace std;
class complexe{
double x, y ;
public :
complexe(double x=0, double y=0);
complexe(const complexe &);
double re(void) const;
double im(void) const;
complexe &operator=(const complexe &);
complexe &operator+(const complexe &);
complexe &operator-(const complexe &);
complexe &operator*(const complexe &);
complexe &operator/(const complexe &);
complexe &operator+=(const complexe &);
complexe &operator-=(const complexe &);
complexe &operator*=(const complexe &);
complexe &operator/=(const complexe &);
};
complexe::complexe(double x, double y){x = x;y = y;}
complexe::complexe(const complexe &s){x = s.x;y = s.y;}
complexe &complexe::operator=(const complexe &s){x = s.x; y = s.y; return *this;}
double complexe::re()const{return x ;}
double complexe::im() const{return y;}
complexe complexe::operator+ (complexe c){
complexe r ; r.x = x + c.x ; r.y = y + c.y ; return r ;}
complexe complexe::operator- (complexe c){
complexe r ; r.x = x - c.x ; r.y = y - c.y ; return r ;}
complexe complexe::operator* (complexe c){
complexe r ; // à completer;
return r ;}
complexe complexe::operator/ (complexe c){
complexe r ; // à completer ; return r ;}
[Link] UCA/FPS/MIPS5/CPP
9
Surcharge des opérateurs
complexe complexe::operator / (complexe c){
complexe r ;
return r ;
}
complexe &complexe::operator+=(const complexe &c){
x += c.x;
y += c.y;
return *this;
}
complexe &complexe::operator-=(const complexe &c){
x -= c.x;
y -= c.y;
return *this;
}
complexe &complexe::operator*=(const complexe &c){
double temp = x*c.x -y*c.y;
y = x*c.y + y*c.x;
x = temp;
return *this;
}
complexe &complexe::operator/=(const complexe &c){
double n= c.x*c.x + c.y*c.y;
double temp = (x*c.x + y*c.y) / n;
y = (-x*c.y + y*c.x) / n;
x = temp;return *this;
}
[Link] UCA/FPS/MIPS5/CPP
10
Surcharge des opérateurs
Opérateurs d’affectation
Nous avons déjà vu des exemples d’opérateur d’affectation. Cet opérateur était très simple,
mais ce n’est généralement pas toujours le cas, et l’implémentation des opérateurs
d’affectation peut parfois soulever quelques problèmes. Le fait de définir un opérateur
d’affectation signale souvent que la classe n’a pas une structure simple et que, par
conséquent, le constructeur de copie et le destructeur fournis par défaut par le
compilateur ne suffisent pas.
Par exemple, si vous ne redéfinissez pas le constructeur de copie, les écritures telles
que : classe object = source; ne fonctionneront pas correctement. En effet, c’est le
constructeur de copie qui est appelé ici, et non l’opérateur d’affectation.
De même, les traitements particuliers effectués lors de la copie ou de l’initialisation
d’un objet devront être effectués en ordre inverse dans le destructeur de l’objet. Les
traitements de destruction consistent généralement à libérer la mémoire et toutes les
ressources allouées dynamiquement.
[Link] UCA/FPS/MIPS5/CPP 11
Surcharge des opérateurs
Opérateurs d’affectation
Lorsque on écrit un opérateur d’affectation, on a généralement à reproduire, à peu
près, le même code que celui qui se trouve dans le constructeur de copie. Il arrive même
parfois que l’on doive libérer les ressources existantes avant de faire l’affectation, et donc
le code de l’opérateur d’affectation ressemble souvent à la concaténation du code du
destructeur et du code du constructeur de copie.
Bien entendu, cette duplication de code est gênante et peu élégante. Une solution simple est
d’implémenter une fonction de duplication et une fonction de libération des données.
Ces deux fonctions, pourront être utilisées dans le destructeur, le constructeur de copie et
l’opérateur d’affectation. Le programme devient ainsi plus beaucoup plus simple. Il
ne faut généralement pas utiliser l’opérateur d’affectation dans le constructeur de copie, car
cela peut poser des problèmes complexes à résoudre. Par exemple, il faut s’assurer que
l’opérateur de copie ne cherche pas à utiliser des données membres non initialisées lors de
son appel.
[Link] UCA/FPS/MIPS5/CPP 12
Surcharge des opérateurs
Opérateurs d’affectation (auto affectation)
Un autre problème important est celui de l’auto affectation. Non seulement affecter un objet à
lui même est inutile et consommateur de ressources, mais en plus cela peut être dangereux. En
effet, l’affectation risque de détruire les données membres de l’objet avant même qu’elles ne
soient copiées, ce qui provoquerait en fin de compte simplement la destruction de l’objet !
Une solution simple consiste ici à ajouter un test sur l’objet source en début d’opérateur,
comme dans l’exemple suivant :
class &class::operator=(const class &source){
if (&source != this){
// Traitement de copie des données :
...
}
return *this;
}
[Link] UCA/FPS/MIPS5/CPP 13
Surcharge des opérateurs
Opérateurs d’affectation =
Enfin, la copie des données peut lancer une exception et laisser l’objet sur lequel
l’affectation se fait dans un état indéterminé. La solution la plus simple dans ce cas est
encore de construire une copie de l’objet source en local, puis d’échanger le contenu
des données de l’objet avec cette copie. Ainsi, si la copie échoue pour une raison ou une
autre, l’objet source n’est pas modifié et reste dans un état stable. Le pseudo-code
permettant de réaliser ceci est le suivant :
class &class::operator=(const class &source){
// Construit une copie temporaire de la source :
class Temp(source);
// Échange le contenu de cette copie avec l’objet courant :
echange(Temp, *this);
// Renvoie l’objet courant (modifié) et détruit les données de la variable
temporaire (contenant les anciennes données) :
return *this;
}
[Link] UCA/FPS/MIPS5/CPP 14
Surcharge des opérateurs
#include<iostream>
using namespace std; ++ obj taille 5 en 006AFDE4 - v. dyn en 007D0340
class vect { int nelem ; int * adr ;
public : ++ obj taille 3 en 006AFDDC - v. dyn en 007D00D0
vect (int n) { ++ obj taille 4 en 006AFDD4 - v. dyn en 007D0090
adr = new int [nelem = n] ;
for (int i=0 ; i<nelem ; i++) adr[i] = 0 ; ** affectation a=b
cout<<"++obj taille"<<nelem<< "en"<<this<<"-
[Link]"<<adr<<endl ; == appel operateur = avec adresses 006AFDE4
}
~vect () { 006AFDDC effacement vecteur dynamique en 007D0340
cout<<"-- obj taille"<<nelem<<"en"<<this<<"-[Link]
en"<<adr <<endl; nouveau vecteur dynamique en 007D0340
delete adr ;
} ** affectation c=c
vect & operator= (const vect &) ; // surdéfinition opérateur = == appel operateur = avec adresses 006AFDD4
}; 006AFDD4 on ne fait rien
vect & vect::operator = (const vect & v){
cout << "== appel operateur = avec adr "<< this << " " << ** affectation a=b=c
&v << endl;
if (this != &v){ == appel operateur = avec adresses 006AFDDC
cout<<"effacement vecteur dynamique en"<<adr <<endl ;
delete adr ; 006AFDD4 effacement vecteur dynamique en 007D00D0
adr = new int [nelem = [Link]] ;
nouveau vecteur dynamique en 007D00D0
cout << " nouveau vecteur dynamique en " << adr << "\n"
;
== appel operateur = avec adresses 006AFDE4
for (int i=0 ; i<nelem ; i++) adr[i] = [Link][i] ;
}
else cout << " on ne fait rien \n" ; return * this ; 006AFDDC effacement vecteur dynamique en 007D0340
} nouveau vecteur dynamique en 007D0340
int main(){
vect a(5), b(3), c(4) ; -- obj taille 4 en 006AFDD4 - v. dyn en 007D0090
cout << "** affectation a=b \n" ; a = b ;
cout << "** affectation c=c \n" ; c = c ; -- obj taille 4 en 006AFDDC - v. dyn en 007D00D0
cout << "** affectation a=b=c \n" ; a = b = c ;
} [Link]
-- obj taille 4 en 006AFDE4 - v. dyn en 007D0340
UCA/FPS/MIPS5/CPP
15
Surcharge des opérateurs
Opérateurs de comparaison
Les opérateurs de comparaison sont très simples à surcharger. La seule chose essentielle à
retenir est qu’ils renvoient une valeur booléenne.
Ainsi, pour la classe chaine, on peut déclarer les opérateurs d’égalité et d’infériorité (dans
l’ordre lexicographique par exemple) de deux chaînes de caractères comme suit :
bool chaine::operator==(const chaine &) const;
bool chaine::operator<(const chaine &) const;
bool chaine::operator>(const chaine &) const;
bool chaine::operator !=(const chaine &) const;
bool chaine::operator<=(const chaine &) const;
bool chaine::operator>=(const chaine &) const;
[Link] UCA/FPS/MIPS5/CPP 16
Surcharge des opérateurs
Les opérateurs ++ et --
On peut définir à la fois un opérateur ++ utilisable en notation préfixée, et un autre utilisable en
notation postfixée.
Pour ce faire, on utilise une convention qui consiste à ajouter un argument fictif
supplémentaire à la version postfixée. Par exemple,
Si T désigne un type classe et que ++ est défini sous la forme d’une fonction membre :
• L’opérateur (usuel) d’en-tête T operator ++ () est utilisé en cas de notation préfixée ;
• L’opérateur d’en-tête T operator ++ (int) est utilisé en cas de notation postfixée.
Notez bien la présence d’un second opérande de type int. Celui-ci est totalement fictif, en
ce sens qu’il permet au compilateur de choisir l’opérateur à utiliser, mais qu’aucune valeur
ne sera réellement transmise lors de l’appel.
[Link] UCA/FPS/MIPS5/CPP 17
Surcharge des opérateurs
De même, si ++ est défini sous forme de fonction amie :
• l’opérateur (usuel) d’en-tête T operator (T) est utilisé en cas de notation
préfixée;
• l’opérateur d’en-tête T operator (T, int) est utilisé en cas de notation postfixée.
Les mêmes considérations s’appliquent à l’opérateur --.
Voici un exemple dans lequel nous avons défini ++ pour qu’il incrémente d’une unité les
deux coordonnées d’un point et fournisse comme valeur soit celle du point avant
incrémentation dans le cas de la notation postfixée, soit celle du point après
incrémentation dans le cas de la notation préfixée :
[Link] UCA/FPS/MIPS5/CPP 18
Surcharge des opérateurs
#include<iostream>
using namespace std;
class point
{ int x, y ;
public :
point (int abs=0, int ord=0) { x=abs ; y=ord ; }
point operator++(){ x++ ; y++ ; return *this ;} // notation préfixée
point operator++(int n){point p = *this ;x++ ; y++ ; return p ;}
// notation postfixée
void affiche () { cout << x << " " << y << "\n" ; }
} ;
int main(){
point a1 (2, 5), a2(2, 5), b ;
b = ++a1 ; cout << "a1 : " ;
[Link] () ; // affiche a1 : 3 6
cout << "b : " ;
[Link] () ; // affiche b : 3 6
b = a2++ ; cout << "a2 : " ;
[Link] () ; // affiche a2 : 3 6
cout << "b : " ;
[Link] () ; // affiche b : 2 5
}
[Link] UCA/FPS/MIPS5/CPP
19
Surcharge des opérateurs
Surcharge des opérateurs d’extraction >> et d’insertion de flux <<
• L’opérateur << surchargé doit posséder un opérande gauche de type ostream & :
c’est le cas de cout dans l’expression
cout << objetClasse
donc l’opérateur << doit être surchargé comme fonction non membre (fonction amie)
friend ostream& operator <<(ostream&, const complexe &)
• L'’opérateur >> surchargé doit posséder un opérande gauche de type istream & : c’est
le cas de cin dans l’expression
cin>>objetClasse
donc l’opérateur >> doit être surchargé comme fonction non membre (fonction amie)
friend istream& operator >>(istream&, complexe&)
[Link] UCA/FPS/MIPS5/CPP 20
Surcharge des opérateurs
#include<iostream>
using namespace std;
friend ostream& operator<<(ostream&,const Complexe&);
friend istream& operator>>(istream&,Complexe&);
ostream& operator<<(ostream& out, const complexe& c){
out << c.m_Reel<<" + i"<<c.m_Imaginaire<<endl; return out;
}
istream& operator>>(istream& in, complexe& c){
In >> c.m_Reel>>c.m_Imaginaire; return in;
}
[Link] UCA/FPS/MIPS5/CPP 21
Exemple avec >> et <<
#include<iostream>
using namespace std;
ostream& operator <<(ostream& sortie, const Vecteur& vect){
sortie<<" la taille du vecteur est: "<<[Link]<<endl;
for(int i=0; i < taille; i++){
if(i == 0) sortie<<[Link][i];
else sortie<<setw(10)<<setfill(' ')<<[Link][i];
}
sortie<<endl; return sortie;
}
istream& operator >>(istream& in, Vecteur& vect){
cout<<"entree les valeurs de votre vecteur"<<endl; int taille = 3;
for(int i=0; i < taille; i++){
cout<<setw(10)<<setfill('*');
cout<<" tab["<<i<<"] = "; in >> [Link][i];
cout<<endl;
} cout<<endl; return in;
}
[Link] UCA/FPS/MIPS5/CPP 22
Surcharge des opérateurs
Exemple de surdéfinition de l’opérateur [ ]
Considérons à nouveau notre classe vect :
#include<iostream>
using namespace std;
class vect
{ int nelem ; int * adr ;
.....
}
Cherchons à la munir d’outils permettant d’accéder à un élément de l’emplacement pointé
par adr à partir de sa position, que l’on repérera par un entier compris entre 0 et nelem-1.
Nous pourrions bien sûr écrire des fonctions membres comme :
void range (int valeur, int position)
[Link] UCA/FPS/MIPS5/CPP 23
Exemple de surdéfinition de l’opérateur [ ]
#include<iostream>
using namespace std;
class vect {
int nelem ; int * adr ;
public :
vect (int n) { adr = new int [nelem=n] ; }
~vect () {delete adr ;} int & operator [] (int) ;
} ;
int & vect::operator [] (int i){return adr[i] ; }
int main(){
int i ;
vect a(3), b(3), c(3) ;
for (i=0 ; i<3 ; i++) {a[i] = i ; b[i] = 2*i ; }
for (i=0 ; i<3 ; i++) c[i] = a[i]+b[i] ;
for (i=0 ; i<3 ; i++) cout << c[i] << " " ;
}
[Link] UCA/FPS/MIPS5/CPP
24
Surdéfinition de l’opérateur () :
Lorsqu’une classe surdéfinit l’opérateur (), on dit que les objets auxquels elle donne
naissance sont des objets fonctions, car ils peuvent être utilisés de la même manière qu’une
fonction ordinaire. En voici un exemple simple, dans lequel nous surdéfinissons
l’opérateur () pour qu’il corresponde à une fonction à deux arguments de type int et
renvoyant un int :
class cl_fct {
public :
cl_fct(float x) { ..... } ; // constructeur
int operator() (int n, int p ) { ..... } // opérateur ()
} ;
Dans ces conditions, une déclaration telle que : cl_fct obj_fct1(2.5) ; construit bien sûr un
objet nommé obj_fct1 de type cl_fct, en transmettant le paramètre 2.5 à son constructeur.
En revanche, la notation suivante réalise l’appel de l’opérateur ( ) de l’objet obj_fct1, en
lui transmettant les valeurs 3 et 5 : obj_fct1(3, 5) Ces possibilités peuvent servir lorsqu’il
est nécessaire d’effectuer certaines opérations d’initialisation d’une fonction, ou de
paramétrer son travail (par le biais des arguments passés à son constructeur).
Mais elles s’avéreront encore plus intéressantes dans le cas des fonctions dites de rappel,
c’est-à-dire transmises en argument à une autre fonction.
[Link] UCA/FPS/MIPS5/CPP
25