Héritage et polymorphisme

Décembre 2016

Héritage et polymorphisme

La notion d’héritagexe "héritage" est très facile à comprendre. En C++, on dit qu’une classe définie à partir d’une classe existante et à laquelle on ajoute une fonctionnalité dérive de la classe initiale. Cette dernière est la classe de base, et la classe dérivée hérite des méthodes et des données de cette classe de base.

La dérivation permet d’exprimer la relation est un. En effet, si nous considérons notre exemple de zoo du chapitre 1, un lion est un carnivore, un carnivore est un animal, etc. (voir fig. 6.1). Les classes dérivées (également nommées classes filles) héritent des données et fonctions de toutes leurs classes de base (également nommées classes mères). La classe Lion va donc hériter de la classe Animal ainsi que des données et fonctions spécifiques à la classe Carnivore. C’est pour cette raison que les données et fonctions partagées par certaines catégories d’objets doivent être définies au niveau le plus haut possible dans cette hiérarchie. Dans notre exemple, nous avions défini les données membres nom, age, couleur, taille et les fonctions membres Manger() et Dormir() au niveau de la classe Animal, puisque tous les animaux possèdent ces caractéristiques et ce comportement.

Figure 6.1 : détail de l’héritage entre la classe Animal et les deux classes Lion et Tigre.

Dérivation des classes

Le premier avantage de l’héritage est que nous pouvons réutiliser des classes existantes comme classes de base et les spécialiser en écrivant uniquement le code correspondant à la nouvelle fonctionnalité.
Oubliées, les opérations de copier-coller qui étaient potentiellement dangereuses en cas de modification du bloc de code reproduit (où a-t-il été dupliqué ? Combien de fois ?).
Dans notre hiérarchie de classes, toute modification au niveau d’une classe de base est automatiquement répercutée dans toutes ses classes dérivées.

Syntaxe

Soit les classes Base et Derivee telles que, comme leur nom l’indique, la seconde dérive de la première.
Le code 6.1 illustre les liens entre une classe de base et une classe dérivée.

Code 6.1 : déclaration d’une classe de base et d’une classe dérivéexe "déclaration:d’une classe de base et d’une classe dérivée"

#include <iostream>    
using namespace std;    

/* Déclaration de la classe de base */    
class Base    
{    
private:             //membres privés    
   int val_privee;    
public:    
   Base(){ cout << "Le constructeur de Base s’exécuten";    
           val_protected=5;            
   }    
   ~Base(){ cout << "Le destructeur de Base s’exécuten"; }    
   void Afficher()     
     { cout << "La fonction Afficher() de Base s’exécuten"; }    
protected:         //membres protégés    
   int val_protected ;    
};    

/* Déclaration de la classe dérivée */    
class Derivee : public Base    
{    
private:    

public:    
   Derivee()    
   { cout << "Le constructeur de Derivee s'exécuten"; }    
   ~Derivee()    
   { cout << "Le destructeur de Derivee s'exécuten"; }    
   //Méthodes d'accès    
   void Lire() const     
   { cout << "Lecture de val_protected depuis la classe Derivee: ";    
     cout << val_protected << endl; }    
};    

int main()    
{    
   Derivee objet_derivee;     //déclare un objet Derivee    
//on accède à une fonction de la classe de base :    
   objet_derivee.Afficher();      
//on accède à un membre protégé de la classe de base :    
   objet_derivee.Lire();          
}


L’exécution donne le résultat suivant :

Le constructeur de Base s’exécute    
Le constructeur de Derivee s’exécute    
La fonction Afficher() de Base s’exécute    
Lecture de val_protected depuis la classe Derivee: 5    
Le destructeur de Derivee s’exécute    
Le destructeur de Base s’exécute


Le lien entre Base et Derivee apparaît sur la première ligne de la déclaration de la classe dérivée : Derivee : public Base. Il s’agit d’une dérivation publique. En effet, la syntaxe de l’héritage en C++ consiste à faire suivre le nom de la classe fille par la liste des classes mères dans la déclaration avec les restrictions d’accès aux données, chaque élément étant séparé des autres par une virgule :

class Base1    
{    
    /* Contenu de la classe mère Base1. */    
};    

class Base2    
{    
    /* Contenu de la classe mère Base2. */    
};    

class Derivee : public Base1 : protected Base2    
{    
    /* Définition de la classe fille. */    
};


La classe dérivée hérite de tous les membres publics et protégés de sa classe de base. Elle n’a pas accès aux membres privés. Les membres protégés de la classe de base se comportent comme des membres privés vis-à-vis de toute autre classe.
Dans le code précédent, la classe Derivee hérite des données membres et méthodes de la classe Base1 avec le mot clé public et des données membres et méthodes de la classe Base2 avec le mot clé protected.
Le type d’héritage modifie le droit d’accès aux membres de la classe de base de la façon suivante (tableau 6.1).

Tableau 6.1 : droits d’accès sur les membres héritésxe "dérivation:types d’héritage"xe "classe:types d’héritage"
Protection des
membres dans la
calsse de base
Mot clé utilisé pour l'héritage
publicprotectedprivate
publicpublicprotectedprivate
protectedprotectedprotectedprivate
privateinterditinterditinterdit


Ainsi, les données publiques de la classe de base deviennent soit publiques, soit protégées, soit privées selon que la classe fille hérite en public, en privé ou protégé. Les données privées de la classe de base sont toujours inaccessibles, et les données protégées deviennent soit protégées, soit privées.
Si vous ne précisez pas le type d’héritage, il s’agit d’un héritage privé par défaut.

Règles

Voici les trois règles de la dérivation que nous allons détailler dans les sections suivantes :
  • La première règle indique que vous pouvez utiliser un objet d’une classe dérivée partout où un objet d’une de ses classes mères peut l’être. Les classes filles héritent en effet de tout ou partie des méthodes et données des classes mères selon le type d’héritage.
  • La deuxième règle indique que vous pouvez affecter une classe dérivée à une classe mère. Vous devrez cependant renoncer aux données qui n’apparaissent pas dans l’initialisation puisque la classe fille ne possède pas les déclarations de membre correspondantes. En revanche, l’inverse est strictement interdit. En effet, les données de la classe fille qui n’existent pas dans la classe mère ne peuvent recevoir de valeur.
  • Pour terminer, la troisième règle indique que les pointeurs des classes dérivées sont compatibles avec les pointeurs des classes mères. Il est donc possible d’affecter un pointeur de classe dérivée à un pointeur d’une de ses classes de base. Il faut bien entendu disposer des droits d’accès à la classe de base, c’est-à-dire qu’au moins un de ses membres puisse être utilisé. Cette condition n’est pas toujours vérifiée, en particulier pour les classes de base dont l’héritage est de type private.

constructeurs et destructeurs

Un objet Derivee est aussi un objet Base avec une fonctionnalité supplémentaire. Comme le démontre le programme précédent, quand vous créez un objet de type Derivee le constructeur Base() est d’abord appelé pour créer un objet de type Base, puis c’est le constructeur Derivee() qui « complète » la fonctionnalité de l’objet Derivee.
En ce qui concerne la suppression, elle s’effectue comme toujours dans l’ordre inverse : le destructeur ~Derivee() est appelé puis ~Base().

Substitution de fonctions

L’autre avantage de l’héritage est qu’il nous offre la possibilité de traiter les objets de façon polymorphique. « Polymorphique » signifie « plusieurs formes », le polymorphisme est donc la capacité à prendre plusieurs formes.
Pour illustrer ce concept, imaginons que nous définissons la fonction membre Manger() dans la classe Animal de la façon suivante :
virtual void Manger()    
{ cout << "L’animal mange!"; }


En C++, vous pouvez « redéfinir » les fonctions et données d’une classe de base dans une des classes dérivées pour l’adapter au nouveau comportement de la classe.
Dans le cas d’une fonction, elle doit conserver le même type de valeur retournée et la même signaturexe "fonction:signature"xe "signature de fonction" (nom + nombre et type des arguments).
Son implémentation sera différente dans la classe dérivée. On dit qu’il y a substitution de fonction. Notre fonction Manger(), par exemple, peut être redéfinie de la façon suivante dans la classe Carnivore :
void Manger()    
{ cout << "L’animal mange de la viande!"; }


Vous ne devez pas confondre substitution de fonction et surcharge de fonction. Le mécanisme est similaire mais, dans le premier cas, vous modifiez le corps de la fonction dans une classe dérivée sans changer le prototype. Dans le second cas, la surcharge consiste à déclarer « plusieurs versions » d’une même fonction dans une portée déterminée, chaque version étant associée à un prototype différent.



Autre exemple. Si la classe Derivee dérive de la classe Base, par exemple, et que toutes deux contiennent une donnée d, les instances de la classe Derivee utiliseront la donnée d de la classe Derivee et les instances de la classe Base utiliseront la donnée d de la classe Base.
Cependant, les objets de classe Derivee contiennent un sous-objet, lui-même instance de la classe Base.
Par conséquent, ils contiendront la donnée d de la classe Base, mais cette dernière sera cachée par la donnée d de la classe la plus dérivée, à savoir la classe Derivee.
Cette règle est générale : quand une classe dérivée redéfinit un membre d’une classe de base, ce membre est caché et on ne peut plus accéder qu’au membre de la redéfinition (celui de la classe dérivée).

À savoir

Il est possible d’accéder aux membres cachés d’une classe de base si vous nommez le membre complètement à l’aide de l’opérateur de résolution de portée :: . Le nom complet d’un membre est constitué du nom de sa classe suivi de l’opérateur de résolution de portée, suivi du nom du membre :
classe::membre


L’exemple qui suit illustre la substitution (fonctions Lire() et Definir()) et l’opérateur de résolution de portéexe "dérivation:accès à une donnée membre cachée".

Code 6.2 : accès à une donnéexe "donnée membre:masquée par dérivation" membre cachée depuis une classe dérivée

#include <iostream>    
#include <string>    
using namespace std;    

//Déclarations des classes    

class Base    
{    
public:    
   string i;    
   Base(){}    
   ~Base(){}    
   //méthodes d’accès    
   void Definir();    
   string Lire();    
};    

class Derivee : public Base    
{    
public:    
   string i;    
   //méthodes d’accès    
   string Lire();    
   void Definir();    
   Derivee(){}    
   ~Derivee(){ }    
};    

//définitions des fonctions    

void Base::Definir()     
{ i="C’est la donnée publique de la classe de base"; }    
void Derivee::Definir()    
{ i="C’est la donnée publique de la classe dérivéen"; }    

string Base::Lire()    
{    
   return i; // renvoie la valeur du membre i    
}    

string Derivee::Lire()    
{    
    return i; // renvoie la valeur du membre i    
}    

int main(void)    
{    
   cout << "On crée un objet de type Derivee" << endl;    
    Derivee objet_derivee;    
       
   cout << "On initialise le membre i de Basen";    
   objet_derivee.Base::Definir();    
   cout << "On initialise le membre i de Deriveen";    
   objet_derivee.Derivee::Definir();       

   cout << "On affiche la valeur renvoyée par la fonction Lire()";    
   cout << " de objet_derivee: n";    
//on affiche le membre i de Derivee :    
   cout << objet_derivee.Lire();      
   cout << "Pour accéder au membre i masqué de la classe de base,n";    
   cout << "il faut appeler explicitement la fonction Lire() de Base: n";    
// on affiche l’entier i de la classe Base :    
   cout << objet_derivee.Base::Lire() << endl;    
   cout << "Ou afficher explicitement le membre masqué: n";    
   cout << objet_derivee.Base::i << endl;    
   return 0;    
}


L’exécution de ce programme produit l’affichage suivant :

On crée un objet de type Derivee    
On initialise le membre i de Base    
On initialise le membre i de Derivee    
On affiche la valeur renvoyée par la fonction Lire() de objet_derivee:     
C’est la donnée publique de la classe dérivée    
Pour accéder au membre i masqué de la classe de base,    
il faut appeler explicitement la fonction Lire() de Base:     
C’est la donnée publique de la classe de base    
Ou afficher explicitement le membre masqué :    
C’est la donnée publique de la classe de base


Le texte original de cette fiche pratique est extrait de
«Tout sur le C++» (Christine EBERHARDT, Collection
CommentCaMarche.net, Dunod, 2009)

A voir également :

Ce document intitulé «  Héritage et polymorphisme  » issu de CommentCaMarche (www.commentcamarche.net) est mis à disposition sous les termes de la licence Creative Commons. Vous pouvez copier, modifier des copies de cette page, dans les conditions fixées par la licence, tant que cette note apparaît clairement.