• Aucun résultat trouvé

Exemple 7-2. Opérateur de résolution de portée int valeur(void) // Fonction globale

7.13. Méthodes virtuelles

Lesméthodes virtuellesn’ont strictement rien à voir avec les classes virtuelles, bien qu’elles utilisent le même mot clévirtual. Ce mot clé est utilisé ici dans un contexte et dans un sens différent.

Nous savons qu’il est possible de redéfinir les méthodes d’une classe mère dans une classe fille. Lors de l’appel d’une fonction ainsi redéfinie, la fonction appelée est la dernière fonction définie dans la hiérarchie de classe. Pour appeler la fonction de la classe mère alors qu’elle a été redéfinie, il faut préciser le nom de la classe à laquelle elle appartient avec l’opérateur de résolution de portée (::).

Bien que simple, cette utilisation de la redéfinition des méthodes peut poser des problèmes. Supposons qu’une classe B hérite de sa classe mère A. Si A possède une méthodexappelant une autre méthodey redéfinie dans la classe fille B, que se passe-t-il lorsqu’un objet de classe B appelle la méthodex? La méthode appelée étant celle de la classe A, elle appellera la méthodeyde la classe A. Par conséquent, la redéfinition deyne sert à rien dès qu’on l’appelle à partir d’une des fonctions d’une des classes mères.

Une première solution consisterait à redéfinir la méthodexdans la classe B. Mais ce n’est ni élégant, ni efficace. Il faut en fait forcer le compilateur à ne pas faire le lien dans la fonctionxde la classe A avec la fonction yde la classe A. Il faut que xappelle soit la fonction yde la classe A si elle est appelée par un objet de la classe A, soit la fonctionyde la classe B si elle est appelée pour un objet de la classe B. Le lien avec l’une des méthodesyne doit être fait qu’au moment de l’exécution, c’est-à-dire qu’on doit faire une édition de liens dynamique.

Le C++ permet de faire cela. Pour cela, il suffit de déclarer virtuelle la fonction de la classe de base qui est redéfinie dans la classe fille, c’est-à-dire la fonctiony. Cela se fait en faisant précéder par le mot clévirtualdans la classe de base.

Chapitre 7. C++ : la couche objet

Exemple 7-25. Redéfinition de méthode de classe de base

#include <iostream>

using namespace std;

// Définit la classe de base des données.

class DonneeBase {

protected:

int Numero; // Les données sont numérotées.

int Valeur; // et sont constituées d’une valeur entière // pour les données de base.

public:

void Entre(void); // Entre une donnée.

void MiseAJour(void); // Met à jour la donnée.

};

void DonneeBase::Entre(void) {

cin >> Numero; // Entre le numéro de la donnée.

cout << endl;

cin >> Valeur; // Entre sa valeur.

cout << endl;

return;

}

void DonneeBase::MiseAJour(void) {

Entre(); // Entre une nouvelle donnée

// à la place de la donnée en cours.

return;

}

/* Définit la classe des données détaillées. */

class DonneeDetaillee : private DonneeBase {

int ValeurEtendue; // Les données détaillées ont en plus // une valeur étendue.

public:

void Entre(void); // Redéfinition de la méthode d’entrée.

};

void DonneeDetaillee::Entre(void) {

DonneeBase::Entre(); // Appelle la méthode de base.

cin >> ValeurEtendue; // Entre la valeur étendue.

cout << endl;

return;

}

Sidest un objet de la classe DonneeDetaillee, l’appel ded.Entrene causera pas de problème. En revanche, l’appel ded.MiseAJourne fonctionnera pas correctement, car la fonctionEntreappelée

Chapitre 7. C++ : la couche objet

dansMiseAJourest la fonction de la classe DonneeBase, et non la fonction redéfinie dans Donnee-Detaille.

Il fallait déclarer la fonctionEntrecomme une fonction virtuelle. Il n’est nécessaire de le faire que dans la classe de base. Celle-ci doit donc être déclarée comme suit :

class DonneeBase {

protected:

int Numero;

int Valeur;

public:

virtual void Entre(void); // Fonction virtuelle.

void MiseAJour(void);

};

Cette fois, la fonctionEntreappelée dansMiseAJourest soit la fonction de la classe DonneeBase, siMiseAJourest appelée pour un objet de classe DonneeBase, soit celle de la classe DonneeDetaille siMiseAJourest appelée pour un objet de la classe DonneeDetaillee.

En résumé, les méthodes virtuelles sont des méthodes qui sont appelées selon la vraie classe de l’objet qui l’appelle. Les objets qui contiennent des méthodes virtuelles peuvent être manipulés en tant qu’objets des classes de base, tout en effectuant les bonnes opérations en fonction de leur type.

Ils apparaissent donc comme étant des objets de la classe de base et des objets de leur classe complète indifféremment, et on peut les considérer soit comme les uns, soit comme les autres. Un tel comporte-ment est appelépolymorphisme(c’est-à-dire qui peut avoir plusieurs aspects différents). Nous verrons une application du polymorphisme dans le cas des pointeurs sur les objets.

7.14. Dérivation

Nous allons voir ici lesrè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 estprivate.

Chapitre 7. C++ : la couche objet 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>

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 {

cout << "Constructeur de la classe fille." << endl;

return;

}

Fille::~Fille(void) {

cout << "Destructeur de la classe fille." << endl;

return;

}

Chapitre 7. C++ : la couche objet

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érateurdeletesoit une fonction statique, le bon destructeur est appe-lé, car le destructeur est déclarévirtual. En effet, l’opérateurdeleterecherche le destructeur à appeler dans la classe de l’objet le plus dérivé. De plus, l’opérateurdeleterestitue 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érateurdeleteutilise le vrai type de l’objet à détruire.

Chapitre 7. C++ : la couche objet