 |
2. CONCEPTION D'UNE CLASSE |
Dans ce chapitre, nous tentons d'expliquer comment développer correctement une classe en
C++ et proposons quelques règles d'usage qui permettent d'éviter certains
pièges. Nous prenons l'exemple d'une classe Chaine dont le rôle est
d'encapsuler une chaîne de caractères, afin d'en proposer une manipulation plus
simple. Dans cette classe, la chaîne de caractères, un tableau au sens C, sera
allouée dynamiquement, ce qui nécessite de surcharger les méthodes qui
étaient fournies par défaut dans la classe Complexe
présentée au chapitre précédent. En effet, à la construction,
l'allocation dynamique du tableau est indispensable, lors de l'affectation, une réallocation
peut être nécessaire et enfin, à la destruction, la mémoire
allouée pour le tableau doit être rendue.
Nous en profiterons aussi pour redéfinir quelques opérateurs comme l'indexation
[], l'addition + (jouant ici un rôle de concaténation) et
les flux << et >>. Nous parlerons également du
mécanisme de conversion qui repose sur la manipulation d'opérateurs. Des informations
concernant certains mots-clé comme inline, const... sont
également disponibles ici. L'écriture de la classe Chaine nous permet de
conclure sur l'utilité et la manière adéquate d'utiliser les
références. Voici la déclaration de la classe Chaine.
class Chaine {
char * _t; // Tableau de caractères.
int _n; // Nombre de caractères.
public:
Chaine(void);
Chaine(const char *);
Chaine(int);
Chaine(const Chaine &);
~Chaine(void);
Chaine & operator=(const Chaine &);
char operator[](int) const;
char & operator[](int);
};
Il est conseillé, lors de la surcharge d'un opérateur, de conserver le plus
possible sa sémantique initiale. La principale raison est simplement d'éviter toute
confusion chez les utilisateurs de cet opérateur. Si la sémantique est trop
différente, il est conseillé de passer par des méthodes classiques pour
fournir les fonctionnalités souhaitées.
Le constructeur par défaut doit être redéfini pour initialiser les
attributs.
Chaine::Chaine(void) : _t(0), _n(0) {}
Un constructeur prenant en paramètre une chaîne de caractères peut
aussi être défini.
Chaine::Chaine(const char * s) : _t(0), _n(std::strlen(s)) {
_t=new char[_n+1];
std::strcpy(_t,s);
}
A noter que le constructeur par défaut peut être proposé simplement en
fournissant un attribut par défaut au constructeur précédent.
Chaine::Chaine(const char * = "");
Le constructeur de copie fourni par défaut doit également être
redéfini, la duplication simple de l'attribut _t (qui est un pointeur)
conduirait à une incohérence dans le nouvel objet: le tableau de caractères
serait partagé avec l'objet qui a été dupliqué. Voici un constructeur
de copie qui évite ce problème en créant un tableau pour le nouvel objet.
Chaine::Chaine(const Chaine & c) : _t(0), _n(c._n) {
_t=new char[_n+1];
std::strcpy(_t,c._t);
}
Il peut être également intéressant de définir un constructeur pour
créer une chaîne vide d'une taille donnée.
Chaine::Chaine(int n) : _t(0), _n(n) {
int i = 0;
_t=new char[_n+1];
while (i<_n) _t[i++]=' ';
_t[i]=0;
}
L'opérateur d'affectation doit être redéfini pour les mêmes raisons
que le constructeur de copie.
Chaine & Chaine::operator=(const Chaine & c) {
if (this!=&c) {
if (_t!=0) delete [] _t;
_n=c._n;
_t=new char[_n+1];
std::strcpy(_t,c._t);
}
return *this;
}
Notons que cet opérateur renvoie une référence de l'objet sur lequel il a
été appliqué, de cette manière, l'expression a=b=c peut
être évaluée, puisqu'elle correspond à la succession d'appels
a.operator=(b.operator=(c)). En outre, pour éviter tout problème, il est
conseillé de vérifier en début de méthode que l'argument passé
n'est pas l'objet lui-même. Ainsi, dans l'expression a=a, rien ne se passe.
Contrairement à l'exemple du chapitre précédent, le destructeur a ici un
rôle, celui de libérer la mémoire allouée par l'objet.
Chaine::~Chaine(void) { if (_t!=0) delete [] _t; }
Il faut remarquer également que la libération du tableau _t
s'effectue ici avec l'opérateur delete [] et non pas avec simplement
delete. Il faut être très attentif lors de la destruction: si le pointeur
référence une zone de mémoire allouée par l'intermédiaire d'un
new simple (i.e. allocation et construction d'un seul objet) alors l'opérateur
delete simple doit être utilisé; si le pointeur référence
une zone mémoire allouée par l'intermédiaire d'un new collectif
(i.e. utilisant les []) alors l'opérateur delete [] doit
être utilisé. Toute erreur d'inversion de ces deux opérateurs de destruction
conduira à des problèmes d'accès mémoire du type segmentation
fault.
En programmation orientée objet, l'une des règles fondamentales consiste à
encapsuler les attributs d'un objet. Par défaut en C++, si l'on ne précise rien, tous
les membres sont privés (private). Les mots-clés
public ou protected peuvent être employés pour
spécifier des membres publics (visibles par tout le monde) ou protégés
(visibles par la classe et ses sous-classes, contrairement aux privés qui ne
sont visibles que par la classe uniquement). Pour accéder à un attribut, on doit
alors passer par des méthodes, appelées souvent accesseurs, qui peuvent
contrôler l'accès en lecture et/ou en écriture des attributs.
Supposons par exemple l'attribut _t auquel on souhaite permettre l'accès
en lecture simple.
inline int Chaine::taille(void) const { return _t; }
Cette simple ligne soulève plusieurs remarques. Tout d'abord, la méthode
est déclarée inline, cela signifie que,
lorsqu'elle est appelée, son code est recopié à la place de son appel (c'est
ce qu'on appelle le déroulement, ou unrolling, de la méthode), aucun
mécanisme d'appel de fonction n'est alors déclenché. Cela signifie que le code
binaire généré est totalement équivalent à celui de
l'accès direct à l'attribut si celui-ci avait été public. En outre, la
définition de toute méthode inline doit impérativement se trouver dans
un header (i.e. un fichier d'entête .h, .hpp...), de sorte
que le compilateur puisse remplacer à tout moment l'appel par le corps de la méthode
(dans le cas contraire, l'unrolling ne sera pas effectué).
La seconde remarque est que la déclaration de la méthode précédente
est suivie du mot clé const, ce qui signifie ici que la méthode ne
modifie pas les attributs de l'objet. En d'autres termes, cette méthode peut être
appelée même si l'objet est constant, contrairement à une méthode qui
n'a pas ce mot-clé. Par la suite, nous désignons une telle méthode comme
constante.
A l'opposé du C, la constance d'un objet est fondamentale. Il est très
important, au moment de l'écriture du prototype d'une méthode de déterminer
quels sont les arguments qui seront constants, si l'objet retourné est constant ou non et
enfin si la méthode est constante ou non. A cet aspect s'ajoute aussi la manière de
passer les arguments et de retourner un objet: soit par copie, soit par référence
(constante ou non).
Dernière remarque, dans la déclaration précédente, une copie de
_t est retournée car on ne souhaite pas que l'attribut soit modifié.
Pour que la méthode fournisse un accès à l'attribut en lecture seule, on
aurait pu l'écrire également de la manière suivante.
inline const int & Chaine::taille(void) const { return _t; }
Au lieu de retourner une copie de l'attribut, on renvoie une référence constante
qui pointe bien physiquement sur l'attribut, mais ne permet pas de le modifier. Cet exemple prend
plus de sens dans le cas d'un attribut qui n'est pas d'un type primitif: supposons un attribut
_n de type Chaine dans une classe Personne. Un accesseur en
lecture s'écrira comme suit.
inline const Chaine & Personne::nom(void) const { return _n; }
On évite ainsi la copie de l'attribut en retournant une référence constante
sur l'objet. De cette manière, l'objet est tout autant protégé contre
l'écriture que dans l'exemple précédent. L'usage veut que, lorsqu'on renvoie
en lecture un type primitif, on retourne une copie (car elle n'est pas plus coûteuse que le
passage par référence), alors que si l'on renvoie un objet, on retourne une
référence constante, cela évite une copie qui pourrait être
coûteuse.
| |
| Accès en lecture/écriture |
| |
Revenons maintenant à notre exemple de la classe Chaine. Voici comment
permettre l'accès à la fois en lecture et en écriture à l'attribut
_t.
inline int & Chaine::taille(void) { return _t; }
Remarquez que le mot-clé const a disparu, ce qui signifie que cette
méthode ne peut être appelée que si l'objet n'est pas constant, ce qui est tout
à fait cohérent avec la sécurité d'écriture de l'objet. Il faut
noter également que le mot-clé const qui identifie une méthode
constante fait partie de sa signature. Cela signifie que le compilateur est capable de distinguer
la version lecture de la méthode taille() de sa version lecture/écriture
et qu'elles peuvent donc tout à fait cohabiter dans un même programme. La version
constante sera appelée uniquement lorsque l'objet est constant. Voici un rappel des deux
méthodes.
inline int Chaine::taille(void) const;
inline int & Chaine::taille(void);
Mais il est également possible de proposer deux méthodes distinctes pour
accéder aux attributs, soit en lecture, soit en écriture.
inline int Chaine::getTaille(void) const { return _t; }
inline void Chaine::setTaille(int t) { _t=t; }
La grande différence entre ces deux possibilités est qu'avec la
seconde solution, on sait quelle méthode sert à la lecture (cout <<
c.getTaille()) et quelle méthode sert à l'écriture
(c.setTaille(20)). Alors que dans la première proposition, la version non
constante peut servir sur un objet non constant aussi bien à la lecture (cout <<
c.taille()) qu'à l'écriture (c.taille()=20). Si un
contrôle différent doit être effectué selon qu'on se trouve en lecture
ou en écriture, alors il faut utiliser la seconde solution.
L'opérateur d'indexation permet d'appliquer les symboles [] directement
à un objet. Il s'agit d'un type particulier d'accesseur. Ainsi, on peut définir
une version lecture simple.
inline char Chaine::operator[](int i) const {
if (i>=_n) {
std::cerr << "Debordement !" << std::endl;
exit(1);
}
return _t[i];
}
Et une version lecture/écriture.
inline char & Chaine::operator[](int i) {
if (i>=_n) {
std::cerr << "Debordement !" << std::endl;
exit(1);
}
return _t[i];
}
On s'aperçoit ici de l'intérêt d'un accesseur plutôt qu'un
accès direct à l'attribut, puisqu'il permet de contrôler que l'indice est
valide avant de tenter l'accès aux données.
| |
| QUELQUES OPERATEURS BINAIRES |
| |
Il existe deux catégories d'opérateurs: les unaires (*,
++, --, []...) et les binaires (+,
-, *, /, <<, >>...).
Les opérateurs unaires n'impliquent qu'un seul objet, ils peuvent donc être
définis comme des méthodes de celui-ci (e.g. la section précédente). En
revanche, les opérateurs binaires mettent en relation deux objets, parfois de classes
différentes. On préfère donc les définir comme des fonctions.
Il reste néanmoins possible de les manipuler comme des méthodes, mais cette
approche est généralement déconseillée. Elle est utile seulement
si l'on souhaite exploiter la redéfinition de ces méthodes dans des sous-classes.
De retour à notre classe Chaine, voici l'exemple de la surcharge de
l'opérateur de concaténation.
Chaine operator+(const Chaine & s1,const Chaine & s2) {
Chaine s3(s1.taille()+s2.taille());
int i = 0;
int j = 0;
while (i<s1.taille()) { s3[i]=s1[i]; ++i; }
while (j<s2.taille()) s3[i++]=s2[j++];
return s3;
}
L'opérateur est implémenté sous la forme d'une fonction, simplement parce
que s1 et s2 ont des rôles symétriques, et que choisir de
faire subir l'opération sous la forme d'une méthode à l'un plutôt
qu'à l'autre n'a pas vraiment de sens. En outre, afin de permettre l'évaluation d'une
expression du genre c=a+b, l'opérateur doit retourner une copie de la
concaténation. Dans l'exemple, nous avons choisi d'utiliser uniquement des méthodes
publiques de la classe Chaine pour effectuer la concaténation, mais il est
possible (et parfois indispensable) que l'opérateur accède à des attributs
cachés (i.e. protégés ou privés). Il faut alors que la fonction
operator+ soit amie de la classe Chaine. Pour cela, il suffit de rajouter
la ligne suivante dans la déclaration de la classe.
Chaine operator=(const Chaine &,const Chaine &);
class Chaine {
...
friend Chaine operator=(const Chaine &,const Chaine &);
};
A noter que cela fonctionne seulement si, au moment de la déclaration de l'amitié,
le prototype de la fonction est connu. Nous reviendrons sur cette notion d'amitié, plus
particulièrement entre deux classes, dans le chapitre sur les classes
génériques.
Les opérateurs de flux sont binaires et impliquent des objets de classes
différentes. Ils sont donc déclarés en tant que fonctions.
#include<iostream>
std::ostream & operator<<(std::ostream & o,const Chaine & c)
{
int i = 0;
while (i<c.taille()) o << c[i++] << ' ';
return o;
}
std::istream & operator>>(std::istream & i,Chaine & c) {
char s[256];
i >> s;
c=Chaine(s);
return i;
}
Afin de permettre le chaînage des flux, e.g. cout << a << " "
<< b, les opérateurs de flux doivent retourner le flux en question. En outre,
il faut éviter de copier des objets flux, leur fonctionnement en est alors
totalement altéré.
| |
| OPERATEURS D'INCREMENTATION |
| |
L'opérateur ++ (ou --) se décline en deux
méthodes, suivant qu'on l'utilise en préfixé (e.g. ++i) ou en
postfixé (e.g. i++). Supposons une classe Entier qui agrège
un attribut entier _p qu'on souhaite incrémenter par l'opérateur
++. La forme préfixée se déclare de la manière suivante.
Entier & Entier::operator++(void) {
++_p;
return *this;
}
Et la forme postfixée de la façon suivante.
Entier Entier::operator++(int) {
Entier p(*this);
++_p;
return p;
}
Cet opérateur doit retourner un objet de la classe en question. Pour le
préfixé, l'objet même peut être retourné, alors que pour le
postfixé, une copie doit être renvoyée. Le paramètre int
passé à la version postfixée est muet: il n'est pas utilisé dans la
méthode. Mais il est nécessaire au compilateur pour distinguer les deux versions
par leur prototype.
Déjà en C, il est possible de convertir une variable d'un type en un autre.
Seulement cela s'effectue sans aucune vérification sur la validité de
l'opération, et encore plus grave, sans que le programmeur ne puisse intervenir sur la
manière d'effectuer la conversion.
Intéressons-nous tout d'abord à la manière de décrire la conversion
d'un type d'objet en un autre. Pour cela, revenons à l'exemple de la classe
Chaine. Le constructeur suivant a été défini.
Chaine::Chaine(const char * s);
Cette méthode est un opérateur de conversion d'une chaîne de
caractères de type char * en un objet de la classe Chaine.
Il faut noter que cette conversion est implicite, cela signifie que, si une méthode a
besoin d'un argument de type Chaine et que le programmeur fourni un objet de type
char *, alors le compilateur décide seul d'appeler le constructeur
qui permet la conversion. Considérons l'exemple suivant.
Chaine a("J'attends ");
Chaine b = a+"la suite";
Le compilateur va interpréter ce code comme suit.
Chaine a("J'attends ");
Chaine b = a+Chaine("la suite");
Cela peut conduire à des confusions importantes. Souvenez-vous, nous avons écrit
un constructeur qui prend comme argument un entier (cf. la section sur la construction), cela nous
semblait pratique pour initialiser une chaine vide. Cependant, si l'on écrit le code
suivant.
Chaine a("Voila le nombre magique: ");
Chaine b = a+245;
Le compilateur va interpréter ce code comme suit.
Chaine a("Voila le nombre magique: ");
Chaine b = a+Chaine(245);
On s'aperçoit immédiatement de la confusion. La compilation est autorisée
mais le résultat n'est probablement pas celui escompté par le programmeur (i.e.
la conversion de l'entier 245 en une chaine "245"). Il vaut
mieux dans cette situation que la compilation soit interdite, ce qui obligera le programmeur
à vérifier que le constructeur à partir d'un entier réalise bien ce
qu'il souhaite. Il pourra ensuite préciser explicitement l'appel au constructeur. Pour
empêcher la conversion implicite, on utilise le mot-clé explicit de la
manière suivante sur le constructeur souhaité.
class Chaine {
...
explicit Chaine(const char *);
...
};
On peut également vouloir effectuer la conversion inverse, i.e. passer d'un objet
Chaine à une chaîne de caractères. Pour cela, il est possible
d'écrire un opérateur de conversion de la manière suivante dans la classe
Chaine.
class Chaine {
...
operator char * (void) const { return _t; }
...
};
La conversion d'un objet d'un type en un autre peut être une opération
délicate. Ainsi, différentes directives sont proposées pour préciser le
type de conversion ainsi que le type de contrôle qu'on désire effectuer afin
d'éviter toute erreur dans le code.
Pour convertir un objet, nous avons vu qu'il était possible d'utiliser l'opérateur
(type) issu du langage C. En supposant une chaîne de caractères
s et une instance c de la classe Chaine, les deux lignes
suivantes sont équivalentes.
c=(Chaine)s;
c=Chaine(s);
Elles permettent la conversion en utilisant les opérateurs définis par
l'utilisateur. Si aucun opérateur n'est disponible, la conversion est interdite par le
compilateur. Certaines conversions sont proposées de base pour les types primitifs, comme par
exemple la conversion d'un float en un int. En ce qui concerne les
pointeurs, l'opérateur (type) autorise toute conversion, que les classes soient
liées par une relation d'héritage ou non. Supposons par exemple trois classes
A, B et C.
class A { ... };
class B : public A { ... };
class C { ... };
A * a = new A();
B * b = new B();
C * c = new C();
A * pa;
B * pb;
Ainsi, les lignes suivantes sont autorisées.
pa=(A *)c; (1)
pb=(B *)a; (2)
Alors que la première ligne devrait être interdite, simplement parce que C
n'est pas une sous-classe de A. La seconde ligne devrait également être
interdite, puisque a n'est pas un objet de la classe B. Cependant, dans
le second cas, on peut imaginer le code suivant.
a=b;
pb=(B *)a; (3)
La conversion de la seconde ligne devrait alors être possible. Cependant, une
vérification est nécessaire au moment de l'exécution pour être sûr
que a pointe bien sur un objet de type B. Nous venons de voir que
l'utilisation de l'opérateur (type) est dangereuse dès qu'on utilise
des pointeurs (avec des références le phénomène serait le même).
Il est donc important de disposer d'autres opérateurs qui soient capables de vérifier
la conversion. Pour cela, l'opérateur static_cast est introduit pour
éviter le cas (1). L'opérateur dynamic_cast est quant à
lui utilisé pour vérifier au moment de son exécution les cas (2) et
(3).
Cet opérateur est donc utilisé pour éviter de convertir un pointeur sur un
type donné en un pointeur sur un type qui n'est pas lié par héritage (cela
marche aussi pour des références). Ainsi la conversion suivante est autorisée
(même si elle risque d'être invalide).
pb=static_cast<B *>(a);
En revanche, les conversions suivantes sont interdites (pi étant un pointeur
sur un int et pf un pointeur sur un float).
pa=static_cast<A *>(c);
pf=static_cast<float *>(pi);
Les règles de conversion concernant le type de pointeur void * restent
obscures dans les spécifications du langage. En effet, la conversion suivante est
autorisée (ce qui est tout à fait logique).
void * pv = static_cast<void *>(a);
En revanche, la conversion suivante devrait être interdite (car il n'y a aucun moyen de
savoir ce que référence pv).
pa=static_cast<A *>(pv);
Cependant, certains compilateurs autorisent cette conversion. Il faut noter que
l'opérateur dynamic_cast chargé de régler les problèmes de
conversion au moment de l'exécution interdit cette action. Dans un souci de
portabilité et pour cette situation particulière uniquement, nous vous recommandons
plutôt l'utilisation de l'opérateur reinterpret_cast (l'opérateur
(type) fonctionne également).
Cet opérateur ne peut être utilisé que pour des pointeurs (ou des
références), et comme nous l'avons vu précédemment, il ne peut pas
être employé avec le type void *. Néanmoins, il permet de
vérifier, au moment de l'exécution, la conversion d'un pointeur d'une classe
A vers un pointeur d'une de ses sous-classes B. Cette conversion est
désignée par le terme downcast, elle est opposée à la
conversion inverse, toujours possible, d'une classe vers l'une de ses super-classes et qui est
nommée upcast. Dans l'exemple suivant, si pa pointe
réellement sur un objet de la classe B, alors la conversion s'effectue. En
revanche, si pa pointe par exemple sur un objet de la classe A, alors la
conversion est impossible et le pointeur NULL est retourné.
pb=dynamic_cast<B *>(pa);
Il existe une petite différence lorsqu'on manipule des références comme
dans l'exemple suivant.
A a;
B b;
A & ra = a;
A & rb = b;
B & ref1 = dynamic_cast<B &>(ra);
B & ref2 = dynamic_cast<B &>(rb);
Dans le cas de ref2, la conversion est possible. Par contre, dans le cas de
ref1, la conversion est impossible, mais comme NULL ne peut pas
être renvoyé, une exception est levée. Remarquez que la conversion par
référence est importante, puisqu'elle permet d'éviter des recopies.
Cet opérateur est utilisé pour simplement retirer l'aspect constant d'un objet. Il
n'a de réel signification que s'il est appliqué sur une référence. En
effet, considérons l'exemple suivant.
const Chaine c1;
Chaine c2 = c1;
La conversion ne pose aucun problème, puisqu'une copie est effectuée et que
celle-ci n'hérite pas de l'aspect constant. En revanche, si l'on considère l'exemple
suivant.
const Chaine c1;
Chaine & c2 = const_cast<Chaine &>(c1);
L'opérateur de conversion const_cast est alors indispensable. Il peut donc
être utilisé lorsqu'on reçoit une référence constante pour la
rendre non constante et ainsi pouvoir modifier l'objet qu'elle référence. Il est bien
évident que l'usage de cet opérateur est à éviter dans la mesure
où il altère totalement les règles fondamentales liées
à la constance d'un objet. Dans la majorité des cas, lorsque le programmeur se
retrouve obligé d'utiliser l'opérateur const_cast sur une variable,
c'est qu'il a commis une erreur de conception, soit en imposant à tort la constance de la
variable, soit en omettant des méthodes qui permettraient un accès non constant
à la variable.
Il n'est pas toujours évident de déterminer le bon opérateur de conversion
pour une situation donnée. Le tableau suivant tente de résumer les
différentes possibilités et d'indiquer si, dans chaque situation, les
opérateurs autorisent ou non la conversion. Cela permet d'identifier l'opérateur le
mieux adapté (repéré par un *) pour chaque type de conversion.
| |
Objet Chaine
vers
char * |
Pointeur B
vers
pointeur A |
Pointeur A
vers
pointeur B |
Pointeur objet
vers
void * |
void *
vers
pointeur objet |
(type) |
Oui * |
Oui |
Oui |
Oui |
Oui |
static_cast |
Oui |
Oui * |
Oui |
Oui * |
Ne devrait pas |
dynamic_cast |
Non applicable |
Oui |
Oui *
(après vérification) |
Oui |
Non applicable |
reinterpret_cast |
Non applicable |
Oui |
Oui |
Oui |
Oui * |
Nous rappelons que la conversion d'une référence d'un type donné en une
référence d'un autre type se déroule de la même manière que la
conversion entre pointeurs (i.e. la 2ème et la 3ème colonne du
tableau).
Il est important de préciser maintenant le rôle des références, qui
est tout aussi fondamental que celui du mot-clé const. Il existe deux
manières classiques de passer un argument à une méthode: soit par valeur, soit
par référence (on oublie le passage par pointeur du C qui n'est plus très
utile ici, comme nous le verrons par la suite). Le passage par valeur est souvent utilisé,
notamment en C, pour éviter qu'on puisse modifier l'argument dans la méthode. En
C++, il est possible d'éviter la copie (qui peut être coûteuse si l'objet est
important) en fournissant une référence sur un objet constant. Pour un argument de
type primitif, fournir une copie ou une référence constante a peu de
différences en termes d'efficacité. Le tableau suivant résume comment passer
un argument nommé arg en fonction de la situation qui se présente.
| |
Type primitif T |
Classe C |
| Argument variable |
T & arg |
C & arg |
| Argument constant |
T arg
ou
const T & arg |
const C & arg |
Le passage par valeur n'est donc plus très utile. Il peut seulement servir à
simplifier l'écriture d'une méthode en utilisant la copie d'un argument plutôt
que de définir une variable locale à l'identique de l'argument. En ce qui concerne
l'objet retourné par une méthode, là aussi il peut s'agir d'un passage par
valeur ou par référence (on oublie pour les mêmes raisons le passage par
pointeur). Ici, la recopie de l'objet retourné peut être primordiale. En effet, il
n'est pas possible de retourner une référence sur une variable locale à la
méthode (le contexte de celle-ci est détruit dès que le retour
s'effectue). Voici un tableau qui résume comment retourner une valeur en fonction des cas
qui se présentent.
| |
Type primitif T |
Classe C |
Retour (en mode lecture)
d'un attribut de la classe |
T m(...) const;
ou
const T & m(...) const; |
const C & m(...) const; |
Retour (en mode lecture/écriture)
d'un attribut de la classe |
T & m(...); |
C & m(...); |
Retour d'un résultat produit
par la méthode |
T m(...) const; |
C m(...) const; |
Il est important de noter également que le passage par référence permet le
polymorphisme (détaillé au chapitre suivant), ce qui serait impossible avec le
passage par valeur. Prenons l'exemple suivant.
class A {
public:
virtual void m(void) const { std::cout << "A::m()"; }
...
};
class B : public A {
public:
virtual void m(void) const { std::cout << "B::m()"; }
...
};
void f1(const A & a) { a.m(); }
void f2(A a) { a.m(); }
Supposons un objet b de classe B. L'appel à la fonction
f1(b) produira le message "B::m()" alors que l'appel
f2(b) produira "A::m()". Dans le premier cas, il n'y a pas de
recopie, donc la variable locale a référence bien b; alors
que dans le second cas, l'argument est recopié (en appelant le constructeur de copie de
A) pour produire une copie de b uniquement sur la partie concernant
la classe A, en d'autres termes la variable locale est simplement un objet de classe
A.
| |
| |
| Copyright (c) 1999-2016 - Bruno Bachelet - bruno@nawouak.net -
http://www.nawouak.net |
| La permission est accordée de copier, distribuer et/ou modifier ce document
sous les termes de la licence GNU Free Documentation License, Version 1.1 ou toute
version ultérieure publiée par la fondation Free Software Foundation. Voir
cette licence pour plus de détails (http://www.gnu.org). |
|
|