Programmation C-C++/C++ : La couche objet/Dérivation

Cours de C/C++
^
C++ : La couche objet
Généralités
Extension de la notion de type du C
Déclaration de classes en C++
Encapsulation des données
Héritage
Classes virtuelles
Fonctions et classes amies
Constructeurs et destructeurs
Pointeur this
Données et fonctions membres statiques
Surcharge des opérateurs
Des entrées - sorties simplifiées
Méthodes virtuelles
Dérivation
Méthodes virtuelles pures - Classes abstraites
Pointeurs sur les membres d'une classe

Livre original de C. Casteyde

Nous allons voir ici les règles de dérivation. Ces règles permettent de savoir ce qui est autorisé et ce qui ne l'est pas lorsqu'on travaille avec des classes de base et leurs classes filles (ou classes dérivées).

La première règle, qui est aussi la plus simple, indique qu'il est possible d'utiliser un objet d'une classe dérivée partout où l'on peut utiliser un objet d'une de ses classes mères. Les méthodes et données des classes mères appartiennent en effet par héritage aux classes filles. Bien entendu, on doit avoir les droits d'accès sur les membres de la classe de base que l'on utilise (l'accès peut être restreint lors de l'héritage).

La deuxième règle indique qu'il est possible de faire une affectation d'une classe dérivée vers une classe mère. Les données qui ne servent pas à l'initialisation sont perdues, puisque la classe mère ne possède pas les champs correspondants. 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 pourraient pas recevoir de valeur, et l'initialisation ne se ferait pas correctement.

Enfin, la troisième règle dit que les pointeurs des classes dérivées sont compatibles avec les pointeurs des classes mères. Cela signifie qu'il est possible d'affecter un pointeur de classe dérivée à un pointeur d'une de ses classes de base. Il faut bien entendu que l'on ait en outre le droit d'accéder à 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 private.

Un objet dérivé pointé par un pointeur d'une des classes mères de sa classe est considéré comme un objet de la classe du pointeur qui le pointe. Les données spécifiques à sa classe ne sont pas supprimées, elles sont seulement momentanément inaccessibles. Cependant, le mécanisme des méthodes virtuelles continue de fonctionner correctement. En particulier, le destructeur de la classe de base doit être déclaré en tant que méthode virtuelle. Cela permet d'appeler le bon destructeur en cas de destruction de l'objet.

Il est possible de convertir un pointeur de classe de base en un pointeur de classe dérivée si la classe de base n'est pas virtuelle. Cependant, même lorsque la classe de base n'est pas virtuelle, cela est dangereux, car la classe dérivée peut avoir des membres qui ne sont pas présents dans la classe de base, et l'utilisation de ce pointeur peut conduire à des erreurs très graves. C'est pour cette raison qu'un transtypage est nécessaire pour ce type de conversion.

Soient par exemple les deux classes définies comme suit :

#include <iostream>

using namespace std;

class Mere
{
public:
    Mere(void);
    ~Mere(void);
};

Mere::Mere(void)
{
    cout << "Constructeur de la classe mère." << endl;
    return;
}

Mere::~Mere(void)
{
    cout << "Destructeur de la classe mère." << endl;
    return;
}

class Fille : public Mere
{
public:
    Fille(void);
    ~Fille(void);
};

Fille::Fille(void) : Mere()
{
    cout << "Constructeur de la classe fille." << endl;
    return;
}

Fille::~Fille(void)
{
    cout << "Destructeur de la classe fille." << endl;
    return;
}

Avec ces définitions, seule la première des deux affectations suivantes est autorisée :

Mere m;   // Instanciation de deux objets.
Fille f;

m=f;      // Cela est autorisé, mais l'inverse ne le serait pas :
f=m;      // ERREUR !! (ne compile pas).

Les mêmes règles sont applicables pour les pointeurs d'objets :

Mere *pm, m;
Fille *pf, f;
pf=&f;    // Autorisé.
pm=pf;    // Autorisé. Les données et les méthodes
          // de la classe fille ne sont plus accessibles
          // avec ce pointeur : *pm est un objet
          // de la classe mère.
pf=&m;    // ILLÉGAL : il faut faire un transtypage :
pf=(Fille *) &m;  // Cette fois, c'est légal, mais DANGEREUX !
          // En effet, les méthodes de la classe filles
          // ne sont pas définies, puisque m est une classe mère.

L'utilisation d'un pointeur sur la classe de base pour accéder à une classe dérivée nécessite d'utiliser des méthodes virtuelles. En particulier, il est nécessaire de rendre virtuels les destructeurs. Par exemple, avec la définition donnée ci-dessus pour les deux classes, le code suivant est faux :

Mere *pm;
Fille *pf = new Fille;
pm = pf;
delete pm; // Appel du destructeur de la classe mère !

Pour résoudre le problème, il faut que le destructeur de la classe mère soit virtuel (il est inutile de déclarer virtuel le destructeur des classes filles) :

class Mere
{
public:
    Mere(void);
    virtual ~Mere(void);
};

On notera que bien que l'opérateur delete soit une fonction statique, le bon destructeur est appelé, car le destructeur est déclaré virtual. En effet, l'opérateur delete recherche le destructeur à appeler dans la classe de l'objet le plus dérivé. De plus, l'opérateur delete restitue la mémoire de l'objet complet, et pas seulement celle du sous-objet référencé par le pointeur utilisé dans l'expression delete. Lorsqu'on utilise la dérivation, il est donc très important de déclarer les destructeurs virtuels pour que l'opérateur delete utilise le vrai type de l'objet à détruire.