Académique Documents
Professionnel Documents
Culture Documents
PROS
OURS BLANC DES CARPATHES
ISIMA1999
2.1 BUTS 13
2.2 CHOISIR ENTRE MTHODE ET FONCTION 13
2.2.1 UN EXEMPLE PARTICULIER : LES OPRATEURS DE REDIRECTION 13
2.2.2 UNE RGLE BIEN UTILE 14
2.2.3 RCAPITULATIF 15
2.3 QUELS OPRATEURS NE PAS SURCHARGER ? 15
2.4 QUELS PARAMTRES DOIVENT PRENDRE LES OPRATEURS 17
2.4.1 PRFRER LES RFRENCES CONSTANTES AUX OBJETS 17
2.4.2 POURQUOI NE PAS TOUJOURS RENVOYER UNE RFRENCE 19
2.5 QUE DOIVENT RENVOYER LES OPRATEURS ? 19
2.6 LE CAS PARTICULIER DES OPRATEURS DINCRMENTATION 22
2.7 LE CAS PARTICULIER DES OPRATEURS NEW ET DELETE 24
2.7.1 BIEN COMPRENDRE LE FONCTIONNEMENT DE NEW ET DELETE 24
2.7.2 UNE VERSION SPCIALE DE NEW : OPERATOR NEW DE PLACEMENT 25
2.7.3 EXERCICE : UN EXEMPLE SIMPLISTE DE SURCHARGE DE NEW ET DELETE 25
5. EN GUISE DE CONCLUSION 40
Une classe T est dite sous forme canonique (ou forme normale ou forme
standard) si elle prsente les mthodes suivantes :
class T
{
public:
T (); // Constructeur par dfaut
T (const T&); // Constructeur de recopie
~T (); // Destructeur ventuellement virtuel
T &operator=(const T&); // Operator daffectation
};
Il est utilis, par exemple, lorsque lon cre des tableaux dobjet. En effet, il nest
pas possible de passer un constructeur loprateur new[] responsable de la cration
des tableaux.
Lorsquun objet est pass en paramtre par valeur une fonction ou une
mthode, il y a appel du constructeur par recopie pour gnrer lobjet utilis
en interne dans celle-ci. Ceci garantit que lobjet original ne peut tre
modifi ! Lautre solution consiste utiliser une rfrence constante.
T::T(const T&)
Dans tous les autres cas, vous aurez besoin de dfinir un constructeur de
recopie :
Agrgation par valeur : il vous faudra recopier les objets agrgs par valeur,
en appelant leur constructeur par recopie !
1.3 Le destructeur
Le destructeur est l pour rendre les ressources occupes par un objet lorsquil
arrive en fin de vie, soit quil soit allou statiquement et sorte de porte, soit quil ait
t allou dynamiquement laide de new et quon lui applique delete pour le
supprimer.
Pour conclure sur le destructeur, celui-ci sera ncessaire ds lors que votre
classe ralise de lagrgation par pointeur ou de lallocation dynamique de mmoire.
Le paramtre est pass par rfrence constante pour les raisons explicites dans
le cas du constructeur par recopie.
Cet oprateur renvoie une rfrence sur T afin de pouvoir le chaner avec
dautres affectations. Rappelons que loprateur daffectation est associatif droite, en
effet, lexpression a=b=c est value comme : a=(b=c). Ainsi, la valeur renvoye par
une affectation doit tre son tour modifiable, aussi, il est naturel de renvoyer une
rfrence.
1.5.1 Structure :
Constructeur par dfaut : nous allons utiliser un constructeur qui cre une
chane de longueur nulle mais susceptible de contenir 10 caractres.
#ifndef __Chaine_H__
#define __Chaine_H__
#include <string.h>
class Chaine
{
private:
int capacite_;
int longueur_;
char *tab;
enum {CAPACITE_DEFAUT=16}; // Capacit par dfaut
// et granularit d'allocation
public:
// constructeur par dfaut
Chaine(int capacite=CAPACITE_DEFAUT) :
capacite_(capacite),
longueur_(0)
{
tab=new char[capacite_];
// second constructeur
Chaine(char *ch)
{
int lChaine=strlen(ch);
int modulo=lChaine % CAPACITE_DEFAUT;
if (modulo)
capacite_=((lChaine / CAPACITE_DEFAUT)+1)*CAPACITE_DEFAUT;
else
capacite_=lChaine;
longueur_=lChaine;
tab=new char[capacite_];
strcpy(tab,ch);
}
int longueur(void)
{
return longueur_;
}
// destructeur
~Chaine(void)
{
if (tab)
delete [] tab; // ne jamais oublier les crochets lorsque
// l'on dsalloue un tableau !
}
// Operateur d'affectation
Chaine & operator=(const Chaine& uneChaine)
{
if (tab)
delete [] tab;
capacite_=uneChaine.capacite_;
longueur_=uneChaine.longueur_;
tab = new char[capacite_];
strcpy(tab,uneChaine.tab);
return *this;
}
};
#endif
Enonc :
1) T t;
2) U u;
3) T z(t);
4) T v();
5) T y=t;
6) z = t;
7) T a(u);
8) t = u;
Correction :
Bien que ce dernier propos soit dtach de la librairie standard du C++, je crois
bon de rappeler que les constructeurs ne supportent pas le polymorphisme. En
particulier, il est impossible dappeler automatiquement depuis le constructeur de la
classe de base une mthode polymorphique.
#include <iostream.h>
class T1
{
public:
virtual void afficher(void)
{
cout << "Classe T1" << endl;
}
T1(void)
{
afficher();
}
virtual ~T1()
{
cout << "On detruit T1" << endl << "Affichage ";
afficher();
}
};
class T2 : public T1
{
public:
T2(void) : T1()
{
}
virtual ~T2()
{
cout << "On detruit T2" << endl << "Affichage ";
afficher();
}
};
/
Programme 1.8 Rsultat (dcevant ) rellement obtenu
Bien entendu, il sagit l dun exemple dcole mais bien des personnes tentaient
dappeler une mthode dinitialisation automatique polymorphique des attributs des
valeurs par dfaut de cette manire. Je vous laisse augurer de leur dception !
2.1 Buts
Hors, leur criture est trs pigeuse. En effet, certains doivent tre surchargs
en tant que fonctions, dautres en tant que mthodes, leur type de retour doit tre
soigneusement tudi etc.
Dans certains cas, la question ne se pose mme pas. Supposons, par exemple,
que lon souhaite fournir pour une classe T une version des oprateurs << et >>
permettant respectivement dcrire le contenu de T sur un flux en sortie (ostream) et
de lire depuis un flux en entre (istream) le contenu de T.
Ces oprations ne peuvent absolument pas tre codes en tant que mthodes de
la classe T. Considrons les rsultats dsastreux que cela pourrait avoir. La
dclaration / dfinition de cet oprateur pourrait tre :
class T
{
public:
ostream &operator<<(ostream &a)
{
// code omis ...
return a;
}
};
T t;
t.operator<<(cout);
T << cout;
Bien que cela pose moins de problmes, il sera galement judicieux de dclarer
sous forme dune fonction externe loprateur de redirection depuis un flux.
Un oprateur ne sera dclar en tant que mthode que dans les cas o this
joue un rle privilgi par rapport aux autres arguments
Donnons deux exemples qui nous permettrons de juger du bien fond de cette
rgle fonde sur des critres conceptuels. En effet, on ne considrera quune mthode
comme lgitime si son impact sur le paramtre par dfaut est suffisant.
Soit le cas de loprateur daffectation que nous crivons comme une mthode :
Au regard de ces deux considrations, il est vident que this joue un rle
privilgi par rapport au second argument, il est donc logique que ce soit une mthode
de la classe T.
T T::operator+(const T&a)
Comme pour le cas prcdent, tudions les rles respectifs de largument par
dfaut this et de largument explicite a. Dans une addition, aucun des deux membres
nest modifi et le rsultat nest ni le membre de gauche, ni celui de droite : en fait les
deux arguments jouent un rle absolument symtrique.
De ce fait, this na pas un rle privilgi par rapport a et loprateur est alors
transform en fonction externe, soit :
2.2.3 Rcapitulatif
Pour commencer, il faut savoir que certains oprateurs ne peuvent pas tre
redfinis car le langage vous linterdit ! Il sagit de
:: Slection de porte
:? Oprateur ternaire
Tableau 2.2 Oprateurs du C++ ne pouvant tre surchargs
Vous noterez au passage, que si la slection dun membre dun objet statique
par loprateur . ne peut tre surcharge, en revanche la slection dun membre dun
objet dynamique par loprateur flche -> peut, elle, ltre ! Cela permettra, par
exemple de crer des classes de pointeurs intelligents.
! Non logique
|| && Ou et Et logiques
Tableau 2.3 Oprateurs C++ quil vaut mieux ne pas surcharger
Les oprateurs surchargs doivent conserver leur rle naturel sous peine de
perturber considrablement leur utilisateur
Par exemple, loprateur dvaluation squentielle est trs bien comme il est. Et,
moins de vouloir rgler son compte un ennemi, vous navez aucun intrt le
surcharger. La mme rgle sapplique directement au non logique.
Pour ce qui concerne les oprateurs logiques dyadiques, il existe une autre rgle
encore plus subtile. En effet, en C++ comme en C, les valuations sont dites court
circuit. En effet, si le premier membre dun et est faux, il est inutile dvaluer le
reste de lexpression, le rsultat sera faux quoiquil arrive. Rciproquement,
lvaluation dune expression ou sarrte ds que lon rencontre un membre vrai.
if (p && *p>0)
qui pouvait tre crite sans danger en C++ ne peut plus ltre du fait de votre
surcharge (la drfrence du pointeur sera faite mme si ce dernier est nul, premire
clause du &&).
Passs par valeur sil sagit de scalaires dont la taille est infrieure celle
dun pointeur, en aucun cas, on ne passera un objet par valeur
Il faut toujours prfrer une rfrence constante un objet pass par valeur.
Comme vous ntes pas obligs de me croire sur parole, je vais tacher de vous le
dmontrer en identifiant les points positifs et ngatifs de la technique
Efficacit de lappel : rappelons en effet que lorsquun objet est pass par
valeur, il y a appel du constructeur de recopie pour gnrer une copie, qui
elle est utilise par la mthode / fonction appele. Hors, une rfrence est
fondamentalement un pointeur, et il sera plus rapide dempiler un simple
pointeur que dappeler un constructeur de recopie aussi optimis soit il.
Vous esprez ainsi pouvoir lutiliser avec toutes les classes drives de
Evaluable par polymorphisme sur la mthode Evaluation.
2.4.1.3 Conclusion
Au regard de ces explications, il est clair quil vaut toujours mieux utiliser une
rfrence constant quun objet pass par valeur. Aussi, usez et abusez des paramtres
passs par rfrences constantes !
Aprs avoir trait le cas des paramtres positionnels des mthodes, tudions
dornavant le cas dun paramtre particulirement spcial : le retour de
mthode / fonction.
Nous y apprendrons que sil est quasi tout le temps recommand dutiliser des
rfrences constantes pour les paramtres, il faut le plus souvent retourner un objet.
Les types de retour objet sont plus complexes grer car lon se retrouve
souvent face un dilemme : doit on retourner un objet ou une rfrence ?
A la question :
Afin dtayer notre propos, considrons une classe modlisant les nombres
rationnels et dclare comme suit :
class Rationnel
{
private:
int numerateur;
int denominateur;
public:
Rationnel (int numVal, int denVal) :
numerateur(numVal), denominateur(denVal)
{
}
// dclarations omises
int num() const
{
return numerateur;
}
int deno() const
{
return denominateur;
}
};
Rationnel c(0,0);
// code omis
c=a*b;
1. Appel de la fonction operator* avec passage par rfrence constantes des objets a et
b.
Vous remarquez que lon renvoie une rfrence sur un objet automatique, ce qui
nest pas sans rappeler une erreur frquente des programmeurs en C qui renvoient
ladresse dune variable automatique : les consquences sont exactement les mmes :
laffectation suivante va se faire sur un objet qui nexiste plus.
Qu cela ne tienne me direz vous, je nai qu construire mon objet sur le tas, on
obtient alors :
Le problme prcdent est en effet limin : la rfrence renvoye nest plus sur
un objet temporaire. Toutefois, qui va se charger dappeler delete sur lobjet cr
dans loprateur pour rendre la mmoire ?
Dans le cas o lon ralise un opration simple, ce nest pas trop grave, on
pourra toujours avoir :
c=a*b;
delete &c;
d=a*b*c;
d=operator*(a,operator*(b*c));
Notez quil a construction dun objet muet sans identificateur que vous navez
absolument pas la possibilit de dtruire. Il y a donc une relle perte de mmoire.
Une fois de plus, nous nous basons sur le comportement de ces oprateurs sur le
type canonique int.
++i=5;
i++=5;
En effet, dans le second cas, il y a conflit dans lordre des oprations entre
laffectation et la post incrmentation. Pour rsumer, il nous suffit de dire que ++X est
une lvalue, au contraire de X++. Ce qui nous indique clairement que loprateur de
pr incrmentation doit renvoyer une rfrence sur *this alors que loprateur de
post incrmentation doit renvoyer un objet constant.
class X
{
// code omis
public:
X & operator++(void); // pr incrmentation
const X operator++(int); // post incrmentation
};
-
Dautre part, reprenons un manuel (un bon, de prfrence ) de gnie logiciel.
Vous y trouverez probablement, sous quelque forme que ce soit, ladage suivant :
const X X::operator++(int)
{
const X temp=const_cast<const X>(*this); // copie de lobjet dans
// son tat actuel
operator++(); // pre incrementation
On voit bien que la post incrmentation est une opration beaucoup plus
onreuse que la pr incrmentation. Aussi, ds lors que lon pourra faire soit lune, soit
lautre, il faudra toujours utiliser la pr incrmentation.
Les oprateurs new et delete sont particulirement spciaux car ils grent
lallocation et la dsallocation des objets. Avant de se poser la question de leur
surcharge, nous allons dabord nous intresser leur fonctionnement dtaill.
Pour plagier Scott Meyers ( qui ce poly doit beaucoup !, merci Scott pour tout le
boulot ralis sur le C++), il faut bien diffrencier deux notions fondamentales.
Loprateur new
X *x=new X()
Considrez par exemple le programme suivant qui alloue des objets dans un
tableau de caractres prvu lavance. Cette technique peut tre utile dans un
environnement o les appels new sont indsirables (surcot important, faible
fiabilit, etc.)
{ }
try
Bloc de code {
protg (unique) // Code susceptible de lever une exception
}
n
{ }
catch (TypeException &identificateur)
Gestionnaires {
d'exceptions
// Code de gestion d'une exception
(multiples)
}
Aussi, il vaut mieux, comme nous le verrons plus loin, utilisez des
hirarchies bien penses de vos exceptions.
Notons galement que terminate est appele par dfaut par la fonction
unexpected.
try
{
}
catch (TypeException &e)
{
// code de traitement local
throw; // Relance lexception
}
Selon la norme, les exceptions peuvent tre de nimporte quel type (y compris
un simple entier). Toutefois, il est pratique de les dfinir en tant que classes. Afin de
ne pas polluer lespace global de nommage, il est possible dencapsuler les exceptions
lintrieur des classes qui les utilisent.
class exception
{
// code omis
virtual const char * what () const throw ()
{
return << pointeur vers une chane quelconque >> ;
}
};
De toute vidence, la mthode what est destine tre surcharge par chaque
classe drive de std::exception afin de fournir lutilisateur un message le
renseignant sur la nature de lerreur leve.
catch()
Erreurs dallocation
std::exception
TasMax::Erreur
TasMax::ErreurAcces TasMax::ErreurAllocation
TasMax::ErreurTasVide TasMax::ErreurTasPlein
class TasMax
{
public:
};
try
{
// bloc de code protg
}
catch (TasMax::ErreurTasVide &e)
{
// code spcifique cette exception
}
catch (TasMax::Erreur &e)
{
cerr << Le Tas Max a provoqu lexception : << e.what() << endl;
cerr << Fin du programme << endl;
exit(1);
}
catch (std::exception &e)
{
cerr << Exception inconnue : << e.what() << endl;
cerr << Fin du programme << endl;
Mettez vite Procol Harum sur la platine 33 tours de vos parents, coiffez vous
dune perruque hippie, fates passez un Censur dans lassistance et souvenez vous
de loprateur de conversion (cast) du langage C que nous aimons tous tellement. Sa
syntaxe est la suivante :
type(expression transtyper)
Y *y;
X *x=(X *)y
Pour lcrire avec la nouvelle syntaxe, il vous vaut passer par un typedef :
typedef X* PX;
Y *y;
X *x=PX(y)
Bien que simplificatrice, la nouvelle syntaxe ntait pas satisfaisante bien des
points de vue car elle ne fait que simplifier lcriture des transtypages et ne corrige
aucun des dfauts de loprateur traditionnel (par ordre croissant de svrit) :
Elle est dure retrouver dans un code source, car ce nest ni plus ni moins
quun nom de type et une paire de parenthses.
4.2.1 Pourquoi ?
Ces nouveaux oprateurs sont destins rsoudre les problmes soulevs par
loprateur traditionnel hrit du langage C. Le tableau suivant indique le nom des
nouveaux oprateurs ainsi que leur principale fonctionnalit. Notez bien que leur
syntaxe commune permet de rsoudre le problme de la recherche des transtypages
dans le code source
4.2.2 Syntaxe
O, vous laurez compris, op prend lune des valeurs (static, const, dynamic
ou reinterpret).
Cette syntaxe a lavantage de bien sparer le type que lon veut obtenir, qui est
plac entre signes < et >, de lexpression transtyper, place, elle, entre parenthses.
int i;
double d;
i=static_cast<int>(d)
4.2.3 Fonctionnalits
Cest loprateur de transtypage tout faire qui remplace dans la plupart des
cas loprateur hrit du C. Toutefois il est limit dans les cas suivants :
X const X
const X X
X volatile X
volatile X X
const X volatile X
volatile X const X
Tableau 4.1 Transtypages ralisables avec const_cast
X t1();
const X t2();
X *t11=&t1;
const X *t22=&t2;
Les downcast posent un problme particulier car leur vrification nest possible
qu lexcution. Aussi, contrairement static_cast et const_cast qui ne sont que
des informations ladresse du compilateur, dynamic_cast gnre du code de
vrification.
Supposons que vous disposez dun conteneur agrgeant par pointeur diffrents
objets issus dune ligne dobjets graphiques reprsente par la figure suivante :
ObjetGraphique
#NombreObjetsGraphiques : Entier
#Couleur : TypeCouleur
#X : Entier
#Y : Entier
#Epaisseur : Entier
+Crer()
+Dtruire()
+getX() : Entier
+getY() : Entier
+setX(valeur : Entier)
+setY(valeur : Entier)
+DeplacerVers(versX : Entier, versY : Entier)
+Afficher()
+Effacer()
Ligne Cercle
ObjetGraphique *o;
const Ligne ligne;
Cercle cercle;
const Cercle *pCercle;
Ligne *pLigne;
Solution :
operator type_desire(void)
class Rationnel
{
//
operator double (void)
{
return double(numerateur)/denominateur;
}
};
{
double a=0.0;
double c;
Rationnel r(1,2);
c = a + r;
}
Ces conversions sont assurment trs pratiques car elles permettent dviter un
effort de programmation supplmentaire au cours de la programmation. Elles ont
toutefois des cts particulirement dtestables :
Le code obtenu lors de leur utilisation est certes plus compact mais peut
induire en erreur sur le type rel des objets un programmeur charg de sa
maintenance.
Dans lexemple suivant, nous allons voir quune simple faute de frappe peut se
transformer en bug trs dlicat dtecter. Nous reprenons la classe Rationnel munie
de son oprateur de conversion explicite vers les rels et utilisons le code suivant :
Rationnel r1(1,2);
Rationnel r2(1,3);
4.3.1.3 Parade
Il est crit dans la norme du C++ que les constructeurs avec un seul paramtre
peuvent tre utiliss la vole pour construire un objet temporaire.
Dans lexemple suivant, la classe Chaine dispose dun constructeur qui prend
un entier en paramtre, soit :
class Chaine
{
public:
Chaine (int param) // Cre une chaine de param caractres non
// initialiss
};
4.3.2.2 Parade
5. En guise de conclusion
Je voudrais ddicacer ce manuel de survie la promotion 1999 de lISIMA au
sein de laquelle je compte de nombreux amis. Bien plus que de simples tudiants, ils
ont su me donner le got de lenseignement. Je sais ce que je leur dois et leur souhaite
bonne chance dans la vie.