La technique de l’héritage
Modalités d’accès à la classe de base
Les membres privés d’une classe de base ne sont jamais accessibles aux fonctions membre de sa classe dérivée. Outre les « statuts » public ou privé (présentés au chapitre 3), il existe un statut « protégé ». Un membre protégé se comporte comme un membre privé pour un utilisateur quelconque de la classe ou de la classe dérivée, mais comme un membre public pour la classe dérivée. D’autre part, il existe trois sortes de dérivation : ■ publique – les membres de la classe de base conservent leur statut dans la classe dérivée ; c’est la situation la plus usuelle ; ■ privée – tous les membres de la classe de base deviennent privés dans la classe dérivée ; ■ protégée (depuis la version 3) – les membres publics de la classe de base deviennent membres protégés de la classe dérivée ; les autres membres conservent leur statut. Lorsqu’un membre (donnée ou fonction) est redéfini dans une classe dérivée, il reste toujours possible (soit dans les fonctions membre de cette classe, soit pour un client de cette classe) d’accéder aux membres de même nom de la classe de base ; il suffit pour cela d’utiliser l’opérateur de résolution de portée (::), sous réserve, bien sûr, qu’un tel accès soit autorisé. Appel des constructeurs et des destructeurs Soit B une classe dérivée d’une classe de base A. Naturellement, dès lors que B possède au moins un constructeur, la création d’un objet de type B implique obligatoirement l’appel d’un constructeur de B. Mais, de plus, ce constructeur de B doit prévoir des arguments à destination d’un constructeur de A (une exception a lieu soit si A n’a pas de constructeur, soit si A possède un constructeur sans argument). Ces arguments sont précisés dans la définition du constructeur de B, comme dans cet exemple : B (int x, int y, char coul) : A (x, y) ; Les arguments mentionnés pour A peuvent éventuellement l’être sous forme d’expressions. Cas particulier du constructeur par recopie En plus des règles ci-dessus, il faut ajouter que si la classe dérivée B ne possède pas de constructeur par recopie, il y aura appel du constructeur par recopie par défaut de B, lequel procédera ainsi : ■ appel du constructeur par recopie de A (soit celui qui y a été défini, soit le constructeur par recopie par défaut) ; ■ initialisation des membres donnée de B qui ne sont pas hérités de A. En revanche, un problème se pose lorsque la classe dérivée définit explicitement un constructeur par recopie. En effet, dans ce cas, il faut tenir compte de ce que l’appel de ce constructeur par recopie entraînera l’appel : ■ du constructeur de la classe de base mentionné dans son en-tête, comme dans cet exemple (il s’agit ici d’un constructeur par recopie de la classe de base, mais il pourrait s’agir de n’importe quel autre constructeur) : exos_c++.book Page 196 Jeudi, 5. juillet 2007 11:10 11 © Éditions Eyrolles 197 chapitre n° 13 La technique de l’héritage B (B & b) : A(b) ; // appel du constructeur par recopie de A // auquel sera transmise la partie de B héritée de A // (grâce aux règles de compatibilité entre // classe dérivée et classe de base) ■ d’un constructeur sans argument, si aucun constructeur de la classe de base n’est mentionné dans l’en-tête ; dans ce cas, il est nécessaire que la classe de base dispose d’un tel constructeur sans argument, faute de quoi on obtiendrait une erreur de compilation. Conséquences de l’héritage Considérons la situation suivante, dans laquelle la classe A possède une fonction membre f (dont nous ne précisons pas les arguments) fournissant un résultat de type t (quelconque : type de base ou type défini par l’utilisateur, éventuellement type classe) : class A class B : public A { ….. { ….. public : } ; t f (…) ; ….. } ; A a ; // a est du type A B b ; // b est du type B, dérivé de A Naturellement, un appel tel que a.f(…) a un sens et il fournit un résultat de type t. Le fait que B hérite publiquement de A permet alors de donner un sens à un appel tel que : b.f (…) La fonction f agira sur b, comme s’il était de type A. Le résultat fourni par f sera cependant toujours de type t, même, notamment, lorsque le type t est précisément le type A (le résultat de f pourra toutefois être soumis à d’éventuelles conversions s’il est affecté à une lvalue). Cas particulier de l’opérateur d’affectation Considérons une classe B dérivant d’une classe A. Si la classe dérivée B n’a pas surdéfini l’opérateur d’affectation, l’affectation de deux objets de type B se déroule membre à membre, en considérant que la « partie héritée de A » constitue un membre. Ainsi, les membres propres à B sont traités par l’affectation prévue pour leur type (par défaut ou surdéfinie, suivant le cas). La partie héritée de A est traitée par l’affectation prévue dans la classe A. Si la classe dérivée B a surdéfini l’opérateur =, l’affectation de deux objets de type B fera nécessairement appel à l’opérateur = défini dans B. Celui de A ne sera pas appelé, même s’il a été surdéfini. Il faudra donc que l’opérateur = de B prenne en charge tout ce qui concerne l’affectation d’objets de type B, y compris pour ce qui est des membres hérités de A (quitte à faire appel à l’opérateur d’affectation de A)
Exercice 104
La seule démarche possible consiste à créer une classe pointcol dans laquelle un des membres donnée est lui-même de type point. Sa déclaration et sa définition se présenteraient alors ainsi : /***** fichier pointcol.h : déclaration de pointcol *****/ #include « point.h » #include using namespace std ; class pointcol { point p ; int cl ; public : pointcol (float = 0.0, float = 0.0, int = 0) ; void colore (int coul) { cl = coul ; } Énoncé On suppose qu’on dispose de la même classe point (et donc du fichier point.h) que dans l’exercice précédent. Créer une classe pointcol possédant les mêmes caractéristiques que ci-dessus, mais sans faire appel à l’héritage. Quelles différences apparaîtront entre cette classe pointcol et celle de l’exercice précédent, au niveau des possibilités d’utilisation ? exos_c++.book void affiche () { p.affiche () ; // affiche doit appeler affiche cout << » couleur : » << cl ; // du point p pour les // coordonnées } } ; /****** définition du constructeur de pointcol *****/ #include « point.h » #include « pointcol.h » pointcol::pointcol (float abs, float ord, int coul) : p (abs, ord) { cl = coul ; } Apparemment, il existe une analogie étroite entre cette classe pointcol et celle de l’exercice précédent. Néanmoins, l’utilisateur de cette nouvelle classe ne peut plus faire directement appel aux fonctions membre héritées de point. Ainsi, pour appliquer la méthode deplace à un objet a de type point, il devrait absolument écrire : a.p.deplace (…) ; or, cela n’est pas autorisé ici, puisque p est un membre privé de pointcol.