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.

 
CONSTRUCTION
 

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;
}

 
OPERATEUR D'AFFECTATION
 

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.

 
DESTRUCTION
 

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.

 
ACCESSEURS
 

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.

 
Accès en lecture simple
 

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.

 
OPERATEUR D'INDEXATION
 

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.

 
OPERATEURS DE FLUX
 

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.

 
CONVERSION
 

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.

 
Opérateurs de 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; }
 ...
};

 
Politiques de conversion
 

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.

 
L'opérateur (type)

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).

 
L'opérateur static_cast

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).

 
L'opérateur dynamic_cast

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.

 
L'opérateur const_cast

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.

 
Conclusion

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).

 
REFERENCES
 

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).